LineageTree 1.6.0__py3-none-any.whl → 1.7.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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.6.0"
1
+ __version__ = "1.7.0"
2
2
  from .lineageTree import lineageTree
3
3
  from .lineageTreeManager import lineageTreeManager
4
4
 
@@ -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
@@ -88,7 +89,7 @@ class lineageTree(lineageTreeLoaders):
88
89
  length (int): The length of the new branch.
89
90
  pos (np.ndarray, optional): The new position of the branch. Defaults to None.
90
91
  move_timepoints (bool): Moves the ti Important only if reverse= True
91
- reverese (bool): If reverse will add a successor branch instead of a predecessor branch
92
+ reverese (bool): If True will create a branch that goes forwards in time otherwise backwards.
92
93
  Returns:
93
94
  (int): Id of the first node of the sublineage.
94
95
  """
@@ -129,9 +130,12 @@ class lineageTree(lineageTreeLoaders):
129
130
  )
130
131
  pred = _next
131
132
  else:
132
- for t in range(length):
133
+ for _ in range(length):
133
134
  _next = self.add_node(
134
- time + t, succ=pred, pos=self.pos[original], reverse=False
135
+ self.time[pred] + 1,
136
+ succ=pred,
137
+ pos=self.pos[original],
138
+ reverse=False,
135
139
  )
136
140
  pred = _next
137
141
  self.labels[pred] = "New branch"
@@ -325,7 +329,10 @@ class lineageTree(lineageTreeLoaders):
325
329
  if length == 1 and new_length != 1:
326
330
  pred = self.predecessor.pop(node, None)
327
331
  new_node = self.add_branch(
328
- node, length=new_length, move_timepoints=True, reverse=False
332
+ node,
333
+ length=new_length - 1,
334
+ move_timepoints=True,
335
+ reverse=False,
329
336
  )
330
337
  if pred:
331
338
  self.successor[pred[0]].remove(node)
@@ -365,6 +372,20 @@ class lineageTree(lineageTreeLoaders):
365
372
  else:
366
373
  return None
367
374
 
375
+ @property
376
+ def time_resolution(self):
377
+ if not hasattr(self, "_time_resolution"):
378
+ self.time_resolution = 1
379
+ return self._time_resolution / 10
380
+
381
+ @time_resolution.setter
382
+ def time_resolution(self, time_resolution):
383
+ if time_resolution is not None:
384
+ self._time_resolution = int(time_resolution * 10)
385
+ else:
386
+ warnings.warn("Time resolution set to default 1", stacklevel=2)
387
+ self._time_resolution = 10
388
+
368
389
  @property
369
390
  def roots(self):
370
391
  if not hasattr(self, "_roots"):
@@ -1178,6 +1199,8 @@ class lineageTree(lineageTreeLoaders):
1178
1199
  with open(fname, "br") as f:
1179
1200
  lT = pkl.load(f)
1180
1201
  f.close()
1202
+ if not hasattr(lT, "time_resolution"):
1203
+ lT.time_resolution = None
1181
1204
  if rm_empty_lists:
1182
1205
  if [] in lT.successor.values():
1183
1206
  for node, succ in lT.successor.items():
@@ -1392,8 +1415,10 @@ class lineageTree(lineageTreeLoaders):
1392
1415
  while to_do:
1393
1416
  current = to_do.pop()
1394
1417
  track = self.get_successors(current, end_time=end_time)
1395
- branches += [track]
1396
- to_do += self[track[-1]]
1418
+ # if len(track) != 1 or self.time[current] <= end_time:
1419
+ if self.time[track[-1]] <= end_time:
1420
+ branches += [track]
1421
+ to_do += self[track[-1]]
1397
1422
  return branches
1398
1423
 
1399
1424
  def get_all_tracks(self, force_recompute: bool = False) -> list:
@@ -1629,6 +1654,8 @@ class lineageTree(lineageTreeLoaders):
1629
1654
  at a given time `time`.
1630
1655
 
1631
1656
  If there is no ancestor, returns `-1`
1657
+ If time is None return the root of the sub tree that spawns
1658
+ the node n.
1632
1659
 
1633
1660
  Args:
1634
1661
  n (int): node for which to look the ancestor
@@ -1645,7 +1672,9 @@ class lineageTree(lineageTreeLoaders):
1645
1672
  if time is None:
1646
1673
  time = self.t_b
1647
1674
  ancestor = n
1648
- while time < self.time.get(ancestor, -1):
1675
+ while (
1676
+ time < self.time.get(ancestor, -1) and ancestor in self.predecessor
1677
+ ):
1649
1678
  ancestor = self.predecessor.get(ancestor, [-1])[0]
1650
1679
  return ancestor
1651
1680
 
@@ -1674,10 +1703,11 @@ class lineageTree(lineageTreeLoaders):
1674
1703
  def unordered_tree_edit_distances_at_time_t(
1675
1704
  self,
1676
1705
  t: int,
1677
- delta: callable = None,
1678
- norm: callable = None,
1679
- recompute: bool = False,
1680
1706
  end_time: int = None,
1707
+ style="simple",
1708
+ downsample: int = 2,
1709
+ normalize: bool = True,
1710
+ recompute: bool = False,
1681
1711
  ) -> dict:
1682
1712
  """
