LineageTree 1.6.1__py3-none-any.whl → 1.8.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.
- LineageTree/__init__.py +1 -1
- LineageTree/lineageTree.py +274 -273
- LineageTree/lineageTreeManager.py +50 -23
- LineageTree/loaders.py +284 -42
- LineageTree/tree_styles.py +99 -66
- LineageTree/utils.py +7 -11
- {LineageTree-1.6.1.dist-info → LineageTree-1.8.0.dist-info}/METADATA +11 -10
- LineageTree-1.8.0.dist-info/RECORD +11 -0
- {LineageTree-1.6.1.dist-info → LineageTree-1.8.0.dist-info}/WHEEL +1 -1
- LineageTree-1.6.1.dist-info/RECORD +0 -11
- {LineageTree-1.6.1.dist-info → LineageTree-1.8.0.dist-info}/LICENSE +0 -0
- {LineageTree-1.6.1.dist-info → LineageTree-1.8.0.dist-info}/top_level.txt +0 -0
LineageTree/lineageTree.py
CHANGED
@@ -20,7 +20,8 @@ try:
|
|
20
20
|
from edist import uted
|
21
21
|
except ImportError:
|
22
22
|
warnings.warn(
|
23
|
-
"No edist installed therefore you will not be able to compute the tree edit distance."
|
23
|
+
"No edist installed therefore you will not be able to compute the tree edit distance.",
|
24
|
+
stacklevel=2,
|
24
25
|
)
|
25
26
|
import matplotlib.pyplot as plt
|
26
27
|
import numpy as np
|
@@ -69,7 +70,12 @@ class lineageTree(lineageTreeLoaders):
|
|
69
70
|
sub = set(self.get_sub_tree(node))
|
70
71
|
specific_leaves = sub.intersection(self.leaves)
|
71
72
|
for leaf in specific_leaves:
|
72
|
-
self.add_branch(
|
73
|
+
self.add_branch(
|
74
|
+
leaf,
|
75
|
+
(self.t_e - self.time[leaf]),
|
76
|
+
reverse=True,
|
77
|
+
move_timepoints=True,
|
78
|
+
)
|
73
79
|
|
74
80
|
###TODO pos can be callable and stay motionless (copy the position of the succ node, use something like optical flow)
|
75
81
|
def add_branch(
|
@@ -87,8 +93,8 @@ class lineageTree(lineageTreeLoaders):
|
|
87
93
|
pred (int): Id of the successor (predecessor if reverse is False)
|
88
94
|
length (int): The length of the new branch.
|
89
95
|
pos (np.ndarray, optional): The new position of the branch. Defaults to None.
|
90
|
-
move_timepoints (bool): Moves the
|
91
|
-
reverese (bool): If
|
96
|
+
move_timepoints (bool): Moves the time, important only if reverse= True
|
97
|
+
reverese (bool): If True will create a branch that goes forwards in time otherwise backwards.
|
92
98
|
Returns:
|
93
99
|
(int): Id of the first node of the sublineage.
|
94
100
|
"""
|
@@ -129,19 +135,20 @@ class lineageTree(lineageTreeLoaders):
|
|
129
135
|
)
|
130
136
|
pred = _next
|
131
137
|
else:
|
132
|
-
for
|
138
|
+
for _ in range(length):
|
133
139
|
_next = self.add_node(
|
134
|
-
time +
|
140
|
+
self.time[pred] + 1,
|
141
|
+
succ=pred,
|
142
|
+
pos=self.pos[original],
|
143
|
+
reverse=False,
|
135
144
|
)
|
136
145
|
pred = _next
|
146
|
+
self.successor[self.get_cycle(pred)[-1]] = []
|
137
147
|
self.labels[pred] = "New branch"
|
138
148
|
if self.time[pred] == self.t_b:
|
139
|
-
self.roots.add(pred)
|
140
149
|
self.labels[pred] = "New branch"
|
141
150
|
if original in self.roots and reverse is True:
|
142
|
-
self.roots.add(pred)
|
143
151
|
self.labels[pred] = "New branch"
|
144
|
-
self.roots.remove(original)
|
145
152
|
self.labels.pop(original, -1)
|
146
153
|
self.t_e = max(self.time_nodes)
|
147
154
|
return pred
|
@@ -163,9 +170,7 @@ class lineageTree(lineageTreeLoaders):
|
|
163
170
|
self.predecessor.pop(new_lT)
|
164
171
|
label_of_root = self.labels.get(cycle[0], cycle[0])
|
165
172
|
self.labels[cycle[0]] = f"L-Split {label_of_root}"
|
166
|
-
new_tr = self.add_branch(
|
167
|
-
new_lT, len(cycle) + 1, move_timepoints=False
|
168
|
-
)
|
173
|
+
new_tr = self.add_branch(new_lT, len(cycle), move_timepoints=False)
|
169
174
|
self.roots.add(new_tr)
|
170
175
|
self.labels[new_tr] = f"R-Split {label_of_root}"
|
171
176
|
return new_tr
|
@@ -205,7 +210,10 @@ class lineageTree(lineageTreeLoaders):
|
|
205
210
|
self.remove_nodes(new_root1)
|
206
211
|
self.successor[new_root2].append(next_root1)
|
207
212
|
self.predecessor[next_root1] = [new_root2]
|
208
|
-
new_branch = self.add_branch(
|
213
|
+
new_branch = self.add_branch(
|
214
|
+
new_root2,
|
215
|
+
length - 1,
|
216
|
+
)
|
209
217
|
self.labels[new_branch] = f"Fusion of {new_root1} and {new_root2}"
|
210
218
|
return new_branch
|
211
219
|
|
@@ -317,7 +325,7 @@ class lineageTree(lineageTreeLoaders):
|
|
317
325
|
new_length (int): The new length of the tree.
|
318
326
|
"""
|
319
327
|
if new_length <= 1:
|
320
|
-
warnings.warn("New length should be more than 1")
|
328
|
+
warnings.warn("New length should be more than 1", stacklevel=2)
|
321
329
|
return None
|
322
330
|
cycle = self.get_cycle(node)
|
323
331
|
length = len(cycle)
|
@@ -325,7 +333,10 @@ class lineageTree(lineageTreeLoaders):
|
|
325
333
|
if length == 1 and new_length != 1:
|
326
334
|
pred = self.predecessor.pop(node, None)
|
327
335
|
new_node = self.add_branch(
|
328
|
-
node,
|
336
|
+
node,
|
337
|
+
length=new_length - 1,
|
338
|
+
move_timepoints=True,
|
339
|
+
reverse=False,
|
329
340
|
)
|
330
341
|
if pred:
|
331
342
|
self.successor[pred[0]].remove(node)
|
@@ -365,11 +376,40 @@ class lineageTree(lineageTreeLoaders):
|
|
365
376
|
else:
|
366
377
|
return None
|
367
378
|
|
379
|
+
@property
|
380
|
+
def time_resolution(self):
|
381
|
+
if not hasattr(self, "_time_resolution"):
|
382
|
+
self.time_resolution = 1
|
383
|
+
return self._time_resolution / 10
|
384
|
+
|
385
|
+
@time_resolution.setter
|
386
|
+
def time_resolution(self, time_resolution):
|
387
|
+
if time_resolution is not None:
|
388
|
+
self._time_resolution = int(time_resolution * 10)
|
389
|
+
else:
|
390
|
+
warnings.warn("Time resolution set to default 0", stacklevel=2)
|
391
|
+
self._time_resolution = 10
|
392
|
+
|
393
|
+
@property
|
394
|
+
def depth(self):
|
395
|
+
if not hasattr(self, "_depth"):
|
396
|
+
self._depth = {}
|
397
|
+
for leaf in self.leaves:
|
398
|
+
self._depth[leaf] = 1
|
399
|
+
while leaf in self.predecessor:
|
400
|
+
parent = self.predecessor[leaf][0]
|
401
|
+
current_depth = self._depth.get(parent, 0)
|
402
|
+
self._depth[parent] = max(
|
403
|
+
self._depth[leaf] + 1, current_depth
|
404
|
+
)
|
405
|
+
leaf = parent
|
406
|
+
for root in self.roots - set(self._depth):
|
407
|
+
self._depth[root] = 1
|
408
|
+
return self._depth
|
409
|
+
|
368
410
|
@property
|
369
411
|
def roots(self):
|
370
|
-
|
371
|
-
self._roots = set(self.nodes).difference(self.predecessor)
|
372
|
-
return self._roots
|
412
|
+
return set(self.nodes).difference(self.predecessor)
|
373
413
|
|
374
414
|
@property
|
375
415
|
def edges(self):
|
@@ -377,7 +417,7 @@ class lineageTree(lineageTreeLoaders):
|
|
377
417
|
|
378
418
|
@property
|
379
419
|
def leaves(self):
|
380
|
-
return
|
420
|
+
return {p for p, s in self.successor.items() if s == []}
|
381
421
|
|
382
422
|
@property
|
383
423
|
def labels(self):
|
@@ -388,10 +428,10 @@ class lineageTree(lineageTreeLoaders):
|
|
388
428
|
}
|
389
429
|
else:
|
390
430
|
self._labels = {
|
391
|
-
|
392
|
-
for
|
393
|
-
for
|
394
|
-
if abs(self.time[
|
431
|
+
root: "Unlabeled"
|
432
|
+
for root in self.roots
|
433
|
+
for leaf in self.find_leaves(root)
|
434
|
+
if abs(self.time[leaf] - self.time[root])
|
395
435
|
>= abs(self.t_e - self.t_b) / 4
|
396
436
|
}
|
397
437
|
return self._labels
|
@@ -483,7 +523,7 @@ class lineageTree(lineageTreeLoaders):
|
|
483
523
|
|
484
524
|
f.write("@5\n")
|
485
525
|
for C in self.to_take_time[t]:
|
486
|
-
f.write(
|
526
|
+
f.write(f"{manual_labels.get(C, default_label):f}\n")
|
487
527
|
f.write(f"{0:f}\n")
|
488
528
|
|
489
529
|
f.write("@6\n")
|
@@ -502,8 +542,7 @@ class lineageTree(lineageTreeLoaders):
|
|
502
542
|
f.write("@7\n")
|
503
543
|
for C in self.to_take_time[t]:
|
504
544
|
f.write(
|
505
|
-
"
|
506
|
-
% (np.linalg.norm(points_v[C][0] - points_v[C][-1]))
|
545
|
+
f"{np.linalg.norm(points_v[C][0] - points_v[C][-1]):f}\n"
|
507
546
|
)
|
508
547
|
|
509
548
|
f.write("@8\n")
|
@@ -932,10 +971,10 @@ class lineageTree(lineageTreeLoaders):
|
|
932
971
|
if node_properties:
|
933
972
|
for p_name, (p_dict, default) in node_properties.items():
|
934
973
|
if isinstance(list(p_dict.values())[0], str):
|
935
|
-
f.write('(property 0 string "
|
974
|
+
f.write(f'(property 0 string "{p_name}"\n')
|
936
975
|
f.write(f"\t(default {default} {default})\n")
|
937
976
|
elif isinstance(list(p_dict.values())[0], Number):
|
938
|
-
f.write('(property 0 double "
|
977
|
+
f.write(f'(property 0 double "{p_name}"\n')
|
939
978
|
f.write('\t(default "0" "0")\n')
|
940
979
|
for n in nodes_to_use:
|
941
980
|
f.write(
|
@@ -1009,148 +1048,6 @@ class lineageTree(lineageTreeLoaders):
|
|
1009
1048
|
|
1010
1049
|
f.close()
|
1011
1050
|
|
1012
|
-
def read_from_binary(self, fname: str):
|
1013
|
-
"""
|
1014
|
-
Reads a binary lineageTree file name.
|
1015
|
-
Format description: see self.to_binary
|
1016
|
-
|
1017
|
-
Args:
|
1018
|
-
fname: string, path to the binary file
|
1019
|
-
reverse_time: bool, not used
|
1020
|
-
"""
|
1021
|
-
q_size = struct.calcsize("q")
|
1022
|
-
H_size = struct.calcsize("H")
|
1023
|
-
d_size = struct.calcsize("d")
|
1024
|
-
|
1025
|
-
with open(fname, "rb") as f:
|
1026
|
-
len_tree = struct.unpack("q", f.read(q_size))[0]
|
1027
|
-
len_time = struct.unpack("q", f.read(q_size))[0]
|
1028
|
-
len_pos = struct.unpack("q", f.read(q_size))[0]
|
1029
|
-
number_sequence = list(
|
1030
|
-
struct.unpack("q" * len_tree, f.read(q_size * len_tree))
|
1031
|
-
)
|
1032
|
-
time_sequence = list(
|
1033
|
-
struct.unpack("H" * len_time, f.read(H_size * len_time))
|
1034
|
-
)
|
1035
|
-
pos_sequence = np.array(
|
1036
|
-
struct.unpack("d" * len_pos, f.read(d_size * len_pos))
|
1037
|
-
)
|
1038
|
-
|
1039
|
-
f.close()
|
1040
|
-
|
1041
|
-
successor = {}
|
1042
|
-
predecessor = {}
|
1043
|
-
time = {}
|
1044
|
-
time_nodes = {}
|
1045
|
-
time_edges = {}
|
1046
|
-
pos = {}
|
1047
|
-
is_root = {}
|
1048
|
-
nodes = []
|
1049
|
-
edges = []
|
1050
|
-
waiting_list = []
|
1051
|
-
print(number_sequence[0])
|
1052
|
-
i = 0
|
1053
|
-
done = False
|
1054
|
-
if max(number_sequence[::2]) == -1:
|
1055
|
-
tmp = number_sequence[1::2]
|
1056
|
-
if len(tmp) * 3 == len(pos_sequence) == len(time_sequence) * 3:
|
1057
|
-
time = dict(list(zip(tmp, time_sequence)))
|
1058
|
-
for c, t in time.items():
|
1059
|
-
time_nodes.setdefault(t, set()).add(c)
|
1060
|
-
pos = dict(
|
1061
|
-
list(zip(tmp, np.reshape(pos_sequence, (len_time, 3))))
|
1062
|
-
)
|
1063
|
-
is_root = {c: True for c in tmp}
|
1064
|
-
nodes = tmp
|
1065
|
-
done = True
|
1066
|
-
while (
|
1067
|
-
i < len(number_sequence) and not done
|
1068
|
-
): # , c in enumerate(number_sequence[:-1]):
|
1069
|
-
c = number_sequence[i]
|
1070
|
-
if c == -1:
|
1071
|
-
if waiting_list != []:
|
1072
|
-
prev_mother = waiting_list.pop()
|
1073
|
-
successor[prev_mother].insert(0, number_sequence[i + 1])
|
1074
|
-
edges.append((prev_mother, number_sequence[i + 1]))
|
1075
|
-
time_edges.setdefault(t, set()).add(
|
1076
|
-
(prev_mother, number_sequence[i + 1])
|
1077
|
-
)
|
1078
|
-
is_root[number_sequence[i + 1]] = False
|
1079
|
-
t = time[prev_mother] + 1
|
1080
|
-
else:
|
1081
|
-
t = time_sequence.pop(0)
|
1082
|
-
is_root[number_sequence[i + 1]] = True
|
1083
|
-
|
1084
|
-
elif c == -2:
|
1085
|
-
successor[waiting_list[-1]] = [number_sequence[i + 1]]
|
1086
|
-
edges.append((waiting_list[-1], number_sequence[i + 1]))
|
1087
|
-
time_edges.setdefault(t, set()).add(
|
1088
|
-
(waiting_list[-1], number_sequence[i + 1])
|
1089
|
-
)
|
1090
|
-
is_root[number_sequence[i + 1]] = False
|
1091
|
-
pos[waiting_list[-1]] = pos_sequence[:3]
|
1092
|
-
pos_sequence = pos_sequence[3:]
|
1093
|
-
nodes.append(waiting_list[-1])
|
1094
|
-
time[waiting_list[-1]] = t
|
1095
|
-
time_nodes.setdefault(t, set()).add(waiting_list[-1])
|
1096
|
-
t += 1
|
1097
|
-
|
1098
|
-
elif number_sequence[i + 1] >= 0:
|
1099
|
-
successor[c] = [number_sequence[i + 1]]
|
1100
|
-
edges.append((c, number_sequence[i + 1]))
|
1101
|
-
time_edges.setdefault(t, set()).add(
|
1102
|
-
(c, number_sequence[i + 1])
|
1103
|
-
)
|
1104
|
-
is_root[number_sequence[i + 1]] = False
|
1105
|
-
pos[c] = pos_sequence[:3]
|
1106
|
-
pos_sequence = pos_sequence[3:]
|
1107
|
-
nodes.append(c)
|
1108
|
-
time[c] = t
|
1109
|
-
time_nodes.setdefault(t, set()).add(c)
|
1110
|
-
t += 1
|
1111
|
-
|
1112
|
-
elif number_sequence[i + 1] == -2:
|
1113
|
-
waiting_list += [c]
|
1114
|
-
|
1115
|
-
elif number_sequence[i + 1] == -1:
|
1116
|
-
pos[c] = pos_sequence[:3]
|
1117
|
-
pos_sequence = pos_sequence[3:]
|
1118
|
-
nodes.append(c)
|
1119
|
-
time[c] = t
|
1120
|
-
time_nodes.setdefault(t, set()).add(c)
|
1121
|
-
t += 1
|
1122
|
-
i += 1
|
1123
|
-
if waiting_list != []:
|
1124
|
-
prev_mother = waiting_list.pop()
|
1125
|
-
successor[prev_mother].insert(0, number_sequence[i + 1])
|
1126
|
-
edges.append((prev_mother, number_sequence[i + 1]))
|
1127
|
-
time_edges.setdefault(t, set()).add(
|
1128
|
-
(prev_mother, number_sequence[i + 1])
|
1129
|
-
)
|
1130
|
-
if i + 1 < len(number_sequence):
|
1131
|
-
is_root[number_sequence[i + 1]] = False
|
1132
|
-
t = time[prev_mother] + 1
|
1133
|
-
else:
|
1134
|
-
if len(time_sequence) > 0:
|
1135
|
-
t = time_sequence.pop(0)
|
1136
|
-
if i + 1 < len(number_sequence):
|
1137
|
-
is_root[number_sequence[i + 1]] = True
|
1138
|
-
i += 1
|
1139
|
-
|
1140
|
-
predecessor = {vi: [k] for k, v in successor.items() for vi in v}
|
1141
|
-
|
1142
|
-
self.successor = successor
|
1143
|
-
self.predecessor = predecessor
|
1144
|
-
self.time = time
|
1145
|
-
self.time_nodes = time_nodes
|
1146
|
-
self.time_edges = time_edges
|
1147
|
-
self.pos = pos
|
1148
|
-
self.nodes = set(nodes)
|
1149
|
-
self.t_b = min(time_nodes.keys())
|
1150
|
-
self.t_e = max(time_nodes.keys())
|
1151
|
-
self.is_root = is_root
|
1152
|
-
self.max_id = max(self.nodes)
|
1153
|
-
|
1154
1051
|
def write(self, fname: str):
|
1155
1052
|
"""
|
1156
1053
|
Write a lineage tree on disk as an .lT file.
|
@@ -1165,7 +1062,7 @@ class lineageTree(lineageTreeLoaders):
|
|
1165
1062
|
f.close()
|
1166
1063
|
|
1167
1064
|
@classmethod
|
1168
|
-
def load(clf, fname: str, rm_empty_lists=
|
1065
|
+
def load(clf, fname: str, rm_empty_lists=False):
|
1169
1066
|
"""
|
1170
1067
|
Loading a lineage tree from a ".lT" file.
|
1171
1068
|
|
@@ -1178,18 +1075,8 @@ class lineageTree(lineageTreeLoaders):
|
|
1178
1075
|
with open(fname, "br") as f:
|
1179
1076
|
lT = pkl.load(f)
|
1180
1077
|
f.close()
|
1181
|
-
if
|
1182
|
-
|
1183
|
-
for node, succ in lT.successor.items():
|
1184
|
-
if succ == []:
|
1185
|
-
lT.successor.pop(node)
|
1186
|
-
if [] in lT.predecessor.values():
|
1187
|
-
for node, succ in lT.predecessor.items():
|
1188
|
-
if succ == []:
|
1189
|
-
lT.predecessor.pop(node)
|
1190
|
-
lT.t_e = max(lT.time_nodes)
|
1191
|
-
lT.t_b = min(lT.time_nodes)
|
1192
|
-
warnings.warn("Empty lists have been removed")
|
1078
|
+
if not hasattr(lT, "time_resolution"):
|
1079
|
+
lT.time_resolution = None
|
1193
1080
|
return lT
|
1194
1081
|
|
1195
1082
|
def get_idx3d(self, t: int) -> tuple:
|
@@ -1392,8 +1279,10 @@ class lineageTree(lineageTreeLoaders):
|
|
1392
1279
|
while to_do:
|
1393
1280
|
current = to_do.pop()
|
1394
1281
|
track = self.get_successors(current, end_time=end_time)
|
1395
|
-
|
1396
|
-
|
1282
|
+
# if len(track) != 1 or self.time[current] <= end_time:
|
1283
|
+
if self.time[track[-1]] <= end_time:
|
1284
|
+
branches += [track]
|
1285
|
+
to_do += self[track[-1]]
|
1397
1286
|
return branches
|
1398
1287
|
|
1399
1288
|
def get_all_tracks(self, force_recompute: bool = False) -> list:
|
@@ -1449,11 +1338,10 @@ class lineageTree(lineageTreeLoaders):
|
|
1449
1338
|
leaves = set()
|
1450
1339
|
while to_do:
|
1451
1340
|
curr = to_do.pop()
|
1452
|
-
succ = self.successor.get(curr)
|
1453
|
-
if
|
1341
|
+
succ = self.successor.get(curr, [])
|
1342
|
+
if not succ:
|
1454
1343
|
leaves.add(curr)
|
1455
|
-
|
1456
|
-
to_do += succ
|
1344
|
+
to_do += succ
|
1457
1345
|
return leaves
|
1458
1346
|
|
1459
1347
|
def get_sub_tree(
|
@@ -1629,6 +1517,8 @@ class lineageTree(lineageTreeLoaders):
|
|
1629
1517
|
at a given time `time`.
|
1630
1518
|
|
1631
1519
|
If there is no ancestor, returns `-1`
|
1520
|
+
If time is None return the root of the sub tree that spawns
|
1521
|
+
the node n.
|
1632
1522
|
|
1633
1523
|
Args:
|
1634
1524
|
n (int): node for which to look the ancestor
|
@@ -1645,7 +1535,9 @@ class lineageTree(lineageTreeLoaders):
|
|
1645
1535
|
if time is None:
|
1646
1536
|
time = self.t_b
|
1647
1537
|
ancestor = n
|
1648
|
-
while
|
1538
|
+
while (
|
1539
|
+
time < self.time.get(ancestor, -1) and ancestor in self.predecessor
|
1540
|
+
):
|
1649
1541
|
ancestor = self.predecessor.get(ancestor, [-1])[0]
|
1650
1542
|
return ancestor
|
1651
1543
|
|
@@ -1674,10 +1566,11 @@ class lineageTree(lineageTreeLoaders):
|
|
1674
1566
|
def unordered_tree_edit_distances_at_time_t(
|
1675
1567
|
self,
|
1676
1568
|
t: int,
|
1677
|
-
delta: callable = None,
|
1678
|
-
norm: callable = None,
|
1679
|
-
recompute: bool = False,
|
1680
1569
|
end_time: int = None,
|
1570
|
+
style="simple",
|
1571
|
+
downsample: int = 2,
|
1572
|
+
normalize: bool = True,
|
1573
|
+
recompute: bool = False,
|
1681
1574
|
) -> dict:
|
1682
1575
|
"""
|
1683
1576
|
Compute all the pairwise unordered tree edit distances from Zhang 996 between the trees spawned at time `t`
|
@@ -1704,7 +1597,12 @@ class lineageTree(lineageTreeLoaders):
|
|
1704
1597
|
for n1, n2 in combinations(roots, 2):
|
1705
1598
|
key = tuple(sorted((n1, n2)))
|
1706
1599
|
self.uted[t][key] = self.unordered_tree_edit_distance(
|
1707
|
-
n1,
|
1600
|
+
n1,
|
1601
|
+
n2,
|
1602
|
+
end_time=end_time,
|
1603
|
+
style=style,
|
1604
|
+
downsample=downsample,
|
1605
|
+
normalize=normalize,
|
1708
1606
|
)
|
1709
1607
|
return self.uted[t]
|
1710
1608
|
|
@@ -1713,11 +1611,11 @@ class lineageTree(lineageTreeLoaders):
|
|
1713
1611
|
n1: int,
|
1714
1612
|
n2: int,
|
1715
1613
|
end_time: int = None,
|
1716
|
-
|
1717
|
-
|
1614
|
+
norm: Union["max", "sum", None] = "max",
|
1615
|
+
style="simple",
|
1616
|
+
downsample: int = 2,
|
1718
1617
|
) -> float:
|
1719
1618
|
"""
|
1720
|
-
TODO: Add option for choosing which tree aproximation should be used (Full, simple, comp)
|
1721
1619
|
Compute the unordered tree edit distance from Zhang 1996 between the trees spawned
|
1722
1620
|
by two nodes `n1` and `n2`. The topology of the trees are compared and the matching
|
1723
1621
|
cost is given by the function delta (see edist doc for more information).
|
@@ -1736,10 +1634,18 @@ class lineageTree(lineageTreeLoaders):
|
|
1736
1634
|
|
1737
1635
|
tree = tree_style[style].value
|
1738
1636
|
tree1 = tree(
|
1739
|
-
lT=self,
|
1637
|
+
lT=self,
|
1638
|
+
downsample=downsample,
|
1639
|
+
end_time=end_time,
|
1640
|
+
root=n1,
|
1641
|
+
time_scale=1,
|
1740
1642
|
)
|
1741
1643
|
tree2 = tree(
|
1742
|
-
lT=self,
|
1644
|
+
lT=self,
|
1645
|
+
downsample=downsample,
|
1646
|
+
end_time=end_time,
|
1647
|
+
root=n2,
|
1648
|
+
time_scale=1,
|
1743
1649
|
)
|
1744
1650
|
delta = tree1.delta
|
1745
1651
|
_, times1 = tree1.tree
|
@@ -1763,71 +1669,140 @@ class lineageTree(lineageTreeLoaders):
|
|
1763
1669
|
times1=times1,
|
1764
1670
|
times2=times2,
|
1765
1671
|
)
|
1672
|
+
norm1 = tree1.get_norm()
|
1673
|
+
norm2 = tree2.get_norm()
|
1674
|
+
norm_dict = {"max": max, "sum": sum, "None": lambda x: 1}
|
1675
|
+
if norm is None:
|
1676
|
+
norm = "None"
|
1677
|
+
if norm not in norm_dict:
|
1678
|
+
raise Warning(
|
1679
|
+
"Select a viable normalization method (max, sum, None)"
|
1680
|
+
)
|
1681
|
+
return uted.uted(
|
1682
|
+
nodes1, adj1, nodes2, adj2, delta=delta_tmp
|
1683
|
+
) / norm_dict[norm]([norm1, norm2])
|
1766
1684
|
|
1767
|
-
|
1768
|
-
|
1685
|
+
@staticmethod
|
1686
|
+
def __plot_nodes(
|
1687
|
+
hier, selected_nodes, color, size, ax, default_color="black", **kwargs
|
1688
|
+
):
|
1689
|
+
"""
|
1690
|
+
Private method that plots the nodes of the tree.
|
1691
|
+
"""
|
1692
|
+
hier_unselected = np.array(
|
1693
|
+
[v for k, v in hier.items() if k not in selected_nodes]
|
1769
1694
|
)
|
1695
|
+
if hier_unselected.any():
|
1696
|
+
ax.scatter(
|
1697
|
+
*hier_unselected.T,
|
1698
|
+
s=size,
|
1699
|
+
zorder=10,
|
1700
|
+
color=default_color,
|
1701
|
+
**kwargs,
|
1702
|
+
)
|
1703
|
+
if selected_nodes.intersection(hier.keys()):
|
1704
|
+
hier_selected = np.array(
|
1705
|
+
[v for k, v in hier.items() if k in selected_nodes]
|
1706
|
+
)
|
1707
|
+
ax.scatter(
|
1708
|
+
*hier_selected.T, s=size, zorder=10, color=color, **kwargs
|
1709
|
+
)
|
1710
|
+
|
1711
|
+
@staticmethod
|
1712
|
+
def __plot_edges(
|
1713
|
+
hier,
|
1714
|
+
lnks_tms,
|
1715
|
+
selected_edges,
|
1716
|
+
color,
|
1717
|
+
ax,
|
1718
|
+
default_color="black",
|
1719
|
+
**kwargs,
|
1720
|
+
):
|
1721
|
+
"""
|
1722
|
+
Private method that plots the edges of the tree.
|
1723
|
+
"""
|
1724
|
+
x, y = [], []
|
1725
|
+
for pred, succs in lnks_tms["links"].items():
|
1726
|
+
for succ in succs:
|
1727
|
+
if pred not in selected_edges or succ not in selected_edges:
|
1728
|
+
x.extend((hier[succ][0], hier[pred][0], None))
|
1729
|
+
y.extend((hier[succ][1], hier[pred][1], None))
|
1730
|
+
ax.plot(x, y, linewidth=0.3, zorder=0.1, c=default_color, **kwargs)
|
1731
|
+
x, y = [], []
|
1732
|
+
for pred, succs in lnks_tms["links"].items():
|
1733
|
+
for succ in succs:
|
1734
|
+
if pred in selected_edges and succ in selected_edges:
|
1735
|
+
x.extend((hier[succ][0], hier[pred][0], None))
|
1736
|
+
y.extend((hier[succ][1], hier[pred][1], None))
|
1737
|
+
ax.plot(x, y, linewidth=0.3, zorder=0.2, c=color, **kwargs)
|
1770
1738
|
|
1771
1739
|
def draw_tree_graph(
|
1772
1740
|
self,
|
1773
1741
|
hier,
|
1774
1742
|
lnks_tms,
|
1775
|
-
|
1776
|
-
|
1743
|
+
selected_nodes=None,
|
1744
|
+
selected_edges=None,
|
1745
|
+
color_of_nodes="magenta",
|
1746
|
+
color_of_edges=None,
|
1747
|
+
size=10,
|
1777
1748
|
ax=None,
|
1778
|
-
|
1749
|
+
default_color="black",
|
1779
1750
|
**kwargs,
|
1780
1751
|
):
|
1781
|
-
|
1782
|
-
|
1752
|
+
"""Function to plot the tree graph.
|
1753
|
+
|
1754
|
+
Args:
|
1755
|
+
hier (dict): Dictinary that contains the positions of all nodes.
|
1756
|
+
lnks_tms (dict): 2 dictionaries: 1 contains all links from start of life cycle to end of life cycle and
|
1757
|
+
the succesors of each cell.
|
1758
|
+
1 contains the length of each life cycle.
|
1759
|
+
selected_nodes (list|set, optional): Which cells are to be selected (Painted with a different color). Defaults to None.
|
1760
|
+
selected_edges (list|set, optional): Which edges are to be selected (Painted with a different color). Defaults to None.
|
1761
|
+
color_of_nodes (str, optional): Color of selected nodes. Defaults to "magenta".
|
1762
|
+
color_of_edges (_type_, optional): Color of selected edges. Defaults to None.
|
1763
|
+
size (int, optional): Size of the nodes. Defaults to 10.
|
1764
|
+
ax (_type_, optional): Plot the graph on existing ax. Defaults to None.
|
1765
|
+
figure (_type_, optional): _description_. Defaults to None.
|
1766
|
+
default_color (str, optional): Default color of nodes. Defaults to "black".
|
1767
|
+
|
1768
|
+
Returns:
|
1769
|
+
figure, ax: The matplotlib figure and ax object.
|
1770
|
+
"""
|
1771
|
+
if selected_nodes is None:
|
1772
|
+
selected_nodes = []
|
1773
|
+
if selected_edges is None:
|
1774
|
+
selected_edges = []
|
1783
1775
|
if ax is None:
|
1784
1776
|
figure, ax = plt.subplots()
|
1785
1777
|
else:
|
1786
1778
|
ax.clear()
|
1787
|
-
if not isinstance(
|
1788
|
-
|
1789
|
-
|
1790
|
-
|
1791
|
-
|
1792
|
-
|
1793
|
-
|
1794
|
-
|
1795
|
-
|
1796
|
-
|
1797
|
-
|
1798
|
-
|
1799
|
-
|
1800
|
-
|
1801
|
-
|
1802
|
-
|
1803
|
-
|
1804
|
-
|
1805
|
-
|
1806
|
-
|
1807
|
-
|
1808
|
-
|
1809
|
-
|
1810
|
-
|
1811
|
-
selected = np.array(tuple(hier_selected.values()))
|
1812
|
-
x = []
|
1813
|
-
y = []
|
1814
|
-
for pred, succs in lnks_tms["links"].items():
|
1815
|
-
if pred in selected_cells:
|
1816
|
-
for succ in succs:
|
1817
|
-
x.extend((hier[succ][0], hier[pred][0], None))
|
1818
|
-
y.extend((hier[succ][1], hier[pred][1], None))
|
1819
|
-
ax.plot(x, y, c=color, linewidth=0.3, zorder=0.4, **kwargs)
|
1820
|
-
ax.scatter(
|
1821
|
-
selected.T[0],
|
1822
|
-
selected.T[1],
|
1823
|
-
s=0.1,
|
1824
|
-
c=color,
|
1825
|
-
zorder=0.9,
|
1826
|
-
**kwargs,
|
1827
|
-
)
|
1779
|
+
if not isinstance(selected_nodes, set):
|
1780
|
+
selected_nodes = set(selected_nodes)
|
1781
|
+
if not isinstance(selected_edges, set):
|
1782
|
+
selected_edges = set(selected_edges)
|
1783
|
+
self.__plot_nodes(
|
1784
|
+
hier,
|
1785
|
+
selected_nodes,
|
1786
|
+
color_of_nodes,
|
1787
|
+
size=size,
|
1788
|
+
ax=ax,
|
1789
|
+
default_color=default_color,
|
1790
|
+
**kwargs,
|
1791
|
+
)
|
1792
|
+
if not color_of_edges:
|
1793
|
+
color_of_edges = color_of_nodes
|
1794
|
+
self.__plot_edges(
|
1795
|
+
hier,
|
1796
|
+
lnks_tms,
|
1797
|
+
selected_edges,
|
1798
|
+
color_of_edges,
|
1799
|
+
ax,
|
1800
|
+
default_color=default_color,
|
1801
|
+
**kwargs,
|
1802
|
+
)
|
1828
1803
|
ax.get_yaxis().set_visible(False)
|
1829
1804
|
ax.get_xaxis().set_visible(False)
|
1830
|
-
return
|
1805
|
+
return ax.get_figure(), ax
|
1831
1806
|
|
1832
1807
|
def to_simple_graph(self, node=None, start_time: int = None):
|
1833
1808
|
"""Generates a dictionary of graphs where the keys are the index of the graph and
|
@@ -1862,8 +1837,8 @@ class lineageTree(lineageTreeLoaders):
|
|
1862
1837
|
figsize=(10, 15),
|
1863
1838
|
dpi=100,
|
1864
1839
|
fontsize=15,
|
1865
|
-
figure=None,
|
1866
1840
|
axes=None,
|
1841
|
+
vert_gap=1,
|
1867
1842
|
**kwargs,
|
1868
1843
|
):
|
1869
1844
|
"""Plots all lineages.
|
@@ -1893,14 +1868,24 @@ class lineageTree(lineageTreeLoaders):
|
|
1893
1868
|
)
|
1894
1869
|
pos = {
|
1895
1870
|
i: hierarchical_pos(
|
1896
|
-
g,
|
1871
|
+
g,
|
1872
|
+
g["root"],
|
1873
|
+
ycenter=-int(self.time[g["root"]]),
|
1874
|
+
vert_gap=vert_gap,
|
1897
1875
|
)
|
1898
1876
|
for i, g in graphs.items()
|
1899
1877
|
}
|
1900
|
-
|
1901
|
-
|
1902
|
-
|
1903
|
-
|
1878
|
+
if axes is None:
|
1879
|
+
ncols = int(len(graphs) // nrows) + (+np.sign(len(graphs) % nrows))
|
1880
|
+
figure, axes = plt.subplots(
|
1881
|
+
figsize=figsize, nrows=nrows, ncols=ncols, dpi=dpi, sharey=True
|
1882
|
+
)
|
1883
|
+
else:
|
1884
|
+
figure, axes = axes.flatten()[0].get_figure(), axes
|
1885
|
+
if len(axes.flatten()) < len(graphs):
|
1886
|
+
raise Exception(
|
1887
|
+
f"Not enough axes, they should be at least {len(graphs)}."
|
1888
|
+
)
|
1904
1889
|
flat_axes = axes.flatten()
|
1905
1890
|
ax2root = {}
|
1906
1891
|
min_width, min_height = float("inf"), float("inf")
|
@@ -1938,9 +1923,17 @@ class lineageTree(lineageTreeLoaders):
|
|
1938
1923
|
},
|
1939
1924
|
)
|
1940
1925
|
[figure.delaxes(ax) for ax in axes.flatten() if not ax.has_data()]
|
1941
|
-
return
|
1926
|
+
return axes.flatten()[0].get_figure(), axes, ax2root
|
1942
1927
|
|
1943
|
-
def plot_node(
|
1928
|
+
def plot_node(
|
1929
|
+
self,
|
1930
|
+
node,
|
1931
|
+
figsize=(4, 7),
|
1932
|
+
dpi=150,
|
1933
|
+
vert_gap=2,
|
1934
|
+
ax=None,
|
1935
|
+
**kwargs,
|
1936
|
+
):
|
1944
1937
|
"""Plots the subtree spawn by a node.
|
1945
1938
|
|
1946
1939
|
Args:
|
@@ -1951,7 +1944,10 @@ class lineageTree(lineageTreeLoaders):
|
|
1951
1944
|
if len(graph) > 1:
|
1952
1945
|
raise Warning("Please enter only one node")
|
1953
1946
|
graph = graph[0]
|
1954
|
-
|
1947
|
+
if not ax:
|
1948
|
+
figure, ax = plt.subplots(
|
1949
|
+
nrows=1, ncols=1, figsize=figsize, dpi=dpi
|
1950
|
+
)
|
1955
1951
|
self.draw_tree_graph(
|
1956
1952
|
hier=hierarchical_pos(
|
1957
1953
|
graph,
|
@@ -1962,7 +1958,7 @@ class lineageTree(lineageTreeLoaders):
|
|
1962
1958
|
lnks_tms=graph,
|
1963
1959
|
ax=ax,
|
1964
1960
|
)
|
1965
|
-
return
|
1961
|
+
return ax.get_figure(), ax
|
1966
1962
|
|
1967
1963
|
# def DTW(self, t1, t2, max_w=None, start_delay=None, end_delay=None,
|
1968
1964
|
# metric='euclidian', **kwargs):
|
@@ -2615,6 +2611,7 @@ class lineageTree(lineageTreeLoaders):
|
|
2615
2611
|
warnings.warn(
|
2616
2612
|
"Error: not possible to show alignment in PCA projection !",
|
2617
2613
|
UserWarning,
|
2614
|
+
stacklevel=2,
|
2618
2615
|
)
|
2619
2616
|
|
2620
2617
|
return distance, fig
|
@@ -2636,6 +2633,7 @@ class lineageTree(lineageTreeLoaders):
|
|
2636
2633
|
reorder: bool = False,
|
2637
2634
|
xml_attributes: tuple = None,
|
2638
2635
|
name: str = None,
|
2636
|
+
time_resolution: Union[int, None] = None,
|
2639
2637
|
):
|
2640
2638
|
"""
|
2641
2639
|
TODO: complete the doc
|
@@ -2646,13 +2644,22 @@ class lineageTree(lineageTreeLoaders):
|
|
2646
2644
|
file_format (str): either - path format to TGMM xmls
|
2647
2645
|
- path to the MaMuT xml
|
2648
2646
|
- path to the binary file
|
2649
|
-
tb (int):
|
2650
|
-
te (int): last time point (necessary for TGMM xmls only)
|
2651
|
-
z_mult (float):
|
2652
|
-
file_type (str):
|
2647
|
+
tb (int, optional):first time point (necessary for TGMM xmls only)
|
2648
|
+
te (int, optional): last time point (necessary for TGMM xmls only)
|
2649
|
+
z_mult (float, optional):z aspect ratio if necessary (usually only for TGMM xmls)
|
2650
|
+
file_type (str, optional):type of input file. Accepts:
|
2653
2651
|
'TGMM, 'ASTEC', MaMuT', 'TrackMate', 'csv', 'celegans', 'binary'
|
2654
2652
|
default is 'binary'
|
2653
|
+
delim (str, optional): _description_. Defaults to ",".
|
2654
|
+
eigen (bool, optional): _description_. Defaults to False.
|
2655
|
+
shape (tuple, optional): _description_. Defaults to None.
|
2656
|
+
raw_size (tuple, optional): _description_. Defaults to None.
|
2657
|
+
reorder (bool, optional): _description_. Defaults to False.
|
2658
|
+
xml_attributes (tuple, optional): _description_. Defaults to None.
|
2659
|
+
name (str, optional): The name of the dataset. Defaults to None.
|
2660
|
+
time_resolution (Union[int, None], optional): Time resolution in mins (If time resolution is smaller than one minute input the time in ms). Defaults to None.
|
2655
2661
|
"""
|
2662
|
+
|
2656
2663
|
self.name = name
|
2657
2664
|
self.time_nodes = {}
|
2658
2665
|
self.time_edges = {}
|
@@ -2664,6 +2671,8 @@ class lineageTree(lineageTreeLoaders):
|
|
2664
2671
|
self.pos = {}
|
2665
2672
|
self.time_id = {}
|
2666
2673
|
self.time = {}
|
2674
|
+
if time_resolution is not None:
|
2675
|
+
self._time_resolution = time_resolution
|
2667
2676
|
self.kdtrees = {}
|
2668
2677
|
self.spatial_density = {}
|
2669
2678
|
if file_type and file_format:
|
@@ -2698,6 +2707,8 @@ class lineageTree(lineageTreeLoaders):
|
|
2698
2707
|
self.read_from_ASTEC(file_format, eigen)
|
2699
2708
|
elif file_type == "csv":
|
2700
2709
|
self.read_from_csv(file_format, z_mult, link=1, delim=delim)
|
2710
|
+
elif file_type == "bao":
|
2711
|
+
self.read_C_elegans_bao(file_format)
|
2701
2712
|
elif file_format and file_format.endswith(".lT"):
|
2702
2713
|
with open(file_format, "br") as f:
|
2703
2714
|
tmp = pkl.load(f)
|
@@ -2708,15 +2719,5 @@ class lineageTree(lineageTreeLoaders):
|
|
2708
2719
|
if self.name is None:
|
2709
2720
|
try:
|
2710
2721
|
self.name = Path(file_format).stem
|
2711
|
-
except:
|
2722
|
+
except TypeError:
|
2712
2723
|
self.name = Path(file_format[0]).stem
|
2713
|
-
if [] in self.successor.values():
|
2714
|
-
successors = list(self.successor.keys())
|
2715
|
-
for succ in successors:
|
2716
|
-
if self[succ] == []:
|
2717
|
-
self.successor.pop(succ)
|
2718
|
-
if [] in self.predecessor.values():
|
2719
|
-
predecessors = list(self.predecessor.keys())
|
2720
|
-
for succ in predecessors:
|
2721
|
-
if self[succ] == []:
|
2722
|
-
self.predecessor.pop(succ)
|