Parsing and drawing networks/admixture

Network inference, or the inference of admixture events on a phylogenetic tree, is becoming increasingly common in evolutionary biology. Toytree provides some tools for parsing and drawing networks, with aim of supporting the analysis of networks inferred by tools like SNAQ, or to describe hypotheses for coalescent simulations in tools like ipcoal.

In terms of how networks are drawn in toytree, we take the approach of assuming that a primary tree exists, which is drawn like a normal toytree, but with minor edges added to the tree as admixture edges.

[1]:
import toytree

Quick example

The goal is to provide a simple, clear, and customizable option for drawing admixture edges on a tree. The simplest method is to provide a (source, destination) tuple to the admixture_edges argument in draw. Source in this context should be viewed backwards in time. The default style of drawing admixture edges in toytree tries to make the direction of introgression explicity by showing the inheritance of admixture towards the tips that inherit the introgressed alleles.

Multiple tuples can be provided to draw multiple admixture edges. The source and destination positions can be described either using node idx labels, or a list of tip names descendant from a node (preferred method since it is not affected by re-rooting the tree). Examples:

[2]:
# get a random tree with 10 tips
tree1 = toytree.rtree.unittree(ntips=10, seed=123)
[3]:
# draw tree with admixture from node 2 to 3
tree1.draw(ts='s', admixture_edges=(2, 3));
0123456789101112131415161718r0r1r2r3r4r5r6r7r8r9
[4]:
# draw tree with admixture from tip r2 to ancestor of r4,r5
tree1.draw(ts='s', admixture_edges=('r2', ['r4', 'r5']));
0123456789101112131415161718r0r1r2r3r4r5r6r7r8r9

Styling admixture edges

The tuple argument to admixture_edges should be a tuple with up to five elements in it. Only the first two are required (source, dest), and any additional elements are used to add style options.

  1. Source
  2. Destination
  3. Admixture timing
  4. Admix edge style dictionary
  5. Label
[28]:
# draw tree with admixture from tip r2 to ancestor of r4,r5
tree1.draw(
    ts='c',
    tip_labels=True,
    admixture_edges=[
        ('r2', 'r4', 0.5, {'stroke': 'blue', 'stroke-opacity': 0.3}, "0.32")
    ],
);
0.32r0r1r2r3r4r5r6r7r8r90.00.51.0

Admixture timing

The third element is the admixture timing. This can be either a single float value (e.g., 0.5) or a tuple of float values (e.g., (0.2, 0.5)). These values describe the proportion of the length of an edge from tip toward the root. If a single value is used it will be the proportion of the shared edge (if one exists).

[53]:
# draw admixture at 0.5 height of the shared edge (default option)
tree1.draw(ts='c', tip_labels=True, admixture_edges=(2, 3, 0.5));
r0r1r2r3r4r5r6r7r8r90.00.51.0
[55]:
# draw admixture at 0.5 height of each edge separately
tree1.draw(ts='c', tip_labels=True, admixture_edges=(2, 3, (0.5, 0.5)));
r0r1r2r3r4r5r6r7r8r90.00.51.0
[57]:
# draw admixture at 0.2 and 0.8 heights, respectively
tree1.draw(ts='c', tip_labels=True, admixture_edges=(2, 3, (0.2, 0.8)));
r0r1r2r3r4r5r6r7r8r90.00.51.0
[67]:
# draw admixture between edges that do not overlap in time
tree1.draw(ts='c', tip_labels=True, admixture_edges=(1, 12, 0.5));
r0r1r2r3r4r5r6r7r8r90.00.51.0

Admixture edge style

You can style edges using CSS stroke style options.

[71]:
# draw admixture at 0.5 height of the shared edge (default option)
style = {'stroke': 'orange', 'stroke-width': 10, 'stroke-opacity': 0.4}
tree1.draw(ts='c', tip_labels=True, admixture_edges=(2, 3, 0.5, style));
r0r1r2r3r4r5r6r7r8r90.00.51.0

Label

The label is automatically placed near the admixture edge.

[81]:
# draw admixture at 0.5 height of the shared edge (default option)
tree1.draw(ts='c', tip_labels=True, admixture_edges=(2, 3, 0.5, {}, "admixture"));
admixturer0r1r2r3r4r5r6r7r8r90.00.51.0

Parsing SNAQ newick format

The hybrid newick format below contains the hybrid node “#H7”. Toytree can parse this format using toytree.utils.parse_network() which returns the major tree as a ToyTree object and the admixture drawing information as a tuple.

[82]:
# the example SNAQ network-1
hnewick = "(C,D,((O,(E,#H7:::0.196):0.314):0.664,(B,((A1,A2))#H7:::0.804):10.0):10.0);"
[83]:
# parse tree and admixture dict
tree, admix = toytree.utils.parse_network(hnewick)
[84]:
# the admix dictionary has key,val pairs where val is an admix drawing tuple
admix
[84]:
{'H7': (['A1', 'A2'], ['E'], 0.5, {}, '0.196')}
[85]:
# draw tree with tuple arg from admix dictionary
tree.draw(ts='s', admixture_edges=admix.values());
0.19601234567891011A2A1BEODC

Works with re-rooting

The direction of introgression, E->A forward in time, is still preserved after re-rooting the tree.

[88]:
tree.root(wildcard="A").draw(ts='s', admixture_edges=admix.values());
0.1960123456789101112DCEOBA2A1

Muliple admixture arguments

You can provide multiple admixture edge tuples as a list. If no style argument is provided then they are automatically cycled through sequential color palette like in the example below. Or, the next example you can provide explicity styles, including low opacity to overlay many edges as a way of showing variation among many analyses.

[93]:
# draw tree with admixture from tip r2 to ancestor of r4,r5
tree1.draw(
    ts='c',
    tip_labels=True,
    admixture_edges=[
        (0, 2), (3, 4), (6, 7)
    ]
);
r0r1r2r3r4r5r6r7r8r90.00.51.0
[96]:
import numpy as np

# generate list of 100 admixture tuples w/ random timing
admix = []
for i in range(100):
    src = 2
    dest = 3
    time = np.random.normal(0.5, 0.2)
    style = {'stroke': 'blue', 'stroke-opacity': 0.01}
    tup = (src, dest, time, style)
    admix.append(tup)

# draw tree with admixture from tip r2 to ancestor of r4,r5
tree1.draw(
    ts='c',
    tip_labels=True,
    admixture_edges=admix
);
r0r1r2r3r4r5r6r7r8r90.00.51.0

Parsing networks extended

If you want to see the actual placement of the hybrid nodes in a network you can parse the newick string with the argument disconnect=False, which will leave the hybrid nodes in the tree with ‘name’ labels indicating their connections. For example, in the network below you can see nodes labeled H7 that connect E to the ancestor of A.

[1]:
hnewick = "(C,D,((O,(E,#H7:::0.196):0.314):0.664,(B,((A1,A2))#H7:::0.804):10.0):10.0);"
[5]:
net, admix = toytree.utils.parse_network(hnewick, disconnect=False)
[7]:
net.draw(ts='s', node_labels="name");
7H7H7101112A2A1BEODC