1683
1713
  Compute all the pairwise unordered tree edit distances from Zhang 996 between the trees spawned at time `t`
@@ -1704,7 +1734,12 @@ class lineageTree(lineageTreeLoaders):
1704
1734
  for n1, n2 in combinations(roots, 2):
1705
1735
  key = tuple(sorted((n1, n2)))
1706
1736
  self.uted[t][key] = self.unordered_tree_edit_distance(
1707
- n1, n2, end_time=end_time
1737
+ n1,
1738
+ n2,
1739
+ end_time=end_time,
1740
+ style=style,
1741
+ downsample=downsample,
1742
+ normalize=normalize,
1708
1743
  )
1709
1744
  return self.uted[t]
1710
1745
 
@@ -1713,11 +1748,11 @@ class lineageTree(lineageTreeLoaders):
1713
1748
  n1: int,
1714
1749
  n2: int,
1715
1750
  end_time: int = None,
1716
- style="fragmented",
1717
- node_lengths: tuple = (1, 5, 7),
1751
+ norm: Union["max", "sum", None] = "max",
1752
+ style="simple",
1753
+ downsample: int = 2,
1718
1754
  ) -> float:
1719
1755
  """
1720
- TODO: Add option for choosing which tree aproximation should be used (Full, simple, comp)
1721
1756
  Compute the unordered tree edit distance from Zhang 1996 between the trees spawned
1722
1757
  by two nodes `n1` and `n2`. The topology of the trees are compared and the matching
1723
1758
  cost is given by the function delta (see edist doc for more information).
@@ -1736,10 +1771,18 @@ class lineageTree(lineageTreeLoaders):
1736
1771
 
1737
1772
  tree = tree_style[style].value
1738
1773
  tree1 = tree(
1739
- lT=self, node_length=node_lengths, end_time=end_time, root=n1
1774
+ lT=self,
1775
+ downsample=downsample,
1776
+ end_time=end_time,
1777
+ root=n1,
1778
+ time_scale=1,
1740
1779
  )
1741
1780
  tree2 = tree(
1742
- lT=self, node_length=node_lengths, end_time=end_time, root=n2
1781
+ lT=self,
1782
+ downsample=downsample,
1783
+ end_time=end_time,
1784
+ root=n2,
1785
+ time_scale=1,
1743
1786
  )
1744
1787
  delta = tree1.delta
1745
1788
  _, times1 = tree1.tree
@@ -1763,70 +1806,141 @@ class lineageTree(lineageTreeLoaders):
1763
1806
  times1=times1,
1764
1807
  times2=times2,
1765
1808
  )
1809
+ norm1 = tree1.get_norm()
1810
+ norm2 = tree2.get_norm()
1811
+ norm_dict = {"max": max, "sum": sum, "None": lambda x: 1}
1812
+ if norm is None:
1813
+ norm = "None"
1814
+ if norm not in norm_dict:
1815
+ raise Warning(
1816
+ "Select a viable normalization method (max, sum, None)"
1817
+ )
1818
+ return uted.uted(
1819
+ nodes1, adj1, nodes2, adj2, delta=delta_tmp
1820
+ ) / norm_dict[norm]([norm1, norm2])
1766
1821
 
1767
- return uted.uted(nodes1, adj1, nodes2, adj2, delta=delta_tmp) / max(
1768
- tree1.get_norm(), tree2.get_norm()
1822
+ @staticmethod
1823
+ def __plot_nodes(
1824
+ hier, selected_nodes, color, size, ax, default_color="black", **kwargs
1825
+ ):
1826
+ """
1827
+ Private method that plots the nodes of the tree.
1828
+ """
1829
+ hier_unselected = np.array(
1830
+ [v for k, v in hier.items() if k not in selected_nodes]
1769
1831
  )
1832
+ if hier_unselected.any():
1833
+ ax.scatter(
1834
+ *hier_unselected.T,
1835
+ s=size,
1836
+ zorder=10,
1837
+ color=default_color,
1838
+ **kwargs,
1839
+ )
1840
+ if selected_nodes.intersection(hier.keys()):
1841
+ hier_selected = np.array(
1842
+ [v for k, v in hier.items() if k in selected_nodes]
1843
+ )
1844
+ ax.scatter(
1845
+ *hier_selected.T, s=size, zorder=10, color=color, **kwargs
1846
+ )
1847
+
1848
+ @staticmethod
1849
+ def __plot_edges(
1850
+ hier,
1851
+ lnks_tms,
1852
+ selected_edges,
1853
+ color,
1854
+ ax,
1855
+ default_color="black",
1856
+ **kwargs,
1857
+ ):
1858
+ """
1859
+ Private method that plots the edges of the tree.
1860
+ """
1861
+ x, y = [], []
1862
+ for pred, succs in lnks_tms["links"].items():
1863
+ for succ in succs:
1864
+ if pred not in selected_edges or succ not in selected_edges:
1865
+ x.extend((hier[succ][0], hier[pred][0], None))
1866
+ y.extend((hier[succ][1], hier[pred][1], None))
1867
+ ax.plot(x, y, linewidth=0.3, zorder=0.1, c=default_color, **kwargs)
1868
+ x, y = [], []
1869
+ for pred, succs in lnks_tms["links"].items():
1870
+ for succ in succs:
1871
+ if pred in selected_edges and succ in selected_edges:
1872
+ x.extend((hier[succ][0], hier[pred][0], None))
1873
+ y.extend((hier[succ][1], hier[pred][1], None))
1874
+ ax.plot(x, y, linewidth=0.3, zorder=0.2, c=color, **kwargs)
1770
1875
 
1771
1876
  def draw_tree_graph(
1772
1877
  self,
1773
1878
  hier,
1774
1879
  lnks_tms,
1775
- selected_cells=None,
1776
- color="magenta",
1880
+ selected_nodes=None,
1881
+ selected_edges=None,
1882
+ color_of_nodes="magenta",
1883
+ color_of_edges=None,
1884
+ size=10,
1777
1885
  ax=None,
1886
+ figure=None,
1887
+ default_color="black",
1778
1888
  **kwargs,
1779
1889
  ):
1780
- if selected_cells is None:
1781
- selected_cells = []
1890
+ """Function to plot the tree graph.
1891
+
1892
+ Args:
1893
+ hier (dict): Dictinary that contains the positions of all nodes.
1894
+ lnks_tms (dict): 2 dictionaries: 1 contains all links from start of life cycle to end of life cycle and
1895
+ the succesors of each cell.
1896
+ 1 contains the length of each life cycle.
1897
+ selected_nodes (list|set, optional): Which cells are to be selected (Painted with a different color). Defaults to None.
1898
+ selected_edges (list|set, optional): Which edges are to be selected (Painted with a different color). Defaults to None.
1899
+ color_of_nodes (str, optional): Color of selected nodes. Defaults to "magenta".
1900
+ color_of_edges (_type_, optional): Color of selected edges. Defaults to None.
1901
+ size (int, optional): Size of the nodes. Defaults to 10.
1902
+ ax (_type_, optional): Plot the graph on existing ax. Defaults to None.
1903
+ figure (_type_, optional): _description_. Defaults to None.
1904
+ default_color (str, optional): Default color of nodes. Defaults to "black".
1905
+
1906
+ Returns:
1907
+ figure, ax: The matplotlib figure and ax object.
1908
+ """
1909
+ if selected_nodes is None:
1910
+ selected_nodes = []
1911
+ if selected_edges is None:
1912
+ selected_edges = []
1782
1913
  if ax is None:
1783
- _, ax = plt.subplots()
1914
+ figure, ax = plt.subplots()
1784
1915
  else:
1785
1916
  ax.clear()
1786
-
1787
- selected_cells = set(selected_cells)
1788
- hier_unselected = {
1789
- k: v for k, v in hier.items() if k not in selected_cells
1790
- }
1791
- hier_selected = {k: v for k, v in hier.items() if k in selected_cells}
1792
- unselected = np.array(tuple(hier_unselected.values()))
1793
- x = []
1794
- y = []
1795
- if hier_unselected:
1796
- for pred, succs in lnks_tms["links"].items():
1797
- if pred not in selected_cells:
1798
- for succ in succs:
1799
- x.extend((hier[succ][0], hier[pred][0], None))
1800
- y.extend((hier[succ][1], hier[pred][1], None))
1801
- ax.plot(x, y, c="black", linewidth=0.3, zorder=0.5, **kwargs)
1802
- ax.scatter(
1803
- *unselected.T,
1804
- s=0.1,
1805
- c="black",
1806
- zorder=1,
1807
- **kwargs,
1808
- )
1809
- if selected_cells:
1810
- selected = np.array(tuple(hier_selected.values()))
1811
- x = []
1812
- y = []
1813
- for pred, succs in lnks_tms["links"].items():
1814
- if pred in selected_cells:
1815
- for succ in succs:
1816
- x.extend((hier[succ][0], hier[pred][0], None))
1817
- y.extend((hier[succ][1], hier[pred][1], None))
1818
- ax.plot(x, y, c=color, linewidth=0.3, zorder=0.4, **kwargs)
1819
- ax.scatter(
1820
- selected.T[0],
1821
- selected.T[1],
1822
- s=0.1,
1823
- c=color,
1824
- zorder=0.9,
1825
- **kwargs,
1826
- )
1917
+ if not isinstance(selected_nodes, set):
1918
+ selected_nodes = set(selected_nodes)
1919
+ if not isinstance(selected_edges, set):
1920
+ selected_edges = set(selected_edges)
1921
+ self.__plot_nodes(
1922
+ hier,
1923
+ selected_nodes,
1924
+ color_of_nodes,
1925
+ size=size,
1926
+ ax=ax,
1927
+ default_color=default_color,
1928
+ **kwargs,
1929
+ )
1930
+ if not color_of_edges:
1931
+ color_of_edges = color_of_nodes
1932
+ self.__plot_edges(
1933
+ hier,
1934
+ lnks_tms,
1935
+ selected_edges,
1936
+ color_of_edges,
1937
+ ax,
1938
+ default_color=default_color,
1939
+ **kwargs,
1940
+ )
1827
1941
  ax.get_yaxis().set_visible(False)
1828
1942
  ax.get_xaxis().set_visible(False)
1829
- return ax
1943
+ return figure, ax
1830
1944
 
1831
1945
  def to_simple_graph(self, node=None, start_time: int = None):
1832
1946
  """Generates a dictionary of graphs where the keys are the index of the graph and
