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 +66 -0
- pycellin/classes/__init__.py +24 -0
- pycellin/classes/data.py +263 -0
- pycellin/classes/exceptions.py +107 -0
- pycellin/classes/feature.py +812 -0
- pycellin/classes/feature_calculator.py +431 -0
- pycellin/classes/lineage.py +1323 -0
- pycellin/classes/model.py +1708 -0
- pycellin/classes/updater.py +192 -0
- pycellin/custom_types.py +18 -0
- pycellin/graph/__init__.py +0 -0
- pycellin/graph/features/__init__.py +14 -0
- pycellin/graph/features/morphology.py +587 -0
- pycellin/graph/features/motion.py +473 -0
- pycellin/graph/features/tracking.py +331 -0
- pycellin/graph/features/utils.py +52 -0
- pycellin/io/__init__.py +4 -0
- pycellin/io/cell_tracking_challenge/__init__.py +2 -0
- pycellin/io/cell_tracking_challenge/exporter.py +286 -0
- pycellin/io/cell_tracking_challenge/loader.py +305 -0
- pycellin/io/trackmate/__init__.py +2 -0
- pycellin/io/trackmate/exporter.py +587 -0
- pycellin/io/trackmate/loader.py +1234 -0
- pycellin/utils.py +10 -0
- pycellin-0.3.5b3.dist-info/LICENSE +27 -0
- pycellin-0.3.5b3.dist-info/METADATA +124 -0
- pycellin-0.3.5b3.dist-info/RECORD +29 -0
- pycellin-0.3.5b3.dist-info/WHEEL +5 -0
- pycellin-0.3.5b3.dist-info/top_level.txt +1 -0
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
|
+
)
|
pycellin/classes/data.py
ADDED
|
@@ -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
|