iplotx 0.0.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ """
2
+ This module focuses on how to ingest network data into standard data structures no matter what library they come from.
3
+ """
4
+
5
+ import pathlib
6
+ import pkgutil
7
+ import importlib
8
+ import warnings
9
+ from typing import (
10
+ Optional,
11
+ Sequence,
12
+ Protocol,
13
+ )
14
+ from collections.abc import Hashable
15
+ import pandas as pd
16
+
17
+ from ..typing import (
18
+ GraphType,
19
+ LayoutType,
20
+ TreeType,
21
+ )
22
+ from .typing import (
23
+ NetworkDataProvider,
24
+ NetworkData,
25
+ TreeDataProvider,
26
+ TreeData,
27
+ )
28
+
29
+ provider_protocols = {
30
+ "network": NetworkDataProvider,
31
+ "tree": TreeDataProvider,
32
+ }
33
+
34
+ # Internally supported data providers
35
+ data_providers: dict[str, dict[str, Protocol]] = {
36
+ kind: {} for kind in provider_protocols
37
+ }
38
+ for kind in data_providers:
39
+ providers_path = pathlib.Path(__file__).parent.joinpath("providers").joinpath(kind)
40
+ for importer, module_name, _ in pkgutil.iter_modules([providers_path]):
41
+ module = importlib.import_module(
42
+ f"iplotx.ingest.providers.{kind}.{module_name}"
43
+ )
44
+ for key, val in module.__dict__.items():
45
+ if key == provider_protocols[kind].__name__:
46
+ continue
47
+ if key.endswith("DataProvider"):
48
+ data_providers[kind][module_name] = val()
49
+ break
50
+ del providers_path
51
+
52
+
53
+ def network_library(network) -> str:
54
+ """Guess the network library used to create the network."""
55
+ for name, provider in data_providers["network"].items():
56
+ if provider.check_dependencies():
57
+ graph_type = provider.graph_type()
58
+ if isinstance(network, graph_type):
59
+ return name
60
+ raise ValueError(
61
+ f"Network {network} did not match any available network library.",
62
+ )
63
+
64
+
65
+ def tree_library(tree) -> str:
66
+ """Guess the tree library used to create the tree."""
67
+ for name, provider in data_providers["tree"].items():
68
+ if provider.check_dependencies():
69
+ tree_type = provider.tree_type()
70
+ if isinstance(tree, tree_type):
71
+ return name
72
+ raise ValueError(
73
+ f"Tree {tree} did not match any available tree library.",
74
+ )
75
+
76
+
77
+ # Functions to ingest data from various libraries
78
+ def ingest_network_data(
79
+ network: GraphType,
80
+ layout: Optional[LayoutType] = None,
81
+ vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
82
+ edge_labels: Optional[Sequence[str] | dict[str,]] = None,
83
+ ) -> NetworkData:
84
+ """Create internal data for the network."""
85
+ _update_data_providers("network")
86
+
87
+ nl = network_library(network)
88
+
89
+ if nl in data_providers["network"]:
90
+ provider: NetworkDataProvider = data_providers["network"][nl]
91
+ else:
92
+ sup = ", ".join(data_providers["network"].keys())
93
+ raise ValueError(
94
+ f"Network library '{nl}' is not installed. "
95
+ f"Currently installed supported libraries: {sup}."
96
+ )
97
+
98
+ result = provider(
99
+ network=network,
100
+ layout=layout,
101
+ vertex_labels=vertex_labels,
102
+ edge_labels=edge_labels,
103
+ )
104
+ result["network_library"] = nl
105
+ return result
106
+
107
+
108
+ def ingest_tree_data(
109
+ tree: TreeType,
110
+ layout: Optional[str] = "horizontal",
111
+ orientation: Optional[str] = "right",
112
+ directed: bool | str = False,
113
+ vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
114
+ edge_labels: Optional[Sequence[str] | dict[str,]] = None,
115
+ ) -> TreeData:
116
+ """Create internal data for the tree."""
117
+ _update_data_providers("tree")
118
+
119
+ tl = tree_library(tree)
120
+
121
+ if tl in data_providers["tree"]:
122
+ provider: TreeDataProvider = data_providers["tree"][tl]
123
+ else:
124
+ sup = ", ".join(data_providers["tree"].keys())
125
+ raise ValueError(
126
+ f"Tree library '{tl}' is not installed. "
127
+ f"Currently installed supported libraries: {sup}."
128
+ )
129
+
130
+ result = provider(
131
+ tree=tree,
132
+ layout=layout,
133
+ orientation=orientation,
134
+ directed=directed,
135
+ vertex_labels=vertex_labels,
136
+ edge_labels=edge_labels,
137
+ )
138
+ result["tree_library"] = tl
139
+ return result
140
+
141
+
142
+ # INTERNAL FUNCTIONS
143
+ def _update_data_providers(kind):
144
+ """Update data provieders dynamically from external packages."""
145
+ discovered_providers = importlib.metadata.entry_points(
146
+ group=f"iplotx.{kind}_data_providers"
147
+ )
148
+ for entry_point in discovered_providers:
149
+ if entry_point.name not in data_providers["network"]:
150
+ try:
151
+ data_providers[kind][entry_point.name] = entry_point.load()
152
+ except Exception as e:
153
+ warnings.warn(
154
+ f"Failed to load {kind} data provider '{entry_point.name}': {e}"
155
+ )
@@ -0,0 +1,209 @@
1
+ """
2
+ Heuristics module to funnel certain variable inputs (e.g. layouts) into a standard format.
3
+ """
4
+
5
+ from typing import (
6
+ Optional,
7
+ Any,
8
+ )
9
+ from collections.abc import Hashable
10
+ from collections import defaultdict
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ from ..layout import compute_tree_layout
15
+ from ..typing import (
16
+ GraphType,
17
+ GroupingType,
18
+ TreeType,
19
+ LayoutType,
20
+ )
21
+
22
+
23
+ def number_of_vertices(network: GraphType) -> int:
24
+ """Get the number of vertices in the network."""
25
+ from . import network_library
26
+
27
+ if network_library(network) == "igraph":
28
+ return network.vcount()
29
+ if network_library(network) == "networkx":
30
+ return network.number_of_nodes()
31
+ raise TypeError("Unsupported graph type. Supported types are igraph and networkx.")
32
+
33
+
34
+ def detect_directedness(
35
+ network: GraphType,
36
+ ) -> bool:
37
+ """Detect if the network is directed or not."""
38
+ from . import network_library
39
+
40
+ nl = network_library(network)
41
+
42
+ if nl == "igraph":
43
+ return network.is_directed()
44
+ if nl == "networkx":
45
+ import networkx as nx
46
+
47
+ if isinstance(network, (nx.DiGraph, nx.MultiDiGraph)):
48
+ return True
49
+ return False
50
+
51
+
52
+ def normalise_layout(layout, network=None):
53
+ """Normalise the layout to a pandas.DataFrame."""
54
+ from . import network_library
55
+
56
+ try:
57
+ import igraph as ig
58
+ except ImportError:
59
+ ig = None
60
+
61
+ if layout is None:
62
+ if (network is not None) and (number_of_vertices(network) == 0):
63
+ return pd.DataFrame(np.zeros((0, 2)))
64
+ return None
65
+ if (network is not None) and isinstance(layout, str):
66
+ if network_library(network) == "igraph":
67
+ if hasattr(network, layout):
68
+ layout = network[layout]
69
+ else:
70
+ layout = network.layout(layout)
71
+ # NOTE: This seems like a legit bug in igraph
72
+ # Sometimes (e.g. sugiyama) the layout has more vertices than the network (?)
73
+ layout = np.asarray(layout.coords)[: network.vcount()]
74
+ if network_library(network) == "networkx":
75
+ layout = dict(network.nodes.data(layout))
76
+
77
+ if (ig is not None) and isinstance(layout, ig.layout.Layout):
78
+ return pd.DataFrame(layout.coords)
79
+ if isinstance(layout, dict):
80
+ return pd.DataFrame(layout).T
81
+ if isinstance(layout, str):
82
+ raise NotImplementedError("Layout as a string is not supported yet.")
83
+ if isinstance(layout, (list, tuple)):
84
+ return pd.DataFrame(np.array(layout))
85
+ if isinstance(layout, pd.DataFrame):
86
+ return layout
87
+ if isinstance(layout, np.ndarray):
88
+ return pd.DataFrame(layout)
89
+ raise TypeError("Layout could not be normalised.")
90
+
91
+
92
+ def normalise_tree_layout(
93
+ layout: str | Any,
94
+ tree: Optional[TreeType] = None,
95
+ **kwargs,
96
+ ) -> pd.DataFrame:
97
+ """Normalise tree layout from a variety of inputs.
98
+
99
+ Parameters:
100
+ layout: The tree layout to normalise.
101
+ tree: The correcponding tree object.
102
+ **kwargs: Additional arguments for the subroutines.
103
+
104
+ Returns:
105
+ A pandas DataFrame with the normalised tree layout.
106
+
107
+ NOTE: This function currently only accepts strings and computes
108
+ the layout internally. This might change in the future.
109
+ """
110
+ if isinstance(layout, str):
111
+ layout = compute_tree_layout(tree, layout, **kwargs)
112
+ else:
113
+ raise NotImplementedError(
114
+ "Only internally computed tree layout currently accepted."
115
+ )
116
+
117
+ if isinstance(layout, dict):
118
+ # Adjust vertex layout
119
+ index = []
120
+ coordinates = []
121
+ for key, coordinate in layout.items():
122
+ index.append(key)
123
+ coordinates.append(coordinate)
124
+ index = pd.Index(index)
125
+ coordinates = np.array(coordinates)
126
+ ndim = len(coordinates[0]) if len(coordinates) > 0 else 2
127
+ layout_columns = [f"_ipx_layout_{i}" for i in range(ndim)]
128
+ layout = pd.DataFrame(
129
+ coordinates,
130
+ index=index,
131
+ columns=layout_columns,
132
+ )
133
+
134
+ return layout
135
+
136
+
137
+ def normalise_grouping(
138
+ grouping: GroupingType,
139
+ layout: LayoutType,
140
+ ) -> dict[Hashable, set]:
141
+ """Normalise network grouping from a variery of inputs.
142
+
143
+ Parameters:
144
+ grouping: Network grouping (e.g. vertex cover).
145
+ layout: Network layout.
146
+
147
+ Returns:
148
+ A dictionary of sets. Each key is the index of a group, each value is a set of vertices
149
+ included in that group. If all sets are mutually exclusive, this is a vertex clustering,
150
+ otherwise it's only a vertex cover.
151
+ """
152
+ try:
153
+ import igraph as ig
154
+ except ImportError:
155
+ ig = None
156
+
157
+ if len(grouping) == 0:
158
+ return {}
159
+
160
+ if isinstance(grouping, dict):
161
+ val0 = next(iter(grouping.values()))
162
+ # If already the right data type or compatible, leave as is
163
+ if isinstance(val0, (set, frozenset)):
164
+ return grouping
165
+
166
+ # If a dict of integers or strings, assume each key is a vertex id and each value is a
167
+ # group, convert (i.e. invert the dict)
168
+ if isinstance(val0, (int, str)):
169
+ group_dic = defaultdict(set)
170
+ for key, val in grouping.items():
171
+ group_dic[val].add(key)
172
+ return group_dic
173
+
174
+ # If an igraph object, convert to a dict of sets
175
+ if ig is not None:
176
+ if isinstance(grouping, ig.clustering.Clustering):
177
+ layout = normalise_layout(layout)
178
+ group_dic = defaultdict(set)
179
+ for i, member in enumerate(grouping.membership):
180
+ group_dic[member].add(i)
181
+ return group_dic
182
+
183
+ if isinstance(grouping, ig.clustering.Cover):
184
+ layout = normalise_layout(layout)
185
+ group_dic = defaultdict(set)
186
+ for i, members in enumerate(grouping.membership):
187
+ for member in members:
188
+ group_dic[member].add(i)
189
+ return group_dic
190
+
191
+ # Assume it's a sequence, so convert to list
192
+ grouping = list(grouping)
193
+
194
+ # If the values are already sets, assume group indices are integers
195
+ # and values are as is
196
+ if isinstance(grouping[0], set):
197
+ return dict(enumerate(grouping))
198
+
199
+ # If the values are integers or strings, assume each key is a vertex id and each value is a
200
+ # group, convert to dict of sets
201
+ if isinstance(grouping[0], (int, str)):
202
+ group_dic = defaultdict(set)
203
+ for i, val in enumerate(grouping):
204
+ group_dic[val].add(i)
205
+ return group_dic
206
+
207
+ raise TypeError(
208
+ "Could not standardise grouping from object.",
209
+ )
@@ -0,0 +1,96 @@
1
+ from typing import (
2
+ Optional,
3
+ Sequence,
4
+ )
5
+ from collections.abc import Hashable
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ from ....typing import (
10
+ GraphType,
11
+ LayoutType,
12
+ )
13
+ from ...heuristics import (
14
+ normalise_layout,
15
+ detect_directedness,
16
+ )
17
+ from ...typing import (
18
+ NetworkDataProvider,
19
+ NetworkData,
20
+ )
21
+ from ....utils.internal import (
22
+ _make_layout_columns,
23
+ )
24
+
25
+
26
+ class IGraphDataProvider(NetworkDataProvider):
27
+ def __call__(
28
+ self,
29
+ network: GraphType,
30
+ layout: Optional[LayoutType] = None,
31
+ vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
32
+ edge_labels: Optional[Sequence[str] | dict[str]] = None,
33
+ ) -> NetworkData:
34
+ """Create network data object for iplotx from any provider."""
35
+
36
+ directed = detect_directedness(network)
37
+
38
+ # Recast vertex_labels=False as vertex_labels=None
39
+ if np.isscalar(vertex_labels) and (not vertex_labels):
40
+ vertex_labels = None
41
+
42
+ # Vertices are ordered integers, no gaps
43
+ vertex_df = normalise_layout(layout, network=network)
44
+ ndim = vertex_df.shape[1]
45
+ vertex_df.columns = _make_layout_columns(ndim)
46
+
47
+ # Vertex labels
48
+ if vertex_labels is not None:
49
+ if np.isscalar(vertex_labels):
50
+ vertex_df["label"] = vertex_df.index.astype(str)
51
+ elif len(vertex_labels) != len(vertex_df):
52
+ raise ValueError(
53
+ "Vertex labels must be the same length as the number of vertices."
54
+ )
55
+ else:
56
+ vertex_df["label"] = vertex_labels
57
+
58
+ # Edges are a list of tuples, because of multiedges
59
+ tmp = []
60
+ for edge in network.es:
61
+ row = {"_ipx_source": edge.source, "_ipx_target": edge.target}
62
+ row.update(edge.attributes())
63
+ tmp.append(row)
64
+ if len(tmp):
65
+ edge_df = pd.DataFrame(tmp)
66
+ else:
67
+ edge_df = pd.DataFrame(columns=["_ipx_source", "_ipx_target"])
68
+ del tmp
69
+
70
+ # Edge labels
71
+ if edge_labels is not None:
72
+ if len(edge_labels) != len(edge_df):
73
+ raise ValueError(
74
+ "Edge labels must be the same length as the number of edges."
75
+ )
76
+ edge_df["label"] = edge_labels
77
+
78
+ network_data = {
79
+ "vertex_df": vertex_df,
80
+ "edge_df": edge_df,
81
+ "directed": directed,
82
+ "ndim": ndim,
83
+ }
84
+ return network_data
85
+
86
+ def check_dependencies(self) -> bool:
87
+ try:
88
+ import igraph
89
+ except ImportError:
90
+ return False
91
+ return True
92
+
93
+ def graph_type(self):
94
+ import igraph as ig
95
+
96
+ return ig.Graph
@@ -0,0 +1,133 @@
1
+ from typing import (
2
+ Optional,
3
+ Sequence,
4
+ )
5
+ from collections.abc import Hashable
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ from ....typing import (
10
+ GraphType,
11
+ LayoutType,
12
+ )
13
+ from ...heuristics import (
14
+ normalise_layout,
15
+ detect_directedness,
16
+ )
17
+ from ...typing import (
18
+ NetworkDataProvider,
19
+ NetworkData,
20
+ )
21
+ from ....utils.internal import (
22
+ _make_layout_columns,
23
+ )
24
+
25
+
26
+ class NetworkXDataProvider(NetworkDataProvider):
27
+ def __call__(
28
+ self,
29
+ network: GraphType,
30
+ layout: Optional[LayoutType] = None,
31
+ vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
32
+ edge_labels: Optional[Sequence[str] | dict[str]] = None,
33
+ ) -> NetworkData:
34
+ """Create network data object for iplotx from any provider."""
35
+
36
+ import networkx as nx
37
+
38
+ directed = detect_directedness(network)
39
+
40
+ # Recast vertex_labels=False as vertex_labels=None
41
+ if np.isscalar(vertex_labels) and (not vertex_labels):
42
+ vertex_labels = None
43
+
44
+ # Vertices are indexed by node ID
45
+ vertex_df = normalise_layout(
46
+ layout,
47
+ network=network,
48
+ ).loc[pd.Index(network.nodes)]
49
+ ndim = vertex_df.shape[1]
50
+ vertex_df.columns = _make_layout_columns(ndim)
51
+
52
+ # Vertex internal properties
53
+ tmp = pd.DataFrame(dict(network.nodes.data())).T
54
+ # Arrays become a single column, which we have already anyway
55
+ if isinstance(layout, str) and (layout in tmp.columns):
56
+ del tmp[layout]
57
+ for col in tmp.columns:
58
+ vertex_df[col] = tmp[col]
59
+ del tmp
60
+
61
+ # Vertex labels
62
+ if vertex_labels is None:
63
+ if "label" in vertex_df:
64
+ del vertex_df["label"]
65
+ else:
66
+ if (
67
+ np.isscalar(vertex_labels)
68
+ and (not vertex_labels)
69
+ and ("label" in vertex_df)
70
+ ):
71
+ del vertex_df["label"]
72
+ elif vertex_labels is True:
73
+ if "label" not in vertex_df:
74
+ vertex_df["label"] = vertex_df.index
75
+ elif (not np.isscalar(vertex_labels)) and (
76
+ len(vertex_labels) != len(vertex_df)
77
+ ):
78
+ raise ValueError(
79
+ "Vertex labels must be the same length as the number of vertices."
80
+ )
81
+ elif isinstance(vertex_labels, nx.classes.reportviews.NodeDataView):
82
+ vertex_df["label"] = pd.Series(dict(vertex_labels))
83
+ else:
84
+ vertex_df["label"] = vertex_labels
85
+
86
+ # Edges are a list of tuples, because of multiedges
87
+ tmp = []
88
+ for u, v, d in network.edges.data():
89
+ row = {"_ipx_source": u, "_ipx_target": v}
90
+ row.update(d)
91
+ tmp.append(row)
92
+ if len(tmp):
93
+ edge_df = pd.DataFrame(tmp)
94
+ else:
95
+ edge_df = pd.DataFrame(columns=["_ipx_source", "_ipx_target"])
96
+ del tmp
97
+
98
+ # Edge labels
99
+ # Even though they could exist in the dataframe, request the to be explicitely mentioned
100
+ if (edge_labels is None) and ("label" in edge_df):
101
+ del edge_df["label"]
102
+ elif edge_labels is not None:
103
+ if np.isscalar(edge_labels):
104
+ if (not edge_labels) and ("label" in edge_df):
105
+ del edge_df["label"]
106
+ if (edge_labels is True) and ("label" not in edge_df):
107
+ edge_df["label"] = [str(i) for i in edge_df.index]
108
+ else:
109
+ if len(edge_labels) != len(edge_df):
110
+ raise ValueError(
111
+ "Edge labels must be the same length as the number of edges."
112
+ )
113
+ edge_df["label"] = edge_labels
114
+
115
+ network_data = {
116
+ "vertex_df": vertex_df,
117
+ "edge_df": edge_df,
118
+ "directed": directed,
119
+ "ndim": ndim,
120
+ }
121
+ return network_data
122
+
123
+ def check_dependencies(self) -> bool:
124
+ try:
125
+ import networkx
126
+ except ImportError:
127
+ return False
128
+ return True
129
+
130
+ def graph_type(self):
131
+ from networkx import Graph
132
+
133
+ return Graph
@@ -0,0 +1,105 @@
1
+ from typing import (
2
+ Optional,
3
+ Sequence,
4
+ )
5
+ from collections.abc import Hashable
6
+ from operator import attrgetter
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ from ....typing import (
11
+ TreeType,
12
+ LayoutType,
13
+ )
14
+ from ...typing import (
15
+ TreeDataProvider,
16
+ TreeData,
17
+ )
18
+ from ...heuristics import (
19
+ normalise_tree_layout,
20
+ )
21
+
22
+
23
+ class BiopythonDataProvider(TreeDataProvider):
24
+ def __call__(
25
+ self,
26
+ tree: TreeType,
27
+ layout: str | LayoutType,
28
+ orientation: str = "horizontal",
29
+ directed: bool | str = False,
30
+ vertex_labels: Optional[
31
+ Sequence[str] | dict[Hashable, str] | pd.Series | bool
32
+ ] = None,
33
+ edge_labels: Optional[Sequence[str] | dict] = None,
34
+ ) -> TreeData:
35
+ """Create tree data object for iplotx from BioPython.Phylo.Tree classes."""
36
+
37
+ tree_data = {
38
+ "root": tree.root,
39
+ "leaves": tree.get_terminals(),
40
+ "rooted": tree.rooted,
41
+ "directed": directed,
42
+ "ndim": 2,
43
+ "layout_name": layout,
44
+ }
45
+
46
+ # Add vertex_df including layout
47
+ tree_data["vertex_df"] = normalise_tree_layout(
48
+ layout,
49
+ tree=tree,
50
+ orientation=orientation,
51
+ root_fun=attrgetter("root"),
52
+ preorder_fun=lambda tree: tree.find_clades(order="preorder"),
53
+ postorder_fun=lambda tree: tree.find_clades(order="postorder"),
54
+ children_fun=attrgetter("clades"),
55
+ branch_length_fun=attrgetter("branch_length"),
56
+ )
57
+ if layout in ("radial",):
58
+ tree_data["layout_coordinate_system"] = "polar"
59
+ else:
60
+ tree_data["layout_coordinate_system"] = "cartesian"
61
+
62
+ # Add edge_df
63
+ edge_data = {"_ipx_source": [], "_ipx_target": []}
64
+ for node in tree.find_clades(order="preorder"):
65
+ for child in node.clades:
66
+ if directed == "parent":
67
+ edge_data["_ipx_source"].append(child)
68
+ edge_data["_ipx_target"].append(node)
69
+ else:
70
+ edge_data["_ipx_source"].append(node)
71
+ edge_data["_ipx_target"].append(child)
72
+ edge_df = pd.DataFrame(edge_data)
73
+ tree_data["edge_df"] = edge_df
74
+
75
+ # Add vertex labels
76
+ if vertex_labels is None:
77
+ vertex_labels = False
78
+ if np.isscalar(vertex_labels) and vertex_labels:
79
+ tree_data["vertex_df"]["label"] = [
80
+ x.name for x in tree_data["vertices"].index
81
+ ]
82
+ elif not np.isscalar(vertex_labels):
83
+ # If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
84
+ # we fill the rest with empty strings which are not going to show up in the plot.
85
+ if isinstance(vertex_labels, pd.Series):
86
+ vertex_labels = dict(vertex_labels)
87
+ if isinstance(vertex_labels, dict):
88
+ for vertex in tree_data["vertex_df"].index:
89
+ if vertex not in vertex_labels:
90
+ vertex_labels[vertex] = ""
91
+ tree_data["vertex_df"]["label"] = pd.Series(vertex_labels)
92
+
93
+ return tree_data
94
+
95
+ def check_dependencies(self) -> bool:
96
+ try:
97
+ from Bio import Phylo
98
+ except ImportError:
99
+ return False
100
+ return True
101
+
102
+ def tree_type(self):
103
+ from Bio import Phylo
104
+
105
+ return Phylo.BaseTree.Tree