@@ -1960,6 +2074,7 @@ class lineageTree(lineageTreeLoaders):
1960
2074
  ),
1961
2075
  lnks_tms=graph,
1962
2076
  ax=ax,
2077
+ **kwargs,
1963
2078
  )
1964
2079
  return figure, ax
1965
2080
 
@@ -2635,6 +2750,7 @@ class lineageTree(lineageTreeLoaders):
2635
2750
  reorder: bool = False,
2636
2751
  xml_attributes: tuple = None,
2637
2752
  name: str = None,
2753
+ time_resolution: Union[int, None] = None,
2638
2754
  ):
2639
2755
  """
2640
2756
  TODO: complete the doc
@@ -2645,13 +2761,22 @@ class lineageTree(lineageTreeLoaders):
2645
2761
  file_format (str): either - path format to TGMM xmls
2646
2762
  - path to the MaMuT xml
2647
2763
  - path to the binary file
2648
- tb (int): first time point (necessary for TGMM xmls only)
2649
- te (int): last time point (necessary for TGMM xmls only)
2650
- z_mult (float): z aspect ratio if necessary (usually only for TGMM xmls)
2651
- file_type (str): type of input file. Accepts:
2764
+ tb (int, optional):first time point (necessary for TGMM xmls only)
2765
+ te (int, optional): last time point (necessary for TGMM xmls only)
2766
+ z_mult (float, optional):z aspect ratio if necessary (usually only for TGMM xmls)
2767
+ file_type (str, optional):type of input file. Accepts:
2652
2768
  'TGMM, 'ASTEC', MaMuT', 'TrackMate', 'csv', 'celegans', 'binary'
2653
2769
  default is 'binary'
2770
+ delim (str, optional): _description_. Defaults to ",".
2771
+ eigen (bool, optional): _description_. Defaults to False.
2772
+ shape (tuple, optional): _description_. Defaults to None.
2773
+ raw_size (tuple, optional): _description_. Defaults to None.
2774
+ reorder (bool, optional): _description_. Defaults to False.
2775
+ xml_attributes (tuple, optional): _description_. Defaults to None.
2776
+ name (str, optional): The name of the dataset. Defaults to None.
2777
+ 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.
2654
2778
  """
