New Upstream Release - jebl2

Ready changes

Summary

Merged new upstream version: 0.1+git20230701.b3c0f25 (was: 0.1+git20210205.1f13ac7).

Resulting package

Built on 2023-07-08T09:00 (took 7m11s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases libjebl2-java-docapt install -t fresh-releases libjebl2-java

Lintian Result

Diff

diff --git a/debian/changelog b/debian/changelog
index bf30bcb..71d06d8 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,10 +1,15 @@
-jebl2 (0.1+git20201011.969bd4b-2) UNRELEASED; urgency=medium
+jebl2 (0.1+git20230701.b3c0f25-1) UNRELEASED; urgency=medium
 
+  [ Pierre Gruet ]
   * Warning: avoid upgrading to new upstream version unless a version of
     figtree >> 1.4.4 is released. figtree is a reverse dependency of jebl2, 
     relying on the old ABI of jebl2 at the moment.
 
- -- Pierre Gruet <pgt@debian.org>  Fri, 06 May 2022 21:31:43 +0200
+  [ Debian Janitor ]
+  * New upstream snapshot.
+  * New upstream snapshot.
+
+ -- Pierre Gruet <pgt@debian.org>  Sat, 08 Jul 2023 08:53:46 -0000
 
 jebl2 (0.1+git20201011.969bd4b-1) unstable; urgency=medium
 
diff --git a/debian/patches/javadoc.patch b/debian/patches/javadoc.patch
index cdfd1c8..fde7b5f 100644
--- a/debian/patches/javadoc.patch
+++ b/debian/patches/javadoc.patch
@@ -3,8 +3,10 @@ Last-Update: Thu, 10 Feb 2011 10:26:38 +0100
 Description: Add javadoc
 Forwarded: https://github.com/rambaut/jebl2/issues/2
 
+Index: jebl2.git/javadoc.xml
+===================================================================
 --- /dev/null
-+++ b/javadoc.xml
++++ jebl2.git/javadoc.xml
 @@ -0,0 +1,22 @@
 +<project name="jebl2" default="javadoc" basedir=".">
 +	<property name="src" location="src" />
diff --git a/src/jebl/evolution/io/FastaExporter.java b/src/jebl/evolution/io/FastaExporter.java
index 9e2c3d0..84af137 100644
--- a/src/jebl/evolution/io/FastaExporter.java
+++ b/src/jebl/evolution/io/FastaExporter.java
@@ -34,13 +34,22 @@ public class FastaExporter implements SequenceExporter {
      */
     public void exportSequences(Collection<? extends Sequence> sequences) throws IOException {
         for (Sequence sequence : sequences) {
-            final Taxon taxon = sequence.getTaxon();
-            String desc = (String) sequence.getAttribute(FastaImporter.descriptionPropertyName);
-            if(desc== null) desc = (String) taxon.getAttribute(FastaImporter.descriptionPropertyName);
-            writer.println(">" + taxon.getName().replace(' ','_') + ((desc != null) ? (" " + desc) : ""));
-            writer.println(sequence.getString());
+            exportSequence(sequence);
         }
     }
 
+    /**
+     * export a sequence.
+     */
+    public void exportSequence(Sequence sequence) throws IOException {
+        final Taxon taxon = sequence.getTaxon();
+        String desc = (String) sequence.getAttribute(FastaImporter.descriptionPropertyName);
+        if (desc== null) {
+            desc = (String) taxon.getAttribute(FastaImporter.descriptionPropertyName);
+        }
+        writer.println(">" + taxon.getName().replace(' ','_') + ((desc != null) ? (" " + desc) : ""));
+        writer.println(sequence.getString());
+    }
+
     private final PrintWriter writer;
 }
diff --git a/src/jebl/evolution/io/NexusExporter.java b/src/jebl/evolution/io/NexusExporter.java
index 994de16..17c6fa3 100644
--- a/src/jebl/evolution/io/NexusExporter.java
+++ b/src/jebl/evolution/io/NexusExporter.java
@@ -30,6 +30,8 @@ import java.util.List;
 
 public class NexusExporter implements AlignmentExporter, SequenceExporter, TreeExporter {
 
+    private static final String BRANCH_LENGTH_FORMAT = "%.6g";
+
     public NexusExporter(Writer writer) {
         this(writer, true);
     }
@@ -65,9 +67,7 @@ public class NexusExporter implements AlignmentExporter, SequenceExporter, TreeE
      */
     public void exportSequences(Collection<? extends Sequence> sequences) throws IllegalArgumentException {
 
-        if (treesBlockOpen) {
-            endWriteTrees();
-        }
+        closeBlock();
 
         establishSequenceTaxa(sequences);
 
@@ -117,6 +117,16 @@ public class NexusExporter implements AlignmentExporter, SequenceExporter, TreeE
         }
     }
 
+    /**
+     * close an existing open trees block
+     */
+    public void closeBlock() {
+        if (treesBlockOpen) {
+            endWriteTrees();
+        }
+    }
+
+
     private boolean treesBlockOpen = false;
 
     /**
@@ -159,9 +169,7 @@ public class NexusExporter implements AlignmentExporter, SequenceExporter, TreeE
     }
 
     public void close() {
-        if (treesBlockOpen) {
-            endWriteTrees();
-        }
+       closeBlock();
         writer.close();
     }
 
@@ -223,9 +231,7 @@ public class NexusExporter implements AlignmentExporter, SequenceExporter, TreeE
     }
 
     public void exportMatrix(final DistanceMatrix distanceMatrix) {
-        if (treesBlockOpen) {
-            endWriteTrees();
-        }
+        closeBlock();
 
         final List<Taxon> taxa = distanceMatrix.getTaxa();
         establishTaxa(taxa);
@@ -365,8 +371,7 @@ public class NexusExporter implements AlignmentExporter, SequenceExporter, TreeE
             appendAttributes(node, null, builder);
 
             if( tree.hasLengths() ) {
-                builder.append(':');
-                builder.append(roundDouble(tree.getLength(node), 6));
+                builder.append(":").append(String.format(BRANCH_LENGTH_FORMAT, tree.getLength(node)));
             }
         } else {
             builder.append('(');
@@ -384,19 +389,12 @@ public class NexusExporter implements AlignmentExporter, SequenceExporter, TreeE
             // whet it is present.
             if (parent != null) {
                 if (tree.hasLengths()) {
-                    builder.append(":").append(roundDouble(tree.getLength(node), 6));
+                    builder.append(":").append(String.format(BRANCH_LENGTH_FORMAT, tree.getLength(node)));
                 }
             }
         }
     }
 
-    public static double roundDouble(double value, int decimalPlace) {
-        double power_of_ten = 1;
-        while (decimalPlace-- > 0)
-            power_of_ten *= 10.0;
-        return Math.round(value * power_of_ten) / power_of_ten;
-    }
-
     private StringBuilder appendAttributes(Attributable item, String[] excludeKeys, StringBuilder builder) {
         if (!writeMetaComments) {
             return builder;
diff --git a/src/jebl/evolution/trees/AbstractRootedTree.java b/src/jebl/evolution/trees/AbstractRootedTree.java
new file mode 100644
index 0000000..eaa553a
--- /dev/null
+++ b/src/jebl/evolution/trees/AbstractRootedTree.java
@@ -0,0 +1,42 @@
+package jebl.evolution.trees;
+
+import jebl.evolution.graphs.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Andrew Rambaut
+ * @version $
+ */
+public abstract class AbstractRootedTree implements RootedTree {
+    @Override
+    public int getExternalNodeCount(Node node) {
+        if (isExternal(node)) return 1;
+
+        int externalNodeCount = 0;
+        for (Node child :  getChildren(node)) {
+            externalNodeCount += getExternalNodeCount(child);
+        }
+
+        return externalNodeCount;
+    }
+
+    /**
+     * @param node the node whose external nodes are being requested.
+     * @return the list of external nodes descendent of the given node.
+     * The set may be empty for a terminal node (a tip).
+     */
+    @Override
+    public List<Node> getExternalNodes(Node node) {
+        if (isExternal(node)) return Collections.singletonList(node);
+
+        List<Node> tips = new ArrayList<Node>();
+        for (Node child :  getChildren(node)) {
+            tips.addAll(getExternalNodes(child));
+        }
+
+        return tips;
+    }
+}
diff --git a/src/jebl/evolution/trees/CompactRootedTree.java b/src/jebl/evolution/trees/CompactRootedTree.java
index bec942c..cc2a89c 100644
--- a/src/jebl/evolution/trees/CompactRootedTree.java
+++ b/src/jebl/evolution/trees/CompactRootedTree.java
@@ -296,6 +296,35 @@ public class CompactRootedTree extends AttributableImp implements RootedTree {
         return clist;
     }
 
+    @Override
+    public int getExternalNodeCount(Node node) {
+        if (isExternal(node)) return 1;
+
+        int externalNodeCount = 0;
+        for (Node child :  getChildren(node)) {
+            externalNodeCount += getExternalNodeCount(child);
+        }
+
+        return externalNodeCount;
+    }
+
+    /**
+     * @param node the node whose external nodes are being requested.
+     * @return the list of external nodes descendent of the given node.
+     * The set may be empty for a terminal node (a tip).
+     */
+    @Override
+    public List<Node> getExternalNodes(Node node) {
+        if (isExternal(node)) return Collections.singletonList(node);
+
+        List<Node> tips = new ArrayList<Node>();
+        for (Node child :  getChildren(node)) {
+            tips.addAll(getExternalNodes(child));
+        }
+
+        return tips;
+    }
+
     public boolean hasHeights() {
         return hasHeights;
     }
@@ -321,6 +350,20 @@ public class CompactRootedTree extends AttributableImp implements RootedTree {
         return heights[index];
     }
 
+    /**
+     * @return Whether the node heights are known or need to be recalculated from the lengths
+     */
+    public boolean isHeightsKnown() {
+           return true;
+    }
+
+    /**
+     * @return Whether the branch lengths are known or need to be recalculated from the heights
+     */
+    public boolean isLengthsKnown() {
+        return true;
+    }
+
     public Node getParent(Node node) {
         final int index = ((SimpleRootedNode) node).index;
         return index == 0 ? null : nodes[parent[index]];
diff --git a/src/jebl/evolution/trees/FilteredRootedTree.java b/src/jebl/evolution/trees/FilteredRootedTree.java
index 804f5b7..cbcc732 100644
--- a/src/jebl/evolution/trees/FilteredRootedTree.java
+++ b/src/jebl/evolution/trees/FilteredRootedTree.java
@@ -4,16 +4,14 @@ import jebl.evolution.graphs.Edge;
 import jebl.evolution.graphs.Node;
 import jebl.evolution.taxa.Taxon;
 
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 /**
  * @author Andrew Rambaut
  * @author Alexei Drummond
  * @version $Id: FilteredRootedTree.java 936 2008-08-06 14:12:07Z rambaut $
  */
-public abstract class FilteredRootedTree implements RootedTree {
+public abstract class FilteredRootedTree extends AbstractRootedTree {
 
     public FilteredRootedTree(final RootedTree source) {
         this.source = source;
@@ -47,6 +45,14 @@ public abstract class FilteredRootedTree implements RootedTree {
         return source.getLength(node);
     }
 
+    public boolean isHeightsKnown() {
+        return source.isHeightsKnown();
+    }
+
+    public boolean isLengthsKnown() {
+        return source.isLengthsKnown();
+    }
+
     public Node getParent(Node node) {
         return source.getParent(node);
     }
diff --git a/src/jebl/evolution/trees/MutableRootedTree.java b/src/jebl/evolution/trees/MutableRootedTree.java
index fb4b4fa..6167d42 100644
--- a/src/jebl/evolution/trees/MutableRootedTree.java
+++ b/src/jebl/evolution/trees/MutableRootedTree.java
@@ -20,7 +20,7 @@ import java.util.*;
  *
  */
 
-public class MutableRootedTree implements RootedTree {
+public class MutableRootedTree extends AbstractRootedTree {
     MutableRootedTree() {  super(); }
 
     /**
@@ -349,6 +349,20 @@ public class MutableRootedTree implements RootedTree {
         return hasLengths;
     }
 
+    /**
+     * @return Whether the node heights are known or need to be recalculated from the lengths
+     */
+    public boolean isHeightsKnown() {
+        return heightsKnown;
+    }
+
+    /**
+     * @return Whether the branch lengths are known or need to be recalculated from the heights
+     */
+    public boolean isLengthsKnown() {
+        return lengthsKnown;
+    }
+
     /**
      * @param node the node whose branch length (to its parent) is being requested.
      * @return the length of the branch to the parent node (0.0 if the node is the root).
diff --git a/src/jebl/evolution/trees/ReRootedTree.java b/src/jebl/evolution/trees/ReRootedTree.java
index b8c195c..ab1b75d 100644
--- a/src/jebl/evolution/trees/ReRootedTree.java
+++ b/src/jebl/evolution/trees/ReRootedTree.java
@@ -1,46 +1,835 @@
 package jebl.evolution.trees;
 
-import jebl.evolution.graphs.Node;
+import jebl.evolution.graphs.*;
+import jebl.evolution.taxa.Taxon;
+import jebl.evolution.trees.*;
+import jebl.util.AttributableHelper;
+import jebl.util.HashPair;
 
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
+import java.util.*;
 
 /**
+ * A rooted tree concrete class that wraps another tree and provides a differently
+ * rooted view of that tree.
+ *
  * @author Andrew Rambaut
- * @author Alexei Drummond
- * @version $Id: ReRootedTree.java 776 2007-09-05 11:17:12Z rambaut $
  */
-public class ReRootedTree extends FilteredRootedTree {
+final public class ReRootedTree extends AbstractRootedTree {
 
-    public enum RootingType {
-		MID_POINT("midpoint"),
-		LEAST_SQUARES("least squares");
+	/**
+	 * Make a copy of the given unrooted tree
+	 * @param source an unrooted source tree
+	 * @param ingroupNode the node on one side of the root
+	 * @param outgroupNode the node on the other side of the root
+	 * @param ingroupBranchLength the branch length from the root to the ingroup node
+	 * @throws jebl.evolution.graphs.Graph.NoEdgeException
+	 */
+	public ReRootedTree(RootedTree source, Node ingroupNode, Node outgroupNode, double ingroupBranchLength) throws NoEdgeException {
 
-		RootingType(String name) {
-			this.name = name;
+		this.source = source;
+		List<Node> children = new ArrayList<Node>();
+
+		Node node1 = createNodes(source, outgroupNode, ingroupNode);
+		setLength(node1, ingroupBranchLength);
+		children.add(node1);
+
+		Node node2 = createNodes(source, ingroupNode, outgroupNode);
+		double l = source.getEdgeLength(ingroupNode, outgroupNode);
+		if (outgroupNode == source.getRootNode()) {
+			// the tree is already rooted at the required location
+			for (Node adj : source.getAdjacencies(outgroupNode)) {
+				if (adj != ingroupNode) {
+					l += source.getEdgeLength(outgroupNode, adj);
+				}
+			}
+		}
+		setLength(node2, Math.max(l - ingroupBranchLength, 0.0));
+		children.add(node2);
+
+		createInternalNode(null, children);
+	}
+
+	/**
+	 * Clones the entire tree structure from the given (unrooted) Tree.
+	 * @param tree the unrooted tree
+	 * @param parent the parent node
+	 * @param child the child node
+	 */
+	public Node createNodes(RootedTree tree, Node parent, Node child) throws NoEdgeException {
+
+		Node newNode = null;
+		double length;
+
+		if (tree.isExternal(child)) {
+			newNode = createExternalNode(child, tree.getTaxon(child));
+			length = tree.getEdgeLength(parent, child);
+		} else {
+			List<Node> adjacencies = tree.getAdjacencies(child);
+
+			if (adjacencies.size() == 2) {
+				// this is the root node so skip over it...
+				if (adjacencies.get(0) == parent) {
+					newNode = createNodes(tree, child, adjacencies.get(1));
+				} else {
+					newNode = createNodes(tree, child, adjacencies.get(0));
+				}
+				length = tree.getEdgeLength(adjacencies.get(0), child) +
+						tree.getEdgeLength(adjacencies.get(1), child);
+
+			} else {
+				List<Node> children = new ArrayList<Node>();
+
+				for (Node child2 : adjacencies) {
+					if (child2 != parent) {
+						children.add(createNodes(tree, child, child2));
+					}
+				}
+
+				if (tree.getParent(parent) == child) {
+					newNode = createInternalNode(parent, children);
+				} else {
+					newNode = createInternalNode(child, children);
+				}
+				length = tree.getEdgeLength(parent, child);
+			}
+		}
+
+		setLength(newNode, length);
+
+		return newNode;
+	}
+
+	/**
+	 * Creates a new external node with the given taxon. See createInternalNode
+	 * for a description of how to use these methods.
+	 * @param source the source node
+	 * @return the created node reference
+	 */
+	private Node createExternalNode(Node source, Taxon taxon) {
+		ReRootedNode node = new ReRootedNode(source, taxon);
+		externalNodes.put(taxon, node);
+		return node;
+	}
+
+	/**
+	 * Once a SimpleRootedTree has been created, the node stucture can be created by
+	 * calling createExternalNode and createInternalNode. First of all createExternalNode
+	 * is called giving Taxon objects for the external nodes. Then these are put into
+	 * sets and passed to createInternalNode to create a parent of these nodes. The
+	 * last node created using createInternalNode is automatically the root so when
+	 * all the nodes are created, the tree is complete.
+	 *
+	 * @param children the child nodes of this nodes
+	 * @return the created node reference
+	 */
+	private ReRootedNode createInternalNode(Node source, List<? extends Node> children) {
+		ReRootedNode node = new ReRootedNode(source, children);
+
+		for (Node child : children) {
+			((ReRootedNode)child).setParent(node);
+		}
+
+		internalNodes.add(node);
+
+		rootNode = node;
+		return node;
+	}
+
+	public Node getSourceNode(Node node) {
+		return ((ReRootedNode)node).source;
+	}
+
+	/**
+	 * @param node the node whose height is being set
+	 * @param height the height
+	 */
+	public void setHeight(Node node, double height) {
+		lengthsKnown = false;
+		heightsKnown = true;
+
+		// If a single height of a single node is set then
+		// assume that all nodes have heights and by extension,
+		// branch lengths as well as these will be calculated
+		// from the heights
+		hasLengths = true;
+		hasHeights = true;
+
+		((ReRootedNode)node).setHeight(height);
+	}
+
+	/**
+	 * @param node the node whose branch length (to its parent) is being set
+	 * @param length the length
+	 */
+	public void setLength(Node node, double length) {
+		heightsKnown = false;
+		lengthsKnown = true;
+
+		// If a single length of a single branch is set then
+		// assume that all branch have lengths and by extension,
+		// node heights as well as these will be calculated
+		// from the lengths
+		hasLengths = true;
+		hasHeights = true;
+
+		((ReRootedNode)node).setLength(length);
+	}
+
+	/**
+	 * @param node the node whose children are being requested.
+	 * @return the list of nodes that are the children of the given node.
+	 *         The list may be empty for a terminal node (a tip).
+	 */
+	public List<Node> getChildren(Node node) {
+		return new ArrayList<Node>(((ReRootedNode)node).getChildren());
+	}
+
+	/**
+	 * @return Whether this tree has node heights available
+	 */
+	public boolean hasHeights() {
+		return hasHeights;
+	}
+
+	/**
+	 * @return Whether the node heights are known or need to be recalculated from the lengths
+	 */
+	@Override
+	public boolean isHeightsKnown() {
+		return heightsKnown;
+	}
+
+	/**
+	 * @param node the node whose height is being requested.
+	 * @return the height of the given node. The height will be
+	 *         less than the parent's height and greater than it children's heights.
+	 */
+	public double getHeight(Node node) {
+		if (!hasHeights) throw new IllegalArgumentException("This tree has no node heights");
+		if (!heightsKnown) calculateNodeHeights();
+		return ((ReRootedNode)node).getHeight();
+	}
+
+	/**
+	 * @return Whether this tree has branch lengths available
+	 */
+	public boolean hasLengths() {
+		return hasLengths;
+	}
+
+	/**
+	 * @return Whether the branch lengths are known or need to be recalculated from the heights
+	 */
+	@Override
+	public boolean isLengthsKnown() {
+		return lengthsKnown;
+	}
+
+	/**
+	 * @param node the node whose branch length (to its parent) is being requested.
+	 * @return the length of the branch to the parent node (0.0 if the node is the root).
+	 */
+	public double getLength(Node node) {
+		if (!hasLengths) throw new IllegalArgumentException("This tree has no branch lengths");
+		if (!lengthsKnown) calculateBranchLengths();
+		return ((ReRootedNode)node).getLength();
+	}
+
+	/**
+	 * @param node the node whose parent is requested
+	 * @return the parent node of the given node, or null
+	 *         if the node is the root node.
+	 */
+	public Node getParent(Node node) {
+		if (!(node instanceof ReRootedNode)) {
+			throw new IllegalArgumentException("Node, " + node.toString() + " is not an instance of SimpleRootedNode");
+		}
+		return ((ReRootedNode)node).getParent();
+	}
+
+	public Edge getParentEdge(Node node) {
+		if (!(node instanceof ReRootedNode)) {
+			throw new IllegalArgumentException("Node, " + node.toString() + " is not an instance of SimpleRootedNode");
+		}
+		return ((ReRootedNode)node).getEdge();
+	}
+
+	/**
+	 * The root of the tree has the largest node height of
+	 * all nodes in the tree.
+	 *
+	 * @return the root of the tree.
+	 */
+	public Node getRootNode() {
+		return rootNode;
+	}
+
+
+	/**
+	 * @return a set of all nodes that have degree 1.
+	 *         These nodes are often refered to as 'tips'.
+	 */
+	public Set<Node> getExternalNodes() {
+		return new LinkedHashSet<Node>(externalNodes.values());
+	}
+
+	/**
+	 * @return a set of all nodes that have degree 2 or more.
+	 *         These nodes are often refered to as internal nodes.
+	 */
+	public Set<Node> getInternalNodes() {
+		return new LinkedHashSet<Node>(internalNodes);
+	}
+
+	/**
+	 * @return the set of taxa associated with the external
+	 *         nodes of this tree. The size of this set should be the
+	 *         same as the size of the external nodes set.
+	 */
+	public Set<Taxon> getTaxa() {
+		return new LinkedHashSet<Taxon>(externalNodes.keySet());
+	}
+
+	/**
+	 * @param node the node whose associated taxon is being requested.
+	 * @return the taxon object associated with the given node, or null
+	 *         if the node is an internal node.
+	 */
+	public Taxon getTaxon(Node node) {
+		if (!(node instanceof ReRootedNode)) {
+			throw new IllegalArgumentException("Node, " + node.toString() + " is not an instance of SimpleRootedNode.  It is an instance of "+node.getClass().getName());
 		}
+		return ((ReRootedNode)node).getTaxon();
+	}
 
-		public String toString() { return name; }
+	/**
+	 * @param node the node
+	 * @return true if the node is of degree 1.
+	 */
+	public boolean isExternal(Node node) {
+		if (!(node instanceof ReRootedNode)) {
+			throw new IllegalArgumentException("Node, " + node.toString() + " is not an instance of SimpleRootedNode.  It is an instance of "+node.getClass().getName());
+		}
+		return ((ReRootedNode)node).getChildren().size() == 0;
+	}
 
-		private String name;
+	/**
+	 * @param taxon the taxon
+	 * @return the external node associated with the given taxon, or null
+	 *         if the taxon is not a member of the taxa set associated with this tree.
+	 */
+	public Node getNode(Taxon taxon) {
+		return externalNodes.get(taxon);
 	}
 
-    public ReRootedTree(final RootedTree source, RootingType rootingType) {
-        super(source);
-	    switch (rootingType) {
-		    case MID_POINT:
-			break;
-		    case LEAST_SQUARES:
-			break;
-		    default:
-			    throw new IllegalArgumentException("Unknown enum value");
-	    }
-    }
+	public void renameTaxa(Taxon from, Taxon to) {
+		ReRootedNode node = (ReRootedNode)externalNodes.get(from);
+
+		// TT: The javadoc doesn't specify whether renameTaxa() should fail or silently do nothing
+		// if Taxon from doesn't exist. But the code already threw a NullPointerException before (bug 4824),
+		// so it's probably ok to throw a more informative IllegalArgumentException instead.
+		if (node == null) {
+			throw new IllegalArgumentException("Unknown taxon " + from + "; can't rename to " + to);
+		}
+
+		node.setTaxon(to);
+
+		externalNodes.remove(from);
+		externalNodes.put(to, node);
+	}
+
+	/**
+	 * Returns a list of edges connected to this node
+	 *
+	 * @param node
+	 * @return the set of nodes that are attached by edges to the given node.
+	 */
+	public List<Edge> getEdges(Node node) {
+		List<Edge> edges = new ArrayList<Edge>();
+		for (Node adjNode : getAdjacencies(node)) {
+			edges.add(((ReRootedNode)adjNode).getEdge());
+
+		}
+		return edges;
+	}
+
+	/**
+	 * @param node
+	 * @return the set of nodes that are attached by edges to the given node.
+	 */
+	public List<Node> getAdjacencies(Node node) {
+		return ((ReRootedNode)node).getAdjacencies();
+	}
+
+	/**
+	 * Returns the Edge that connects these two nodes
+	 *
+	 * @param node1
+	 * @param node2
+	 * @return the edge object.
+	 * @throws jebl.evolution.graphs.Graph.NoEdgeException
+	 *          if the nodes are not directly connected by an edge.
+	 */
+	public Edge getEdge(Node node1, Node node2) throws NoEdgeException {
+		if (((ReRootedNode)node1).getParent() == node2) {
+			return ((ReRootedNode)node1).getEdge();
+		} else if (((ReRootedNode)node2).getParent() == node1) {
+			return ((ReRootedNode)node2).getEdge();
+		} else {
+			throw new NoEdgeException();
+		}
+	}
+
+	/**
+	 * @param node1
+	 * @param node2
+	 * @return the length of the edge connecting node1 and node2.
+	 * @throws jebl.evolution.graphs.Graph.NoEdgeException
+	 *          if the nodes are not directly connected by an edge.
+	 */
+	public double getEdgeLength(Node node1, Node node2) throws NoEdgeException {
+		if (((ReRootedNode)node1).getParent() == node2) {
+			if (heightsKnown) {
+				return ((ReRootedNode)node2).getHeight() - ((ReRootedNode)node1).getHeight();
+			} else {
+				return ((ReRootedNode)node1).getLength();
+			}
+		} else if (((ReRootedNode)node2).getParent() == node1) {
+			if (heightsKnown) {
+				return ((ReRootedNode)node1).getHeight() - ((ReRootedNode)node2).getHeight();
+			} else {
+				return ((ReRootedNode)node2).getLength();
+			}
+		} else {
+			throw new NoEdgeException();
+		}
+	}
+
+	/**
+	 * Returns an array of 2 nodes which are the nodes at either end of the edge.
+	 *
+	 * @param edge
+	 * @return an array of 2 edges
+	 */
+	public Node[] getNodes(Edge edge) {
+		for (Node node : getNodes()) {
+			if (((ReRootedNode)node).getEdge() == edge) {
+				return new Node[] { node, ((ReRootedNode)node).getParent() };
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * @return the set of all nodes in this graph.
+	 */
+	public Set<Node> getNodes() {
+		Set<Node> nodes = new LinkedHashSet<Node>(internalNodes);
+		nodes.addAll(externalNodes.values());
+		return nodes;
+	}
+
+	/**
+	 * @return the set of all edges in this graph.
+	 */
+	public Set<Edge> getEdges() {
+		Set<Edge> edges = new LinkedHashSet<Edge>();
+		for (Node node : getNodes()) {
+			if (node != getRootNode()) {
+				edges.add(((ReRootedNode)node).getEdge());
+			}
+
+		}
+		return edges;
+	}
+
+	/**
+	 * The set of external edges. This is a pretty inefficient implementation because
+	 * a new set is constructed each time this is called.
+	 * @return the set of external edges.
+	 */
+	public Set<Edge> getExternalEdges() {
+		Set<Edge> edges = new LinkedHashSet<Edge>();
+		for (Node node : getExternalNodes()) {
+			edges.add(((ReRootedNode)node).getEdge());
+		}
+		return edges;
+	}
+
+	/**
+	 * The set of internal edges. This is a pretty inefficient implementation because
+	 * a new set is constructed each time this is called.
+	 * @return the set of internal edges.
+	 */
+	public Set<Edge> getInternalEdges() {
+		Set<Edge> edges = new LinkedHashSet<Edge>();
+		for (Node node : getInternalNodes()) {
+			if (node != getRootNode()) {
+				edges.add(((ReRootedNode)node).getEdge());
+			}
+		}
+		return edges;
+	}
+
+	/**
+	 * @param degree the number of edges connected to a node
+	 * @return a set containing all nodes in this graph of the given degree.
+	 */
+	public Set<Node> getNodes(int degree) {
+		Set<Node> nodes = new LinkedHashSet<Node>();
+		for (Node node : getNodes()) {
+			// Account for no anncesstor of root, assumed by default in getDegree
+			final int deg = node.getDegree() ;
+			if (deg == degree) nodes.add(node);
+		}
+		return nodes;
+	}
+
+	/**
+	 * Set the node heights from the current branch lengths.
+	 */
+	private void calculateNodeHeights() {
+
+		if (!lengthsKnown) {
+			throw new IllegalArgumentException("Can't calculate node heights because branch lengths not known");
+		}
+
+		nodeLengthsToHeights(rootNode, 0.0);
+
+		double maxHeight = 0.0;
+		for (Node externalNode : getExternalNodes()) {
+			if (((ReRootedNode)externalNode).getHeight() > maxHeight) {
+				maxHeight = ((ReRootedNode)externalNode).getHeight();
+			}
+		}
+
+		for (Node node : getNodes()) {
+			((ReRootedNode)node).setHeight(maxHeight - ((ReRootedNode)node).getHeight());
+		}
+
+		heightsKnown = true;
+	}
+
+	/**
+	 * Set the node heights from the current node branch lengths. Actually
+	 * sets distance from root so the heights then need to be reversed.
+	 */
+	private void nodeLengthsToHeights(ReRootedNode node, double height) {
+
+		double newHeight = height;
+
+		if (node.getLength() > 0.0) {
+			newHeight += node.getLength();
+		}
+
+		node.setHeight(newHeight);
+
+		for (Node child : node.getChildren()) {
+			nodeLengthsToHeights((ReRootedNode)child, newHeight);
+		}
+	}
+
+	/**
+	 * Calculate branch lengths from the current node heights.
+	 */
+	protected void calculateBranchLengths() {
+
+		if (!hasLengths) {
+			throw new IllegalArgumentException("Can't calculate branch lengths because node heights not known");
+		}
+
+		nodeHeightsToLengths(rootNode, getHeight(rootNode));
+
+		lengthsKnown = true;
+	}
+
+	/**
+	 * Calculate branch lengths from the current node heights.
+	 */
+	private void nodeHeightsToLengths(ReRootedNode node, double height) {
+		final double h = node.getHeight();
+		node.setLength(h >= 0 ? height - h : 1);
+
+		for (Node child : node.getChildren()) {
+			nodeHeightsToLengths((ReRootedNode)child, node.getHeight());
+		}
+
+	}
+
+	public boolean conceptuallyUnrooted() {
+		return false;
+	}
+
+	public boolean isRoot(Node node) {
+		return node == rootNode;
+	}
+
+	// Attributable IMPLEMENTATION
+
+	public void setAttribute(String name, Object value) {
+		source.setAttribute(name, value);
+	}
+
+	public Object getAttribute(String name) {
+		return source.getAttribute(name);
+	}
+
+	public void removeAttribute(String name) {
+		source.removeAttribute(name);
+	}
+
+	public Set<String> getAttributeNames() {
+		return source.getAttributeNames();
+	}
+
+	public Map<String, Object> getAttributeMap() {
+		return source.getAttributeMap();
+	}
+
+	/**
+	 * Root any tree by locating the "center" of tree and adding a new root node at that point
+	 * <p/>
+	 * for any point on the tree x let D(x) = Max{distance between x and t : for all tips t}
+	 * The "center" c is the point with the smallest distance, i.e. D(c) = min{ D(x) : x in tree }
+	 *
+	 * @param tree to root
+	 * @return rooted tree
+	 */
+	public static RootedTree rootTreeAtCenter(RootedTree tree) {
+		// Method - find the pair of tips with the longest distance. It is easy to see that the center
+		// is at the midpoint of the path between them.
+
+		HashMap<HashPair<Node>, Double> dists = new LinkedHashMap<HashPair<Node>, Double>();
+		try {
+			double maxDistance = -Double.MAX_VALUE;
+			// node on maximal path
+			Node current = null;
+			// next node on maximal path
+			Node direction = null;
+
+			// locate one terminal node of longest path
+			for (Node e : tree.getExternalNodes()) {
+				for (Node n : tree.getAdjacencies(e)) {
+					final double d = dist(tree, e, n, dists);
+					if (d > maxDistance) {
+						maxDistance = d;
+						current = e;
+						direction = n;
+					}
+				}
+			}
+
+			// traverse along maximal path to it's middle
+			double distanceLeft = maxDistance / 2.0;
+
+			while (true) {
+				final double len = tree.getEdgeLength(current, direction);
+				if (distanceLeft <= len) {
+					//System.out.println(toNewick(rtree));
+					return new ReRootedTree(tree, current, direction, distanceLeft);
+				}
+				distanceLeft -= len;
+
+				maxDistance = -Double.MAX_VALUE;
+				Node next = null;
+				for (Node n : tree.getAdjacencies(direction)) {
+					if (n == current) continue;
+					final double d = dist(tree, direction, n, dists);
+					if (d > maxDistance) {
+						maxDistance = d;
+						next = n;
+					}
+				}
+				current = direction;
+				direction = next;
+			}
+		} catch (Graph.NoEdgeException e1) {
+			return null; // serious bug, should not happen
+		}
+	}
+
+	private static double dist(Tree tree, Node root, Node node, Map<HashPair<Node>, Double> dists) throws Graph.NoEdgeException {
+		HashPair<Node> p = new HashPair<Node>(root, node);
+		if (dists.containsKey(p)) {
+			return dists.get(p);
+		}
+
+		// assume positive branches
+		double maxDist = 0;
+		for (Node n : tree.getAdjacencies(node)) {
+			if (n != root) {
+				double d = dist(tree, node, n, dists);
+				maxDist = Math.max(maxDist, d);
+			}
+		}
+		double dist = tree.getEdgeLength(node, root) + maxDist;
+
+		dists.put(p, dist);
+		return dist;
+	}
 
 	// PRIVATE members
-//    private final Node rootChild1;
-//    private final Node rootChild2;
-//    private final double rootLength1;
-//    private final double rootLength2;
+
+	private RootedTree source = null;
+
+	private ReRootedNode rootNode = null;
+	private final Set<Node> internalNodes = new LinkedHashSet<Node>();
+	private final Map<Taxon, Node> externalNodes = new LinkedHashMap<Taxon, Node>();
+
+	private boolean heightsKnown = false;
+	private boolean lengthsKnown = false;
+
+	private boolean hasHeights = false;
+	private boolean hasLengths = false;
+
+	private class ReRootedNode implements Node {
+		public ReRootedNode(Node source, Taxon taxon) {
+			this.source = source;
+			this.children = Collections.unmodifiableList(new ArrayList<Node>());
+			this.taxon = taxon;
+		}
+
+		public ReRootedNode(Node source, List<? extends Node> children) {
+			this.source = source;
+			this.children = Collections.unmodifiableList(new ArrayList<Node>(children));
+			this.taxon = null;
+		}
+
+		public Node getParent() {
+			return parent;
+		}
+
+		public void setParent(Node parent) {
+			this.parent = parent;
+		}
+
+		public List<Node> getChildren() {
+			return children;
+		}
+
+		public double getHeight() {
+			return height;
+		}
+
+		// height above latest tip
+		public void setHeight(double height) {
+			this.height = height;
+		}
+
+		// length of branch to parent
+		public double getLength() {
+			return length;
+		}
+
+		public void setLength(double length) {
+			this.length = length;
+		}
+
+		public int getDegree() {
+			return children.size() +(this==rootNode?0:1);
+		}
+
+		public void setTaxon(Taxon to) {
+			taxon = to;
+		}
+
+		/**
+		 * returns the edge connecting this node to the parent node
+		 * @return the edge
+		 */
+		public Edge getEdge() {
+			if (edge == null) {
+				edge = new BaseEdge() {
+					public double getLength() {
+						return length;
+					}
+				};
+			}
+
+			return edge;
+		}
+
+		/**
+		 * For a rooted tree, getting the adjacencies is not the most efficient
+		 * operation as it makes a new set containing the children and the parent.
+		 * @return the adjacaencies
+		 */
+		public List<Node> getAdjacencies() {
+			List<Node> adjacencies = new ArrayList<Node>();
+			if (children != null) adjacencies.addAll(children);
+			if (parent != null) adjacencies.add(parent);
+			return adjacencies;
+		}
+
+		public Taxon getTaxon() {
+			return taxon;
+		}
+
+		// Attributable IMPLEMENTATION
+
+		public void setAttribute(String name, Object value) {
+			if (source == null) {
+				if (helper == null) {
+					helper = new AttributableHelper();
+				}
+				helper.setAttribute(name, value);
+			} else {
+				source.setAttribute(name, value);
+			}
+		}
+
+		public Object getAttribute(String name) {
+			if (source == null) {
+				if (helper == null) {
+					return null;
+				}
+				return helper.getAttribute(name);
+			}
+			return source.getAttribute(name);
+		}
+
+		public void removeAttribute(String name) {
+			if (source == null) {
+				if( helper != null ) {
+					helper.removeAttribute(name);
+				}
+			} else {
+				source.removeAttribute(name);
+			}
+		}
+
+		public Set<String> getAttributeNames() {
+			if (source == null) {
+				if (helper == null) {
+					return Collections.emptySet();
+				}
+				return helper.getAttributeNames();
+			}
+			return source.getAttributeNames();
+		}
+
+		public Map<String, Object> getAttributeMap() {
+			if (source == null) {
+				if (helper == null) {
+					return Collections.emptyMap();
+				}
+				return helper.getAttributeMap();
+			}
+			return source.getAttributeMap();
+		}
+
+		private final Node source;
+
+		private List<Node> children;
+		private Taxon taxon;
+
+		private Node parent;
+		private double height;
+		private double length;
+
+		private Edge edge = null;
+
+		private AttributableHelper helper = null;
+
+	}
 }
\ No newline at end of file
diff --git a/src/jebl/evolution/trees/RootedFromUnrooted.java b/src/jebl/evolution/trees/RootedFromUnrooted.java
index 87cadf6..1034eec 100644
--- a/src/jebl/evolution/trees/RootedFromUnrooted.java
+++ b/src/jebl/evolution/trees/RootedFromUnrooted.java
@@ -17,7 +17,7 @@ import java.util.*;
  *
  */
 
-public class RootedFromUnrooted implements RootedTree {
+public class RootedFromUnrooted extends AbstractRootedTree {
 	/**
 	 * The unrooted tree
 	 */
@@ -157,6 +157,20 @@ public class RootedFromUnrooted implements RootedTree {
 		return l;
 	}
 
+	/**
+	 * @return Whether the node heights are known or need to be recalculated from the lengths
+	 */
+	public boolean isHeightsKnown() {
+		return false;
+	}
+
+	/**
+	 * @return Whether the branch lengths are known or need to be recalculated from the heights
+	 */
+	public boolean isLengthsKnown() {
+		return true;
+	}
+
 	public Node getParent(Node node) {
 		return parents.get(node);
 	}
diff --git a/src/jebl/evolution/trees/RootedSubtree.java b/src/jebl/evolution/trees/RootedSubtree.java
index 27ef3cb..8357c36 100644
--- a/src/jebl/evolution/trees/RootedSubtree.java
+++ b/src/jebl/evolution/trees/RootedSubtree.java
@@ -18,7 +18,7 @@ import java.util.*;
  * @author Alexei Drummond
  * @version $Id: SimpleRootedTree.java 935 2008-07-22 16:52:04Z rambaut $
  */
-final public class RootedSubtree implements RootedTree {
+final public class RootedSubtree extends AbstractRootedTree {
 
     /**
      * Make a copy of the given rooted tree
@@ -199,6 +199,20 @@ final public class RootedSubtree implements RootedTree {
         return ((RootedSubtreeNode)node).getLength();
     }
 
+    /**
+     * @return Whether the node heights are known or need to be recalculated from the lengths
+     */
+    public boolean isHeightsKnown() {
+        return heightsKnown;
+    }
+
+    /**
+     * @return Whether the branch lengths are known or need to be recalculated from the heights
+     */
+    public boolean isLengthsKnown() {
+        return lengthsKnown;
+    }
+
     /**
      * @param node the node whose parent is requested
      * @return the parent node of the given node, or null
diff --git a/src/jebl/evolution/trees/RootedTree.java b/src/jebl/evolution/trees/RootedTree.java
index 6e8319d..b3661c0 100644
--- a/src/jebl/evolution/trees/RootedTree.java
+++ b/src/jebl/evolution/trees/RootedTree.java
@@ -33,11 +33,25 @@ public interface RootedTree extends Tree {
      */
     List<Node> getChildren(Node node);
 
+    int getExternalNodeCount(Node node);
+
+    /**
+     * @param node the node whose external nodes are being requested.
+     * @return the list of external nodes descendent of the given node.
+     * The set may be empty for a terminal node (a tip).
+     */
+    List<Node> getExternalNodes(Node node);
+
     /**
      * @return Whether this tree has node heights available
      */
     boolean hasHeights();
 
+    /**
+     * @return Whether the node heights are known or need to be recalculated from the lengths
+     */
+    boolean isHeightsKnown();
+
     /**
      * @param node the node whose height is being requested.
      * @return the height of the given node. The height will be
@@ -50,6 +64,11 @@ public interface RootedTree extends Tree {
      */
     boolean hasLengths();
 
+    /**
+     * @return Whether the branch lengths are known or need to be recalculated from the heights
+     */
+    boolean isLengthsKnown();
+
     /**
      * @param node the node whose branch length (to its parent) is being requested.
      * @return the length of the branch to the parent node (0.0 if the node is the root).
diff --git a/src/jebl/evolution/trees/RootedTreeUtils.java b/src/jebl/evolution/trees/RootedTreeUtils.java
index dc50e55..5eeb50a 100644
--- a/src/jebl/evolution/trees/RootedTreeUtils.java
+++ b/src/jebl/evolution/trees/RootedTreeUtils.java
@@ -25,15 +25,7 @@ public class RootedTreeUtils {
 	 * @return the number of leaves under this node.
 	 */
 	public static final int getTipCount(RootedTree tree, Node node) {
-		int tipCount = 0;
-		for (Node child : tree.getChildren(node)) {
-			tipCount += getTipCount(tree, child);
-		}
-
-		// is external
-		if (tipCount == 0) return 1;
-
-		return tipCount;
+		return tree.getExternalNodeCount(node);
 	}
 
     public static double getMinTipHeight(RootedTree tree, Node node) {
@@ -68,7 +60,28 @@ public class RootedTreeUtils {
         return maxTipHeight;
     }
 
-    /**
+	/**
+	 * returns the average distance from the given node to all the tips below it
+	 * @param tree
+	 * @param node
+	 * @return
+	 */
+	public static double getAverageTipDistance(RootedTree tree, Node node) {
+		if (tree.isExternal(node)) {
+			return 0.0;
+		}
+
+		double nodeHeight = tree.getHeight(node);
+		double sumTipDistance = 0.0;
+		for (Node tip : tree.getExternalNodes(node)) {
+			sumTipDistance += nodeHeight - tree.getHeight(tip);
+		}
+
+		return sumTipDistance / tree.getExternalNodeCount(node);
+	}
+
+
+	/**
 	 * @return true only if all tips have height 0.0
 	 */
 	public static boolean isUltrametric(RootedTree tree, double tolerance) {
diff --git a/src/jebl/evolution/trees/SimpleRootedTree.java b/src/jebl/evolution/trees/SimpleRootedTree.java
index b10fe52..bf5a078 100644
--- a/src/jebl/evolution/trees/SimpleRootedTree.java
+++ b/src/jebl/evolution/trees/SimpleRootedTree.java
@@ -1,6 +1,7 @@
 package jebl.evolution.trees;
 
 import jebl.evolution.graphs.Edge;
+import jebl.evolution.graphs.Graph.NoEdgeException;
 import jebl.evolution.graphs.Node;
 import jebl.evolution.taxa.Taxon;
 import jebl.util.AttributableHelper;
@@ -68,6 +69,10 @@ final public class SimpleRootedTree implements RootedTree {
      * @return created node
      */
     public Node createNodes(RootedTree tree, Node node) {
+        hasHeights = tree.hasHeights();
+        heightsKnown = tree.isHeightsKnown();
+        hasLengths = tree.hasLengths();
+        lengthsKnown = tree.isLengthsKnown();
         return createNodes(tree, node, (Map<Node, Node>)null);
     }
 
@@ -100,8 +105,12 @@ final public class SimpleRootedTree implements RootedTree {
             newNode.setAttribute(e.getKey(), e.getValue());
         }
         // }
-        setHeight(newNode, tree.getHeight(node));
-
+        if (tree.hasHeights() && tree.isHeightsKnown()) {
+            setHeight(newNode, tree.getHeight(node));
+        }
+        if (tree.hasLengths() && tree.isLengthsKnown()) {
+            setLength(newNode, tree.getLength(node));
+        }
         return newNode;
     }
 
@@ -304,6 +313,38 @@ final public class SimpleRootedTree implements RootedTree {
         return new ArrayList<Node>(((SimpleRootedNode)node).getChildren());
     }
 
+    @Override
+    public int getExternalNodeCount(Node node) {
+        if (((SimpleRootedNode)node).externalNodeCount < 0) {
+            if (isExternal(node)) {
+                ((SimpleRootedNode)node).externalNodeCount = 1;
+            } else {
+                ((SimpleRootedNode)node).externalNodeCount = 0;
+                for (Node child : getChildren(node)) {
+                    ((SimpleRootedNode)node).externalNodeCount += getExternalNodeCount(child);
+                }
+            }
+        }
+        return ((SimpleRootedNode)node).externalNodeCount;
+    }
+
+    /**
+     * @param node the node whose external nodes are being requested.
+     * @return the list of external nodes descendent of the given node.
+     * The set may be empty for a terminal node (a tip).
+     */
+    @Override
+    public List<Node> getExternalNodes(Node node) {
+        if (isExternal(node)) return Collections.singletonList(node);
+
+        List<Node> tips = new ArrayList<Node>();
+        for (Node child :  getChildren(node)) {
+            tips.addAll(getExternalNodes(child));
+        }
+
+        return tips;
+    }
+
     /**
      * @return Whether this tree has node heights available
      */
@@ -311,6 +352,13 @@ final public class SimpleRootedTree implements RootedTree {
         return hasHeights;
     }
 
+    /**
+     * @return Whether the node heights are known or need to be recalculated from the lengths
+     */
+    public boolean isHeightsKnown() {
+        return heightsKnown;
+    }
+
     /**
      * @param node the node whose height is being requested.
      * @return the height of the given node. The height will be
@@ -329,6 +377,13 @@ final public class SimpleRootedTree implements RootedTree {
         return hasLengths;
     }
 
+    /**
+     * @return Whether the branch lengths are known or need to be recalculated from the heights
+     */
+    public boolean isLengthsKnown() {
+        return lengthsKnown;
+    }
+
     /**
      * @param node the node whose branch length (to its parent) is being requested.
      * @return the length of the branch to the parent node (0.0 if the node is the root).
@@ -351,12 +406,12 @@ final public class SimpleRootedTree implements RootedTree {
         return ((SimpleRootedNode)node).getParent();
     }
 
-	public Edge getParentEdge(Node node) {
-	    if (!(node instanceof SimpleRootedNode)) {
-	        throw new IllegalArgumentException("Node, " + node.toString() + " is not an instance of SimpleRootedNode");
-	    }
-	    return ((SimpleRootedNode)node).getEdge();
-	}
+    public Edge getParentEdge(Node node) {
+        if (!(node instanceof SimpleRootedNode)) {
+            throw new IllegalArgumentException("Node, " + node.toString() + " is not an instance of SimpleRootedNode");
+        }
+        return ((SimpleRootedNode)node).getEdge();
+    }
 
     /**
      * The root of the tree has the largest node height of
@@ -517,29 +572,29 @@ final public class SimpleRootedTree implements RootedTree {
         }
     }
 
-	/**
-	 * Returns an array of 2 nodes which are the nodes at either end of the edge.
-	 *
-	 * @param edge
-	 * @return an array of 2 edges
-	 */
-	public Node[] getNodes(Edge edge) {
-		for (Node node : getNodes()) {
-			if (((SimpleRootedNode)node).getEdge() == edge) {
-				return new Node[] { node, ((SimpleRootedNode)node).getParent() };
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * @return the set of all nodes in this graph.
-	 */
-	public Set<Node> getNodes() {
-	    Set<Node> nodes = new LinkedHashSet<Node>(internalNodes);
-	    nodes.addAll(getExternalNodes());
-	    return nodes;
-	}
+    /**
+     * Returns an array of 2 nodes which are the nodes at either end of the edge.
+     *
+     * @param edge
+     * @return an array of 2 edges
+     */
+    public Node[] getNodes(Edge edge) {
+        for (Node node : getNodes()) {
+            if (((SimpleRootedNode)node).getEdge() == edge) {
+                return new Node[] { node, ((SimpleRootedNode)node).getParent() };
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return the set of all nodes in this graph.
+     */
+    public Set<Node> getNodes() {
+        Set<Node> nodes = new LinkedHashSet<Node>(internalNodes);
+        nodes.addAll(getExternalNodes());
+        return nodes;
+    }
 
     /**
      * @return the set of all edges in this graph.
@@ -555,33 +610,33 @@ final public class SimpleRootedTree implements RootedTree {
         return edges;
     }
 
-	/**
-	 * The set of external edges. This is a pretty inefficient implementation because
-	 * a new set is constructed each time this is called.
-	 * @return the set of external edges.
-	 */
-	public Set<Edge> getExternalEdges() {
-		Set<Edge> edges = new LinkedHashSet<Edge>();
-		for (Node node : getExternalNodes()) {
-			edges.add(((SimpleRootedNode)node).getEdge());
-		}
-		return edges;
-	}
-
-	/**
-	 * The set of internal edges. This is a pretty inefficient implementation because
-	 * a new set is constructed each time this is called.
-	 * @return the set of internal edges.
-	 */
-	public Set<Edge> getInternalEdges() {
-		Set<Edge> edges = new LinkedHashSet<Edge>();
-		for (Node node : getInternalNodes()) {
-			if (node != getRootNode()) {
-			    edges.add(((SimpleRootedNode)node).getEdge());
-			}
-		}
-		return edges;
-	}
+    /**
+     * The set of external edges. This is a pretty inefficient implementation because
+     * a new set is constructed each time this is called.
+     * @return the set of external edges.
+     */
+    public Set<Edge> getExternalEdges() {
+        Set<Edge> edges = new LinkedHashSet<Edge>();
+        for (Node node : getExternalNodes()) {
+            edges.add(((SimpleRootedNode)node).getEdge());
+        }
+        return edges;
+    }
+
+    /**
+     * The set of internal edges. This is a pretty inefficient implementation because
+     * a new set is constructed each time this is called.
+     * @return the set of internal edges.
+     */
+    public Set<Edge> getInternalEdges() {
+        Set<Edge> edges = new LinkedHashSet<Edge>();
+        for (Node node : getInternalNodes()) {
+            if (node != getRootNode()) {
+                edges.add(((SimpleRootedNode)node).getEdge());
+            }
+        }
+        return edges;
+    }
 
     /**
      * @param degree the number of edges connected to a node
@@ -676,11 +731,11 @@ final public class SimpleRootedTree implements RootedTree {
         return conceptuallyUnrooted;
     }
 
-	public boolean isRoot(Node node) {
-		return node == rootNode;
-	}
+    public boolean isRoot(Node node) {
+        return node == rootNode;
+    }
 
-	// Attributable IMPLEMENTATION
+    // Attributable IMPLEMENTATION
 
     public void setAttribute(String name, Object value) {
         if (helper == null) {
@@ -882,5 +937,7 @@ final public class SimpleRootedTree implements RootedTree {
         private double length;
 
         private Edge edge = null;
+
+        private int externalNodeCount = -1;
     }
 }
\ No newline at end of file
diff --git a/src/jebl/evolution/trees/TransformedRootedTree.java b/src/jebl/evolution/trees/TransformedRootedTree.java
index 48fe9e7..10e31a0 100644
--- a/src/jebl/evolution/trees/TransformedRootedTree.java
+++ b/src/jebl/evolution/trees/TransformedRootedTree.java
@@ -71,6 +71,20 @@ public class TransformedRootedTree extends FilteredRootedTree {
         }
     }
 
+    /**
+     * @return Whether the node heights are known or need to be recalculated from the lengths
+     */
+    public boolean isHeightsKnown() {
+        return true;
+    }
+
+    /**
+     * @return Whether the branch lengths are known or need to be recalculated from the heights
+     */
+    public boolean isLengthsKnown() {
+        return true;
+    }
+
     private int getCladeSize(Node node) {
         if (isExternal(node)) {
             return 1;
diff --git a/src/jebl/evolution/trees/Utils.java b/src/jebl/evolution/trees/Utils.java
index 6179682..5bfef65 100644
--- a/src/jebl/evolution/trees/Utils.java
+++ b/src/jebl/evolution/trees/Utils.java
@@ -8,6 +8,7 @@
  */
 package jebl.evolution.trees;
 
+import jebl.evolution.graphs.Edge;
 import jebl.evolution.graphs.Graph;
 import jebl.evolution.graphs.Node;
 import jebl.evolution.taxa.Taxon;
@@ -23,6 +24,8 @@ import java.util.*;
  * @version $Id: Utils.java 979 2009-02-24 11:15:18Z rambaut $
  */
 public final class Utils {
+	private static final String BRANCH_LENGTH_FORMAT = "%.6g";
+
 	private Utils() { }  // make class uninstantiable
 
 	/**
@@ -89,8 +92,7 @@ public final class Utils {
 			}
 			buffer.append(name);
 			if (tree.hasLengths()) {
-				buffer.append(':');
-				buffer.append(tree.getLength(node));
+				buffer.append(":").append(String.format(BRANCH_LENGTH_FORMAT, tree.getLength(node)));
 			}
 		} else {
 			buffer.append('(');
@@ -105,7 +107,7 @@ public final class Utils {
 			// Don't write root length. This is ignored elsewhere and the nexus importer fails
 			// whet it is present.
 			if (parent != null && tree.hasLengths()) {
-				buffer.append(":").append(tree.getLength(node));
+				buffer.append(":").append(String.format(BRANCH_LENGTH_FORMAT, tree.getLength(node)));
 			}
 		}
 	}
@@ -421,6 +423,33 @@ public final class Utils {
 		return true;
 	}
 
+	/**
+	 * @param tree the tree
+	 * @return the sum of all the branch lengths
+	 */
+	public static double getLength(Tree tree) {
+		double length = 0.0;
+		for (Edge edge : tree.getEdges()) {
+			length += edge.getLength();
+		}
+		return length;
+	}
+
+	/**
+	 * @param tree the tree
+	 * @return the sum of all the branch lengths
+	 */
+	public static double getLength(RootedTree tree) {
+		double length = 0.0;
+		for (Node node : tree.getNodes()) {
+			if (!tree.isRoot(node)) {
+				length += tree.getLength(node);
+			}
+		}
+		return length;
+	}
+
+
 	/**
 	 * Return the number of external nodes under this node.
 	 *
@@ -429,16 +458,7 @@ public final class Utils {
 	 * @return the number of external nodes under this node.
 	 */
 	public static int getExternalNodeCount(RootedTree tree, Node node) {
-
-		final List<Node> children = tree.getChildren(node);
-		if (children.size() == 0) return 1;
-
-		int externalNodeCount = 0;
-		for (Node child : children) {
-			externalNodeCount += getExternalNodeCount(tree, child);
-		}
-
-		return externalNodeCount;
+		return tree.getExternalNodeCount(node);
 	}
 
 	/**

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/doc/libjebl2-java/api/jebl/evolution/trees/AbstractRootedTree.html
-rw-r--r--  root/root   /usr/share/doc/libjebl2-java/api/jebl/evolution/trees/class-use/AbstractRootedTree.html
-rw-r--r--  root/root   /usr/share/doc/libjebl2-java/api/script-dir/jquery-3.6.1.min.js
-rw-r--r--  root/root   /usr/share/java/jebl-0.1+git20230701.b3c0f25.jar
lrwxrwxrwx  root/root   /usr/share/java/jebl.jar -> jebl-0.1+git20230701.b3c0f25.jar

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/doc/libjebl2-java/api/jebl/evolution/trees/ReRootedTree.RootingType.html
-rw-r--r--  root/root   /usr/share/doc/libjebl2-java/api/jebl/evolution/trees/class-use/ReRootedTree.RootingType.html
-rw-r--r--  root/root   /usr/share/doc/libjebl2-java/api/script-dir/jquery-3.6.0.min.js
-rw-r--r--  root/root   /usr/share/java/jebl-0.1+git20201011.969bd4b.jar
lrwxrwxrwx  root/root   /usr/share/java/jebl.jar -> jebl-0.1+git20201011.969bd4b.jar

No differences were encountered between the control files of package libjebl2-java

No differences were encountered between the control files of package libjebl2-java-doc

More details

Full run details