pycellin 0.3.5b3__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.
pycellin/__init__.py ADDED
@@ -0,0 +1,66 @@
1
+ from .classes.data import Data
2
+ from .classes.lineage import CellLineage, CycleLineage
3
+ from .classes.feature import FeaturesDeclaration, Feature
4
+ from .classes.feature import (
5
+ frame_Feature,
6
+ cell_ID_Feature,
7
+ lineage_ID_Feature,
8
+ cell_coord_Feature,
9
+ link_coord_Feature,
10
+ lineage_coord_Feature,
11
+ cycle_ID_Feature,
12
+ cells_Feature,
13
+ cycle_length_Feature,
14
+ level_Feature,
15
+ )
16
+ from .classes.model import Model
17
+ from .classes.feature_calculator import (
18
+ NodeLocalFeatureCalculator,
19
+ EdgeLocalFeatureCalculator,
20
+ LineageLocalFeatureCalculator,
21
+ NodeGlobalFeatureCalculator,
22
+ EdgeGlobalFeatureCalculator,
23
+ LineageGlobalFeatureCalculator,
24
+ )
25
+
26
+ from .io.cell_tracking_challenge.loader import load_CTC_file
27
+ from .io.cell_tracking_challenge.exporter import export_CTC_file
28
+ from .io.trackmate.loader import load_TrackMate_XML
29
+ from .io.trackmate.exporter import export_TrackMate_XML
30
+
31
+ from .graph.features.utils import (
32
+ get_pycellin_cell_lineage_features,
33
+ get_pycellin_cycle_lineage_features,
34
+ )
35
+
36
+
37
+ __all__ = [
38
+ "Data",
39
+ "CellLineage",
40
+ "CycleLineage",
41
+ "FeaturesDeclaration",
42
+ "Feature",
43
+ "frame_Feature",
44
+ "cell_ID_Feature",
45
+ "lineage_ID_Feature",
46
+ "cell_coord_Feature",
47
+ "link_coord_Feature",
48
+ "lineage_coord_Feature",
49
+ "cycle_ID_Feature",
50
+ "cells_Feature",
51
+ "cycle_length_Feature",
52
+ "level_Feature",
53
+ "Model",
54
+ "NodeLocalFeatureCalculator",
55
+ "EdgeLocalFeatureCalculator",
56
+ "LineageLocalFeatureCalculator",
57
+ "NodeGlobalFeatureCalculator",
58
+ "EdgeGlobalFeatureCalculator",
59
+ "LineageGlobalFeatureCalculator",
60
+ "load_CTC_file",
61
+ "export_CTC_file",
62
+ "load_TrackMate_XML",
63
+ "export_TrackMate_XML",
64
+ "get_pycellin_cell_lineage_features",
65
+ "get_pycellin_cycle_lineage_features",
66
+ ]
@@ -0,0 +1,24 @@
1
+ from .data import Data
2
+ from .lineage import CellLineage, CycleLineage
3
+ from .feature import FeaturesDeclaration, Feature
4
+ from .feature import (
5
+ frame_Feature,
6
+ cell_ID_Feature,
7
+ lineage_ID_Feature,
8
+ cell_coord_Feature,
9
+ link_coord_Feature,
10
+ lineage_coord_Feature,
11
+ cycle_ID_Feature,
12
+ cells_Feature,
13
+ cycle_length_Feature,
14
+ level_Feature,
15
+ )
16
+ from .model import Model
17
+ from .feature_calculator import (
18
+ NodeLocalFeatureCalculator,
19
+ EdgeLocalFeatureCalculator,
20
+ LineageLocalFeatureCalculator,
21
+ NodeGlobalFeatureCalculator,
22
+ EdgeGlobalFeatureCalculator,
23
+ LineageGlobalFeatureCalculator,
24
+ )
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import math
5
+ from typing import Literal
6
+ import warnings
7
+
8
+ import networkx as nx
9
+
10
+ from pycellin.classes.lineage import Lineage, CellLineage, CycleLineage
11
+
12
+
13
+ class Data:
14
+ """
15
+ Class to store and manipulate cell lineages and cell cycle lineages.
16
+ """
17
+
18
+ def __init__(
19
+ self, data: dict[str, CellLineage], add_cycle_data: bool = False
20
+ ) -> None:
21
+ self.cell_data = data
22
+ if add_cycle_data:
23
+ self._compute_cycle_lineages()
24
+ else:
25
+ self.cycle_data = None
26
+
27
+ def __repr__(self) -> str:
28
+ return f"Data(cell_data={self.cell_data!r}, cycle_data={self.cycle_data!r})"
29
+
30
+ def __str__(self) -> str:
31
+ if self.cycle_data:
32
+ txt = f" and {self.number_of_lineages()} cycle lineages"
33
+ else:
34
+ txt = ""
35
+ return f"Data object with {self.number_of_lineages()} cell lineages{txt}."
36
+
37
+ def _add_cycle_lineages(self, lineage_IDs: list[int] | None = None) -> None:
38
+ """
39
+ Add the cell cycle lineages from the cell lineages.
40
+
41
+ Parameters
42
+ ----------
43
+ lineage_IDs : list[int], optional
44
+ The IDs of the lineages to compute the cycle lineages for,
45
+ by default None i.e. all lineages.
46
+ """
47
+ if lineage_IDs is None:
48
+ lineage_IDs = list(self.cell_data.keys())
49
+ self.cycle_data = {
50
+ lin_id: self._compute_cycle_lineage(lin_id) for lin_id in lineage_IDs
51
+ }
52
+
53
+ def _compute_cycle_lineage(self, lineage_ID: int) -> CycleLineage:
54
+ """
55
+ Compute and return the cycle lineage corresponding to a given cell lineage.
56
+
57
+ Parameters
58
+ ----------
59
+ lineage_ID : int
60
+ The ID of the cell lineage.
61
+
62
+ Returns
63
+ -------
64
+ CycleLineage
65
+ The cycle lineage corresponding to the cell lineage.
66
+ """
67
+ return CycleLineage(self.cell_data[lineage_ID])
68
+
69
+ def _freeze_lineage_data(self):
70
+ """
71
+ Freeze all cell lineages.
72
+
73
+ When a cell lineage is frozen, its structure cannot be modified:
74
+ nodes and edges cannot be added or removed. However, graph, node and edge
75
+ attributes can still be modified.
76
+ """
77
+ for lineage in self.cell_data.values():
78
+ if not nx.is_frozen(lineage):
79
+ nx.freeze(lineage)
80
+
81
+ def _unfreeze_lineage_data(self):
82
+ """
83
+ Unfreeze all cell lineages.
84
+ """
85
+ for lineage in self.cell_data.values():
86
+ Lineage.unfreeze(lineage)
87
+
88
+ def number_of_lineages(self) -> int:
89
+ """
90
+ Return the number of lineages in the data.
91
+
92
+ Returns
93
+ -------
94
+ int
95
+ The number of cell lineages in the data.
96
+
97
+ Raises
98
+ ------
99
+ Warning
100
+ If the number of cell lineages and cycle lineages do not match.
101
+ """
102
+ if self.cycle_data:
103
+ if len(self.cell_data) != len(self.cycle_data):
104
+ msg = (
105
+ f"Number of cell lineages ({len(self.cell_data)}) "
106
+ f"and cycle lineages ({len(self.cycle_data)}) do not match. "
107
+ "An update of the model is required. "
108
+ )
109
+ warnings.warn(msg)
110
+ return len(self.cell_data)
111
+
112
+ def get_closest_cell(
113
+ self,
114
+ noi: int,
115
+ lineage: CellLineage,
116
+ radius: float = 0,
117
+ time_window: int = 0,
118
+ time_window_type: Literal["before", "after", "symetric"] = "symetric",
119
+ lineages_to_search: list[CellLineage] = None,
120
+ reference: Literal["center", "border"] = "center",
121
+ ) -> tuple[CellLineage, int]:
122
+ """
123
+ Find the closest cell to a given cell of a lineage.
124
+
125
+ Parameters
126
+ ----------
127
+ noi : int
128
+ Node of interest, the one for which to find the closest cell.
129
+ lineage : CellLineage
130
+ The lineage the node belongs to.
131
+ radius : float, optional
132
+ The maximum distance to consider, by default 0.
133
+ If 0, the whole space is considered.
134
+ time_window : int, optional
135
+ The time window to consider, by default 0 i.e. only the current frame.
136
+ time_window_type : Literal["before", "after", "symetric"], optional
137
+ The type of time window to consider, by default "symetric".
138
+ lineages_to_search : list[CellLineage], optional
139
+ The lineages to search in, by default None i.e. all lineages.
140
+ reference : Literal["center", "border"], optional
141
+ The reference point to consider for the distance, by default "center".
142
+
143
+ Returns
144
+ -------
145
+ tuple[int, CellLineage]
146
+ The node ID of the closest cell and the lineage it belongs to.
147
+ """
148
+ distances = self.get_closest_cells(
149
+ noi=noi,
150
+ lineage=lineage,
151
+ radius=radius,
152
+ time_window=time_window,
153
+ time_window_type=time_window_type,
154
+ lineages_to_search=lineages_to_search,
155
+ reference=reference,
156
+ )
157
+ return distances[0]
158
+
159
+ def get_closest_cells(
160
+ self,
161
+ noi: int,
162
+ lineage: CellLineage,
163
+ radius: float = 0,
164
+ time_window: int = 0,
165
+ time_window_type: Literal["before", "after", "symetric"] = "symetric",
166
+ lineages_to_search: list[CellLineage] = None,
167
+ reference: Literal["center", "border"] = "center",
168
+ ) -> list[tuple[int, CellLineage]]:
169
+ """
170
+ Find the closest cells to a given cell of a lineage.
171
+
172
+ Parameters
173
+ ----------
174
+ noi : int
175
+ Node of interest, the one for which to find the closest cell.
176
+ lineage : CellLineage
177
+ The lineage the node belongs to.
178
+ radius : float, optional
179
+ The maximum distance to consider, by default 0.
180
+ If 0, the whole space is considered.
181
+ time_window : int, optional
182
+ The time window to consider, by default 0 i.e. only the current frame.
183
+ time_window_type : Literal["before", "after", "symetric"], optional
184
+ The type of time window to consider, by default "symetric".
185
+ lineages_to_search : list[CellLineage], optional
186
+ The lineages to search in, by default None i.e. all lineages.
187
+ reference : Literal["center", "border"], optional
188
+ The reference point to consider for the distance, by default "center".
189
+
190
+ Returns
191
+ -------
192
+ tuple[int, CellLineage]
193
+ The node ID of the closest cells and the lineages it belongs to,
194
+ sorted by increasing distance.
195
+ """
196
+ # TODO: implement the reference parameter
197
+
198
+ # Identification of the frames to search in.
199
+ center_frame = lineage.nodes[noi]["frame"]
200
+ if time_window == 0:
201
+ frames_to_search = [center_frame]
202
+ else:
203
+ if time_window_type == "symetric":
204
+ frames_to_search = list(
205
+ range(center_frame - time_window, center_frame + time_window + 1)
206
+ )
207
+ elif time_window_type == "before":
208
+ frames_to_search = list(
209
+ range(center_frame - time_window, center_frame + 1)
210
+ )
211
+ elif time_window_type == "after":
212
+ frames_to_search = list(
213
+ range(center_frame, center_frame + time_window + 1)
214
+ )
215
+ else:
216
+ raise ValueError(
217
+ f"Unknown time window type: '{time_window_type}'."
218
+ " Should be 'before', 'after' or 'symetric'."
219
+ )
220
+ frames_to_search.sort()
221
+
222
+ # Identification of nodes that are good candidates,
223
+ # i.e. nodes that are in the time window
224
+ # and in the lineages to search in.
225
+ if not lineages_to_search:
226
+ lineages_to_search = list(self.cell_data.values())
227
+ candidate_cells = {}
228
+ for lin in lineages_to_search:
229
+ nodes = [
230
+ node
231
+ for node, frame in lin.nodes(data="frame")
232
+ if frame in frames_to_search
233
+ ]
234
+ if nodes:
235
+ candidate_cells[lin] = nodes
236
+ # Need to remove the node itself from the candidates.
237
+ candidate_cells[lineage].remove(noi)
238
+
239
+ # Identification of the closest cell.
240
+ distances = []
241
+ for lin, nodes in candidate_cells.items():
242
+ for node in nodes:
243
+ distance = math.dist(
244
+ lineage.nodes[noi]["location"], lin.nodes[node]["location"]
245
+ )
246
+ if radius == 0 or distance <= radius:
247
+ distances.append((node, lin, distance))
248
+ distances.sort(key=lambda x: x[2])
249
+ return [(node, lin) for node, lin, _ in distances]
250
+
251
+ # def get_neighbouring_cells(
252
+ # lineage: CellLineage,
253
+ # node: int,
254
+ # radius: float,
255
+ # time_window: int | tuple[int, int],
256
+ # ) -> list[tuple[CellLineage, int]]:
257
+ # """ """
258
+ # # TODO: implement get_neighbouring_cells()
259
+ # # Parameter to define sort order? By default closest to farthest
260
+ # # Need to implement get_distance() between 2 nodes, not necessarily
261
+ # # from the same lineage...
262
+ # # To identify a node, need to have lineage_ID and cell_ID
263
+ # pass
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # TODO: add a Warning when a feature is not present across all cells,
5
+ # links, or lineages?
6
+
7
+
8
+ class LineageStructureError(Exception):
9
+ """
10
+ Raised when an incorrect lineage structure is detected.
11
+ """
12
+
13
+ def __init__(self, message):
14
+ self.message = message
15
+ super().__init__(self.message)
16
+
17
+
18
+ class FusionError(LineageStructureError):
19
+ """
20
+ Raised when a fusion event is detected in the lineage structure.
21
+
22
+ A fusion event happens when a node has more than one parent,
23
+ i. e. an in_degree greater than 1.
24
+
25
+ Parameters
26
+ ----------
27
+ node_ID : int
28
+ The ID of the node where the fusion event was detected.
29
+ lineage_ID : int, optional
30
+ The ID of the lineage where the fusion event was detected.
31
+ None by default.
32
+ message : str, optional
33
+ The error message to display.
34
+ If not provided, a default message is displayed.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ node_ID: int,
40
+ lineage_ID: int | None = None,
41
+ message: str = None,
42
+ ):
43
+ self.node_ID = node_ID
44
+ self.lineage_ID = lineage_ID
45
+ if message is None:
46
+ if lineage_ID is None:
47
+ message = f"Node {node_ID} already has a parent node."
48
+ else:
49
+ message = (
50
+ f"Node {node_ID} in lineage {lineage_ID} already has "
51
+ f"a parent node."
52
+ )
53
+ super().__init__(message)
54
+
55
+
56
+ class TimeFlowError(LineageStructureError):
57
+ """
58
+ Raised when a time flow error is detected in the lineage structure.
59
+
60
+ In a lineage graph, time flows from the root of the graph to the leaves.
61
+ As a result, a node should always have a time value greater than its parent.
62
+
63
+ Parameters
64
+ ----------
65
+ source_noi : int
66
+ The ID of the source node.
67
+ target_noi : int
68
+ The ID of the target node.
69
+ source_lineage_ID : int, optional
70
+ The ID of the lineage of the source node.
71
+ None by default.
72
+ target_lineage_ID : int, optional
73
+ The ID of the lineage of the target node.
74
+ None by default.
75
+ message : str, optional
76
+ The error message to display.
77
+ If not provided, a default message is displayed.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ source_noi: int,
83
+ target_noi: int,
84
+ source_lineage_ID: int | None = None,
85
+ target_lineage_ID: int | None = None,
86
+ message: str = None,
87
+ ):
88
+ self.source_noi = source_noi
89
+ self.source_lineage_ID = source_lineage_ID
90
+ self.target_noi = target_noi
91
+ self.target_lineage_ID = target_lineage_ID
92
+ if message is None:
93
+ txt_source_lin = (
94
+ "" if source_lineage_ID is None else f" in lineage {source_lineage_ID}"
95
+ )
96
+ txt_target_lin = (
97
+ "" if target_lineage_ID is None else f" in lineage {target_lineage_ID}"
98
+ )
99
+ message = (
100
+ f"Node {target_noi}{txt_target_lin} "
101
+ f"has a time value lower than its parent node, "
102
+ f"node {source_noi}{txt_source_lin}."
103
+ )
104
+ super().__init__(message)
105
+
106
+
107
+ # TODO: maybe add a FeatureTypeValueError and a LineageTypeValueError