2779
+
2655
2780
  self.name = name
2656
2781
  self.time_nodes = {}
2657
2782
  self.time_edges = {}
@@ -2663,6 +2788,8 @@ class lineageTree(lineageTreeLoaders):
2663
2788
  self.pos = {}
2664
2789
  self.time_id = {}
2665
2790
  self.time = {}
2791
+ if time_resolution is not None:
2792
+ self._time_resolution = time_resolution
2666
2793
  self.kdtrees = {}
2667
2794
  self.spatial_density = {}
2668
2795
  if file_type and file_format:
@@ -2,12 +2,14 @@ import os
2
2
  import pickle as pkl
3
3
  import warnings
4
4
  from functools import partial
5
+ import numpy as np
5
6
 
6
7
  try:
7
8
  from edist import uted
8
9
  except ImportError:
9
10
  warnings.warn(
10
- "No edist installed therefore you will not be able to compute the tree edit distance."
11
+ "No edist installed therefore you will not be able to compute the tree edit distance.",
12
+ stacklevel=2,
11
13
  )
12
14
  from LineageTree import lineageTree
13
15
 
@@ -17,14 +19,24 @@ from .tree_styles import tree_style
17
19
  class lineageTreeManager:
18
20
  def __init__(self):
19
21
  self.lineagetrees = {}
20
- # self.classification = {"Wt": {}, "Ptb": {}}
21
22
  self.lineageTree_counter = 0
22
23
  self.registered = {}
24
+ self.greatest_common_divisors = {}
23
25
 
24
26
  def __next__(self):
25
27
  self.lineageTree_counter += 1
26
28
  return self.lineageTree_counter - 1
27
29
 
30
+ def gcd_update(self, new_embryo):
31
+ if len(self.lineagetrees) > 1:
32
+ for lineagetree in self.lineagetrees:
33
+ self.greatest_common_divisors[lineagetree, new_embryo.name] = (
34
+ np.gcd(
35
+ self.lineagetrees[lineagetree].time_resolution,
36
+ new_embryo.time_resolution,
37
+ )
38
+ )
39
+
28
40
  def add(
29
41
  self, other_tree: lineageTree, name: str = "", classification: str = ""
30
42
  ):
