This repository contains a Python implementation of the Displacement-Optimized (DO) Tanglegram algorithm for comparing phylogenetic trees.
The implementation was developed by Mathis Lorenzen as part of his Bachelor thesis in Bioinformatics under the supervision of Daniel H. Huson. It is an independent re-implementation of the published algorithm and is not a direct translation of the original Java source code.
The software is based on the algorithm described in:
Huson DH. Displacement-Optimized Tanglegrams for Trees and Networks. Molecular Biology and Evolution. 2026;43(3):msag066. https://doi.org/10.1093/molbev/msag066
While the original publication presents a general framework for displacement-optimized tanglegrams for both trees and phylogenetic networks, this implementation focuses specifically on the tree case. In addition, it extends the original algorithm by supporting many-to-many correspondences between leaves of the two input trees, allowing more general mappings than the one-to-one taxon correspondences considered in the original formulation.
The goal of the project is to provide a clear, accessible, and extensible Python implementation that can serve both as a practical tool for phylogenetic tree comparison and as a basis for further research on tanglegram optimization and visualization.
If you use this software in scientific work, please cite the original publication describing the displacement-optimized tanglegram algorithm.
pip install dotanglegramThis installs both the library as well as the CLI tool into your environment.
First import the Tanglegram class from the dotanglegram library.
from dotanglegram import TanglegramWrap your code in a main() function and add an entry point:
def main():
# your code goes here
if __name__ == "__main__":
main()This is required because dotanglegram uses Python's multiprocessing module. On platforms that use the spawn start method (such as Windows and macOS), each child process starts a new Python interpreter and imports the main module. Without the if __name__ == "__main__": guard, the module would be re-imported recursively, causing an infinite process creation loop.
Next create a Tanglegram instance.
tanglegram = Tanglegram(
tree1_newick,
tree2_newick,
mapping=None,
)tree1_newick (str, required):
The first tree in Newick format.
tree2_newick (str, required):
The second tree in Newick format.
The extended Newick format is not supported. Any comments in square brackets ([...]) will be removed from the reordered Newick strings. Both trees may contain duplicate taxa and unresolved nodes.
mapping (list[tuple[str, str]], optional):
Defines a mapping between taxa in the two trees. Each tuple must have the form:
(tree1-taxon-name, tree2-taxon-name)
Every taxon in the first tree with the specified name is connected to every taxon with the corresponding name in the second tree.
Every tuple that contains names not present in their respective tree is ignored. If none of the tuples are valid or if no mapping is provided, a mapping is generated automatically by connecting taxa with identical names.
You can now untangle the two trees:
tanglegram.untangle(
use_nn=True,
runs=32,
processes=5,
fix_tree=None,
objective="displacement"
)use_nn (bool, optional, default=True):
Whether to presort the trees using a nearest-neighbor circular ordering before performing the exhaustive optimization.
runs (int, optional, default=32):
The number of optimization runs to perform.
Each run starts from a different randomized ordering of the two trees. After all runs have completed, the ordering with the lowest number of crossings is returned.
The default value of 32 is usually sufficient to find a near-optimal ordering. Increasing this value may improve the result at the cost of longer computation times.
processes (int, optional, default=os.cpu_count()):
The number of worker processes used to perform the optimization runs.
The actual number of processes is the minimum of the specified value, the number of runs, and the number of available CPU cores.
If processes is set to 0 or a negative value, the specified value is ignored, and the number of processes is automatically set to the minimum of the number of runs and the number of available CPU cores.
fix_tree (int, optional, default=None):
Can be used to fix tree 1 or tree 2 and only reorder the other tree.
If only one tree is reordered, the nn presort step is skipped.
objective (str, optional, default="displacement"):
Can be used to change how the best result is chosen. Either "displacement" or "crossings"
If "displacement" is chosen, the optimal layout is determined by the smallest displacement. If "crossings" is chosen, the optimal layout is determined by the smallest number of crossings.
Each individual change in the ordering is evaluated based on the taxon displacement inside each run. This decision is made among the results of all runs.
Once the optimization is finished, retrieve the result:
result = tanglegram.get_result()This returns a TanglegramResult object with the following fields:
tree1_newick(str) - the reordered first tree in Newick formattree2_newick(str) - the reordered second tree in Newick formatmapping(list[tuple[str, str]]) - the mapping between taxa used for the optimizationdisplacement(int) - the total taxon displacement of the final orderingcrossings(int) - the number of crossings of the final ordering
These fields can be accessed directly as object attributes:
t1 = result.tree1_newick
...
cx = result.crossingsYou can also generate an SVG visualization of the tanglegram.
Before using this feature, make sure you have R 4.5 or later installed, along with phytools 2.5-2 or later.
If you encounter issues related to R in combination with rpy2, please refer to the official rpy2 documentation.
tanglegram.plot(
output_path="plot.svg",
width=7,
height=7,
)output_path (str, required):
The path to the output SVG file.
width (int, optional, default=7):
The width of the generated SVG in inches.
height (int, optional, default=7):
The height of the generated SVG in inches.
The package also installs the dotanglegram command-line tool.
The simplest usage is:
dotanglegram tree1.nwk tree2.nwk -v tanglegram.svgThis reads the two trees, untangles them, and plots the tanglegram to the SVG file.
tree1 (str, required):
Path to the first tree in Newick format.
tree2 (str, required):
Path to the second tree in Newick format.
-m, --mapping (str, optional):
Path to a mapping file. The file should contain one mapping per line in the format:
tree1_taxon tree2_taxon
-n, --nn (bool, optional, default=True):
Whether to presort the trees using a nearest-neighbor circular ordering.
-r, --runs (int, optional, default=32):
The number of optimization runs to perform.
-p, --processes (int, optional, default=os.cpu_count()):
The number of worker processes used to perform the optimization runs.
-f, --fix (int, optional, default=None):
Keep the given tree fixed and only reorder the other one.
--objective (str, optional, default="displacement"):
Objective to optimize. This is only used to determine the optimal result among all runs, the optimization itself always uses displacement as objective function.
-o, --output (str, optional):
Write the result as a JSON file.
The generated JSON contains the following fields:
tree1_newick(str) - the reordered first tree in Newick formattree2_newick(str) - the reordered second tree in Newick formatmapping(list[list[str]]) - the mapping used during the optimizationdisplacement(int) - the total taxon displacement of the final orderingcrossings(int) - the number of crossings in the final ordering
-v, --visualize (str, optional):
Generate an SVG visualization of the final tanglegram. The argument specifies the output path of the SVG file.
Before using this feature, make sure you have R 4.5 or later installed, along with ape 5.7 or later and phytools 2.5-2 or later.
If you encounter issues related to R in combination with rpy2, please refer to the official rpy2 documentation.
--height (int, optional, default=7):
The height in inches of the generated SVG.
--width (int, optional, default=7):
The width in inches of the generated SVG.
from dotanglegram import Tanglegram
def main():
tree1_newick = "(A,B,(C,D));"
tree2_newick = "((B,A),(C,D));"
mapping = [("A", "D"),
("A", "C"),
("B", "B"),
("C", "A"),
("D", "A")
]
tanglegram = Tanglegram(tree1_newick, tree2_newick, mapping)
tanglegram.untangle()
tanglegram.plot("tanglegram.svg")
result = tanglegram.get_result()
print(f"TD={result.displacement}, CX={result.crossings}")
if __name__ == "__main__":
main()dotanglegram \
tree1.nwk \
tree2.nwk \
--mapping mapping.txt \
--runs 50 \
--processes 8 \
--output result.json \
--visualize tanglegram.svg \
--height 10 \
--width 10You can also use dotanglegram from R via the reticulate package. The following example is equivalent to the Python example:
library(reticulate)
dotanglegram <- import("dotanglegram")
tree1 <- "(A,B,(C,D));"
tree2 <- "((B,A),(C,D));"
mapping <- list(
c("A", "D"),
c("A", "C"),
c("B", "B"),
c("C", "A"),
c("D", "A")
)
tanglegram <- dotanglegram$Tanglegram(tree1, tree2, mapping)
tanglegram$untangle()
tanglegram$plot("tanglegram.svg")
result <- tanglegram$get_result()
cat(sprintf("TD=%s, CX=%s\n", result$displacement, result$crossings))If you use the plot() function, make sure you have phytools 2.5-2 or later installed.
When passing integer arguments (for example, runs, processes, width, or height), wrap them in as.integer() to ensure they are passed to Python as integers:
tanglegram$untangle(
runs = as.integer(30),
processes = as.integer(8)
)
tanglegram$plot(
"tanglegram.svg",
width = as.integer(10),
height = as.integer(8)
)