@@ -38,7 +50,7 @@ class lineageTreeManager:
38
50
  name (str, optional): Then name of. Defaults to "".
39
51
 
40
52
  """
41
- if isinstance(other_tree, lineageTree):
53
+ if isinstance(other_tree, lineageTree) and other_tree.time_resolution:
42
54
  for tree in self.lineagetrees.values():
43
55
  if tree == other_tree:
44
56
  return False
@@ -52,25 +64,15 @@ class lineageTreeManager:
52
64
  name = f"Lineagetree {next(self)}"
53
65
  self.lineagetrees[name] = other_tree
54
66
  self.lineagetrees[name].name = name
55
- # try:
56
- # name = other_tree.name
57
- # self.lineagetrees[name] = other_tree
58
- # except:
59
- # self.lineagetrees[
60
- # f"Lineagetree {next(self)}"
61
- # ] = other_tree
62
- # if classification in ("Wt", "Ptb"):
63
- # self.classification[type] = {name: other_tree}
67
+ # self.greatest_common_divisors[name] = gcd
68
+ else:
69
+ raise Exception(
70
+ "Please add a LineageTree object or add time resolution to the LineageTree added."
71
+ )
64
72
 
65
73
  def __add__(self, other):
66
74
  self.add(other)
67
75
 
68
- # def classify_existing(self, key, classification: str):
69
- # if classification in ("Wt", "Ptb"):
70
- # self.classification[classification] = {key: self.lineagetrees[key]}
71
- # else:
72
- # return False
73
-
74
76
  def write(self, fname: str):
75
77
  """Saves the manager
76
78
 
@@ -119,8 +121,8 @@ class lineageTreeManager:
119
121
  embryo_2,
120
122
  end_time2: int,
121
123
  style="fragmented",
122
- node_lengths: tuple = (1, 5, 7),
123
- registration=None,
124
+ downsample: int = 2,
125
+ registration=None, # will be added as a later feature
124
126
  ):
125
127
  """Compute the unordered tree edit distance from Zhang 1996 between the trees spawned
126
128
  by two nodes `n1` from lineagetree1 and `n2` lineagetree2. The topology of the trees
@@ -141,13 +143,13 @@ class lineageTreeManager:
141
143
  tree = tree_style[style].value
142
144
  tree1 = tree(
143
145
  lT=self.lineagetrees[embryo_1],
144
- node_length=node_lengths,
146
+ downsample=downsample,
145
147
  end_time=end_time1,
146
148
  root=n1,
147
149
  )
148
150
  tree2 = tree(
149
151
  lT=self.lineagetrees[embryo_2],
150
- node_length=node_lengths,
152
+ downsample=downsample,
151
153
  end_time=end_time2,
152
154
  root=n2,
153
155
  )
LineageTree/loaders.py CHANGED
@@ -1,11 +1,79 @@
1
1
  import csv
2
+ import os
2
3
  import pickle as pkl
3
4
  import xml.etree.ElementTree as ET
4
- import os
5
+
5
6
  import numpy as np
6
7
 
7
8
 
8
9
  class lineageTreeLoaders:
10
+
11
+ def read_from_csv(
12
+ self, file_path: str, z_mult: float, link: int = 1, delim: str = ","
13
+ ):
14
+ """
15
+ TODO: write doc
16
+ """
17
+ with open(file_path) as f:
18
+ lines = f.readlines()
19
+ f.close()
20
+ self.time_nodes = {}
21
+ self.time_edges = {}
22
+ unique_id = 0
23
+ self.nodes = set()
24
+ self.edges = set()
25
+ self.successor = {}
26
+ self.predecessor = {}
27
+ self.pos = {}
28
+ self.time_id = {}
29
+ self.time = {}
30
+ self.lin = {}
31
+ self.C_lin = {}
32
+ if not link:
33
+ self.displacement = {}
34
+ lines_to_int = []
35
+ corres = {}
36
+ for line in lines:
37
+ lines_to_int += [[eval(v.strip()) for v in line.split(delim)]]
38
+ lines_to_int = np.array(lines_to_int)
39
+ if link == 2:
40
+ lines_to_int = lines_to_int[np.argsort(lines_to_int[:, 0])]
41
+ else:
42
+ lines_to_int = lines_to_int[np.argsort(lines_to_int[:, 1])]
43
+ for line in lines_to_int:
44
+ if link == 1:
45
+ id_, t, z, y, x, pred, lin_id = line
46
+ elif link == 2:
47
+ t, z, y, x, id_, pred, lin_id = line
48
+ else:
49
+ id_, t, z, y, x, dz, dy, dx = line
50
+ pred = None
51
+ lin_id = None
52
+ t = int(t)
53
+ pos = np.array([x, y, z])
54
+ C = unique_id
55
+ corres[id_] = C
56
+ pos[-1] = pos[-1] * z_mult
57
+ if pred in corres:
58
+ M = corres[pred]
59
+ self.predecessor[C] = [M]
60
+ self.successor.setdefault(M, []).append(C)
61
+ self.edges.add((M, C))
62
+ self.time_edges.setdefault(t, set()).add((M, C))
63
+ self.lin.setdefault(lin_id, []).append(C)
64
+ self.C_lin[C] = lin_id
65
+ self.pos[C] = pos
66
+ self.nodes.add(C)
67
+ self.time_nodes.setdefault(t, set()).add(C)
68
+ # self.time_id[(t, cell_id)] = C
69
+ self.time[C] = t
70
+ if not link:
71
+ self.displacement[C] = np.array([dx, dy, dz * z_mult])
72
+ unique_id += 1
73
+ self.max_id = unique_id - 1
74
+ self.t_b = min(self.time_nodes)
75
+ self.t_e = max(self.time_nodes)
76
+
9
77
  def read_from_ASTEC(self, file_path: str, eigen: bool = False):
10
78
  """
11
79
  Read an `xml` or `pkl` file produced by the ASTEC algorithm.
@@ -7,14 +7,30 @@ from LineageTree import lineageTree
7
7
 
8
8
 
9
9
  class abstract_trees(ABC):
10
+ """Template class to produce different techniques to comapare lineageTrees.
11
+ To add a new technique you need to iherit this class or one of its children
12
+ and add them to the tree_style enum.
13
+ For a class to be valid you need a
14
+ - tree constructor (get_tree) that produces one dictionary that contains
15
+ arbitary unique labels and one dictionary that contains the duration of each node.
16
+ - delta function: A function that handles the cost of comparing nodes to each other.
17
+ - normalization function, a function that returns the length of the tree or any interger.
18
+ """
19
+
10
20
  def __init__(
11
- self, lT: lineageTree, root: int, node_length: int, end_time: int
21
+ self,
22
+ lT: lineageTree,
23
+ root: int,
24
+ downsample: int,
25
+ end_time: int = None,
26
+ time_scale: int = None,
12
27
  ):
13
- self.lT = lT
14
- self.root = root
15
- self.node_length = node_length
16
- self.end_time = end_time
17
- self.tree = self.get_tree()
28
+ self.lT: lineageTree = lT
29
+ self.root: int = root
30
+ self.downsample: int = downsample
31
+ self.end_time: int = end_time if end_time else self.lT.t_e
32
+ self.time_scale: int = time_scale if time_scale is not None else 1
33
+ self.tree: tuple = self.get_tree()
18
34
  self.edist = self._edist_format(self.tree[0])
19
35
 
20
36
  @abstractmethod
@@ -124,7 +140,7 @@ class mini_tree(abstract_trees):
124
140
  cycle = cycle[cycle_times <= self.end_time]
125
141
  if cycle.size:
126
142
  _next = self.lT[cycle[-1]]
127
- if len(_next) > 1:
143
+ if 1 < len(_next):
128
144
  out_dict[current] = _next
129
145
  to_do.extend(_next)
130
146
  else:
@@ -177,86 +193,76 @@ class simple_tree(abstract_trees):
177
193
  to_do.extend(_next)
178
194
  else:
179
195
  out_dict[current] = []
180
- self.times[current] = len(
181
- cycle
182
- ) # * time_resolution will be fixed when working on registered trees.
196
+ self.times[current] = len(cycle) * self.time_scale
183
197
  return out_dict, self.times
184
198
 
185
199
  def delta(self, x, y, corres1, corres2, times1, times2):
186
200
  return super().delta(x, y, corres1, corres2, times1, times2)
187
201
 
188
202
  def get_norm(self):
189
- return len(self.lT.get_sub_tree(self.root, end_time=self.end_time))
203
+ return (
204
+ len(self.lT.get_sub_tree(self.root, end_time=self.end_time))
205
+ * self.time_scale
206
+ )
190
207
 
191
208
 
192
- class fragmented_tree(abstract_trees):
193
- """Similar idea to simple tree, but tries to correct its flaws.
194
- Instead of having branches with length == life cycle of cell,nodes of specific length are added on the
195
- edges of the branch, providing both accuratr results and speed.
196
- It's the recommended method for calculating edit distances on developing embryos.
197
- """
209
+ class downsample_tree(abstract_trees):
210
+ """Downsamples a tree so every n nodes are being used as one."""
198
211
 
199
212
  def __init__(self, **kwargs):
200
213
  super().__init__(**kwargs)
201
214
 
202
215
  def get_tree(self):
203
- if self.end_time is None:
204
- self.end_time = self.lT.t_e
205
216
  self.out_dict = {}
206
217
  self.times = {}
207
218
  to_do = [self.root]
208
- if not isinstance(self.node_length, list):
209
- self.node_length = list(self.node_length)
210
219
  while to_do:
211
220
  current = to_do.pop()
212
- cycle = np.array(
213
- self.lT.get_successors(current, end_time=self.end_time)
221
+ _next = self.lT.get_cells_at_t_from_root(
222
+ current, self.lT.time[current] + self.downsample
214
223
  )
215
- if cycle.size > 0:
216
- cumul_sum_of_nodes = np.cumsum(self.node_length) * 2 + 1
217
- max_number_fragments = len(
218
- cumul_sum_of_nodes[cumul_sum_of_nodes < len(cycle)]
219
- )
220
- if max_number_fragments > 0:
221
- current_node_lengths = self.node_length[
222
- :max_number_fragments
223
- ].copy()
224
- length_middle_node = (
225
- len(cycle) - sum(current_node_lengths) * 2
226
- )
227
- times_tmp = (
228
- current_node_lengths
229
- + [length_middle_node]
230
- + current_node_lengths[::-1]
231
- )
232
- pos_all_nodes = np.concatenate(
233
- [[0], np.cumsum(times_tmp[:-1])]
234
- )
235
- track = cycle[pos_all_nodes]
236
- self.out_dict.update(
237
- {k: [v] for k, v in zip(track[:-1], track[1:])}
238
- )
239
- self.times.update(zip(track, times_tmp))
240
- else:
241
- for i, cell in enumerate(cycle[:-1]):
242
- self.out_dict[cell] = [cycle[i + 1]]
243
- self.times[cell] = 1
244
- current = cycle[-1]
245
- _next = self.lT[current]
246
- self.times[current] = 1
247
- if _next and self.lT.time[_next[0]] <= self.end_time:
248
- to_do.extend(_next)
249
- self.out_dict[current] = _next
250
- else:
251
- self.out_dict[current] = []
252
-
224
+ if _next == [current]:
225
+ _next = None
226
+ if _next and self.lT.time[_next[0]] <= self.end_time:
227
+ self.out_dict[current] = _next
228
+ to_do.extend(_next)
229
+ else:
230
+ self.out_dict[current] = []
231
+ self.times[current] = self.downsample
253
232
  return self.out_dict, self.times
254
233
 
255
234
  def get_norm(self):
256
- return len(self.lT.get_sub_tree(self.root, end_time=self.end_time))
235
+ return sum(self.times.values())
257
236
 
258
237
  def delta(self, x, y, corres1, corres2, times1, times2):
259
- return super().delta(x, y, corres1, corres2, times1, times2)
238
+ if x is None and y is None:
239
+ return 0
240
+ if x is None:
241
+ return self.downsample
242
+ if y is None:
243
+ return self.downsample
244
+ return 0
245
+
246
+
247
+ class normalized_simple_tree(simple_tree):
248
+ def __init__(self, **kwargs):
249
+ super().__init__(**kwargs)
250
+
251
+ def delta(self, x, y, corres1, corres2, times1, times2):
252
+ if x is None and y is None:
253
+ return 0
254
+ if x is None:
255
+ return 1
256
+ if y is None:
257
+ return 1
258
+ return abs(times1[corres1[x]] - times2[corres2[y]]) / (
259
+ times1[corres1[x]] + times2[corres2[y]]
260
+ )
261
+
262
+ def get_norm(self):
263
+ return len(
264
+ self.lT.get_all_branches_of_node(self.root, end_time=self.end_time)
265
+ )
260
266
 
261
267
 
262
268
  class full_tree(abstract_trees):
@@ -276,6 +282,11 @@ class full_tree(abstract_trees):
276
282
  current = to_do.pop()
277
283
  _next = self.lT.successor.get(current, [])
278
284
  if _next and self.lT.time[_next[0]] <= self.end_time:
285
+ if self.time_scale > 1:
286
+ for _ in range(self.time_scale):
287
+ next_id = self.lT.get_next_id()
288
+ self.out_dict[current] = next_id
289
+ current = next_id
279
290
  self.out_dict[current] = _next
280
291
  to_do.extend(_next)
281
292
  else:
@@ -284,16 +295,26 @@ class full_tree(abstract_trees):
284
295
  return self.out_dict, self.times
285
296
 
286
297
  def get_norm(self):
287
- return len(self.lT.get_sub_tree(self.root, end_time=self.end_time))
298
+ return (
299
+ len(self.lT.get_sub_tree(self.root, end_time=self.end_time))
300
+ * self.time_scale
301
+ )
288
302
 
289
303
  def delta(self, x, y, corres1, corres2, times1, times2):
290
- return super().delta(x, y, corres1, corres2, times1, times2)
304
+ if x is None and y is None:
305
+ return 0
306
+ if x is None:
307
+ return 1
308
+ if y is None:
309
+ return 1
310
+ return 0
291
311
 
292
312
 
293
313
  class tree_style(Enum):
294
314
  mini = mini_tree
295
315
  simple = simple_tree
296
- fragmented = fragmented_tree
316
+ normalized_simple = normalized_simple_tree
317
+ downsampled = downsample_tree
297
318
  full = full_tree
298
319
 
299
320
  @classmethod
LineageTree/utils.py CHANGED
@@ -1,8 +1,6 @@
1
1
  import csv
2
- import random
3
2
  import warnings
4
3
 
5
-
6
4
  from LineageTree import lineageTree
7
5
 
8
6
  try:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: LineageTree
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: Lineage tree structure
5
5
  Home-page: https://github.com/leoguignard/LineageTree
6
6
  Author: Léo Guignard
@@ -28,14 +28,6 @@ Requires-Dist: scipy>=1.9
28
28
  Requires-Dist: numpy>=1.23
29
29
  Requires-Dist: mastodon-reader
30
30
  Requires-Dist: matplotlib
31
- Provides-Extra: all
32
- Requires-Dist: svgwrite; extra == "all"
33
- Requires-Dist: matplotlib; extra == "all"
34
- Requires-Dist: tox; extra == "all"
35
- Requires-Dist: pytest; extra == "all"
36
- Requires-Dist: pytest-cov; extra == "all"
37
- Requires-Dist: edist; extra == "all"
38
- Requires-Dist: mastodon-reader; extra == "all"
39
31
  Provides-Extra: svg
40
32
  Requires-Dist: svgwrite; extra == "svg"
41
33
  Requires-Dist: matplotlib; extra == "svg"
@@ -48,6 +40,14 @@ Requires-Dist: pytest-cov; extra == "test"
48
40
  Requires-Dist: mastodon-reader; extra == "test"
49
41
  Provides-Extra: treeedit
50
42
  Requires-Dist: edist; extra == "treeedit"
43
+ Provides-Extra: all
44
+ Requires-Dist: svgwrite; extra == "all"
45
+ Requires-Dist: matplotlib; extra == "all"
46
+ Requires-Dist: tox; extra == "all"
47
+ Requires-Dist: pytest; extra == "all"
48
+ Requires-Dist: pytest-cov; extra == "all"
49
+ Requires-Dist: edist; extra == "all"
50
+ Requires-Dist: mastodon-reader; extra == "all"
51
51
 
52
52
  # LineageTree
53
53
 
@@ -0,0 +1,11 @@
1
+ LineageTree/__init__.py,sha256=gRnB16cDWVXa8GC6errEK0JxnllNypanG8HOJeO9mgo,155
2
+ LineageTree/lineageTree.py,sha256=WLpKDzlt0vDL4zMOuyEsWYiLgNJcX8qMORgD0wPwQJg,107536
3
+ LineageTree/lineageTreeManager.py,sha256=OFcW4DmUtsPQb6dw1jKy3EPezs4UCQ666-Ki3IYuzbc,5641
4
+ LineageTree/loaders.py,sha256=D5hE928OD2RUGwG8CBW-69V8tp-k-VyNCm4I9AmtK2A,25976
5
+ LineageTree/tree_styles.py,sha256=U4mkcAYCK1WjWT3rdP1Hc-KxNXqEnwgZy0HAxTsUNpE,10746
6
+ LineageTree/utils.py,sha256=nhOThtLVA29cvt9SwrC9LE7kf6DZMaBr0OC7_hZf3Rk,4947
7
+ LineageTree-1.7.0.dist-info/LICENSE,sha256=IKncNCNpq93Kq7Ywg1V4I9Bu_99VMK-mX1EJyjTKLyc,1068
8
+ LineageTree-1.7.0.dist-info/METADATA,sha256=rdZOPPbmNa5YRbjOhLpLJPEeDPuFR4eHcPfOiXyud18,4195
9
+ LineageTree-1.7.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
10
+ LineageTree-1.7.0.dist-info/top_level.txt,sha256=CCqPCTX76zPTEUagUgTWbLaZun8752n59iOOwfUlvxs,12
11
+ LineageTree-1.7.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- LineageTree/__init__.py,sha256=P4WGjjokRD5ugh27WC263Pwll1PmMdLgHyshMVLNvIc,155
2
- LineageTree/lineageTree.py,sha256=srhZ8UI4iBsKmSEIVhn0OpKuTSyrE6npEMxjj4v666M,102657
3
- LineageTree/lineageTreeManager.py,sha256=tQMNmxAvUEd4fNLJViB-9DGItpgCRnSF-bS2_IdebMo,5566
4
- LineageTree/loaders.py,sha256=hKd-69d_csjcsV0zabSTEOyy0NhprFqvnRAI18tYa9w,23718
5
- LineageTree/tree_styles.py,sha256=IUEq8RHfwXTv3WXylpLVa5XoThaWt1i1cRsgZGXdd50,10634
6
- LineageTree/utils.py,sha256=kC90fi8TFm8Pz7oom_bDsq7HPPKnFdJ6wI-x36fLvHE,4962
7
- LineageTree-1.6.0.dist-info/LICENSE,sha256=IKncNCNpq93Kq7Ywg1V4I9Bu_99VMK-mX1EJyjTKLyc,1068
8
- LineageTree-1.6.0.dist-info/METADATA,sha256=XlCCU5B8WqbKT0EiGqgf-kvVFH5D2aSWQzygUCc3YT4,4195
9
- LineageTree-1.6.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
10
- LineageTree-1.6.0.dist-info/top_level.txt,sha256=CCqPCTX76zPTEUagUgTWbLaZun8752n59iOOwfUlvxs,12
11
- LineageTree-1.6.0.dist-info/RECORD,,