From b62ec95c535d224ba47546422e35f0b4b6b4198f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Fri, 25 Jul 2025 16:32:12 -0400 Subject: [PATCH 01/27] Add geff files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b7fdf30..3fbd880 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ cov.xml # Data sample_data/FakeTracks.tif +sample_data/**.geff From e409be5b726ef99f108dfa9ee0bd7f96c9a375cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Fri, 25 Jul 2025 16:32:39 -0400 Subject: [PATCH 02/27] Add folder structure for geff io --- pycellin/io/geff/__init__.py | 0 pycellin/io/geff/exporter.py | 44 +++++++++++++++++++++ pycellin/io/geff/loader.py | 76 ++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 pycellin/io/geff/__init__.py create mode 100644 pycellin/io/geff/exporter.py create mode 100644 pycellin/io/geff/loader.py diff --git a/pycellin/io/geff/__init__.py b/pycellin/io/geff/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycellin/io/geff/exporter.py b/pycellin/io/geff/exporter.py new file mode 100644 index 0000000..cf0d548 --- /dev/null +++ b/pycellin/io/geff/exporter.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +exporter.py + +References: +- geff GitHub: https://github.com/live-image-tracking-tools/geff +- geff Documentation: https://live-image-tracking-tools.github.io/geff/latest/ +""" + +import geff + + +if __name__ == "__main__": + # xml_in = "sample_data/Ecoli_growth_on_agar_pad.xml" + # xml_in = "sample_data/FakeTracks.xml" + ctc_in = "sample_data/FakeTracks_TMtoCTC.txt" + # ctc_in = "sample_data/Ecoli_growth_on_agar_pad_TMtoCTC.txt" + geff_out = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" + + from pycellin.io.trackmate.loader import load_TrackMate_XML + from pycellin.io.cell_tracking_challenge.loader import load_CTC_file + + # model = load_TrackMate_XML(xml_in) + # model.remove_feature("ROI_coords") + model = load_CTC_file(ctc_in) + print(model) + print(model.get_cell_lineage_features().keys()) + print(model.data.cell_data.keys()) + lin1 = model.data.cell_data[1] + print(lin1) + print(lin1.nodes[77]) + # lin4 = model.data.cell_data[4] + # print(lin4) + + geff.write_nx( + model.data.cell_data[1], + # model.data.cell_data[4], + geff_out, + # axis_names=["cell_x", "cell_y", "cell_z", "frame"], + # axis_units=["um", "um", "um", "s"], + # zarr_format=2, + ) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py new file mode 100644 index 0000000..1b1a3ff --- /dev/null +++ b/pycellin/io/geff/loader.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +loader.py + +References: +- geff GitHub: https://github.com/live-image-tracking-tools/geff +- geff Documentation: https://live-image-tracking-tools.github.io/geff/latest/ +""" + +from datetime import datetime +from importlib.metadata import version +from pathlib import Path +from typing import Any + +import geff +import networkx as nx + +from pycellin.classes import Model + + +def load_geff_file(geff_file: Path | str) -> Model: + """ + Load a geff file and return a pycellin model containing the data. + + Parameters + ---------- + geff_file : Path | str + Path to the geff file to load. + + Returns + ------- + Model + A pycellin model containing the data from the geff file. + """ + pass + + # Read the geff file + # Check for fusions + # Extract and dispatch metadata + # Rename features to match pycellin conventions + # Split the graph into lineages + # Return the model + + +if __name__ == "__main__": + geff_file = ( + "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/mouse-20250719.zarr/tracks" + ) + # geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" + + geff_graph, geff_md = geff.read_nx(geff_file, validate=True) + print(geff_graph) + # Check how many weakly connected components there are. + print( + f"Number of weakly connected components: {len(list(nx.weakly_connected_components(geff_graph)))}" + ) + for k, v in geff_graph.graph.items(): + print(f"{k}: {v}") + # print(graph.graph["axes"][0].unit) + + if geff_md.directed: + print("The graph is directed.") + + metadata = {} # type: dict[str, Any] + metadata["name"] = Path(geff_file).stem + metadata["file_location"] = geff_file + metadata["provenance"] = "geff" + metadata["date"] = str(datetime.now()) + # metadata["space_unit"] = + # metadata["time_unit"] = + metadata["pycellin_version"] = version("pycellin") + metadata["geff_version"] = geff_md.geff_version + for md in geff_md: + print(md) From 341f1eff3568e478dd5b8030780ea5b7eef76337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Fri, 25 Jul 2025 16:43:48 -0400 Subject: [PATCH 03/27] Put explicit units in TM XML files --- sample_data/Ecoli_growth_on_agar_pad.xml | 2 +- sample_data/FakeTracks.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sample_data/Ecoli_growth_on_agar_pad.xml b/sample_data/Ecoli_growth_on_agar_pad.xml index 01cdca0..191970b 100644 --- a/sample_data/Ecoli_growth_on_agar_pad.xml +++ b/sample_data/Ecoli_growth_on_agar_pad.xml @@ -330,7 +330,7 @@ Computing track features: - Track quality in 0 ms. - Track motility analysis in 1 ms. Computation done in 6 ms. - + diff --git a/sample_data/FakeTracks.xml b/sample_data/FakeTracks.xml index dfbc76c..ed81f7a 100644 --- a/sample_data/FakeTracks.xml +++ b/sample_data/FakeTracks.xml @@ -306,7 +306,7 @@ Computing track features: - Track quality in 0 ms. - Track motility analysis in 1 ms. Computation done in 5 ms. - + From 37e0030019497bdce5599426c7e7d7063743521e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Fri, 25 Jul 2025 17:56:32 -0400 Subject: [PATCH 04/27] Move check_fusions() to utils.py This function will be used by different loaders. --- pycellin/io/utils.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 pycellin/io/utils.py diff --git a/pycellin/io/utils.py b/pycellin/io/utils.py new file mode 100644 index 0000000..643fca2 --- /dev/null +++ b/pycellin/io/utils.py @@ -0,0 +1,28 @@ +import warnings + +from pycellin.classes import Model + + +def check_fusions(model: Model) -> None: + """ + Check if the model contains fusions and issue a warning if so. + + Parameters + ---------- + model : Model + The pycellin model to check for fusions. + + Returns + ------- + None + """ + all_fusions = model.get_fusions() + if all_fusions: + # TODO: link toward correct documentation when it is written. + fusion_warning = ( + f"Unsupported data, {len(all_fusions)} cell fusions detected. " + "It is advised to deal with them before any other processing, " + "especially for tracking related features. Crashes and incorrect " + "results can occur. See documentation for more details." + ) + warnings.warn(fusion_warning) From 99632dfca215a4def8a4a2f2f9580ff1f45d195f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Sat, 26 Jul 2025 14:31:37 -0400 Subject: [PATCH 05/27] Add pytest.ini_options --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c81b896..ff870f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,4 +53,8 @@ packages = [ "pycellin.io.cell_tracking_challenge", "pycellin.io.trackmate", "pycellin.io.trackpy", -] \ No newline at end of file +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["tests"] \ No newline at end of file From 558df056cc0cb66fa2429aa418d560de1de37114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Sat, 26 Jul 2025 14:36:43 -0400 Subject: [PATCH 06/27] Rename and move common functions in loaders and tests --- pycellin/io/trackmate/loader.py | 155 +++--------------- pycellin/io/utils.py | 111 ++++++++++++- pycellin/utils.py | 66 +++++++- tests/io/test_utils.py | 234 +++++++++++++++++++++++++++ tests/io/trackmate/test_loader.py | 253 ++---------------------------- 5 files changed, 449 insertions(+), 370 deletions(-) create mode 100644 tests/io/test_utils.py diff --git a/pycellin/io/trackmate/loader.py b/pycellin/io/trackmate/loader.py index 760f143..78ffb91 100644 --- a/pycellin/io/trackmate/loader.py +++ b/pycellin/io/trackmate/loader.py @@ -1,20 +1,24 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +import importlib +import warnings from copy import deepcopy from datetime import datetime -import importlib from pathlib import Path from typing import Any -import warnings -from lxml import etree as ET import networkx as nx +from lxml import etree as ET -from pycellin.classes import Model -from pycellin.classes import FeaturesDeclaration, Feature, cell_ID_Feature -from pycellin.classes import Data -from pycellin.classes import CellLineage +from pycellin.classes import ( + CellLineage, + Data, + Feature, + FeaturesDeclaration, + Model, + cell_ID_Feature, +) +from pycellin.io.utils import _split_graph_into_lineages, check_fusions def _get_units( @@ -532,7 +536,6 @@ def _build_tracks( current_track_id = None event, element = next(iterator) while (event, element) != ("end", ancestor): - # Saving the current track information. if element.tag == "Track" and event == "start": attribs = deepcopy(element.attrib) @@ -614,103 +617,6 @@ def _get_filtered_tracks_ID( return filtered_tracks_ID -def _add_tracks_info( - lineages: list[CellLineage], - tracks_attributes: list[dict[str, Any]], -) -> None: - """ - Update each CellLineage in the list with corresponding track attributes. - - This function iterates over a list of CellLineage objects, - attempting to match each lineage with its corresponding track - attributes based on the 'TRACK_ID' attribute present in the - lineage nodes. It then updates the lineage graph with these - attributes. - - Parameters - ---------- - lineages : list[CellLineage] - A list of the lineages to update. - tracks_attributes : list[dict[str, Any]] - A list of dictionaries, where each dictionary contains - attributes for a specific track, identified by a 'TRACK_ID' key. - - Raises - ------ - ValueError - If a lineage is found to contain nodes with multiple distinct - 'TRACK_ID' values, indicating an inconsistency in track ID - assignment. - """ - for lin in lineages: - # Finding the dict of attributes matching the track. - tmp = set(t_id for _, t_id in lin.nodes(data="TRACK_ID")) - - if not tmp: - # 'tmp' is empty because there's no nodes in the current graph. - # Even if it can't be updated, we still want to return this graph. - continue - elif tmp == {None}: - # Happens when all the nodes do not have a TRACK_ID attribute. - continue - elif None in tmp: - # Happens when at least one node does not have a TRACK_ID - # attribute, so we clean 'tmp' and carry on. - tmp.remove(None) - elif len(tmp) != 1: - raise ValueError("Impossible state: several IDs for one track.") - - current_track_id = list(tmp)[0] - current_track_attr = [ - d_attr - for d_attr in tracks_attributes - if d_attr["TRACK_ID"] == current_track_id - ][0] - - # Adding the attributes to the lineage. - for k, v in current_track_attr.items(): - lin.graph[k] = v - - -def _split_graph_into_lineages( - graph: nx.DiGraph, - tracks_attributes: list[dict[str, Any]], -) -> list[CellLineage]: - """ - Split a graph into several subgraphs, each representing a lineage. - - Parameters - ---------- - lineage : nx.DiGraph - The graph to split. - tracks_attributes : list[dict[str, Any]] - A list of dictionaries, where each dictionary contains TrackMate - attributes for a specific track, identified by a 'TRACK_ID' key. - - Returns - ------- - list[CellLineage] - A list of subgraphs, each representing a lineage. - """ - # One subgraph is created per lineage, so each subgraph is - # a connected component of `graph`. - lineages = [ - CellLineage(graph.subgraph(c).copy()) - for c in nx.weakly_connected_components(graph) - ] - del graph # Redondant with the subgraphs. - - # Adding TrackMate tracks attributes to each lineage. - try: - _add_tracks_info(lineages, tracks_attributes) - except ValueError as err: - print(err) - # The program is in an impossible state so we need to stop. - raise - - return lineages - - def _update_features_declaration( fdec: FeaturesDeclaration, units: dict[str, str], @@ -885,9 +791,9 @@ def _update_location_related_features( else: # One-node graph don't have the TRACK_X_LOCATION, TRACK_Y_LOCATION # and TRACK_Z_LOCATION features in the graph, so we have to create it. - assert ( - len(lineage) == 1 - ), "TRACK_X_LOCATION not found and not a one-node lineage." + assert len(lineage) == 1, ( + "TRACK_X_LOCATION not found and not a one-node lineage." + ) node = [n for n in lineage.nodes][0] for axis in ["x", "y", "z"]: lineage.graph[f"lineage_{axis}"] = lineage.nodes[node][f"cell_{axis}"] @@ -980,7 +886,11 @@ def _parse_model_tag( # We want one lineage per track, so we need to split the graph # into its connected components. - lineages = _split_graph_into_lineages(graph, tracks_attributes) + lineages = _split_graph_into_lineages( + graph, + lin_features=tracks_attributes, + lineage_ID_key="TRACK_ID", + ) # For pycellin compatibility, some TrackMate features have to be renamed. # We only rename features that are either essential to the functioning of @@ -1148,8 +1058,7 @@ def _get_pixel_size(settings: ET._Element) -> dict[str, float]: pixel_size[key_pycellin] = float(element.attrib[key_TM]) except KeyError: raise KeyError( - f"The {key_TM} attribute is missing " - "in the 'ImageData' element." + f"The {key_TM} attribute is missing in the 'ImageData' element." ) except ValueError: raise ValueError( @@ -1219,29 +1128,17 @@ def load_TrackMate_XML( metadata[tag_name] = element_string model = Model(metadata, fdec, data) - - # Pycellin DOES NOT support fusion events. - all_fusions = model.get_fusions() - if all_fusions: - # TODO: link toward correct documentation when it is written. - fusion_warning = ( - f"Unsupported data, {len(all_fusions)} cell fusions detected. " - "It is advised to deal with them before any other processing, " - "especially for tracking related features. Crashes and incorrect " - "results can occur. See documentation for more details." - ) - warnings.warn(fusion_warning) + check_fusions(model) # pycellin DOES NOT support fusion events return model if __name__ == "__main__": - # xml = "sample_data/FakeTracks.xml" # xml = "sample_data/FakeTracks_no_tracks.xml" - # xml = "sample_data/Ecoli_growth_on_agar_pad.xml" + xml = "sample_data/Ecoli_growth_on_agar_pad.xml" # xml = "sample_data/Ecoli_growth_on_agar_pad_with_fusions.xml" - xml = "sample_data/Celegans-5pc-17timepoints.xml" + # xml = "sample_data/Celegans-5pc-17timepoints.xml" model = load_TrackMate_XML(xml, keep_all_spots=True, keep_all_tracks=True) print(model) @@ -1251,5 +1148,5 @@ def load_TrackMate_XML( # print(model.fdec.node_feats.keys()) # print(model.data) - lineage = model.data.cell_data[0] - lineage.plot(node_hover_features=["cell_ID", "cell_name"]) + # lineage = model.data.cell_data[0] + # lineage.plot(node_hover_features=["cell_ID", "cell_name"]) diff --git a/pycellin/io/utils.py b/pycellin/io/utils.py index 643fca2..81f3db4 100644 --- a/pycellin/io/utils.py +++ b/pycellin/io/utils.py @@ -1,6 +1,9 @@ import warnings +from typing import Any -from pycellin.classes import Model +import networkx as nx + +from pycellin.classes import CellLineage, Model def check_fusions(model: Model) -> None: @@ -26,3 +29,109 @@ def check_fusions(model: Model) -> None: "results can occur. See documentation for more details." ) warnings.warn(fusion_warning) + + +def _add_lineages_features( + lineages: list[CellLineage], + lin_features: list[dict[str, Any]], + lineage_ID_key: str | None = "lineage_ID", +) -> None: + """ + Update each CellLineage in the list with corresponding lineage features. + + This function iterates over a list of CellLineage objects, + attempting to match each lineage with its corresponding lineage + features based on the 'lineage_ID_key' feature present in the + lineage nodes. It then updates the lineage graph with these + attributes. + + Parameters + ---------- + lineages : list[CellLineage] + A list of the lineages to update. + lin_features : list[dict[str, Any]] + A list of dictionaries, where each dictionary contains features + for a specific lineage, identified by a the given 'lineage_ID_key' key. + lineage_ID_key : str | None, optional + The key used to identify the lineage in the attributes. + + Raises + ------ + ValueError + If a lineage is found to contain nodes with multiple distinct + 'lineage_ID_key' values, indicating an inconsistency in lineage ID + assignment. + """ + for lin in lineages: + # Finding the dict of features matching the lineage. + tmp = set(t_id for _, t_id in lin.nodes(data=lineage_ID_key)) + + if not tmp: + # 'tmp' is empty because there's no nodes in the current graph. + # Even if it can't be updated, we still want to return this graph. + continue + elif tmp == {None}: + # Happens when all the nodes do not have a 'lineage_ID_key' feature. + continue + elif None in tmp: + # Happens when at least one node does not have a 'lineage_ID_key' + # feature, so we clean 'tmp' and carry on. + tmp.remove(None) + elif len(tmp) != 1: + raise ValueError("Impossible state: several IDs for one lineage.") + + current_lineage_id = list(tmp)[0] + current_lineage_attr = [ + d_attr + for d_attr in lin_features + if d_attr[lineage_ID_key] == current_lineage_id + ][0] + + # Adding the features to the lineage. + for k, v in current_lineage_attr.items(): + lin.graph[k] = v + + +def _split_graph_into_lineages( + graph: nx.DiGraph, + lin_features: list[dict[str, Any]] | None = None, + lineage_ID_key: str | None = "lineage_ID", +) -> list[CellLineage]: + """ + Split a graph into several subgraphs, each representing a lineage. + + Parameters + ---------- + lineage : nx.DiGraph + The graph to split. + lin_features : list[dict[str, Any]] | None + A list of dictionaries, where each dictionary contains TrackMate + attributes for a specific track, identified by a 'TRACK_ID' key. + If None, no attributes will be added to the lineages. + + Returns + ------- + list[CellLineage] + A list of subgraphs, each representing a lineage. + """ + # One subgraph is created per lineage, so each subgraph is + # a connected component of `graph`. + lineages = [ + CellLineage(graph.subgraph(c).copy()) + for c in nx.weakly_connected_components(graph) + ] + del graph # Redondant with the subgraphs. + if not lin_features: + # We need to create and add a lineage_ID key to each lineage. + for i, lin in enumerate(lineages): + lin.graph["lineage_ID"] = i + else: + # Adding lineage features to each lineage. + try: + _add_lineages_features(lineages, lin_features, lineage_ID_key) + except ValueError as err: + print(err) + # The program is in an impossible state so we need to stop. + raise + + return lineages diff --git a/pycellin/utils.py b/pycellin/utils.py index d09978c..2498b82 100644 --- a/pycellin/utils.py +++ b/pycellin/utils.py @@ -1,10 +1,72 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from typing import get_args, get_origin, Literal +from typing import Literal, get_args, get_origin + +import networkx as nx +import networkx.algorithms.isomorphism as iso def check_literal_type(value, literal_type) -> bool: if get_origin(literal_type) is Literal: return value in get_args(literal_type) raise TypeError(f"{literal_type} is not a Literal type") + + +# TODO: this function should move into a tests/utils.py file +def is_equal(obt, exp): + """Check if two graphs are perfectly identical. + + It checks that the graphs are isomorphic, and that their graph, + nodes and edges attributes are all identical. + + Args: + obt (nx.DiGraph): The obtained graph, built from XML_reader.py. + exp (nx.DiGraph): The expected graph, built from here. + + Returns: + bool: True if the graphs are identical, False otherwise. + """ + edges_attr = list(set([k for (n1, n2, d) in exp.edges.data() for k in d])) + edges_default = len(edges_attr) * [0] + em = iso.categorical_edge_match(edges_attr, edges_default) + nodes_attr = list(set([k for (n, d) in exp.nodes.data() for k in d])) + nodes_default = len(nodes_attr) * [0] + nm = iso.categorical_node_match(nodes_attr, nodes_default) + + if not obt.nodes.data() and not exp.nodes.data(): + same_nodes = True + elif len(obt.nodes.data()) != len(exp.nodes.data()): + same_nodes = False + else: + for data1, data2 in zip(sorted(obt.nodes.data()), sorted(exp.nodes.data())): + n1, attr1 = data1 + n2, attr2 = data2 + if sorted(attr1) == sorted(attr2) and n1 == n2: + same_nodes = True + else: + same_nodes = False + + if not obt.edges.data() and not exp.edges.data(): + same_edges = True + elif len(obt.edges.data()) != len(exp.edges.data()): + same_edges = False + else: + for data1, data2 in zip(sorted(obt.edges.data()), sorted(exp.edges.data())): + n11, n12, attr1 = data1 + n21, n22, attr2 = data2 + if sorted(attr1) == sorted(attr2) and sorted((n11, n12)) == sorted( + (n21, n22) + ): + same_edges = True + else: + same_edges = False + + if ( + nx.is_isomorphic(obt, exp, edge_match=em, node_match=nm) + and obt.graph == exp.graph + and same_nodes + and same_edges + ): + return True + else: + return False diff --git a/tests/io/test_utils.py b/tests/io/test_utils.py new file mode 100644 index 0000000..aee1379 --- /dev/null +++ b/tests/io/test_utils.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 + +"""Unit test for IO utilities functions.""" + +import networkx as nx +import pytest + +from pycellin.classes import CellLineage +from pycellin.io.utils import _add_lineages_features, _split_graph_into_lineages +from pycellin.utils import is_equal + +# _add_lineages_features ############################################################ + + +def test_add_lineages_features(): + g1_attr = {"name": "blob", "lineage_ID": 0} + g2_attr = {"name": "blub", "lineage_ID": 1} + + g1_obt = nx.DiGraph() + g1_obt.add_node(1, lineage_ID=0) + g2_obt = nx.DiGraph() + g2_obt.add_node(2, lineage_ID=1) + _add_lineages_features([g1_obt, g2_obt], [g1_attr, g2_attr]) + + g1_exp = nx.DiGraph() + g1_exp.graph["name"] = "blob" + g1_exp.graph["lineage_ID"] = 0 + g1_exp.add_node(1, lineage_ID=0) + g2_exp = nx.DiGraph() + g2_exp.graph["name"] = "blub" + g2_exp.graph["lineage_ID"] = 1 + g2_exp.add_node(2, lineage_ID=1) + + assert is_equal(g1_obt, g1_exp) + assert is_equal(g2_obt, g2_exp) + + +def test_add_lineages_features_different_lin_ID_key(): + g1_attr = {"name": "blob", "TRACK_ID": 0} + g2_attr = {"name": "blub", "TRACK_ID": 1} + + g1_obt = nx.DiGraph() + g1_obt.add_node(1, TRACK_ID=0) + g2_obt = nx.DiGraph() + g2_obt.add_node(2, TRACK_ID=1) + _add_lineages_features( + [g1_obt, g2_obt], [g1_attr, g2_attr], lineage_ID_key="TRACK_ID" + ) + + g1_exp = nx.DiGraph() + g1_exp.graph["name"] = "blob" + g1_exp.graph["TRACK_ID"] = 0 + g1_exp.add_node(1, TRACK_ID=0) + g2_exp = nx.DiGraph() + g2_exp.graph["name"] = "blub" + g2_exp.graph["TRACK_ID"] = 1 + g2_exp.add_node(2, TRACK_ID=1) + + assert is_equal(g1_obt, g1_exp) + assert is_equal(g2_obt, g2_exp) + + +def test_add_lineages_features_no_lin_ID_on_all_nodes(): + g1_attr = {"name": "blob", "lineage_ID": 0} + g2_attr = {"name": "blub", "lineage_ID": 1} + + g1_obt = nx.DiGraph() + g1_obt.add_node(1) + g1_obt.add_node(3) + g2_obt = nx.DiGraph() + g2_obt.add_node(2, lineage_ID=1) + _add_lineages_features( + [g1_obt, g2_obt], [g1_attr, g2_attr], lineage_ID_key="lineage_ID" + ) + + g1_exp = nx.DiGraph() + g1_exp.add_node(1) + g1_exp.add_node(3) + g2_exp = nx.DiGraph() + g2_exp.graph["name"] = "blub" + g2_exp.graph["lineage_ID"] = 1 + g2_exp.add_node(2, lineage_ID=1) + + assert is_equal(g1_obt, g1_exp) + assert is_equal(g2_obt, g2_exp) + + +def test_add_lineages_features_no_lin_ID_on_one_node(): + g1_attr = {"name": "blob", "lineage_ID": 0} + g2_attr = {"name": "blub", "lineage_ID": 1} + + g1_obt = nx.DiGraph() + g1_obt.add_node(1) + g1_obt.add_node(3) + g1_obt.add_node(4, lineage_ID=0) + + g2_obt = nx.DiGraph() + g2_obt.add_node(2, lineage_ID=1) + _add_lineages_features([g1_obt, g2_obt], [g1_attr, g2_attr]) + + g1_exp = nx.DiGraph() + g1_exp.graph["name"] = "blob" + g1_exp.graph["lineage_ID"] = 0 + g1_exp.add_node(1) + g1_exp.add_node(3) + g1_exp.add_node(4, lineage_ID=0) + g2_exp = nx.DiGraph() + g2_exp.graph["name"] = "blub" + g2_exp.graph["lineage_ID"] = 1 + g2_exp.add_node(2, lineage_ID=1) + + assert is_equal(g1_obt, g1_exp) + assert is_equal(g2_obt, g2_exp) + + +def test_add_lineages_features_different_ID_for_one_track(): + g1_attr = {"name": "blob", "lineage_ID": 0} + g2_attr = {"name": "blub", "lineage_ID": 1} + + g1_obt = nx.DiGraph() + g1_obt.add_node(1, lineage_ID=0) + g1_obt.add_node(3, lineage_ID=2) + g1_obt.add_node(4, lineage_ID=0) + + g2_obt = nx.DiGraph() + g2_obt.add_node(2, lineage_ID=1) + with pytest.raises(ValueError): + _add_lineages_features([g1_obt, g2_obt], [g1_attr, g2_attr]) + + +def test_add_lineages_features_no_nodes(): + g1_attr = {"name": "blob", "lineage_ID": 0} + g2_attr = {"name": "blub", "lineage_ID": 1} + + g1_obt = nx.DiGraph() + g2_obt = nx.DiGraph() + g2_obt.add_node(2, lineage_ID=1) + _add_lineages_features([g1_obt, g2_obt], [g1_attr, g2_attr]) + + g1_exp = nx.DiGraph() + g2_exp = nx.DiGraph() + g2_exp.graph["name"] = "blub" + g2_exp.graph["lineage_ID"] = 1 + g2_exp.add_node(2, lineage_ID=1) + + assert is_equal(g1_obt, g1_exp) + assert is_equal(g2_obt, g2_exp) + + +# _split_graph_into_lineages ################################################## + + +def test_split_graph_into_lineages(): + g1_attr = {"name": "blob", "lineage_ID": 1} + g2_attr = {"name": "blub", "lineage_ID": 2} + + g = nx.DiGraph() + g.add_node(1, lineage_ID=1) + g.add_node(2, lineage_ID=1) + g.add_edge(1, 2) + g.add_node(3, lineage_ID=2) + g.add_node(4, lineage_ID=2) + g.add_edge(3, 4) + obtained = _split_graph_into_lineages(g, [g1_attr, g2_attr]) + + g1_exp = CellLineage(g.subgraph([1, 2])) + g1_exp.graph["name"] = "blob" + g1_exp.graph["lineage_ID"] = 1 + g2_exp = CellLineage(g.subgraph([3, 4])) + g2_exp.graph["name"] = "blub" + g2_exp.graph["lineage_ID"] = 2 + + assert len(obtained) == 2 + assert is_equal(obtained[0], g1_exp) + assert is_equal(obtained[1], g2_exp) + + +def test_split_graph_into_lineages_different_lin_ID_key(): + g1_attr = {"name": "blob", "TRACK_ID": 1} + g2_attr = {"name": "blub", "TRACK_ID": 2} + + g = nx.DiGraph() + g.add_node(1, TRACK_ID=1) + g.add_node(2, TRACK_ID=1) + g.add_edge(1, 2) + g.add_node(3, TRACK_ID=2) + g.add_node(4, TRACK_ID=2) + g.add_edge(3, 4) + obtained = _split_graph_into_lineages( + g, [g1_attr, g2_attr], lineage_ID_key="TRACK_ID" + ) + + g1_exp = CellLineage(g.subgraph([1, 2])) + g1_exp.graph["name"] = "blob" + g1_exp.graph["TRACK_ID"] = 1 + g2_exp = CellLineage(g.subgraph([3, 4])) + g2_exp.graph["name"] = "blub" + g2_exp.graph["TRACK_ID"] = 2 + + assert len(obtained) == 2 + assert is_equal(obtained[0], g1_exp) + assert is_equal(obtained[1], g2_exp) + + +def test_split_graph_into_lineages_no_lin_features(): + g = nx.DiGraph() + g.add_edges_from([(1, 2), (3, 4)]) + + obtained = _split_graph_into_lineages(g) + + g1_exp = CellLineage(g.subgraph([1, 2])) + g1_exp.graph["lineage_ID"] = 0 + g2_exp = CellLineage(g.subgraph([3, 4])) + g2_exp.graph["lineage_ID"] = 1 + + assert len(obtained) == 2 + assert is_equal(obtained[0], g1_exp) + assert is_equal(obtained[1], g2_exp) + + +def test_split_graph_into_lineages_different_ID(): + g1_attr = {"name": "blob", "lineage_ID": 1} + g2_attr = {"name": "blub", "lineage_ID": 2} + + g = nx.DiGraph() + g.add_node(1, lineage_ID=0) + g.add_node(2, lineage_ID=1) + g.add_edge(1, 2) + g.add_node(3, lineage_ID=2) + g.add_node(4, lineage_ID=2) + g.add_edge(3, 4) + + with pytest.raises(ValueError): + _split_graph_into_lineages(g, [g1_attr, g2_attr]) diff --git a/tests/io/trackmate/test_loader.py b/tests/io/trackmate/test_loader.py index f252d2d..d9320b9 100644 --- a/tests/io/trackmate/test_loader.py +++ b/tests/io/trackmate/test_loader.py @@ -1,79 +1,18 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """Unit test for TrackMate XML file loader.""" -from copy import deepcopy import io +from copy import deepcopy from typing import Any -from lxml import etree as ET import networkx as nx -import networkx.algorithms.isomorphism as iso import pytest +from lxml import etree as ET -from pycellin.classes import CellLineage, Feature, FeaturesDeclaration import pycellin.io.trackmate.loader as tml - - -def is_equal(obt, exp): - """Check if two graphs are perfectly identical. - - It checks that the graphs are isomorphic, and that their graph, - nodes and edges attributes are all identical. - - Args: - obt (nx.DiGraph): The obtained graph, built from XML_reader.py. - exp (nx.DiGraph): The expected graph, built from here. - - Returns: - bool: True if the graphs are identical, False otherwise. - """ - edges_attr = list(set([k for (n1, n2, d) in exp.edges.data() for k in d])) - edges_default = len(edges_attr) * [0] - em = iso.categorical_edge_match(edges_attr, edges_default) - nodes_attr = list(set([k for (n, d) in exp.nodes.data() for k in d])) - nodes_default = len(nodes_attr) * [0] - nm = iso.categorical_node_match(nodes_attr, nodes_default) - - if not obt.nodes.data() and not exp.nodes.data(): - same_nodes = True - elif len(obt.nodes.data()) != len(exp.nodes.data()): - same_nodes = False - else: - for data1, data2 in zip(sorted(obt.nodes.data()), sorted(exp.nodes.data())): - n1, attr1 = data1 - n2, attr2 = data2 - if sorted(attr1) == sorted(attr2) and n1 == n2: - same_nodes = True - else: - same_nodes = False - - if not obt.edges.data() and not exp.edges.data(): - same_edges = True - elif len(obt.edges.data()) != len(exp.edges.data()): - same_edges = False - else: - for data1, data2 in zip(sorted(obt.edges.data()), sorted(exp.edges.data())): - n11, n12, attr1 = data1 - n21, n22, attr2 = data2 - if sorted(attr1) == sorted(attr2) and sorted((n11, n12)) == sorted( - (n21, n22) - ): - same_edges = True - else: - same_edges = False - - if ( - nx.is_isomorphic(obt, exp, edge_match=em, node_match=nm) - and obt.graph == exp.graph - and same_nodes - and same_edges - ): - return True - else: - return False - +from pycellin.classes import CellLineage, Feature, FeaturesDeclaration +from pycellin.utils import is_equal # Fixtures ##################################################################### @@ -211,7 +150,7 @@ def track_feats( def test_get_units(): - xml_data = '' "" + xml_data = '' it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) obtained = tml._get_units(element) @@ -222,7 +161,7 @@ def test_get_units(): def test_get_units_missing_spaceunits(): - xml_data = '' "" + xml_data = '' it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) obtained = tml._get_units(element) @@ -233,7 +172,7 @@ def test_get_units_missing_spaceunits(): def test_get_units_missing_timeunits(): - xml_data = '' "" + xml_data = '' it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) obtained = tml._get_units(element) @@ -244,7 +183,7 @@ def test_get_units_missing_timeunits(): def test_get_units_no_units(): - xml_data = "" "" + xml_data = "" it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) obtained = tml._get_units(element) @@ -275,7 +214,7 @@ def test_get_features_dict(): def test_get_features_dict_no_feature_tag(): - xml_data = "" "" + xml_data = "" it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) features = tml._get_features_dict(it, element) @@ -408,7 +347,7 @@ def test_add_all_features( def test_add_all_features_empty(): - xml_data = "" "" + xml_data = "" it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) @@ -626,12 +565,7 @@ def test_add_all_nodes_only_ID_attribute(): def test_add_all_nodes_no_node_attributes(): xml_data = ( - "" - " " - " " - ' ' - " " - "" + ' ' ) it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) @@ -646,7 +580,7 @@ def test_add_all_nodes_no_node_attributes(): def test_add_all_nodes_no_nodes(): - xml_data = "" " " "" + xml_data = " " it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) @@ -885,9 +819,7 @@ def test_build_tracks_no_track_ID(): def test_get_filtered_tracks_ID(): - xml_data = ( - "" ' ' ' ' "" - ) + xml_data = ' ' it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) @@ -897,7 +829,7 @@ def test_get_filtered_tracks_ID(): def test_get_filtered_tracks_ID_no_ID(): - xml_data = "" " " " " "" + xml_data = " " it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) obtained_ID = tml._get_filtered_tracks_ID(it, element) @@ -905,168 +837,13 @@ def test_get_filtered_tracks_ID_no_ID(): def test_get_filtered_tracks_ID_no_tracks(): - xml_data = "" " " " " "" + xml_data = " " it = ET.iterparse(io.BytesIO(xml_data.encode("utf-8")), events=["start", "end"]) _, element = next(it) obtained_ID = tml._get_filtered_tracks_ID(it, element) assert not obtained_ID -# _add_tracks_info ############################################################ - - -def test_add_tracks_info(): - g1_attr = {"name": "blob", "TRACK_ID": 0} - g2_attr = {"name": "blub", "TRACK_ID": 1} - - g1_obt = nx.DiGraph() - g1_obt.add_node(1, TRACK_ID=0) - g2_obt = nx.DiGraph() - g2_obt.add_node(2, TRACK_ID=1) - tml._add_tracks_info([g1_obt, g2_obt], [g1_attr, g2_attr]) - - g1_exp = nx.DiGraph() - g1_exp.graph["name"] = "blob" - g1_exp.graph["TRACK_ID"] = 0 - g1_exp.add_node(1, TRACK_ID=0) - g2_exp = nx.DiGraph() - g2_exp.graph["name"] = "blub" - g2_exp.graph["TRACK_ID"] = 1 - g2_exp.add_node(2, TRACK_ID=1) - - assert is_equal(g1_obt, g1_exp) - assert is_equal(g2_obt, g2_exp) - - -def test_add_tracks_info_no_track_ID_on_all_nodes(): - g1_attr = {"name": "blob", "TRACK_ID": 0} - g2_attr = {"name": "blub", "TRACK_ID": 1} - - g1_obt = nx.DiGraph() - g1_obt.add_node(1) - g1_obt.add_node(3) - g2_obt = nx.DiGraph() - g2_obt.add_node(2, TRACK_ID=1) - tml._add_tracks_info([g1_obt, g2_obt], [g1_attr, g2_attr]) - - g1_exp = nx.DiGraph() - g1_exp.add_node(1) - g1_exp.add_node(3) - g2_exp = nx.DiGraph() - g2_exp.graph["name"] = "blub" - g2_exp.graph["TRACK_ID"] = 1 - g2_exp.add_node(2, TRACK_ID=1) - - assert is_equal(g1_obt, g1_exp) - assert is_equal(g2_obt, g2_exp) - - -def test_add_tracks_info_no_track_ID_on_one_node(): - g1_attr = {"name": "blob", "TRACK_ID": 0} - g2_attr = {"name": "blub", "TRACK_ID": 1} - - g1_obt = nx.DiGraph() - g1_obt.add_node(1) - g1_obt.add_node(3) - g1_obt.add_node(4, TRACK_ID=0) - - g2_obt = nx.DiGraph() - g2_obt.add_node(2, TRACK_ID=1) - tml._add_tracks_info([g1_obt, g2_obt], [g1_attr, g2_attr]) - - g1_exp = nx.DiGraph() - g1_exp.graph["name"] = "blob" - g1_exp.graph["TRACK_ID"] = 0 - g1_exp.add_node(1) - g1_exp.add_node(3) - g1_exp.add_node(4, TRACK_ID=0) - g2_exp = nx.DiGraph() - g2_exp.graph["name"] = "blub" - g2_exp.graph["TRACK_ID"] = 1 - g2_exp.add_node(2, TRACK_ID=1) - - assert is_equal(g1_obt, g1_exp) - assert is_equal(g2_obt, g2_exp) - - -def test_add_tracks_info_different_ID_for_one_track(): - g1_attr = {"name": "blob", "TRACK_ID": 0} - g2_attr = {"name": "blub", "TRACK_ID": 1} - - g1_obt = nx.DiGraph() - g1_obt.add_node(1, TRACK_ID=0) - g1_obt.add_node(3, TRACK_ID=2) - g1_obt.add_node(4, TRACK_ID=0) - - g2_obt = nx.DiGraph() - g2_obt.add_node(2, TRACK_ID=1) - with pytest.raises(ValueError): - tml._add_tracks_info([g1_obt, g2_obt], [g1_attr, g2_attr]) - - -def test_add_tracks_info_no_nodes(): - g1_attr = {"name": "blob", "TRACK_ID": 0} - g2_attr = {"name": "blub", "TRACK_ID": 1} - - g1_obt = nx.DiGraph() - g2_obt = nx.DiGraph() - g2_obt.add_node(2, TRACK_ID=1) - tml._add_tracks_info([g1_obt, g2_obt], [g1_attr, g2_attr]) - - g1_exp = nx.DiGraph() - g2_exp = nx.DiGraph() - g2_exp.graph["name"] = "blub" - g2_exp.graph["TRACK_ID"] = 1 - g2_exp.add_node(2, TRACK_ID=1) - - assert is_equal(g1_obt, g1_exp) - assert is_equal(g2_obt, g2_exp) - - -# _split_graph_into_lineages ################################################## - - -def test_split_graph_into_lineages(): - g1_attr = {"name": "blob", "TRACK_ID": 1} - g2_attr = {"name": "blub", "TRACK_ID": 2} - - g = nx.DiGraph() - g.add_node(1, TRACK_ID=1) - g.add_node(2, TRACK_ID=1) - g.add_edge(1, 2) - g.add_node(3, TRACK_ID=2) - g.add_node(4, TRACK_ID=2) - g.add_edge(3, 4) - obtained = tml._split_graph_into_lineages(g, [g1_attr, g2_attr]) - - g1_exp = CellLineage(g.subgraph([1, 2])) - g1_exp.graph["name"] = "blob" - g1_exp.graph["TRACK_ID"] = 1 - g2_exp = CellLineage(g.subgraph([3, 4])) - g2_exp.graph["name"] = "blub" - g2_exp.graph["TRACK_ID"] = 2 - - assert len(obtained) == 2 - assert is_equal(obtained[0], g1_exp) - assert is_equal(obtained[1], g2_exp) - - -def test_split_graph_into_lineages_different_ID(): - g1_attr = {"name": "blob", "TRACK_ID": 1} - g2_attr = {"name": "blub", "TRACK_ID": 2} - - g = nx.DiGraph() - g.add_node(1, TRACK_ID=0) - g.add_node(2, TRACK_ID=1) - g.add_edge(1, 2) - g.add_node(3, TRACK_ID=2) - g.add_node(4, TRACK_ID=2) - g.add_edge(3, 4) - - with pytest.raises(ValueError): - tml._split_graph_into_lineages(g, [g1_attr, g2_attr]) - - # _update_node_feature_key #################################################### From 6428ecddcc38939a9ae25c6f96992dac7acbaa08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Sat, 26 Jul 2025 16:12:48 -0400 Subject: [PATCH 07/27] Extract update feature names related functions from TM loader, rename and move to io.utils --- pycellin/io/trackmate/loader.py | 80 +++---------------------- pycellin/io/utils.py | 99 +++++++++++++++++++++++++++++++ tests/io/test_utils.py | 84 +++++++++++++++++++++++++- tests/io/trackmate/test_loader.py | 48 --------------- 4 files changed, 190 insertions(+), 121 deletions(-) diff --git a/pycellin/io/trackmate/loader.py b/pycellin/io/trackmate/loader.py index 78ffb91..26760fa 100644 --- a/pycellin/io/trackmate/loader.py +++ b/pycellin/io/trackmate/loader.py @@ -18,7 +18,13 @@ Model, cell_ID_Feature, ) -from pycellin.io.utils import _split_graph_into_lineages, check_fusions +from pycellin.io.utils import ( + _split_graph_into_lineages, + check_fusions, + _update_node_feature_key, + _update_lineage_feature_key, + _update_lineage_ID_key, +) def _get_units( @@ -691,76 +697,6 @@ def _update_features_declaration( fdec._modify_feature_description(f"lineage_{axis}", desc) -def _update_node_feature_key( - lineage: CellLineage, - old_key: str, - new_key: str, -) -> None: - """ - Update the key of a feature in all the nodes of a lineage. - - Parameters - ---------- - lineage : CellLineage - The lineage to update. - old_key : str - The old key of the feature. - new_key : str - The new key of the feature. - """ - for node in lineage.nodes: - if old_key in lineage.nodes[node]: - lineage.nodes[node][new_key] = lineage.nodes[node].pop(old_key) - - -def _update_lineage_feature_key( - lineage: CellLineage, - old_key: str, - new_key: str, -) -> None: - """ - Update the key of a feature in the graph of a lineage. - - Parameters - ---------- - lineage : CellLineage - The lineage to update. - old_key : str - The old key of the feature. - new_key : str - The new key of the feature. - """ - if old_key in lineage.graph: - lineage.graph[new_key] = lineage.graph.pop(old_key) - - -def _update_TRACK_ID( - lineage: CellLineage, -) -> None: - """ - Update the TRACK_ID feature in the nodes and in the graph of a lineage. - - In the case of a one-node lineage, TRACK_ID does not exist in the graph - nor in the nodes. So we define the lineage_ID as minus the node ID. - That way, it is easy to discriminate between one-node lineages - (negative IDs) and multi-nodes lineages (positive IDs). - - Parameters - ---------- - lineage : CellLineage - The lineage to update. - """ - if "TRACK_ID" in lineage.graph: - lineage.graph["lineage_ID"] = lineage.graph.pop("TRACK_ID") - else: - # One-node graph don't have the TRACK_ID feature in the graph - # or in the nodes, so we have to create it. - # We set the ID of a one-node lineage to the negative of the node ID. - assert len(lineage) == 1, "TRACK_ID not found and not a one-node lineage." - node = [n for n in lineage.nodes][0] - lineage.graph["lineage_ID"] = -node - - def _update_location_related_features( lineage: CellLineage, ) -> None: @@ -904,7 +840,7 @@ def _parse_model_tag( ]: _update_node_feature_key(lin, key_name, new_key) _update_lineage_feature_key(lin, "name", "lineage_name") - _update_TRACK_ID(lin) + _update_lineage_ID_key(lin, "TRACK_ID") _update_location_related_features(lin) # Adding if each track was present in the 'FilteredTracks' tag diff --git a/pycellin/io/utils.py b/pycellin/io/utils.py index 81f3db4..90d708b 100644 --- a/pycellin/io/utils.py +++ b/pycellin/io/utils.py @@ -135,3 +135,102 @@ def _split_graph_into_lineages( raise return lineages + + +def _update_node_feature_key( + lineage: CellLineage, + old_key: str, + new_key: str, +) -> None: + """ + Update the key of a feature in all the nodes of a lineage. + + Parameters + ---------- + lineage : CellLineage + The lineage to update. + old_key : str + The old key of the feature. + new_key : str + The new key of the feature. + """ + for node in lineage.nodes: + if old_key in lineage.nodes[node]: + lineage.nodes[node][new_key] = lineage.nodes[node].pop(old_key) + + +def _update_lineage_feature_key( + lineage: CellLineage, + old_key: str, + new_key: str, +) -> None: + """ + Update the key of a feature in the graph of a lineage. + + Parameters + ---------- + lineage : CellLineage + The lineage to update. + old_key : str + The old key of the feature. + new_key : str + The new key of the feature. + """ + if old_key in lineage.graph: + lineage.graph[new_key] = lineage.graph.pop(old_key) + + +def _update_lineage_ID_key( + lineage: CellLineage, + lineage_ID_key: str, + available_ID: int | None = None, +) -> int | None: + """ + Update the lineage ID key in the lineage graph to match pycellin convention. + + In the case of a one-node lineage, it is possible that the lineage does not have + a key to identify it. So we define the lineage_ID as minus the node ID. + That way, it is easy to discriminate between one-node lineages + (negative IDs) and multi-nodes lineages (positive IDs). + + Parameters + ---------- + lineage : CellLineage + The lineage to update. + lineage_ID_key : str + The key that is the lineage identifier in the lineage graph. + available_ID : int | None, optional + The next available lineage ID to use if the lineage does not have one. + + Returns + ------- + int | None + The lineage ID if it was created, or None if it was already present. + + Raises + ------ + TypeError + If the lineage is a multi-node lineage and no available_ID is provided. + """ + try: + # If the lineage has a lineage_ID_key, we rename it to "lineage_ID". + # This is the key used by pycellin to identify lineages. + lineage.graph["lineage_ID"] = lineage.graph.pop(lineage_ID_key) + return None + except KeyError: + if len(lineage) == 1: + # If the lineage has only one node, we set the lineage ID to minus the node ID. + # This is a convention used by pycellin to identify one-node lineages. + node = list(lineage.nodes)[0] + lineage.graph["lineage_ID"] = -node + return -node + else: + # If the lineage does not have a lineage_ID_key, we create it. + # We set the ID of a multi-node lineage to the next available lineage ID. + if available_ID is None: + raise TypeError( + "Missing available_ID argument for multi-node lineage " + f"with no {lineage_ID_key} key." + ) + lineage.graph["lineage_ID"] = available_ID + return available_ID diff --git a/tests/io/test_utils.py b/tests/io/test_utils.py index aee1379..b9a98e8 100644 --- a/tests/io/test_utils.py +++ b/tests/io/test_utils.py @@ -6,9 +6,91 @@ import pytest from pycellin.classes import CellLineage -from pycellin.io.utils import _add_lineages_features, _split_graph_into_lineages +from pycellin.io.utils import ( + _add_lineages_features, + _split_graph_into_lineages, + _update_node_feature_key, + _update_lineage_feature_key, + _update_lineage_ID_key, +) from pycellin.utils import is_equal + +# _update_node_feature_key #################################################### + + +def test_update_node_feature_key(): + lineage = CellLineage() + old_key_values = ["value1", "value2", "value3"] + lineage.add_node(1, old_key=old_key_values[0]) + lineage.add_node(2, old_key=old_key_values[1]) + lineage.add_node(3, old_key=old_key_values[2]) + + _update_node_feature_key(lineage, "old_key", "new_key") + + for i, node in enumerate(lineage.nodes): + assert "new_key" in lineage.nodes[node] + assert "old_key" not in lineage.nodes[node] + assert lineage.nodes[node]["new_key"] == old_key_values[i] + + +# _update_lineage_feature_key ################################################# + + +def test_update_lineage_feature_key(): + lineage = CellLineage() + lineage.graph["old_key"] = "old_value" + _update_lineage_feature_key(lineage, "old_key", "new_key") + + assert "new_key" in lineage.graph + assert lineage.graph["new_key"] == "old_value" + assert "old_key" not in lineage.graph + + +# _update_lineage_feature_key ################################################# + + +def test_update_lineage_ID_key(): + lineage = CellLineage() + lineage.add_nodes_from([1, 2, 3]) + lineage.graph["TRACK_ID"] = 10 + new_lin_ID = _update_lineage_ID_key(lineage, "TRACK_ID") + assert new_lin_ID is None + assert "lineage_ID" in lineage.graph + assert lineage.graph["lineage_ID"] == 10 + assert "lineage_ID" not in lineage.nodes[1] + + +def test_update_lineage_ID_key_no_key_multi_node(): + lineage = CellLineage() + lineage.add_nodes_from([1, 2, 3]) + new_lin_ID = _update_lineage_ID_key(lineage, "TRACK_ID", 0) + assert new_lin_ID == 0 + assert "lineage_ID" in lineage.graph + assert lineage.graph["lineage_ID"] == 0 + + +def test_update_lineage_ID_key_no_key_one_node(): + lineage = CellLineage() + lineage.add_node(1) + new_lin_ID = _update_lineage_ID_key(lineage, "TRACK_ID") + assert new_lin_ID == -1 + assert "lineage_ID" in lineage.graph + assert lineage.graph["lineage_ID"] == -1 + + +def test_update_lineage_ID_key_no_key_no_new_ID(): + lineage = CellLineage() + lineage.add_nodes_from([1, 2, 3]) + with pytest.raises( + TypeError, + match=( + "Missing available_ID argument for multi-node lineage with no TRACK_ID key." + ), + ): + _update_lineage_ID_key(lineage, "TRACK_ID", None) + + # _add_lineages_features ############################################################ diff --git a/tests/io/trackmate/test_loader.py b/tests/io/trackmate/test_loader.py index d9320b9..5a5848e 100644 --- a/tests/io/trackmate/test_loader.py +++ b/tests/io/trackmate/test_loader.py @@ -844,54 +844,6 @@ def test_get_filtered_tracks_ID_no_tracks(): assert not obtained_ID -# _update_node_feature_key #################################################### - - -def test_update_node_feature_key(): - lineage = CellLineage() - old_key_values = ["value1", "value2", "value3"] - lineage.add_node(1, old_key=old_key_values[0]) - lineage.add_node(2, old_key=old_key_values[1]) - lineage.add_node(3, old_key=old_key_values[2]) - - tml._update_node_feature_key(lineage, "old_key", "new_key") - - for i, node in enumerate(lineage.nodes): - assert "new_key" in lineage.nodes[node] - assert "old_key" not in lineage.nodes[node] - assert lineage.nodes[node]["new_key"] == old_key_values[i] - - -# _update_TRACK_ID ############################################################ - - -def test_update_TRACK_ID(): - lineage = CellLineage() - lineage.add_node(1) - lineage.graph["TRACK_ID"] = 10 - tml._update_TRACK_ID(lineage) - assert "lineage_ID" in lineage.graph - assert lineage.graph["lineage_ID"] == 10 - assert "lineage_ID" not in lineage.nodes[1] - - -def test_update_TRACK_ID_no_TRACK_ID(): - lineage = CellLineage() - lineage.add_node(1) - tml._update_TRACK_ID(lineage) - assert "lineage_ID" in lineage.graph - assert lineage.graph["lineage_ID"] == -1 - - -def test_update_TRACK_ID_several_subgraphs(): - lineage = CellLineage() - lineage.add_node(1) - lineage.add_node(2) - - with pytest.raises(AssertionError): - tml._update_TRACK_ID(lineage) - - # _update_location_related_features ########################################### From 947a5686ffd6163c2b331f9c963e7ceed5b34571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Sat, 26 Jul 2025 17:16:03 -0400 Subject: [PATCH 08/27] Improve _update_node_feature_key() --- pycellin/io/utils.py | 27 +++++++++ tests/io/test_utils.py | 130 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/pycellin/io/utils.py b/pycellin/io/utils.py index 90d708b..ec54ba1 100644 --- a/pycellin/io/utils.py +++ b/pycellin/io/utils.py @@ -141,6 +141,9 @@ def _update_node_feature_key( lineage: CellLineage, old_key: str, new_key: str, + enforce_old_key_existence: bool = False, + set_default_if_missing: bool = False, + default_value: Any | None = None, ) -> None: """ Update the key of a feature in all the nodes of a lineage. @@ -153,10 +156,34 @@ def _update_node_feature_key( The old key of the feature. new_key : str The new key of the feature. + enforce_old_key_existence : bool, optional + If True, raises an error when the old key does not exist in a node. + If False, the function will skip nodes that do not have the old key. + Defaults to False. + set_default_if_missing : bool, optional + If True, set the new key to `default_value` when the old key does not exist. + If False, the new key will not be set when the old key does not exist. + Defaults to False. + default_value : Any | None, optional + The default value to set if the old key does not exist + and set_default_if_missing is True. Defaults to None. + + Raises + ------ + ValueError + If enforce_old_key_existence is True and the old key does not exist + in a node. """ for node in lineage.nodes: if old_key in lineage.nodes[node]: lineage.nodes[node][new_key] = lineage.nodes[node].pop(old_key) + else: + if enforce_old_key_existence: + raise ValueError( + f"Node {node} does not have the required key '{old_key}'." + ) + if set_default_if_missing: + lineage.nodes[node][new_key] = default_value def _update_lineage_feature_key( diff --git a/tests/io/test_utils.py b/tests/io/test_utils.py index b9a98e8..52b826c 100644 --- a/tests/io/test_utils.py +++ b/tests/io/test_utils.py @@ -19,7 +19,51 @@ # _update_node_feature_key #################################################### +# def _update_node_feature_key( +# lineage: CellLineage, +# old_key: str, +# new_key: str, +# enforce_old_key_existence: bool = False, +# set_default_if_missing: bool = False, +# default_value: Any | None = None, +# ) -> None: +# """ +# Update the key of a feature in all the nodes of a lineage. + +# Parameters +# ---------- +# lineage : CellLineage +# The lineage to update. +# old_key : str +# The old key of the feature. +# new_key : str +# The new key of the feature. +# enforce_old_key_existence : bool, optional +# If True, raises an error when the old key does not exist in a node. +# If False, the function will skip nodes that do not have the old key. +# Defaults to False. +# set_default_if_missing : bool, optional +# If True, set the new key to `default_value` when the old key does not exist. +# If False, the new key will not be set when the old key does not exist. +# Defaults to False. +# default_value : Any | None, optional +# The default value to set if the old key does not exist +# and set_default_if_missing is True. Defaults to None. +# """ +# for node in lineage.nodes: +# if old_key in lineage.nodes[node]: +# lineage.nodes[node][new_key] = lineage.nodes[node].pop(old_key) +# else: +# if enforce_old_key_existence: +# raise ValueError( +# f"Node {node} does not have the required key '{old_key}'." +# ) +# if set_default_if_missing: +# lineage.nodes[node][new_key] = default_value + + def test_update_node_feature_key(): + """Test updating a node feature key.""" lineage = CellLineage() old_key_values = ["value1", "value2", "value3"] lineage.add_node(1, old_key=old_key_values[0]) @@ -34,6 +78,92 @@ def test_update_node_feature_key(): assert lineage.nodes[node]["new_key"] == old_key_values[i] +def test_update_node_feature_key_missing_old_key_skip(): + """Test that nodes without old_key are skipped when enforce_old_key_existence=False.""" + lineage = CellLineage() + lineage.add_node(1, old_key="value1") + lineage.add_node(2) # No old_key + lineage.add_node(3, old_key="value3") + + _update_node_feature_key(lineage, "old_key", "new_key") + + assert lineage.nodes[1]["new_key"] == "value1" + assert "old_key" not in lineage.nodes[1] + assert "new_key" not in lineage.nodes[2] + assert "old_key" not in lineage.nodes[2] + assert lineage.nodes[3]["new_key"] == "value3" + assert "old_key" not in lineage.nodes[3] + + +def test_update_node_feature_key_enforce_old_key_existence(): + """Test that missing old_key raises error when enforce_old_key_existence=True.""" + lineage = CellLineage() + lineage.add_node(1, old_key="value1") + lineage.add_node(2) # No old_key + + with pytest.raises( + ValueError, match="Node 2 does not have the required key 'old_key'" + ): + _update_node_feature_key( + lineage, "old_key", "new_key", enforce_old_key_existence=True + ) + + +def test_update_node_feature_key_set_default_if_missing(): + """Test setting default value when old_key is missing and set_default_if_missing=True.""" + lineage = CellLineage() + lineage.add_node(1, old_key="value1") + lineage.add_node(2) # No old_key + lineage.add_node(3, old_key="value3") + + _update_node_feature_key( + lineage, + "old_key", + "new_key", + set_default_if_missing=True, + default_value="default", + ) + + assert lineage.nodes[1]["new_key"] == "value1" + assert "old_key" not in lineage.nodes[1] + assert lineage.nodes[2]["new_key"] == "default" + assert "old_key" not in lineage.nodes[2] + assert lineage.nodes[3]["new_key"] == "value3" + assert "old_key" not in lineage.nodes[3] + + +def test_update_node_feature_key_set_default_none(): + """Test setting None as default value when old_key is missing.""" + lineage = CellLineage() + lineage.add_node(1, old_key="value1") + lineage.add_node(2) # No old_key + + _update_node_feature_key(lineage, "old_key", "new_key", set_default_if_missing=True) + + assert lineage.nodes[1]["new_key"] == "value1" + assert lineage.nodes[2]["new_key"] is None + + +def test_update_node_feature_key_empty_lineage(): + """Test function with empty lineage (no nodes).""" + lineage = CellLineage() + # Should not raise an error and do nothing + _update_node_feature_key(lineage, "old_key", "new_key") + assert len(lineage.nodes) == 0 + + +def test_update_node_feature_key_same_key_name(): + """Test updating a key to itself (should work without issues).""" + lineage = CellLineage() + lineage.add_node(1, test_key="value1") + lineage.add_node(2, test_key="value2") + + _update_node_feature_key(lineage, "test_key", "test_key") + + assert lineage.nodes[1]["test_key"] == "value1" + assert lineage.nodes[2]["test_key"] == "value2" + + # _update_lineage_feature_key ################################################# From 373c37dfbdc65b8169ba918f5d782fe156ccd0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Sat, 26 Jul 2025 18:10:04 -0400 Subject: [PATCH 09/27] Reimplement _update_lineage_ID_key() into _update_lineages_IDs_key() --- pycellin/io/trackmate/loader.py | 18 ++-- pycellin/io/utils.py | 62 +++++--------- tests/io/test_utils.py | 143 ++++++++++++++++++++++---------- 3 files changed, 132 insertions(+), 91 deletions(-) diff --git a/pycellin/io/trackmate/loader.py b/pycellin/io/trackmate/loader.py index 26760fa..0a3ed25 100644 --- a/pycellin/io/trackmate/loader.py +++ b/pycellin/io/trackmate/loader.py @@ -23,7 +23,7 @@ check_fusions, _update_node_feature_key, _update_lineage_feature_key, - _update_lineage_ID_key, + _update_lineages_IDs_key, ) @@ -832,15 +832,16 @@ def _parse_model_tag( # We only rename features that are either essential to the functioning of # pycellin or confusing (e.g. "name" is a spot and a track feature). _update_features_declaration(fd, units, segmentation) + _update_lineages_IDs_key(lineages, "TRACK_ID") for lin in lineages: for key_name, new_key in [ + ("TRACK_ID", "lineage_ID"), # mandatory ("ID", "cell_ID"), # mandatory ("FRAME", "frame"), # mandatory ("name", "cell_name"), # confusing ]: _update_node_feature_key(lin, key_name, new_key) _update_lineage_feature_key(lin, "name", "lineage_name") - _update_lineage_ID_key(lin, "TRACK_ID") _update_location_related_features(lin) # Adding if each track was present in the 'FilteredTracks' tag @@ -1077,12 +1078,15 @@ def load_TrackMate_XML( # xml = "sample_data/Celegans-5pc-17timepoints.xml" model = load_TrackMate_XML(xml, keep_all_spots=True, keep_all_tracks=True) - print(model) - print(model.feat_declaration) - print(model.metadata["Pycellin_version"]) + # print(model) + # print(model.feat_declaration) + # print(model.metadata["Pycellin_version"]) # print(model.metadata) # print(model.fdec.node_feats.keys()) # print(model.data) + # for lin in model.get_cell_lineages(): + # print(lin) - # lineage = model.data.cell_data[0] - # lineage.plot(node_hover_features=["cell_ID", "cell_name"]) + lineage = model.data.cell_data[1] + # print(lineage.nodes(data="lineage_ID")) + lineage.plot(node_hover_features=["cell_ID", "cell_name", "lineage_ID"]) diff --git a/pycellin/io/utils.py b/pycellin/io/utils.py index ec54ba1..060ada6 100644 --- a/pycellin/io/utils.py +++ b/pycellin/io/utils.py @@ -207,57 +207,37 @@ def _update_lineage_feature_key( lineage.graph[new_key] = lineage.graph.pop(old_key) -def _update_lineage_ID_key( - lineage: CellLineage, +def _update_lineages_IDs_key( + lineages: list[CellLineage], lineage_ID_key: str, - available_ID: int | None = None, ) -> int | None: """ - Update the lineage ID key in the lineage graph to match pycellin convention. + Update the lineage ID key of lineage graphs to match pycellin convention. In the case of a one-node lineage, it is possible that the lineage does not have a key to identify it. So we define the lineage_ID as minus the node ID. That way, it is easy to discriminate between one-node lineages (negative IDs) and multi-nodes lineages (positive IDs). + If the lineage_ID_key is not present in the lineage graph, a "lineage_ID" key + is created and set to the next available lineage ID. Parameters ---------- - lineage : CellLineage - The lineage to update. + lineages : list[CellLineage] + The lineages to update. lineage_ID_key : str - The key that is the lineage identifier in the lineage graph. - available_ID : int | None, optional - The next available lineage ID to use if the lineage does not have one. - - Returns - ------- - int | None - The lineage ID if it was created, or None if it was already present. - - Raises - ------ - TypeError - If the lineage is a multi-node lineage and no available_ID is provided. + The key that is the lineage identifier in lineage graphs. """ - try: - # If the lineage has a lineage_ID_key, we rename it to "lineage_ID". - # This is the key used by pycellin to identify lineages. - lineage.graph["lineage_ID"] = lineage.graph.pop(lineage_ID_key) - return None - except KeyError: - if len(lineage) == 1: - # If the lineage has only one node, we set the lineage ID to minus the node ID. - # This is a convention used by pycellin to identify one-node lineages. - node = list(lineage.nodes)[0] - lineage.graph["lineage_ID"] = -node - return -node - else: - # If the lineage does not have a lineage_ID_key, we create it. - # We set the ID of a multi-node lineage to the next available lineage ID. - if available_ID is None: - raise TypeError( - "Missing available_ID argument for multi-node lineage " - f"with no {lineage_ID_key} key." - ) - lineage.graph["lineage_ID"] = available_ID - return available_ID + ids = [lin.graph[lineage_ID_key] for lin in lineages if lineage_ID_key in lin.graph] + next_id = max(ids) + 1 if ids else 0 + + for lin in lineages: + try: + lin.graph["lineage_ID"] = lin.graph.pop(lineage_ID_key) + except KeyError: + if len(lin) == 1: + node = list(lin.nodes)[0] + lin.graph["lineage_ID"] = -node + else: + lin.graph["lineage_ID"] = next_id + next_id += 1 diff --git a/tests/io/test_utils.py b/tests/io/test_utils.py index 52b826c..55228cc 100644 --- a/tests/io/test_utils.py +++ b/tests/io/test_utils.py @@ -11,7 +11,7 @@ _split_graph_into_lineages, _update_node_feature_key, _update_lineage_feature_key, - _update_lineage_ID_key, + _update_lineages_IDs_key, ) from pycellin.utils import is_equal @@ -177,48 +177,105 @@ def test_update_lineage_feature_key(): assert "old_key" not in lineage.graph -# _update_lineage_feature_key ################################################# - - -def test_update_lineage_ID_key(): - lineage = CellLineage() - lineage.add_nodes_from([1, 2, 3]) - lineage.graph["TRACK_ID"] = 10 - new_lin_ID = _update_lineage_ID_key(lineage, "TRACK_ID") - assert new_lin_ID is None - assert "lineage_ID" in lineage.graph - assert lineage.graph["lineage_ID"] == 10 - assert "lineage_ID" not in lineage.nodes[1] - - -def test_update_lineage_ID_key_no_key_multi_node(): - lineage = CellLineage() - lineage.add_nodes_from([1, 2, 3]) - new_lin_ID = _update_lineage_ID_key(lineage, "TRACK_ID", 0) - assert new_lin_ID == 0 - assert "lineage_ID" in lineage.graph - assert lineage.graph["lineage_ID"] == 0 - - -def test_update_lineage_ID_key_no_key_one_node(): - lineage = CellLineage() - lineage.add_node(1) - new_lin_ID = _update_lineage_ID_key(lineage, "TRACK_ID") - assert new_lin_ID == -1 - assert "lineage_ID" in lineage.graph - assert lineage.graph["lineage_ID"] == -1 - - -def test_update_lineage_ID_key_no_key_no_new_ID(): - lineage = CellLineage() - lineage.add_nodes_from([1, 2, 3]) - with pytest.raises( - TypeError, - match=( - "Missing available_ID argument for multi-node lineage with no TRACK_ID key." - ), - ): - _update_lineage_ID_key(lineage, "TRACK_ID", None) +# _update_lineages_IDs_key ################################################# + + +def test_update_lineages_IDs_key(): + """Test updating lineage IDs key.""" + lin1 = CellLineage() + lin1.add_nodes_from([1, 2, 3]) + lin1.graph["TRACK_ID"] = 10 + lin2 = CellLineage() + lin2.add_nodes_from([4, 5]) + lin2.graph["TRACK_ID"] = 20 + + _update_lineages_IDs_key([lin1, lin2], "TRACK_ID") + assert lin1.graph["lineage_ID"] == 10 + assert lin2.graph["lineage_ID"] == 20 + assert "TRACK_ID" not in lin1.graph + assert "TRACK_ID" not in lin2.graph + + +def test_update_lineages_IDs_key_no_key_multi_node(): + """Test updating lineage IDs key when no TRACK_ID key is present in a multi-node lineage.""" + lin1 = CellLineage() + lin1.add_nodes_from([1, 2, 3]) + lin2 = CellLineage() + lin2.add_nodes_from([4, 5]) + lin2.graph["TRACK_ID"] = 20 + + _update_lineages_IDs_key([lin1, lin2], "TRACK_ID") + assert lin1.graph["lineage_ID"] == 21 + assert lin2.graph["lineage_ID"] == 20 + assert "TRACK_ID" not in lin1.graph + assert "TRACK_ID" not in lin2.graph + + +def test_update_lineages_IDs_key_no_key_one_node(): + """Test updating lineage IDs key when no TRACK_ID key is present in a one-node lineage.""" + lin1 = CellLineage() + lin1.add_node(1) + lin2 = CellLineage() + lin2.add_nodes_from([4, 5]) + lin2.graph["TRACK_ID"] = 20 + _update_lineages_IDs_key([lin1, lin2], "TRACK_ID") + assert lin1.graph["lineage_ID"] == -1 + assert lin2.graph["lineage_ID"] == 20 + + +def test_update_lineages_IDs_key_all_lineages_no_key(): + """Test updating lineage IDs key when no lineages have the key.""" + lin1 = CellLineage() + lin1.add_nodes_from([1, 2, 3]) + lin2 = CellLineage() + lin2.add_nodes_from([4, 5]) + lin3 = CellLineage() + lin3.add_node(6) + + _update_lineages_IDs_key([lin1, lin2, lin3], "TRACK_ID") + assert lin1.graph["lineage_ID"] == 0 + assert lin2.graph["lineage_ID"] == 1 + assert lin3.graph["lineage_ID"] == -6 + assert "TRACK_ID" not in lin1.graph + assert "TRACK_ID" not in lin2.graph + assert "TRACK_ID" not in lin3.graph + + +def test_update_lineages_IDs_key_empty_list(): + """Test updating lineage IDs key with empty lineages list.""" + _update_lineages_IDs_key([], "TRACK_ID") + + +def test_update_lineages_IDs_key_mixed_scenarios(): + """Test with mix of single-node, multi-node, and lineages with existing keys.""" + lin1 = CellLineage() # single node, no key + lin1.add_node(1) + lin2 = CellLineage() # multi-node, no key + lin2.add_nodes_from([2, 3]) + lin3 = CellLineage() # has key + lin3.add_node(4) + lin3.graph["TRACK_ID"] = 10 + lin4 = CellLineage() # single node, no key + lin4.add_node(5) + + _update_lineages_IDs_key([lin1, lin2, lin3, lin4], "TRACK_ID") + assert lin1.graph["lineage_ID"] == -1 + assert lin2.graph["lineage_ID"] == 11 + assert lin3.graph["lineage_ID"] == 10 + assert lin4.graph["lineage_ID"] == -5 + + +def test_update_lineages_IDs_key_preserves_other_graph_attributes(): + """Test that other graph attributes are preserved.""" + lin1 = CellLineage() + lin1.add_node(1) + lin1.graph["TRACK_ID"] = 10 + lin1.graph["other_attr"] = "value" + + _update_lineages_IDs_key([lin1], "TRACK_ID") + assert lin1.graph["lineage_ID"] == 10 + assert lin1.graph["other_attr"] == "value" + assert "TRACK_ID" not in lin1.graph # _add_lineages_features ############################################################ From 1fcd697a230c42a1c7f0b6a61ec480f77c0280cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Tue, 29 Jul 2025 14:26:53 -0400 Subject: [PATCH 10/27] WIP geff loader main --- pycellin/io/geff/loader.py | 131 ++++++++++++++++++++++++++++--------- 1 file changed, 101 insertions(+), 30 deletions(-) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index 1b1a3ff..8c1e348 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ loader.py @@ -18,9 +17,19 @@ import networkx as nx from pycellin.classes import Model +from pycellin.classes.data import Data +from pycellin.io.utils import ( + check_fusions, + _split_graph_into_lineages, + _update_lineages_IDs_key, + _update_node_feature_key, +) -def load_geff_file(geff_file: Path | str) -> Model: +def load_geff_file( + geff_file: Path | str, + cell_id_key: str | None = None, +) -> Model: """ Load a geff file and return a pycellin model containing the data. @@ -28,6 +37,9 @@ def load_geff_file(geff_file: Path | str) -> Model: ---------- geff_file : Path | str Path to the geff file to load. + cell_id_key : str | None, optional + The key used to identify cells in the geff file. If None, the default + key 'cell_ID' will be created and populated based on the node IDs. Returns ------- @@ -37,40 +49,99 @@ def load_geff_file(geff_file: Path | str) -> Model: pass # Read the geff file - # Check for fusions + geff_graph, geff_md = geff.read_nx(geff_file, validate=True) + for node in geff_graph.nodes: + print(f"Node {node}: {geff_graph.nodes[node]}") + + print(type(geff_graph)) + print(geff_md.directed) + if "track_node_props" in geff_md and "lineage" in geff_md.track_node_props: + lin_id_key = geff_md.track_node_props["lineage"] + else: + lin_id_key = None + print("lin_id_key:", lin_id_key) + # Determine axes + # If no axes, need to have them as arguments...? Set a default to x, y, z, t...? + + # Is int ID ensured in geff? YES + # int_graph = nx.relabel_nodes(geff_graph, {node: int(node) for node in geff_graph.nodes()}) + # Extract and dispatch metadata - # Rename features to match pycellin conventions + # TODO: but for now we wait for the change in geff metadata specs + # Split the graph into lineages - # Return the model + lineages = _split_graph_into_lineages(geff_graph, lineage_ID_key=lin_id_key) + print(f"Number of lineages: {len(lineages)}") + + # Rename features to match pycellin conventions + _update_lineages_IDs_key(lineages, lin_id_key) + for lin in lineages: + if cell_id_key is None: + for node in lin.nodes: + lin.nodes[node]["cell_ID"] = int( + node + ) # do I need to ensure this is an int? + else: + _update_node_feature_key(lin, cell_id_key, "cell_ID") + + # Check for fusions + data = Data({lin.graph["lineage_ID"]: lin for lin in lineages}) + model = Model(data=data) + # print(model.data) + # print(model.data.cell_data) + check_fusions(model) # pycellin DOES NOT support fusion events + + return model if __name__ == "__main__": geff_file = ( - "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/mouse-20250719.zarr/tracks" + "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/reader_test_graph.geff" ) + # geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/mouse-20250719.zarr/tracks" # geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" + geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" - geff_graph, geff_md = geff.read_nx(geff_file, validate=True) - print(geff_graph) - # Check how many weakly connected components there are. - print( - f"Number of weakly connected components: {len(list(nx.weakly_connected_components(geff_graph)))}" - ) - for k, v in geff_graph.graph.items(): - print(f"{k}: {v}") - # print(graph.graph["axes"][0].unit) - - if geff_md.directed: - print("The graph is directed.") - - metadata = {} # type: dict[str, Any] - metadata["name"] = Path(geff_file).stem - metadata["file_location"] = geff_file - metadata["provenance"] = "geff" - metadata["date"] = str(datetime.now()) - # metadata["space_unit"] = - # metadata["time_unit"] = - metadata["pycellin_version"] = version("pycellin") - metadata["geff_version"] = geff_md.geff_version - for md in geff_md: - print(md) + print(geff_file) + model = load_geff_file(geff_file) + # print(model) + print(model.feat_declaration.feats_dict) + # lineages = model.get_cell_lineages() + # print(f"Number of lineages: {len(lineages)}") + # for lin in lineages: + # print(lin) + # lin0 = lineages[0] + # print(lin0.nodes(data=True)) + # lin0.plot() + + # cell_id_key + # lineage_id_key + # time_key + # cell_x_key + # cell_y_key + # cell_z_key + + # geff_graph, geff_md = geff.read_nx(geff_file, validate=True) + # print(geff_graph) + # # Check how many weakly connected components there are. + # print( + # f"Number of weakly connected components: {len(list(nx.weakly_connected_components(geff_graph)))}" + # ) + # for k, v in geff_graph.graph.items(): + # print(f"{k}: {v}") + # # print(graph.graph["axes"][0].unit) + + # if geff_md.directed: + # print("The graph is directed.") + + # metadata = {} # type: dict[str, Any] + # metadata["name"] = Path(geff_file).stem + # metadata["file_location"] = geff_file + # metadata["provenance"] = "geff" + # metadata["date"] = str(datetime.now()) + # # metadata["space_unit"] = + # # metadata["time_unit"] = + # metadata["pycellin_version"] = version("pycellin") + # metadata["geff_version"] = geff_md.geff_version + # for md in geff_md: + # print(md) From 60c352b5885f93b97d242281f397a7d20c4d59e6 Mon Sep 17 00:00:00 2001 From: lxenard Date: Wed, 6 Aug 2025 17:40:23 +0200 Subject: [PATCH 11/27] WIP GEFF loader --- pycellin/io/geff/loader.py | 86 ++++++++++++++++++++++++++++++++------ pycellin/io/utils.py | 17 +++----- 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index 8c1e348..6916d2d 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -16,16 +16,64 @@ import geff import networkx as nx -from pycellin.classes import Model -from pycellin.classes.data import Data +from pycellin.classes import Data, Feature, Model +from pycellin.custom_types import FeatureType from pycellin.io.utils import ( - check_fusions, _split_graph_into_lineages, _update_lineages_IDs_key, _update_node_feature_key, + check_fusions, ) +def _extract_feats_metadata( + md: dict[str, geff.metadata_schema.PropMetadata], + feats_dict: dict[str, Feature], + feat_type: FeatureType, +) -> None: + for key, prop in md.items(): + if key not in feats_dict: + feats_dict[key] = Feature( + name=key, + description=prop.description if prop.description else key, + provenance="geff", + feat_type=feat_type, + lin_type="CellLineage", + data_type=prop.dtype, + unit=prop.unit, + ) + else: + if feats_dict[key].feat_type != feat_type: + # If the key is already taken, we rename with prefix. + if feat_type == "node": + prefix = "cell" + elif feat_type == "edge": + prefix = "link" + else: + raise ValueError( + f"Unsupported feature type: {feat_type}. Expected 'node' or 'edge'." + ) + # TODO: should we rename both features? + new_key = f"{prefix}_{key}" + feats_dict[new_key] = Feature( + name=new_key, + description=prop.description if prop.description else new_key, + provenance="geff", + feat_type=feat_type, + lin_type="CellLineage", + data_type=prop.dtype, + unit=prop.unit, + ) + else: + raise KeyError( + f"Feature '{key}' already exists in feats_dict for nodes and edges. " + "Please ensure unique feature names." + ) + # TODO: but then, what does the user do? They might not be able to rename + # the feature from the tool that generated the geff file. + # Directly ask the user how to rename? + + def load_geff_file( geff_file: Path | str, cell_id_key: str | None = None, @@ -55,19 +103,30 @@ def load_geff_file( print(type(geff_graph)) print(geff_md.directed) - if "track_node_props" in geff_md and "lineage" in geff_md.track_node_props: + if geff_md.track_node_props is not None and "lineage" in geff_md.track_node_props: lin_id_key = geff_md.track_node_props["lineage"] else: lin_id_key = None print("lin_id_key:", lin_id_key) # Determine axes # If no axes, need to have them as arguments...? Set a default to x, y, z, t...? + print("Axes:", geff_md.axes) + # display_hints=DisplayHint( + # display_horizontal="POSITION_X", + # display_vertical="POSITION_Y", + # display_depth="POSITION_Z", + # display_time="POSITION_T", + # ), # Is int ID ensured in geff? YES # int_graph = nx.relabel_nodes(geff_graph, {node: int(node) for node in geff_graph.nodes()}) # Extract and dispatch metadata # TODO: but for now we wait for the change in geff metadata specs + feats_dict = {} + if geff_md.node_props_metadata is not None: + # print(geff_md.node_props_metadata) + _extract_feats_metadata(geff_md.node_props_metadata, feats_dict, "node") # Split the graph into lineages lineages = _split_graph_into_lineages(geff_graph, lineage_ID_key=lin_id_key) @@ -78,11 +137,11 @@ def load_geff_file( for lin in lineages: if cell_id_key is None: for node in lin.nodes: - lin.nodes[node]["cell_ID"] = int( - node - ) # do I need to ensure this is an int? + lin.nodes[node]["cell_ID"] = node else: - _update_node_feature_key(lin, cell_id_key, "cell_ID") + _update_node_feature_key(lin, old_key=cell_id_key, new_key="cell_ID") + # TODO: cells positions and edges positions (keys from axes) + # Time? # Check for fusions data = Data({lin.graph["lineage_ID"]: lin for lin in lineages}) @@ -95,17 +154,18 @@ def load_geff_file( if __name__ == "__main__": - geff_file = ( - "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/reader_test_graph.geff" - ) + geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/reader_test_graph.geff" # geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/mouse-20250719.zarr/tracks" # geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" - geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" + geff_file = ( + "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" + ) + geff_file = "/mnt/data/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" print(geff_file) model = load_geff_file(geff_file) # print(model) - print(model.feat_declaration.feats_dict) + print("feats_dict", model.feat_declaration.feats_dict) # lineages = model.get_cell_lineages() # print(f"Number of lineages: {len(lineages)}") # for lin in lineages: diff --git a/pycellin/io/utils.py b/pycellin/io/utils.py index 060ada6..e19574a 100644 --- a/pycellin/io/utils.py +++ b/pycellin/io/utils.py @@ -82,9 +82,7 @@ def _add_lineages_features( current_lineage_id = list(tmp)[0] current_lineage_attr = [ - d_attr - for d_attr in lin_features - if d_attr[lineage_ID_key] == current_lineage_id + d_attr for d_attr in lin_features if d_attr[lineage_ID_key] == current_lineage_id ][0] # Adding the features to the lineage. @@ -117,8 +115,7 @@ def _split_graph_into_lineages( # One subgraph is created per lineage, so each subgraph is # a connected component of `graph`. lineages = [ - CellLineage(graph.subgraph(c).copy()) - for c in nx.weakly_connected_components(graph) + CellLineage(graph.subgraph(c).copy()) for c in nx.weakly_connected_components(graph) ] del graph # Redondant with the subgraphs. if not lin_features: @@ -179,9 +176,7 @@ def _update_node_feature_key( lineage.nodes[node][new_key] = lineage.nodes[node].pop(old_key) else: if enforce_old_key_existence: - raise ValueError( - f"Node {node} does not have the required key '{old_key}'." - ) + raise ValueError(f"Node {node} does not have the required key '{old_key}'.") if set_default_if_missing: lineage.nodes[node][new_key] = default_value @@ -209,8 +204,8 @@ def _update_lineage_feature_key( def _update_lineages_IDs_key( lineages: list[CellLineage], - lineage_ID_key: str, -) -> int | None: + lineage_ID_key: str | None, +) -> None: """ Update the lineage ID key of lineage graphs to match pycellin convention. @@ -225,7 +220,7 @@ def _update_lineages_IDs_key( ---------- lineages : list[CellLineage] The lineages to update. - lineage_ID_key : str + lineage_ID_key : str | None The key that is the lineage identifier in lineage graphs. """ ids = [lin.graph[lineage_ID_key] for lin in lineages if lineage_ID_key in lin.graph] From 075c271f71b1d3adfd7ba556bb998edae9e048ce Mon Sep 17 00:00:00 2001 From: lxenard Date: Thu, 11 Sep 2025 17:16:52 +0200 Subject: [PATCH 12/27] Replace features by properties --- notebooks/Custom properties.ipynb | 16 +- notebooks/Getting started.ipynb | 310 ++++++++++++++-------------- notebooks/Managing properties.ipynb | 182 ++++++++-------- pycellin/io/geff/loader.py | 79 +++---- pycellin/io/trackmate/loader.py | 10 +- pycellin/io/utils.py | 50 ++--- tests/io/test_utils.py | 94 ++++----- 7 files changed, 366 insertions(+), 375 deletions(-) diff --git a/notebooks/Custom properties.ipynb b/notebooks/Custom properties.ipynb index 791cf66..dd33126 100644 --- a/notebooks/Custom properties.ipynb +++ b/notebooks/Custom properties.ipynb @@ -2614,13 +2614,13 @@ "evalue": "ParityCalculator_incorrect.compute() takes 2 positional arguments but 3 were given", "output_type": "error", "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[11], line 3\u001b[0m\n\u001b[0;32m 1\u001b[0m calc \u001b[38;5;241m=\u001b[39m ParityCalculator_incorrect(prop_incorrect)\n\u001b[0;32m 2\u001b[0m model\u001b[38;5;241m.\u001b[39madd_custom_property(calc)\n\u001b[1;32m----> 3\u001b[0m \u001b[43mmodel\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mupdate\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mE:\\Code\\pycellin\\pycellin\\pycellin\\classes\\model.py:533\u001b[0m, in \u001b[0;36mModel.update\u001b[1;34m(self, props_to_update)\u001b[0m\n\u001b[0;32m 525\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[0;32m 527\u001b[0m \u001b[38;5;66;03m# self.data._freeze_lineage_data()\u001b[39;00m\n\u001b[0;32m 528\u001b[0m \n\u001b[0;32m 529\u001b[0m \u001b[38;5;66;03m# TODO: need to handle all the errors that can be raised\u001b[39;00m\n\u001b[0;32m 530\u001b[0m \u001b[38;5;66;03m# by the updater methods to avoid incoherent states.\u001b[39;00m\n\u001b[0;32m 531\u001b[0m \u001b[38;5;66;03m# => saving a copy of the model before the update so we can roll back?\u001b[39;00m\n\u001b[1;32m--> 533\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_updater\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_update\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mprops_to_update\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mE:\\Code\\pycellin\\pycellin\\pycellin\\classes\\updater.py:194\u001b[0m, in \u001b[0;36mModelUpdater._update\u001b[1;34m(self, data, props_to_update)\u001b[0m\n\u001b[0;32m 190\u001b[0m \u001b[38;5;66;03m# Recompute the properties as needed.\u001b[39;00m\n\u001b[0;32m 191\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m calc \u001b[38;5;129;01min\u001b[39;00m cell_calculators:\n\u001b[0;32m 192\u001b[0m \u001b[38;5;66;03m# Depending on the class of the calculator, a different version of\u001b[39;00m\n\u001b[0;32m 193\u001b[0m \u001b[38;5;66;03m# the enrich() method is called.\u001b[39;00m\n\u001b[1;32m--> 194\u001b[0m \u001b[43mcalc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43menrich\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 195\u001b[0m \u001b[43m \u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 196\u001b[0m \u001b[43m \u001b[49m\u001b[43mnodes_to_enrich\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_added_cells\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 197\u001b[0m \u001b[43m \u001b[49m\u001b[43medges_to_enrich\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_added_links\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 198\u001b[0m \u001b[43m \u001b[49m\u001b[43mlineages_to_enrich\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_added_lineages\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m|\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_modified_lineages\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 199\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 201\u001b[0m \u001b[38;5;66;03m# In case of modifications in the structure of some cell lineages,\u001b[39;00m\n\u001b[0;32m 202\u001b[0m \u001b[38;5;66;03m# we need to recompute the cycle lineages and their properties.\u001b[39;00m\n\u001b[0;32m 203\u001b[0m \u001b[38;5;66;03m# TODO: optimize so we don't have to recompute EVERYTHING for cycle lineages?\u001b[39;00m\n\u001b[0;32m 204\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m lin_ID \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_modified_lineages \u001b[38;5;241m|\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_added_lineages) \u001b[38;5;241m-\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_removed_lineages:\n", - "File \u001b[1;32mE:\\Code\\pycellin\\pycellin\\pycellin\\classes\\property_calculator.py:181\u001b[0m, in \u001b[0;36mNodeLocalPropCalculator.enrich\u001b[1;34m(self, data, nodes_to_enrich, **kwargs)\u001b[0m\n\u001b[0;32m 179\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m nid, lin_ID \u001b[38;5;129;01min\u001b[39;00m nodes_to_enrich:\n\u001b[0;32m 180\u001b[0m lin \u001b[38;5;241m=\u001b[39m lineages[lin_ID]\n\u001b[1;32m--> 181\u001b[0m lin\u001b[38;5;241m.\u001b[39mnodes[nid][\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mprop\u001b[38;5;241m.\u001b[39midentifier] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcompute\u001b[49m\u001b[43m(\u001b[49m\u001b[43mlin\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnid\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[1;31mTypeError\u001b[0m: ParityCalculator_incorrect.compute() takes 2 positional arguments but 3 were given" + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m calc = ParityCalculator_incorrect(prop_incorrect)\n\u001b[32m 2\u001b[39m model.add_custom_property(calc)\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[43mmodel\u001b[49m\u001b[43m.\u001b[49m\u001b[43mupdate\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/media/lxenard/data/Code/pycellin/pycellin/pycellin/classes/model.py:533\u001b[39m, in \u001b[36mModel.update\u001b[39m\u001b[34m(self, props_to_update)\u001b[39m\n\u001b[32m 525\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m 527\u001b[39m \u001b[38;5;66;03m# self.data._freeze_lineage_data()\u001b[39;00m\n\u001b[32m 528\u001b[39m \n\u001b[32m 529\u001b[39m \u001b[38;5;66;03m# TODO: need to handle all the errors that can be raised\u001b[39;00m\n\u001b[32m 530\u001b[39m \u001b[38;5;66;03m# by the updater methods to avoid incoherent states.\u001b[39;00m\n\u001b[32m 531\u001b[39m \u001b[38;5;66;03m# => saving a copy of the model before the update so we can roll back?\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m533\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_updater\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_update\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mprops_to_update\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/media/lxenard/data/Code/pycellin/pycellin/pycellin/classes/updater.py:194\u001b[39m, in \u001b[36mModelUpdater._update\u001b[39m\u001b[34m(self, data, props_to_update)\u001b[39m\n\u001b[32m 190\u001b[39m \u001b[38;5;66;03m# Recompute the properties as needed.\u001b[39;00m\n\u001b[32m 191\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m calc \u001b[38;5;129;01min\u001b[39;00m cell_calculators:\n\u001b[32m 192\u001b[39m \u001b[38;5;66;03m# Depending on the class of the calculator, a different version of\u001b[39;00m\n\u001b[32m 193\u001b[39m \u001b[38;5;66;03m# the enrich() method is called.\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m194\u001b[39m \u001b[43mcalc\u001b[49m\u001b[43m.\u001b[49m\u001b[43menrich\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 195\u001b[39m \u001b[43m \u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 196\u001b[39m \u001b[43m \u001b[49m\u001b[43mnodes_to_enrich\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_added_cells\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 197\u001b[39m \u001b[43m \u001b[49m\u001b[43medges_to_enrich\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_added_links\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 198\u001b[39m \u001b[43m \u001b[49m\u001b[43mlineages_to_enrich\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_added_lineages\u001b[49m\u001b[43m \u001b[49m\u001b[43m|\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_modified_lineages\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 199\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 201\u001b[39m \u001b[38;5;66;03m# In case of modifications in the structure of some cell lineages,\u001b[39;00m\n\u001b[32m 202\u001b[39m \u001b[38;5;66;03m# we need to recompute the cycle lineages and their properties.\u001b[39;00m\n\u001b[32m 203\u001b[39m \u001b[38;5;66;03m# TODO: optimize so we don't have to recompute EVERYTHING for cycle lineages?\u001b[39;00m\n\u001b[32m 204\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m lin_ID \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;28mself\u001b[39m._modified_lineages | \u001b[38;5;28mself\u001b[39m._added_lineages) - \u001b[38;5;28mself\u001b[39m._removed_lineages:\n", + "\u001b[36mFile \u001b[39m\u001b[32m/media/lxenard/data/Code/pycellin/pycellin/pycellin/classes/property_calculator.py:181\u001b[39m, in \u001b[36mNodeLocalPropCalculator.enrich\u001b[39m\u001b[34m(self, data, nodes_to_enrich, **kwargs)\u001b[39m\n\u001b[32m 179\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m nid, lin_ID \u001b[38;5;129;01min\u001b[39;00m nodes_to_enrich:\n\u001b[32m 180\u001b[39m lin = lineages[lin_ID]\n\u001b[32m--> \u001b[39m\u001b[32m181\u001b[39m lin.nodes[nid][\u001b[38;5;28mself\u001b[39m.prop.identifier] = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mcompute\u001b[49m\u001b[43m(\u001b[49m\u001b[43mlin\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnid\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[31mTypeError\u001b[39m: ParityCalculator_incorrect.compute() takes 2 positional arguments but 3 were given" ] } ], @@ -6223,7 +6223,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.18" + "version": "3.11.13" } }, "nbformat": 4, diff --git a/notebooks/Getting started.ipynb b/notebooks/Getting started.ipynb index 38e7a47..f297144 100644 --- a/notebooks/Getting started.ipynb +++ b/notebooks/Getting started.ipynb @@ -371,10 +371,10 @@ "name": "stdout", "output_type": "stream", "text": [ - "pixel width = 0.06587 µm\n", - "pixel height = 0.06587 µm\n", - "pixel depth = 0.06587 µm\n", - "frame = 5.0 min\n" + "pixel width = 0.06587 micrometer\n", + "pixel height = 0.06587 micrometer\n", + "pixel depth = 0.06587 micrometer\n", + "frame = 5.0 minute\n" ] } ], @@ -415,7 +415,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "E:\\Code\\pycellin\\pycellin\\pycellin\\io\\trackmate\\loader.py:1225: UserWarning: Unsupported data, 3 cell fusions detected. It is advised to deal with them before any other processing, especially for tracking related properties. Crashes and incorrect results can occur. See documentation for more details.\n", + "/media/lxenard/data/Code/pycellin/pycellin/pycellin/io/utils.py:31: UserWarning: Unsupported data, 3 cell fusions detected. It is advised to deal with them before any other processing, especially for tracking related properties. Crashes and incorrect results can occur. See documentation for more details.\n", " warnings.warn(fusion_warning)\n" ] } @@ -525,9 +525,9 @@ { "data": { "text/plain": [ - "{0: ,\n", - " 1: ,\n", - " 2: }" + "{0: ,\n", + " 1: ,\n", + " 2: }" ] }, "execution_count": 16, @@ -6085,7 +6085,7 @@ " (-2.027093888288606, 0.9125132536486831),\n", " (-1.961223884190833, 1.0442532618442293),\n", " (-1.6977438677997423, 1.0442532618442293)],\n", - " 'TRACK_ID': 1,\n", + " 'lineage_ID': 1,\n", " 'cell_ID': 8985,\n", " 'frame': 0,\n", " 'cell_name': 'ID8985',\n", @@ -6227,8 +6227,8 @@ " 'MEAN_STRAIGHT_LINE_SPEED': 0.09315523695815782,\n", " 'LINEARITY_OF_FORWARD_PROGRESSION': 0.38549094669096795,\n", " 'MEAN_DIRECTIONAL_CHANGE_RATE': 0.23933007611391033,\n", - " 'lineage_name': 'Track_1',\n", " 'lineage_ID': 1,\n", + " 'lineage_name': 'Track_1',\n", " 'lineage_x': 11.299502880687372,\n", " 'lineage_y': 20.138498761300276,\n", " 'lineage_z': 0.0,\n", @@ -6587,9 +6587,9 @@ { "data": { "text/plain": [ - "{0: ,\n", - " 1: ,\n", - " 2: }" + "{0: ,\n", + " 1: ,\n", + " 2: }" ] }, "execution_count": 37, @@ -10365,8 +10365,8 @@ "Lineage properties: TRACK_INDEX, DIVISION_TIME_MEAN, DIVISION_TIME_STD, NUMBER_SPOTS, NUMBER_GAPS, NUMBER_SPLITS, NUMBER_MERGES, NUMBER_COMPLEX, LONGEST_GAP, TRACK_DURATION, TRACK_START, TRACK_STOP, TRACK_DISPLACEMENT, TRACK_MEAN_SPEED, TRACK_MAX_SPEED, TRACK_MIN_SPEED, TRACK_MEDIAN_SPEED, TRACK_STD_SPEED, TRACK_MEAN_QUALITY, TOTAL_DISTANCE_TRAVELED, MAX_DISTANCE_TRAVELED, CONFINEMENT_RATIO, MEAN_STRAIGHT_LINE_SPEED, LINEARITY_OF_FORWARD_PROGRESSION, MEAN_DIRECTIONAL_CHANGE_RATE, lineage_name, lineage_ID, FilteredTrack, lineage_x, lineage_y, lineage_z\n", "\n", "Property(identifier='QUALITY', name='Quality', description='Quality', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None)\n", - "Property(identifier='POSITION_T', name='T', description='T', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='min')\n", - "Property(identifier='RADIUS', name='Radius', description='Radius', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", + "Property(identifier='POSITION_T', name='T', description='T', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='minute')\n", + "Property(identifier='RADIUS', name='Radius', description='Radius', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", "Property(identifier='VISIBILITY', name='Visibility', description='Visibility', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='MANUAL_SPOT_COLOR', name='Manual spot color', description='Manual spot color', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='MEAN_INTENSITY_CH1', name='Mean intensity ch1', description='Mean intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None)\n", @@ -10385,14 +10385,14 @@ "Property(identifier='SNR_CH1', name='Signal/Noise ratio ch1', description='Signal/Noise ratio ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None)\n", "Property(identifier='CONTRAST_CH2', name='Contrast ch2', description='Contrast ch2', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None)\n", "Property(identifier='SNR_CH2', name='Signal/Noise ratio ch2', description='Signal/Noise ratio ch2', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None)\n", - "Property(identifier='ELLIPSE_X0', name='Ellipse center x0', description='Ellipse center x0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='ELLIPSE_Y0', name='Ellipse center y0', description='Ellipse center y0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='ELLIPSE_MAJOR', name='Ellipse long axis', description='Ellipse long axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='ELLIPSE_MINOR', name='Ellipse short axis', description='Ellipse short axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", + "Property(identifier='ELLIPSE_X0', name='Ellipse center x0', description='Ellipse center x0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='ELLIPSE_Y0', name='Ellipse center y0', description='Ellipse center y0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='ELLIPSE_MAJOR', name='Ellipse long axis', description='Ellipse long axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='ELLIPSE_MINOR', name='Ellipse short axis', description='Ellipse short axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", "Property(identifier='ELLIPSE_THETA', name='Ellipse angle', description='Ellipse angle', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='rad')\n", "Property(identifier='ELLIPSE_ASPECTRATIO', name='Ellipse aspect ratio', description='Ellipse aspect ratio', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None)\n", - "Property(identifier='AREA', name='Area', description='Area', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm^2')\n", - "Property(identifier='PERIMETER', name='Perimeter', description='Perimeter', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", + "Property(identifier='AREA', name='Area', description='Area', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer^2')\n", + "Property(identifier='PERIMETER', name='Perimeter', description='Perimeter', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", "Property(identifier='CIRCULARITY', name='Circularity', description='Circularity', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None)\n", "Property(identifier='SOLIDITY', name='Solidity', description='Solidity', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None)\n", "Property(identifier='SHAPE_INDEX', name='Shape index', description='Shape index', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None)\n", @@ -10400,51 +10400,51 @@ "Property(identifier='SPOT_SOURCE_ID', name='Source spot ID', description='Source spot ID', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='SPOT_TARGET_ID', name='Target spot ID', description='Target spot ID', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='LINK_COST', name='Edge cost', description='Edge cost', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit=None)\n", - "Property(identifier='DIRECTIONAL_CHANGE_RATE', name='Directional change rate', description='Directional change rate', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='rad/min')\n", - "Property(identifier='SPEED', name='Speed', description='Speed', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm/min')\n", - "Property(identifier='DISPLACEMENT', name='Displacement', description='Displacement', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='EDGE_TIME', name='Edge time', description='Edge time', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='min')\n", + "Property(identifier='DIRECTIONAL_CHANGE_RATE', name='Directional change rate', description='Directional change rate', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='rad/minute')\n", + "Property(identifier='SPEED', name='Speed', description='Speed', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer/minute')\n", + "Property(identifier='DISPLACEMENT', name='Displacement', description='Displacement', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='EDGE_TIME', name='Edge time', description='Edge time', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='minute')\n", "Property(identifier='MANUAL_EDGE_COLOR', name='Manual edge color', description='Manual edge color', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='TRACK_INDEX', name='Track index', description='Track index', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None)\n", - "Property(identifier='DIVISION_TIME_MEAN', name='Mean cell division time', description='Mean cell division time', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min')\n", - "Property(identifier='DIVISION_TIME_STD', name='Std cell division time', description='Std cell division time', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min')\n", + "Property(identifier='DIVISION_TIME_MEAN', name='Mean cell division time', description='Mean cell division time', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute')\n", + "Property(identifier='DIVISION_TIME_STD', name='Std cell division time', description='Std cell division time', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute')\n", "Property(identifier='NUMBER_SPOTS', name='Number of spots in track', description='Number of spots in track', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='NUMBER_GAPS', name='Number of gaps', description='Number of gaps', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='NUMBER_SPLITS', name='Number of split events', description='Number of split events', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='NUMBER_MERGES', name='Number of merge events', description='Number of merge events', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='NUMBER_COMPLEX', name='Number of complex points', description='Number of complex points', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='LONGEST_GAP', name='Longest gap', description='Longest gap', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None)\n", - "Property(identifier='TRACK_DURATION', name='Track duration', description='Track duration', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min')\n", - "Property(identifier='TRACK_START', name='Track start', description='Track start', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min')\n", - "Property(identifier='TRACK_STOP', name='Track stop', description='Track stop', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min')\n", - "Property(identifier='TRACK_DISPLACEMENT', name='Track displacement', description='Track displacement', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='TRACK_MEAN_SPEED', name='Track mean speed', description='Track mean speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min')\n", - "Property(identifier='TRACK_MAX_SPEED', name='Track max speed', description='Track max speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min')\n", - "Property(identifier='TRACK_MIN_SPEED', name='Track min speed', description='Track min speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min')\n", - "Property(identifier='TRACK_MEDIAN_SPEED', name='Track median speed', description='Track median speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min')\n", - "Property(identifier='TRACK_STD_SPEED', name='Track std speed', description='Track std speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min')\n", + "Property(identifier='TRACK_DURATION', name='Track duration', description='Track duration', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute')\n", + "Property(identifier='TRACK_START', name='Track start', description='Track start', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute')\n", + "Property(identifier='TRACK_STOP', name='Track stop', description='Track stop', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute')\n", + "Property(identifier='TRACK_DISPLACEMENT', name='Track displacement', description='Track displacement', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='TRACK_MEAN_SPEED', name='Track mean speed', description='Track mean speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute')\n", + "Property(identifier='TRACK_MAX_SPEED', name='Track max speed', description='Track max speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute')\n", + "Property(identifier='TRACK_MIN_SPEED', name='Track min speed', description='Track min speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute')\n", + "Property(identifier='TRACK_MEDIAN_SPEED', name='Track median speed', description='Track median speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute')\n", + "Property(identifier='TRACK_STD_SPEED', name='Track std speed', description='Track std speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute')\n", "Property(identifier='TRACK_MEAN_QUALITY', name='Track mean quality', description='Track mean quality', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None)\n", - "Property(identifier='TOTAL_DISTANCE_TRAVELED', name='Total distance traveled', description='Total distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='MAX_DISTANCE_TRAVELED', name='Max distance traveled', description='Max distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm')\n", + "Property(identifier='TOTAL_DISTANCE_TRAVELED', name='Total distance traveled', description='Total distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='MAX_DISTANCE_TRAVELED', name='Max distance traveled', description='Max distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer')\n", "Property(identifier='CONFINEMENT_RATIO', name='Confinement ratio', description='Confinement ratio', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None)\n", - "Property(identifier='MEAN_STRAIGHT_LINE_SPEED', name='Mean straight line speed', description='Mean straight line speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min')\n", + "Property(identifier='MEAN_STRAIGHT_LINE_SPEED', name='Mean straight line speed', description='Mean straight line speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute')\n", "Property(identifier='LINEARITY_OF_FORWARD_PROGRESSION', name='Linearity of forward progression', description='Linearity of forward progression', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None)\n", - "Property(identifier='MEAN_DIRECTIONAL_CHANGE_RATE', name='Mean directional change rate', description='Mean directional change rate', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='rad/min')\n", + "Property(identifier='MEAN_DIRECTIONAL_CHANGE_RATE', name='Mean directional change rate', description='Mean directional change rate', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='rad/minute')\n", "Property(identifier='lineage_name', name='lineage name', description='Name of the track', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='string', unit=None)\n", "Property(identifier='cell_ID', name='cell ID', description='Unique identifier of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None)\n", - "Property(identifier='cell_x', name='X', description='X coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='cell_y', name='Y', description='Y coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='cell_z', name='Z', description='Z coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", + "Property(identifier='cell_x', name='X', description='X coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='cell_y', name='Y', description='Y coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='cell_z', name='Z', description='Z coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", "Property(identifier='frame', name='Frame', description='Frame', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None)\n", - "Property(identifier='ROI_coords', name='ROI coords', description='List of coordinates of the region of interest', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='link_x', name='Edge X', description='X coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='link_y', name='Edge Y', description='Y coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='link_z', name='Edge Z', description='Z coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm')\n", + "Property(identifier='ROI_coords', name='ROI coords', description='List of coordinates of the region of interest', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='link_x', name='Edge X', description='X coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='link_y', name='Edge Y', description='Y coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='link_z', name='Edge Z', description='Z coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer')\n", "Property(identifier='lineage_ID', name='Track ID', description='Unique identifier of the lineage', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None)\n", "Property(identifier='FilteredTrack', name='FilteredTrack', description='True if the track was not filtered out in TrackMate', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None)\n", - "Property(identifier='lineage_x', name='Track mean X', description='X coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='lineage_y', name='Track mean Y', description='Y coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm')\n", - "Property(identifier='lineage_z', name='Track mean Z', description='Z coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm')\n", + "Property(identifier='lineage_x', name='Track mean X', description='X coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='lineage_y', name='Track mean Y', description='Y coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer')\n", + "Property(identifier='lineage_z', name='Track mean Z', description='Z coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer')\n", "Property(identifier='cycle_ID', name='cycle ID', description='Unique identifier of the cell cycle, i.e. cell_ID of the last cell in the cell cycle', provenance='pycellin', prop_type='node', lin_type='CycleLineage', dtype='int', unit=None)\n", "Property(identifier='cells', name='cells', description='cell_IDs of the cells in the cell cycle, in chronological order', provenance='pycellin', prop_type='node', lin_type='CycleLineage', dtype='list[int]', unit=None)\n", "Property(identifier='cycle_length', name='cycle length', description='Number of cells in the cell cycle, minding gaps', provenance='pycellin', prop_type='node', lin_type='CycleLineage', dtype='int', unit=None)\n", @@ -13585,7 +13585,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "(529, 41)\n", + "(529, 40)\n", "Index(['lineage_ID', 'frame', 'cell_ID', 'STD_INTENSITY_CH1', 'SOLIDITY',\n", " 'STD_INTENSITY_CH2', 'QUALITY', 'POSITION_T', 'TOTAL_INTENSITY_CH2',\n", " 'TOTAL_INTENSITY_CH1', 'CONTRAST_CH1', 'ELLIPSE_MINOR', 'ELLIPSE_THETA',\n", @@ -13595,7 +13595,7 @@ " 'SNR_CH1', 'ELLIPSE_X0', 'SHAPE_INDEX', 'SNR_CH2',\n", " 'MEDIAN_INTENSITY_CH1', 'VISIBILITY', 'RADIUS', 'MEDIAN_INTENSITY_CH2',\n", " 'ELLIPSE_ASPECTRATIO', 'PERIMETER', 'ROI_N_POINTS', 'ROI_coords',\n", - " 'TRACK_ID', 'cell_name', 'cell_x', 'cell_y', 'cell_z'],\n", + " 'cell_name', 'cell_x', 'cell_y', 'cell_z'],\n", " dtype='object')\n" ] } @@ -13643,12 +13643,12 @@ " TOTAL_INTENSITY_CH2\n", " TOTAL_INTENSITY_CH1\n", " ...\n", + " RADIUS\n", " MEDIAN_INTENSITY_CH2\n", " ELLIPSE_ASPECTRATIO\n", " PERIMETER\n", " ROI_N_POINTS\n", " ROI_coords\n", - " TRACK_ID\n", " cell_name\n", " cell_x\n", " cell_y\n", @@ -13717,12 +13717,12 @@ " 70098.0\n", " 26356.0\n", " ...\n", + " 0.921242\n", " 114.0\n", " 6.703090\n", " 10.285699\n", " 52\n", " [(-1.5894197951076556, 1.1551815744407925), (-...\n", - " 0.0\n", " ID8993\n", " 15.389186\n", " 19.363325\n", @@ -13741,12 +13741,12 @@ " 34098.0\n", " 4245.0\n", " ...\n", + " 0.638839\n", " 115.0\n", " 4.279183\n", " 5.944787\n", " 30\n", " [(-0.727319272545234, 0.8544524671426856), (-0...\n", - " 0.0\n", " ID9013\n", " 16.832535\n", " 18.214914\n", @@ -13765,12 +13765,12 @@ " 33523.0\n", " 4350.0\n", " ...\n", + " 0.633956\n", " 115.0\n", " 3.903724\n", " 5.747940\n", " 34\n", " [(-0.6141453589387584, 0.5208483829178867), (-...\n", - " 0.0\n", " ID9014\n", " 14.282171\n", " 19.470698\n", @@ -13778,7 +13778,7 @@ " \n", " \n", "\n", - "

5 rows × 41 columns

\n", + "

5 rows × 40 columns

\n", "" ], "text/plain": [ @@ -13796,19 +13796,19 @@ "3 293.0 5.0 34098.0 4245.0 ... \n", "4 291.0 5.0 33523.0 4350.0 ... \n", "\n", - " MEDIAN_INTENSITY_CH2 ELLIPSE_ASPECTRATIO PERIMETER ROI_N_POINTS \\\n", - "0 NaN NaN NaN NaN \n", - "1 NaN NaN NaN NaN \n", - "2 114.0 6.703090 10.285699 52 \n", - "3 115.0 4.279183 5.944787 30 \n", - "4 115.0 3.903724 5.747940 34 \n", + " RADIUS MEDIAN_INTENSITY_CH2 ELLIPSE_ASPECTRATIO PERIMETER \\\n", + "0 NaN NaN NaN NaN \n", + "1 NaN NaN NaN NaN \n", + "2 0.921242 114.0 6.703090 10.285699 \n", + "3 0.638839 115.0 4.279183 5.944787 \n", + "4 0.633956 115.0 3.903724 5.747940 \n", "\n", - " ROI_coords TRACK_ID cell_name \\\n", - "0 NaN NaN NaN \n", - "1 NaN NaN NaN \n", - "2 [(-1.5894197951076556, 1.1551815744407925), (-... 0.0 ID8993 \n", - "3 [(-0.727319272545234, 0.8544524671426856), (-0... 0.0 ID9013 \n", - "4 [(-0.6141453589387584, 0.5208483829178867), (-... 0.0 ID9014 \n", + " ROI_N_POINTS ROI_coords cell_name \\\n", + "0 NaN NaN NaN \n", + "1 NaN NaN NaN \n", + "2 52 [(-1.5894197951076556, 1.1551815744407925), (-... ID8993 \n", + "3 30 [(-0.727319272545234, 0.8544524671426856), (-0... ID9013 \n", + "4 34 [(-0.6141453589387584, 0.5208483829178867), (-... ID9014 \n", "\n", " cell_x cell_y cell_z \n", "0 1.000000 2.000000 0.0 \n", @@ -13817,7 +13817,7 @@ "3 16.832535 18.214914 0.0 \n", "4 14.282171 19.470698 0.0 \n", "\n", - "[5 rows x 41 columns]" + "[5 rows x 40 columns]" ] }, "execution_count": 66, @@ -13870,9 +13870,9 @@ "output_type": "stream", "text": [ "(524, 13)\n", - "Index(['lineage_ID', 'source_cell_ID', 'target_cell_ID', 'EDGE_TIME', 'link_y',\n", - " 'SPEED', 'DIRECTIONAL_CHANGE_RATE', 'DISPLACEMENT', 'SPOT_SOURCE_ID',\n", - " 'LINK_COST', 'link_x', 'SPOT_TARGET_ID', 'link_z'],\n", + "Index(['lineage_ID', 'source_cell_ID', 'target_cell_ID', 'DISPLACEMENT',\n", + " 'link_z', 'DIRECTIONAL_CHANGE_RATE', 'SPOT_SOURCE_ID', 'SPEED',\n", + " 'EDGE_TIME', 'SPOT_TARGET_ID', 'LINK_COST', 'link_y', 'link_x'],\n", " dtype='object')\n" ] } @@ -13912,16 +13912,16 @@ " lineage_ID\n", " source_cell_ID\n", " target_cell_ID\n", - " EDGE_TIME\n", - " link_y\n", - " SPEED\n", - " DIRECTIONAL_CHANGE_RATE\n", " DISPLACEMENT\n", + " link_z\n", + " DIRECTIONAL_CHANGE_RATE\n", " SPOT_SOURCE_ID\n", + " SPEED\n", + " EDGE_TIME\n", + " SPOT_TARGET_ID\n", " LINK_COST\n", + " link_y\n", " link_x\n", - " SPOT_TARGET_ID\n", - " link_z\n", " \n", " \n", " \n", @@ -13930,106 +13930,106 @@ " 0\n", " 9216.0\n", " 9290.0\n", - " 92.5\n", - " 18.700692\n", - " 0.087533\n", - " 0.556800\n", " 0.437667\n", + " 0.0\n", + " 0.556800\n", " 9216.0\n", + " 0.087533\n", + " 92.5\n", + " 9290.0\n", " 0.397900\n", + " 18.700692\n", " 15.776474\n", - " 9290.0\n", - " 0.0\n", " \n", " \n", " 1\n", " 0\n", " 9218.0\n", " 9294.0\n", - " 92.5\n", - " 15.808225\n", - " 0.284526\n", - " 0.608195\n", " 1.422631\n", + " 0.0\n", + " 0.608195\n", " 9218.0\n", + " 0.284526\n", + " 92.5\n", + " 9294.0\n", " 0.467842\n", + " 15.808225\n", " 20.106089\n", - " 9294.0\n", - " 0.0\n", " \n", " \n", " 2\n", " 0\n", " 9222.0\n", " 9306.0\n", - " 92.5\n", - " 22.138865\n", - " 0.126264\n", - " 0.055708\n", " 0.631321\n", + " 0.0\n", + " 0.055708\n", " 9222.0\n", + " 0.126264\n", + " 92.5\n", + " 9306.0\n", " 0.403972\n", + " 22.138865\n", " 11.621851\n", - " 9306.0\n", - " 0.0\n", " \n", " \n", " 3\n", " 0\n", " 9223.0\n", " 9293.0\n", - " 92.5\n", - " 15.591736\n", - " 0.273041\n", - " 0.606456\n", " 1.365206\n", + " 0.0\n", + " 0.606456\n", " 9223.0\n", + " 0.273041\n", + " 92.5\n", + " 9293.0\n", " 0.491012\n", + " 15.591736\n", " 18.851734\n", - " 9293.0\n", - " 0.0\n", " \n", " \n", " 4\n", " 0\n", - " 9332.0\n", - " 9492.0\n", - " 102.5\n", - " 18.724855\n", - " 0.159162\n", - " 0.614101\n", - " 0.795810\n", - " 9332.0\n", - " 0.710950\n", - " 15.892708\n", - " 9492.0\n", + " 9227.0\n", + " 9308.0\n", + " 2.009642\n", " 0.0\n", + " 0.011716\n", + " 9227.0\n", + " 0.401928\n", + " 92.5\n", + " 9308.0\n", + " 0.616573\n", + " 12.818797\n", + " 22.279749\n", " \n", " \n", "\n", "" ], "text/plain": [ - " lineage_ID source_cell_ID target_cell_ID EDGE_TIME link_y SPEED \\\n", - "0 0 9216.0 9290.0 92.5 18.700692 0.087533 \n", - "1 0 9218.0 9294.0 92.5 15.808225 0.284526 \n", - "2 0 9222.0 9306.0 92.5 22.138865 0.126264 \n", - "3 0 9223.0 9293.0 92.5 15.591736 0.273041 \n", - "4 0 9332.0 9492.0 102.5 18.724855 0.159162 \n", + " lineage_ID source_cell_ID target_cell_ID DISPLACEMENT link_z \\\n", + "0 0 9216.0 9290.0 0.437667 0.0 \n", + "1 0 9218.0 9294.0 1.422631 0.0 \n", + "2 0 9222.0 9306.0 0.631321 0.0 \n", + "3 0 9223.0 9293.0 1.365206 0.0 \n", + "4 0 9227.0 9308.0 2.009642 0.0 \n", "\n", - " DIRECTIONAL_CHANGE_RATE DISPLACEMENT SPOT_SOURCE_ID LINK_COST \\\n", - "0 0.556800 0.437667 9216.0 0.397900 \n", - "1 0.608195 1.422631 9218.0 0.467842 \n", - "2 0.055708 0.631321 9222.0 0.403972 \n", - "3 0.606456 1.365206 9223.0 0.491012 \n", - "4 0.614101 0.795810 9332.0 0.710950 \n", + " DIRECTIONAL_CHANGE_RATE SPOT_SOURCE_ID SPEED EDGE_TIME \\\n", + "0 0.556800 9216.0 0.087533 92.5 \n", + "1 0.608195 9218.0 0.284526 92.5 \n", + "2 0.055708 9222.0 0.126264 92.5 \n", + "3 0.606456 9223.0 0.273041 92.5 \n", + "4 0.011716 9227.0 0.401928 92.5 \n", "\n", - " link_x SPOT_TARGET_ID link_z \n", - "0 15.776474 9290.0 0.0 \n", - "1 20.106089 9294.0 0.0 \n", - "2 11.621851 9306.0 0.0 \n", - "3 18.851734 9293.0 0.0 \n", - "4 15.892708 9492.0 0.0 " + " SPOT_TARGET_ID LINK_COST link_y link_x \n", + "0 9290.0 0.397900 18.700692 15.776474 \n", + "1 9294.0 0.467842 15.808225 20.106089 \n", + "2 9306.0 0.403972 22.138865 11.621851 \n", + "3 9293.0 0.491012 15.591736 18.851734 \n", + "4 9308.0 0.616573 12.818797 22.279749 " ] }, "execution_count": 69, @@ -14513,7 +14513,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "(152, 41)\n" + "(152, 40)\n" ] }, { @@ -14548,12 +14548,12 @@ " TOTAL_INTENSITY_CH2\n", " TOTAL_INTENSITY_CH1\n", " ...\n", + " RADIUS\n", " MEDIAN_INTENSITY_CH2\n", " ELLIPSE_ASPECTRATIO\n", " PERIMETER\n", " ROI_N_POINTS\n", " ROI_coords\n", - " TRACK_ID\n", " cell_name\n", " cell_x\n", " cell_y\n", @@ -14574,12 +14574,12 @@ " 70098.0\n", " 26356.0\n", " ...\n", + " 0.921242\n", " 114.0\n", " 6.703090\n", " 10.285699\n", " 52\n", " [(-1.5894197951076556, 1.1551815744407925), (-...\n", - " 0\n", " ID8993\n", " 15.389186\n", " 19.363325\n", @@ -14598,12 +14598,12 @@ " 34098.0\n", " 4245.0\n", " ...\n", + " 0.638839\n", " 115.0\n", " 4.279183\n", " 5.944787\n", " 30\n", " [(-0.727319272545234, 0.8544524671426856), (-0...\n", - " 0\n", " ID9013\n", " 16.832535\n", " 18.214914\n", @@ -14622,12 +14622,12 @@ " 33523.0\n", " 4350.0\n", " ...\n", + " 0.633956\n", " 115.0\n", " 3.903724\n", " 5.747940\n", " 34\n", " [(-0.6141453589387584, 0.5208483829178867), (-...\n", - " 0\n", " ID9014\n", " 14.282171\n", " 19.470698\n", @@ -14646,12 +14646,12 @@ " 39655.0\n", " 1014.0\n", " ...\n", + " 0.682225\n", " 117.0\n", " 4.359176\n", " 6.355476\n", " 31\n", " [(-0.6357009198638028, 0.43737422107603763), (...\n", - " 0\n", " ID8986\n", " 13.908507\n", " 19.290692\n", @@ -14670,12 +14670,12 @@ " 32526.0\n", " 1124.0\n", " ...\n", + " 0.625181\n", " 115.0\n", " 5.287340\n", " 6.341672\n", " 35\n", " [(-0.801263212391051, 0.8421507178785816), (-0...\n", - " 0\n", " ID8989\n", " 16.774739\n", " 18.029605\n", @@ -14683,7 +14683,7 @@ " \n", " \n", "\n", - "

5 rows × 41 columns

\n", + "

5 rows × 40 columns

\n", "" ], "text/plain": [ @@ -14701,19 +14701,19 @@ "3 338.0 10.0 39655.0 1014.0 ... \n", "4 288.0 10.0 32526.0 1124.0 ... \n", "\n", - " MEDIAN_INTENSITY_CH2 ELLIPSE_ASPECTRATIO PERIMETER ROI_N_POINTS \\\n", - "0 114.0 6.703090 10.285699 52 \n", - "1 115.0 4.279183 5.944787 30 \n", - "2 115.0 3.903724 5.747940 34 \n", - "3 117.0 4.359176 6.355476 31 \n", - "4 115.0 5.287340 6.341672 35 \n", + " RADIUS MEDIAN_INTENSITY_CH2 ELLIPSE_ASPECTRATIO PERIMETER \\\n", + "0 0.921242 114.0 6.703090 10.285699 \n", + "1 0.638839 115.0 4.279183 5.944787 \n", + "2 0.633956 115.0 3.903724 5.747940 \n", + "3 0.682225 117.0 4.359176 6.355476 \n", + "4 0.625181 115.0 5.287340 6.341672 \n", "\n", - " ROI_coords TRACK_ID cell_name \\\n", - "0 [(-1.5894197951076556, 1.1551815744407925), (-... 0 ID8993 \n", - "1 [(-0.727319272545234, 0.8544524671426856), (-0... 0 ID9013 \n", - "2 [(-0.6141453589387584, 0.5208483829178867), (-... 0 ID9014 \n", - "3 [(-0.6357009198638028, 0.43737422107603763), (... 0 ID8986 \n", - "4 [(-0.801263212391051, 0.8421507178785816), (-0... 0 ID8989 \n", + " ROI_N_POINTS ROI_coords cell_name \\\n", + "0 52 [(-1.5894197951076556, 1.1551815744407925), (-... ID8993 \n", + "1 30 [(-0.727319272545234, 0.8544524671426856), (-0... ID9013 \n", + "2 34 [(-0.6141453589387584, 0.5208483829178867), (-... ID9014 \n", + "3 31 [(-0.6357009198638028, 0.43737422107603763), (... ID8986 \n", + "4 35 [(-0.801263212391051, 0.8421507178785816), (-0... ID8989 \n", "\n", " cell_x cell_y cell_z \n", "0 15.389186 19.363325 0.0 \n", @@ -14722,7 +14722,7 @@ "3 13.908507 19.290692 0.0 \n", "4 16.774739 18.029605 0.0 \n", "\n", - "[5 rows x 41 columns]" + "[5 rows x 40 columns]" ] }, "execution_count": 77, @@ -14961,7 +14961,7 @@ { "data": { "text/plain": [ - "Model(model_metadata={'name': 'FakeTracks', 'file_location': '../sample_data/FakeTracks.xml', 'provenance': 'TrackMate', 'date': '2025-09-10 17:22:15.792372', 'space_unit': 'pixel', 'time_unit': 'sec', 'pycellin_version': '0.4.0', 'TrackMate_version': '8.0.0-SNAPSHOT-f411154ed1a4b9de350bbfe91c230cf3ae7639a3', 'time_step': 1.0, 'pixel_size': {'width': 1.0, 'height': 1.0, 'depth': 1.0}, 'Log': \"0.1101/2021.09.03.458852\\nand / or:\\nTinevez, JY.; Perry, N. & Schindelin, J. et al. (2017), 'TrackMate: An open and extensible platform for single-particle tracking.', Methods 115: 80-90, PMID 27713081.\\nhttps://www.sciencedirect.com/science/article/pii/S1046202316303346\\n\\nNumerical feature analyzers:\\nSpot feature analyzers:\\n - Manual spot color provides: Spot color; is manual.\\n - Spot intensity provides: Mean ch1, Median ch1, Min ch1, Max ch1, Sum ch1, Std ch1.\\n - Spot contrast and SNR provides: Ctrst ch1, SNR ch1.\\n - Spot fit 2D ellipse provides: El. x0, El. y0, El. long axis, El. sh. axis, El. angle, El. a.r.\\n - Spot 2D shape descriptors provides: Area, Perim., Circ., Solidity.\\nEdge feature analyzers:\\n - Directional change provides: γ rate.\\n - Edge speed provides: Speed, Disp.\\n - Edge target provides: Source ID, Target ID, Cost.\\n - Edge location provides: Edge T, Edge X, Edge Y, Edge Z.\\n - Manual edge color provides: Edge color; is manual.\\nTrack feature analyzers:\\n - Branching analyzer provides: N spots, N gaps, N splits, N merges, N complex, Lgst gap.\\n - Track duration provides: Duration, Track start, Track stop, Track disp.\\n - Track index provides: Index, ID.\\n - Track location provides: Track X, Track Y, Track Z.\\n - Track speed provides: Mean sp., Max speed, Min speed, Med. speed, Std speed.\\n - Track quality provides: Mean Q.\\n - Track motility analysis provides: Total dist., Max dist., Cfn. ratio, Mn. v. line, Fwd. progr., Mn. γ rate.\\n\\nImage region of interest:\\nImage data:\\nFor the image named: FakeTracks.tif.\\nMatching file FakeTracks.tif in current folder.\\nGeometry:\\n X = 0 - 127, dx = 1.00000\\n Y = 0 - 127, dy = 1.00000\\n Z = 0 - 0, dz = 1.00000\\n T = 0 - 49, dt = 1.00000\\n\\nConfigured detector StarDist detector with settings:\\n - target channel: 1\\n\\nStarting detection process using 24 threads.\\nDetection processes 1 frame simultaneously and allocates 24 threads per frame.\\nFound 107 spots.\\nDetection done in 2.3 s.\\n\\nComputing spot quality histogram...\\nInitial thresholding with a quality threshold above 0.4 ...\\nStarting initial filtering process.\\nRetained 107 spots out of 107.\\n\\nAdding morphology analyzers...\\n - Spot fit 2D ellipse provides: El. x0, El. y0, El. long axis, El. sh. axis, El. angle, El. a.r.\\n - Spot 2D shape descriptors provides: Area, Perim., Circ., Solidity.\\n\\nCalculating spot features...\\nCalculating features done in 0.1 s.\\n\\nPerforming spot filtering on the following features:\\n - on Area below 49.9\\nKept 106 spots out of 107.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 5.0\\n - gap closing max distance: 5.0\\n - merging feature penalties: \\n - splitting max distance: 5.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 5.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 17 tracks.\\n - avg size: 5.1 spots.\\n - min size: 2 spots.\\n - max size: 12 spots.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 8.0\\n - gap closing max distance: 5.0\\n - merging feature penalties: \\n - splitting max distance: 5.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 5.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 9 tracks.\\n - avg size: 10.7 spots.\\n - min size: 2 spots.\\n - max size: 42 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 23.9\\nKept 4 spots out of 9.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 23.9\\nKept 4 spots out of 9.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 8.0\\n - gap closing max distance: 5.0\\n - merging feature penalties: \\n - splitting max distance: 8.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 8.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 7 tracks.\\n - avg size: 13.7 spots.\\n - min size: 2 spots.\\n - max size: 74 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 10.0\\n - gap closing max distance: 5.0\\n - merging feature penalties: \\n - splitting max distance: 8.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 8.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 7 tracks.\\n - avg size: 13.7 spots.\\n - min size: 2 spots.\\n - max size: 74 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 10.0\\n - gap closing max distance: 8.0\\n - merging feature penalties: \\n - splitting max distance: 8.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 8.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 7 tracks.\\n - avg size: 13.7 spots.\\n - min size: 2 spots.\\n - max size: 74 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 10.0\\n - gap closing max distance: 10.0\\n - merging feature penalties: \\n - splitting max distance: 10.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 10.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 7 tracks.\\n - avg size: 13.7 spots.\\n - min size: 2 spots.\\n - max size: 74 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\nSaving data...\\nComputing edge features:\\n - Directional change in 2 ms.\\n - Edge speed in 0 ms.\\n - Edge target in 1 ms.\\n - Edge location in 1 ms.\\nComputation done in 4 ms.\\nComputing track features:\\n - Branching analyzer in 0 ms.\\n - Track duration in 0 ms.\\n - Track index in 0 ms.\\n - Track location in 4 ms.\\n - Track speed in 0 ms.\\n - Track quality in 1 ms.\\n - Track motility analysis in 0 ms.\\nComputation done in 6 ms.\\n--------------------\\nWarnings occurred during reading the file:\\n--------------------\\nCannot read image file: /mnt/data/Code/pycellin/sample_data/FakeTracks.tif.\\n--------------------\\nFile loaded on Tue, 17 Oct 2023 18:15:18\\nTrackMate v8.0.0-SNAPSHOT-f411154ed1a4b9de350bbfe91c230cf3ae7639a3\\nPlease note that TrackMate is available through Fiji, and is based on a publication. If you use it successfully for your research please be so kind to cite our work:\\nErshov, D., Phan, MS., Pylvänäinen, J.W., Rigaud S.U., et al. TrackMate 7: integrating state-of-the-art segmentation algorithms into tracking pipelines. Nat Methods (2022). https://doi.org/10.1038/s41592-022-01507-1\\nhttps://doi.org/10.1038/s41592-022-01507-1\\nand / or:\\nTinevez, JY.; Perry, N. & Schindelin, J. et al. (2017), 'TrackMate: An open and extensible platform for single-particle tracking.', Methods 115: 80-90, PMID 27713081.\\nhttps://www.sciencedirect.com/science/article/pii/S1046202316303346\\nSaving data...\\nWarning: The source image does not match a file on the system.TrackMate won't be able to reload it when opening this XML file.\\nTo fix this, save the source image to a TIF file before saving the TrackMate session.\\nComputing edge features:\\n - Directional change in 4 ms.\\n - Edge speed in 14 ms.\\n - Edge target in 11 ms.\\n - Edge location in 5 ms.\\nComputation done in 34 ms.\\nComputing track features:\\n - Branching analyzer in 0 ms.\\n - Track duration in 3 ms.\\n - Track index in 0 ms.\\n - Track location in 0 ms.\\n - Track speed in 1 ms.\\n - Track quality in 0 ms.\\n - Track motility analysis in 1 ms.\\nComputation done in 5 ms.\\n \", 'Settings': '\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n ', 'GUIState': '\\n ', 'DisplaySettings': '{\\n \"name\": \"CurrentDisplaySettings\",\\n \"spotUniformColor\": \"204, 51, 204, 255\",\\n \"spotColorByType\": \"TRACKS\",\\n \"spotColorByFeature\": \"TRACK_INDEX\",\\n \"spotDisplayRadius\": 1.0,\\n \"spotDisplayedAsRoi\": true,\\n \"spotMin\": 0.0,\\n \"spotMax\": 75.0,\\n \"spotShowName\": false,\\n \"trackMin\": 0.0,\\n \"trackMax\": 10.0,\\n \"trackColorByType\": \"TRACKS\",\\n \"trackColorByFeature\": \"TRACK_INDEX\",\\n \"trackUniformColor\": \"204, 204, 51, 255\",\\n \"undefinedValueColor\": \"0, 0, 0, 255\",\\n \"missingValueColor\": \"89, 89, 89, 255\",\\n \"highlightColor\": \"51, 230, 51, 255\",\\n \"trackDisplayMode\": \"LOCAL\",\\n \"colormap\": \"Jet\",\\n \"limitZDrawingDepth\": false,\\n \"drawingZDepth\": 10.0,\\n \"fadeTracks\": true,\\n \"fadeTrackRange\": 15,\\n \"useAntialiasing\": true,\\n \"spotVisible\": true,\\n \"trackVisible\": true,\\n \"font\": {\\n \"name\": \"Arial\",\\n \"style\": 1,\\n \"size\": 12,\\n \"pointSize\": 12.0,\\n \"fontSerializedDataVersion\": 1\\n },\\n \"lineThickness\": 1.0,\\n \"selectionLineThickness\": 4.0,\\n \"trackschemeBackgroundColor1\": \"128, 128, 128, 255\",\\n \"trackschemeBackgroundColor2\": \"192, 192, 192, 255\",\\n \"trackschemeForegroundColor\": \"0, 0, 0, 255\",\\n \"trackschemeDecorationColor\": \"0, 0, 0, 255\",\\n \"trackschemeFillBox\": false,\\n \"spotFilled\": true,\\n \"spotTransparencyAlpha\": 0.42000000000000004\\n}\\n'}, props_metadata=PropsMetadata(props={'QUALITY': Property(identifier='QUALITY', name='Quality', description='Quality', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'POSITION_T': Property(identifier='POSITION_T', name='T', description='T', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='sec'), 'RADIUS': Property(identifier='RADIUS', name='Radius', description='Radius', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'VISIBILITY': Property(identifier='VISIBILITY', name='Visibility', description='Visibility', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None), 'MANUAL_SPOT_COLOR': Property(identifier='MANUAL_SPOT_COLOR', name='Manual spot color', description='Manual spot color', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None), 'MEAN_INTENSITY_CH1': Property(identifier='MEAN_INTENSITY_CH1', name='Mean intensity ch1', description='Mean intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'MEDIAN_INTENSITY_CH1': Property(identifier='MEDIAN_INTENSITY_CH1', name='Median intensity ch1', description='Median intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'MIN_INTENSITY_CH1': Property(identifier='MIN_INTENSITY_CH1', name='Min intensity ch1', description='Min intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'MAX_INTENSITY_CH1': Property(identifier='MAX_INTENSITY_CH1', name='Max intensity ch1', description='Max intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'TOTAL_INTENSITY_CH1': Property(identifier='TOTAL_INTENSITY_CH1', name='Sum intensity ch1', description='Sum intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'STD_INTENSITY_CH1': Property(identifier='STD_INTENSITY_CH1', name='Std intensity ch1', description='Std intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'CONTRAST_CH1': Property(identifier='CONTRAST_CH1', name='Contrast ch1', description='Contrast ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'SNR_CH1': Property(identifier='SNR_CH1', name='Signal/Noise ratio ch1', description='Signal/Noise ratio ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'ELLIPSE_X0': Property(identifier='ELLIPSE_X0', name='Ellipse center x0', description='Ellipse center x0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'ELLIPSE_Y0': Property(identifier='ELLIPSE_Y0', name='Ellipse center y0', description='Ellipse center y0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'ELLIPSE_MAJOR': Property(identifier='ELLIPSE_MAJOR', name='Ellipse long axis', description='Ellipse long axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'ELLIPSE_MINOR': Property(identifier='ELLIPSE_MINOR', name='Ellipse short axis', description='Ellipse short axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'ELLIPSE_THETA': Property(identifier='ELLIPSE_THETA', name='Ellipse angle', description='Ellipse angle', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='rad'), 'ELLIPSE_ASPECTRATIO': Property(identifier='ELLIPSE_ASPECTRATIO', name='Ellipse aspect ratio', description='Ellipse aspect ratio', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'AREA': Property(identifier='AREA', name='Area', description='Area', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel^2'), 'PERIMETER': Property(identifier='PERIMETER', name='Perimeter', description='Perimeter', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'CIRCULARITY': Property(identifier='CIRCULARITY', name='Circularity', description='Circularity', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'SOLIDITY': Property(identifier='SOLIDITY', name='Solidity', description='Solidity', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'SHAPE_INDEX': Property(identifier='SHAPE_INDEX', name='Shape index', description='Shape index', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'SPOT_SOURCE_ID': Property(identifier='SPOT_SOURCE_ID', name='Source spot ID', description='Source spot ID', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None), 'SPOT_TARGET_ID': Property(identifier='SPOT_TARGET_ID', name='Target spot ID', description='Target spot ID', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None), 'LINK_COST': Property(identifier='LINK_COST', name='Edge cost', description='Edge cost', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit=None), 'DIRECTIONAL_CHANGE_RATE': Property(identifier='DIRECTIONAL_CHANGE_RATE', name='Directional change rate', description='Directional change rate', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='rad/sec'), 'SPEED': Property(identifier='SPEED', name='Speed', description='Speed', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'DISPLACEMENT': Property(identifier='DISPLACEMENT', name='Displacement', description='Displacement', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel'), 'EDGE_TIME': Property(identifier='EDGE_TIME', name='Edge time', description='Edge time', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='sec'), 'MANUAL_EDGE_COLOR': Property(identifier='MANUAL_EDGE_COLOR', name='Manual edge color', description='Manual edge color', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None), 'TRACK_INDEX': Property(identifier='TRACK_INDEX', name='Track index', description='Track index', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_SPOTS': Property(identifier='NUMBER_SPOTS', name='Number of spots in track', description='Number of spots in track', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_GAPS': Property(identifier='NUMBER_GAPS', name='Number of gaps', description='Number of gaps', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_SPLITS': Property(identifier='NUMBER_SPLITS', name='Number of split events', description='Number of split events', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_MERGES': Property(identifier='NUMBER_MERGES', name='Number of merge events', description='Number of merge events', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_COMPLEX': Property(identifier='NUMBER_COMPLEX', name='Number of complex points', description='Number of complex points', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'LONGEST_GAP': Property(identifier='LONGEST_GAP', name='Longest gap', description='Longest gap', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'TRACK_DURATION': Property(identifier='TRACK_DURATION', name='Track duration', description='Track duration', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='sec'), 'TRACK_START': Property(identifier='TRACK_START', name='Track start', description='Track start', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='sec'), 'TRACK_STOP': Property(identifier='TRACK_STOP', name='Track stop', description='Track stop', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='sec'), 'TRACK_DISPLACEMENT': Property(identifier='TRACK_DISPLACEMENT', name='Track displacement', description='Track displacement', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'TRACK_MEAN_SPEED': Property(identifier='TRACK_MEAN_SPEED', name='Track mean speed', description='Track mean speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_MAX_SPEED': Property(identifier='TRACK_MAX_SPEED', name='Track max speed', description='Track max speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_MIN_SPEED': Property(identifier='TRACK_MIN_SPEED', name='Track min speed', description='Track min speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_MEDIAN_SPEED': Property(identifier='TRACK_MEDIAN_SPEED', name='Track median speed', description='Track median speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_STD_SPEED': Property(identifier='TRACK_STD_SPEED', name='Track std speed', description='Track std speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_MEAN_QUALITY': Property(identifier='TRACK_MEAN_QUALITY', name='Track mean quality', description='Track mean quality', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None), 'TOTAL_DISTANCE_TRAVELED': Property(identifier='TOTAL_DISTANCE_TRAVELED', name='Total distance traveled', description='Total distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'MAX_DISTANCE_TRAVELED': Property(identifier='MAX_DISTANCE_TRAVELED', name='Max distance traveled', description='Max distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'CONFINEMENT_RATIO': Property(identifier='CONFINEMENT_RATIO', name='Confinement ratio', description='Confinement ratio', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None), 'MEAN_STRAIGHT_LINE_SPEED': Property(identifier='MEAN_STRAIGHT_LINE_SPEED', name='Mean straight line speed', description='Mean straight line speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'LINEARITY_OF_FORWARD_PROGRESSION': Property(identifier='LINEARITY_OF_FORWARD_PROGRESSION', name='Linearity of forward progression', description='Linearity of forward progression', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None), 'MEAN_DIRECTIONAL_CHANGE_RATE': Property(identifier='MEAN_DIRECTIONAL_CHANGE_RATE', name='Mean directional change rate', description='Mean directional change rate', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='rad/sec'), 'TRACK_ID': Property(identifier='TRACK_ID', name='Track ID', description='Track ID', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'FRAME': Property(identifier='FRAME', name='Frame', description='Frame', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None), 'POSITION_X': Property(identifier='POSITION_X', name='X', description='X coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'EDGE_X_LOCATION': Property(identifier='EDGE_X_LOCATION', name='Edge X', description='X coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel'), 'TRACK_X_LOCATION': Property(identifier='TRACK_X_LOCATION', name='Track mean X', description='X coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'POSITION_Y': Property(identifier='POSITION_Y', name='Y', description='Y coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'EDGE_Y_LOCATION': Property(identifier='EDGE_Y_LOCATION', name='Edge Y', description='Y coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel'), 'TRACK_Y_LOCATION': Property(identifier='TRACK_Y_LOCATION', name='Track mean Y', description='Y coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'POSITION_Z': Property(identifier='POSITION_Z', name='Z', description='Z coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'EDGE_Z_LOCATION': Property(identifier='EDGE_Z_LOCATION', name='Edge Z', description='Z coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel'), 'TRACK_Z_LOCATION': Property(identifier='TRACK_Z_LOCATION', name='Track mean Z', description='Z coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel')}), data=Data(cell_data={0: , 4: }, cycle_data=None))" + "Model(model_metadata={'name': 'FakeTracks', 'file_location': '../sample_data/FakeTracks.xml', 'provenance': 'TrackMate', 'date': '2025-09-10 17:22:15.792372', 'space_unit': 'pixel', 'time_unit': 'sec', 'pycellin_version': '0.4.0', 'TrackMate_version': '8.0.0-SNAPSHOT-f411154ed1a4b9de350bbfe91c230cf3ae7639a3', 'time_step': 1.0, 'pixel_size': {'width': 1.0, 'height': 1.0, 'depth': 1.0}, 'Log': \"0.1101/2021.09.03.458852\\nand / or:\\nTinevez, JY.; Perry, N. & Schindelin, J. et al. (2017), 'TrackMate: An open and extensible platform for single-particle tracking.', Methods 115: 80-90, PMID 27713081.\\nhttps://www.sciencedirect.com/science/article/pii/S1046202316303346\\n\\nNumerical feature analyzers:\\nSpot feature analyzers:\\n - Manual spot color provides: Spot color; is manual.\\n - Spot intensity provides: Mean ch1, Median ch1, Min ch1, Max ch1, Sum ch1, Std ch1.\\n - Spot contrast and SNR provides: Ctrst ch1, SNR ch1.\\n - Spot fit 2D ellipse provides: El. x0, El. y0, El. long axis, El. sh. axis, El. angle, El. a.r.\\n - Spot 2D shape descriptors provides: Area, Perim., Circ., Solidity.\\nEdge feature analyzers:\\n - Directional change provides: γ rate.\\n - Edge speed provides: Speed, Disp.\\n - Edge target provides: Source ID, Target ID, Cost.\\n - Edge location provides: Edge T, Edge X, Edge Y, Edge Z.\\n - Manual edge color provides: Edge color; is manual.\\nTrack feature analyzers:\\n - Branching analyzer provides: N spots, N gaps, N splits, N merges, N complex, Lgst gap.\\n - Track duration provides: Duration, Track start, Track stop, Track disp.\\n - Track index provides: Index, ID.\\n - Track location provides: Track X, Track Y, Track Z.\\n - Track speed provides: Mean sp., Max speed, Min speed, Med. speed, Std speed.\\n - Track quality provides: Mean Q.\\n - Track motility analysis provides: Total dist., Max dist., Cfn. ratio, Mn. v. line, Fwd. progr., Mn. γ rate.\\n\\nImage region of interest:\\nImage data:\\nFor the image named: FakeTracks.tif.\\nMatching file FakeTracks.tif in current folder.\\nGeometry:\\n X = 0 - 127, dx = 1.00000\\n Y = 0 - 127, dy = 1.00000\\n Z = 0 - 0, dz = 1.00000\\n T = 0 - 49, dt = 1.00000\\n\\nConfigured detector StarDist detector with settings:\\n - target channel: 1\\n\\nStarting detection process using 24 threads.\\nDetection processes 1 frame simultaneously and allocates 24 threads per frame.\\nFound 107 spots.\\nDetection done in 2.3 s.\\n\\nComputing spot quality histogram...\\nInitial thresholding with a quality threshold above 0.4 ...\\nStarting initial filtering process.\\nRetained 107 spots out of 107.\\n\\nAdding morphology analyzers...\\n - Spot fit 2D ellipse provides: El. x0, El. y0, El. long axis, El. sh. axis, El. angle, El. a.r.\\n - Spot 2D shape descriptors provides: Area, Perim., Circ., Solidity.\\n\\nCalculating spot features...\\nCalculating features done in 0.1 s.\\n\\nPerforming spot filtering on the following features:\\n - on Area below 49.9\\nKept 106 spots out of 107.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 5.0\\n - gap closing max distance: 5.0\\n - merging feature penalties: \\n - splitting max distance: 5.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 5.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 17 tracks.\\n - avg size: 5.1 spots.\\n - min size: 2 spots.\\n - max size: 12 spots.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 8.0\\n - gap closing max distance: 5.0\\n - merging feature penalties: \\n - splitting max distance: 5.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 5.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 9 tracks.\\n - avg size: 10.7 spots.\\n - min size: 2 spots.\\n - max size: 42 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 23.9\\nKept 4 spots out of 9.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 23.9\\nKept 4 spots out of 9.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 8.0\\n - gap closing max distance: 5.0\\n - merging feature penalties: \\n - splitting max distance: 8.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 8.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 7 tracks.\\n - avg size: 13.7 spots.\\n - min size: 2 spots.\\n - max size: 74 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 10.0\\n - gap closing max distance: 5.0\\n - merging feature penalties: \\n - splitting max distance: 8.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 8.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 7 tracks.\\n - avg size: 13.7 spots.\\n - min size: 2 spots.\\n - max size: 74 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 10.0\\n - gap closing max distance: 8.0\\n - merging feature penalties: \\n - splitting max distance: 8.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 8.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 7 tracks.\\n - avg size: 13.7 spots.\\n - min size: 2 spots.\\n - max size: 74 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\n\\nConfigured tracker LAP Tracker with settings:\\n - max frame gap: 2\\n - alternative linking cost factor: 1.05\\n - linking feature penalties: \\n - linking max distance: 10.0\\n - gap closing max distance: 10.0\\n - merging feature penalties: \\n - splitting max distance: 10.0\\n - blocking value: Infinity\\n - allow gap closing: true\\n - allow track splitting: true\\n - allow track merging: true\\n - merging max distance: 10.0\\n - splitting feature penalties: \\n - cutoff percentile: 0.9\\n - gap closing feature penalties: \\n\\nStarting tracking process.\\nTracking done in 0.0 s.\\nFound 7 tracks.\\n - avg size: 13.7 spots.\\n - min size: 2 spots.\\n - max size: 74 spots.\\n\\nCalculating features done in 0.0 s.\\n\\nPerforming track filtering on the following features:\\n - on Track start below 78.0\\n - on Total distance traveled above 20.8\\nKept 2 spots out of 7.\\nSaving data...\\nComputing edge features:\\n - Directional change in 2 ms.\\n - Edge speed in 0 ms.\\n - Edge target in 1 ms.\\n - Edge location in 1 ms.\\nComputation done in 4 ms.\\nComputing track features:\\n - Branching analyzer in 0 ms.\\n - Track duration in 0 ms.\\n - Track index in 0 ms.\\n - Track location in 4 ms.\\n - Track speed in 0 ms.\\n - Track quality in 1 ms.\\n - Track motility analysis in 0 ms.\\nComputation done in 6 ms.\\n--------------------\\nWarnings occurred during reading the file:\\n--------------------\\nCannot read image file: /mnt/data/Code/pycellin/sample_data/FakeTracks.tif.\\n--------------------\\nFile loaded on Tue, 17 Oct 2023 18:15:18\\nTrackMate v8.0.0-SNAPSHOT-f411154ed1a4b9de350bbfe91c230cf3ae7639a3\\nPlease note that TrackMate is available through Fiji, and is based on a publication. If you use it successfully for your research please be so kind to cite our work:\\nErshov, D., Phan, MS., Pylvänäinen, J.W., Rigaud S.U., et al. TrackMate 7: integrating state-of-the-art segmentation algorithms into tracking pipelines. Nat Methods (2022). https://doi.org/10.1038/s41592-022-01507-1\\nhttps://doi.org/10.1038/s41592-022-01507-1\\nand / or:\\nTinevez, JY.; Perry, N. & Schindelin, J. et al. (2017), 'TrackMate: An open and extensible platform for single-particle tracking.', Methods 115: 80-90, PMID 27713081.\\nhttps://www.sciencedirect.com/science/article/pii/S1046202316303346\\nSaving data...\\nWarning: The source image does not match a file on the system.TrackMate won't be able to reload it when opening this XML file.\\nTo fix this, save the source image to a TIF file before saving the TrackMate session.\\nComputing edge features:\\n - Directional change in 4 ms.\\n - Edge speed in 14 ms.\\n - Edge target in 11 ms.\\n - Edge location in 5 ms.\\nComputation done in 34 ms.\\nComputing track features:\\n - Branching analyzer in 0 ms.\\n - Track duration in 3 ms.\\n - Track index in 0 ms.\\n - Track location in 0 ms.\\n - Track speed in 1 ms.\\n - Track quality in 0 ms.\\n - Track motility analysis in 1 ms.\\nComputation done in 5 ms.\\n \", 'Settings': '\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n ', 'GUIState': '\\n ', 'DisplaySettings': '{\\n \"name\": \"CurrentDisplaySettings\",\\n \"spotUniformColor\": \"204, 51, 204, 255\",\\n \"spotColorByType\": \"TRACKS\",\\n \"spotColorByFeature\": \"TRACK_INDEX\",\\n \"spotDisplayRadius\": 1.0,\\n \"spotDisplayedAsRoi\": true,\\n \"spotMin\": 0.0,\\n \"spotMax\": 75.0,\\n \"spotShowName\": false,\\n \"trackMin\": 0.0,\\n \"trackMax\": 10.0,\\n \"trackColorByType\": \"TRACKS\",\\n \"trackColorByFeature\": \"TRACK_INDEX\",\\n \"trackUniformColor\": \"204, 204, 51, 255\",\\n \"undefinedValueColor\": \"0, 0, 0, 255\",\\n \"missingValueColor\": \"89, 89, 89, 255\",\\n \"highlightColor\": \"51, 230, 51, 255\",\\n \"trackDisplayMode\": \"LOCAL\",\\n \"colormap\": \"Jet\",\\n \"limitZDrawingDepth\": false,\\n \"drawingZDepth\": 10.0,\\n \"fadeTracks\": true,\\n \"fadeTrackRange\": 15,\\n \"useAntialiasing\": true,\\n \"spotVisible\": true,\\n \"trackVisible\": true,\\n \"font\": {\\n \"name\": \"Arial\",\\n \"style\": 1,\\n \"size\": 12,\\n \"pointSize\": 12.0,\\n \"fontSerializedDataVersion\": 1\\n },\\n \"lineThickness\": 1.0,\\n \"selectionLineThickness\": 4.0,\\n \"trackschemeBackgroundColor1\": \"128, 128, 128, 255\",\\n \"trackschemeBackgroundColor2\": \"192, 192, 192, 255\",\\n \"trackschemeForegroundColor\": \"0, 0, 0, 255\",\\n \"trackschemeDecorationColor\": \"0, 0, 0, 255\",\\n \"trackschemeFillBox\": false,\\n \"spotFilled\": true,\\n \"spotTransparencyAlpha\": 0.42000000000000004\\n}\\n'}, props_metadata=PropsMetadata(props={'QUALITY': Property(identifier='QUALITY', name='Quality', description='Quality', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'POSITION_T': Property(identifier='POSITION_T', name='T', description='T', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='sec'), 'RADIUS': Property(identifier='RADIUS', name='Radius', description='Radius', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'VISIBILITY': Property(identifier='VISIBILITY', name='Visibility', description='Visibility', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None), 'MANUAL_SPOT_COLOR': Property(identifier='MANUAL_SPOT_COLOR', name='Manual spot color', description='Manual spot color', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None), 'MEAN_INTENSITY_CH1': Property(identifier='MEAN_INTENSITY_CH1', name='Mean intensity ch1', description='Mean intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'MEDIAN_INTENSITY_CH1': Property(identifier='MEDIAN_INTENSITY_CH1', name='Median intensity ch1', description='Median intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'MIN_INTENSITY_CH1': Property(identifier='MIN_INTENSITY_CH1', name='Min intensity ch1', description='Min intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'MAX_INTENSITY_CH1': Property(identifier='MAX_INTENSITY_CH1', name='Max intensity ch1', description='Max intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'TOTAL_INTENSITY_CH1': Property(identifier='TOTAL_INTENSITY_CH1', name='Sum intensity ch1', description='Sum intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'STD_INTENSITY_CH1': Property(identifier='STD_INTENSITY_CH1', name='Std intensity ch1', description='Std intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'CONTRAST_CH1': Property(identifier='CONTRAST_CH1', name='Contrast ch1', description='Contrast ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'SNR_CH1': Property(identifier='SNR_CH1', name='Signal/Noise ratio ch1', description='Signal/Noise ratio ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'ELLIPSE_X0': Property(identifier='ELLIPSE_X0', name='Ellipse center x0', description='Ellipse center x0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'ELLIPSE_Y0': Property(identifier='ELLIPSE_Y0', name='Ellipse center y0', description='Ellipse center y0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'ELLIPSE_MAJOR': Property(identifier='ELLIPSE_MAJOR', name='Ellipse long axis', description='Ellipse long axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'ELLIPSE_MINOR': Property(identifier='ELLIPSE_MINOR', name='Ellipse short axis', description='Ellipse short axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'ELLIPSE_THETA': Property(identifier='ELLIPSE_THETA', name='Ellipse angle', description='Ellipse angle', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='rad'), 'ELLIPSE_ASPECTRATIO': Property(identifier='ELLIPSE_ASPECTRATIO', name='Ellipse aspect ratio', description='Ellipse aspect ratio', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'AREA': Property(identifier='AREA', name='Area', description='Area', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel^2'), 'PERIMETER': Property(identifier='PERIMETER', name='Perimeter', description='Perimeter', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'CIRCULARITY': Property(identifier='CIRCULARITY', name='Circularity', description='Circularity', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'SOLIDITY': Property(identifier='SOLIDITY', name='Solidity', description='Solidity', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'SHAPE_INDEX': Property(identifier='SHAPE_INDEX', name='Shape index', description='Shape index', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None), 'SPOT_SOURCE_ID': Property(identifier='SPOT_SOURCE_ID', name='Source spot ID', description='Source spot ID', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None), 'SPOT_TARGET_ID': Property(identifier='SPOT_TARGET_ID', name='Target spot ID', description='Target spot ID', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None), 'LINK_COST': Property(identifier='LINK_COST', name='Edge cost', description='Edge cost', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit=None), 'DIRECTIONAL_CHANGE_RATE': Property(identifier='DIRECTIONAL_CHANGE_RATE', name='Directional change rate', description='Directional change rate', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='rad/sec'), 'SPEED': Property(identifier='SPEED', name='Speed', description='Speed', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'DISPLACEMENT': Property(identifier='DISPLACEMENT', name='Displacement', description='Displacement', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel'), 'EDGE_TIME': Property(identifier='EDGE_TIME', name='Edge time', description='Edge time', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='sec'), 'MANUAL_EDGE_COLOR': Property(identifier='MANUAL_EDGE_COLOR', name='Manual edge color', description='Manual edge color', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None), 'TRACK_INDEX': Property(identifier='TRACK_INDEX', name='Track index', description='Track index', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_SPOTS': Property(identifier='NUMBER_SPOTS', name='Number of spots in track', description='Number of spots in track', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_GAPS': Property(identifier='NUMBER_GAPS', name='Number of gaps', description='Number of gaps', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_SPLITS': Property(identifier='NUMBER_SPLITS', name='Number of split events', description='Number of split events', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_MERGES': Property(identifier='NUMBER_MERGES', name='Number of merge events', description='Number of merge events', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'NUMBER_COMPLEX': Property(identifier='NUMBER_COMPLEX', name='Number of complex points', description='Number of complex points', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'LONGEST_GAP': Property(identifier='LONGEST_GAP', name='Longest gap', description='Longest gap', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'TRACK_DURATION': Property(identifier='TRACK_DURATION', name='Track duration', description='Track duration', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='sec'), 'TRACK_START': Property(identifier='TRACK_START', name='Track start', description='Track start', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='sec'), 'TRACK_STOP': Property(identifier='TRACK_STOP', name='Track stop', description='Track stop', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='sec'), 'TRACK_DISPLACEMENT': Property(identifier='TRACK_DISPLACEMENT', name='Track displacement', description='Track displacement', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'TRACK_MEAN_SPEED': Property(identifier='TRACK_MEAN_SPEED', name='Track mean speed', description='Track mean speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_MAX_SPEED': Property(identifier='TRACK_MAX_SPEED', name='Track max speed', description='Track max speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_MIN_SPEED': Property(identifier='TRACK_MIN_SPEED', name='Track min speed', description='Track min speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_MEDIAN_SPEED': Property(identifier='TRACK_MEDIAN_SPEED', name='Track median speed', description='Track median speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_STD_SPEED': Property(identifier='TRACK_STD_SPEED', name='Track std speed', description='Track std speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'TRACK_MEAN_QUALITY': Property(identifier='TRACK_MEAN_QUALITY', name='Track mean quality', description='Track mean quality', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None), 'TOTAL_DISTANCE_TRAVELED': Property(identifier='TOTAL_DISTANCE_TRAVELED', name='Total distance traveled', description='Total distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'MAX_DISTANCE_TRAVELED': Property(identifier='MAX_DISTANCE_TRAVELED', name='Max distance traveled', description='Max distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'CONFINEMENT_RATIO': Property(identifier='CONFINEMENT_RATIO', name='Confinement ratio', description='Confinement ratio', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None), 'MEAN_STRAIGHT_LINE_SPEED': Property(identifier='MEAN_STRAIGHT_LINE_SPEED', name='Mean straight line speed', description='Mean straight line speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel/sec'), 'LINEARITY_OF_FORWARD_PROGRESSION': Property(identifier='LINEARITY_OF_FORWARD_PROGRESSION', name='Linearity of forward progression', description='Linearity of forward progression', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None), 'MEAN_DIRECTIONAL_CHANGE_RATE': Property(identifier='MEAN_DIRECTIONAL_CHANGE_RATE', name='Mean directional change rate', description='Mean directional change rate', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='rad/sec'), 'TRACK_ID': Property(identifier='TRACK_ID', name='Track ID', description='Track ID', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None), 'FRAME': Property(identifier='FRAME', name='Frame', description='Frame', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None), 'POSITION_X': Property(identifier='POSITION_X', name='X', description='X coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'EDGE_X_LOCATION': Property(identifier='EDGE_X_LOCATION', name='Edge X', description='X coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel'), 'TRACK_X_LOCATION': Property(identifier='TRACK_X_LOCATION', name='Track mean X', description='X coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'POSITION_Y': Property(identifier='POSITION_Y', name='Y', description='Y coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'EDGE_Y_LOCATION': Property(identifier='EDGE_Y_LOCATION', name='Edge Y', description='Y coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel'), 'TRACK_Y_LOCATION': Property(identifier='TRACK_Y_LOCATION', name='Track mean Y', description='Y coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel'), 'POSITION_Z': Property(identifier='POSITION_Z', name='Z', description='Z coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='pixel'), 'EDGE_Z_LOCATION': Property(identifier='EDGE_Z_LOCATION', name='Edge Z', description='Z coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='pixel'), 'TRACK_Z_LOCATION': Property(identifier='TRACK_Z_LOCATION', name='Track mean Z', description='Z coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='pixel')}), data=Data(cell_data={0: , 4: }, cycle_data=None))" ] }, "execution_count": 80, @@ -15001,7 +15001,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.18" + "version": "3.11.13" } }, "nbformat": 4, diff --git a/notebooks/Managing properties.ipynb b/notebooks/Managing properties.ipynb index 78a5eb1..e68f843 100644 --- a/notebooks/Managing properties.ipynb +++ b/notebooks/Managing properties.ipynb @@ -153,7 +153,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: min\n", + " Unit: minute\n", "Property 'RADIUS'\n", " Name: Radius\n", " Description: Radius\n", @@ -161,7 +161,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'VISIBILITY'\n", " Name: Visibility\n", " Description: Visibility\n", @@ -313,7 +313,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'ELLIPSE_Y0'\n", " Name: Ellipse center y0\n", " Description: Ellipse center y0\n", @@ -321,7 +321,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'ELLIPSE_MAJOR'\n", " Name: Ellipse long axis\n", " Description: Ellipse long axis\n", @@ -329,7 +329,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'ELLIPSE_MINOR'\n", " Name: Ellipse short axis\n", " Description: Ellipse short axis\n", @@ -337,7 +337,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'ELLIPSE_THETA'\n", " Name: Ellipse angle\n", " Description: Ellipse angle\n", @@ -361,7 +361,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm^2\n", + " Unit: micrometer^2\n", "Property 'PERIMETER'\n", " Name: Perimeter\n", " Description: Perimeter\n", @@ -369,7 +369,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'CIRCULARITY'\n", " Name: Circularity\n", " Description: Circularity\n", @@ -433,7 +433,7 @@ " Type: edge\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: rad/min\n", + " Unit: rad/minute\n", "Property 'SPEED'\n", " Name: Speed\n", " Description: Speed\n", @@ -441,7 +441,7 @@ " Type: edge\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm/min\n", + " Unit: micrometer/minute\n", "Property 'DISPLACEMENT'\n", " Name: Displacement\n", " Description: Displacement\n", @@ -449,7 +449,7 @@ " Type: edge\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'EDGE_TIME'\n", " Name: Edge time\n", " Description: Edge time\n", @@ -457,7 +457,7 @@ " Type: edge\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: min\n", + " Unit: minute\n", "Property 'MANUAL_EDGE_COLOR'\n", " Name: Manual edge color\n", " Description: Manual edge color\n", @@ -481,7 +481,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: min\n", + " Unit: minute\n", "Property 'DIVISION_TIME_STD'\n", " Name: Std cell division time\n", " Description: Std cell division time\n", @@ -489,7 +489,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: min\n", + " Unit: minute\n", "Property 'NUMBER_SPOTS'\n", " Name: Number of spots in track\n", " Description: Number of spots in track\n", @@ -545,7 +545,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: min\n", + " Unit: minute\n", "Property 'TRACK_START'\n", " Name: Track start\n", " Description: Track start\n", @@ -553,7 +553,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: min\n", + " Unit: minute\n", "Property 'TRACK_STOP'\n", " Name: Track stop\n", " Description: Track stop\n", @@ -561,7 +561,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: min\n", + " Unit: minute\n", "Property 'TRACK_DISPLACEMENT'\n", " Name: Track displacement\n", " Description: Track displacement\n", @@ -569,7 +569,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'TRACK_MEAN_SPEED'\n", " Name: Track mean speed\n", " Description: Track mean speed\n", @@ -577,7 +577,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm/min\n", + " Unit: micrometer/minute\n", "Property 'TRACK_MAX_SPEED'\n", " Name: Track max speed\n", " Description: Track max speed\n", @@ -585,7 +585,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm/min\n", + " Unit: micrometer/minute\n", "Property 'TRACK_MIN_SPEED'\n", " Name: Track min speed\n", " Description: Track min speed\n", @@ -593,7 +593,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm/min\n", + " Unit: micrometer/minute\n", "Property 'TRACK_MEDIAN_SPEED'\n", " Name: Track median speed\n", " Description: Track median speed\n", @@ -601,7 +601,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm/min\n", + " Unit: micrometer/minute\n", "Property 'TRACK_STD_SPEED'\n", " Name: Track std speed\n", " Description: Track std speed\n", @@ -609,7 +609,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm/min\n", + " Unit: micrometer/minute\n", "Property 'TRACK_MEAN_QUALITY'\n", " Name: Track mean quality\n", " Description: Track mean quality\n", @@ -625,7 +625,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'MAX_DISTANCE_TRAVELED'\n", " Name: Max distance traveled\n", " Description: Max distance traveled\n", @@ -633,7 +633,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'CONFINEMENT_RATIO'\n", " Name: Confinement ratio\n", " Description: Confinement ratio\n", @@ -649,7 +649,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm/min\n", + " Unit: micrometer/minute\n", "Property 'LINEARITY_OF_FORWARD_PROGRESSION'\n", " Name: Linearity of forward progression\n", " Description: Linearity of forward progression\n", @@ -665,7 +665,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: rad/min\n", + " Unit: rad/minute\n", "Property 'lineage_name'\n", " Name: lineage name\n", " Description: Name of the track\n", @@ -689,7 +689,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'cell_y'\n", " Name: Y\n", " Description: Y coordinate of the cell\n", @@ -697,7 +697,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'cell_z'\n", " Name: Z\n", " Description: Z coordinate of the cell\n", @@ -705,7 +705,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'frame'\n", " Name: Frame\n", " Description: Frame\n", @@ -721,7 +721,7 @@ " Type: node\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'link_x'\n", " Name: Edge X\n", " Description: X coordinate of the link, i.e. mean coordinate of its two cells\n", @@ -729,7 +729,7 @@ " Type: edge\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'link_y'\n", " Name: Edge Y\n", " Description: Y coordinate of the link, i.e. mean coordinate of its two cells\n", @@ -737,7 +737,7 @@ " Type: edge\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'link_z'\n", " Name: Edge Z\n", " Description: Z coordinate of the link, i.e. mean coordinate of its two cells\n", @@ -745,7 +745,7 @@ " Type: edge\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'lineage_ID'\n", " Name: Track ID\n", " Description: Unique identifier of the lineage\n", @@ -769,7 +769,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'lineage_y'\n", " Name: Track mean Y\n", " Description: Y coordinate of the lineage, i.e. mean coordinate of its cells\n", @@ -777,7 +777,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n", + " Unit: micrometer\n", "Property 'lineage_z'\n", " Name: Track mean Z\n", " Description: Z coordinate of the lineage, i.e. mean coordinate of its cells\n", @@ -785,7 +785,7 @@ " Type: lineage\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm\n" + " Unit: micrometer\n" ] } ], @@ -3098,7 +3098,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "E:\\Code\\pycellin\\pycellin\\pycellin\\classes\\model.py:936: UserWarning:\n", + "/media/lxenard/data/Code/pycellin/pycellin/pycellin/classes/model.py:936: UserWarning:\n", "\n", "A Property 'absolute_age' already exists with the same type. Not overwriting the old Property.\n", "\n" @@ -3141,8 +3141,8 @@ "data": { "text/plain": [ "{'QUALITY': Property(identifier='QUALITY', name='Quality', description='Quality', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None),\n", - " 'POSITION_T': Property(identifier='POSITION_T', name='T', description='T', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='min'),\n", - " 'RADIUS': Property(identifier='RADIUS', name='Radius', description='Radius', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", + " 'POSITION_T': Property(identifier='POSITION_T', name='T', description='T', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='minute'),\n", + " 'RADIUS': Property(identifier='RADIUS', name='Radius', description='Radius', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", " 'VISIBILITY': Property(identifier='VISIBILITY', name='Visibility', description='Visibility', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None),\n", " 'MANUAL_SPOT_COLOR': Property(identifier='MANUAL_SPOT_COLOR', name='Manual spot color', description='Manual spot color', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None),\n", " 'MEAN_INTENSITY_CH1': Property(identifier='MEAN_INTENSITY_CH1', name='Mean intensity ch1', description='Mean intensity ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None),\n", @@ -3161,14 +3161,14 @@ " 'SNR_CH1': Property(identifier='SNR_CH1', name='Signal/Noise ratio ch1', description='Signal/Noise ratio ch1', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None),\n", " 'CONTRAST_CH2': Property(identifier='CONTRAST_CH2', name='Contrast ch2', description='Contrast ch2', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None),\n", " 'SNR_CH2': Property(identifier='SNR_CH2', name='Signal/Noise ratio ch2', description='Signal/Noise ratio ch2', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None),\n", - " 'ELLIPSE_X0': Property(identifier='ELLIPSE_X0', name='Ellipse center x0', description='Ellipse center x0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'ELLIPSE_Y0': Property(identifier='ELLIPSE_Y0', name='Ellipse center y0', description='Ellipse center y0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'ELLIPSE_MAJOR': Property(identifier='ELLIPSE_MAJOR', name='Ellipse long axis', description='Ellipse long axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'ELLIPSE_MINOR': Property(identifier='ELLIPSE_MINOR', name='Ellipse short axis', description='Ellipse short axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", + " 'ELLIPSE_X0': Property(identifier='ELLIPSE_X0', name='Ellipse center x0', description='Ellipse center x0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'ELLIPSE_Y0': Property(identifier='ELLIPSE_Y0', name='Ellipse center y0', description='Ellipse center y0', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'ELLIPSE_MAJOR': Property(identifier='ELLIPSE_MAJOR', name='Ellipse long axis', description='Ellipse long axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'ELLIPSE_MINOR': Property(identifier='ELLIPSE_MINOR', name='Ellipse short axis', description='Ellipse short axis', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", " 'ELLIPSE_THETA': Property(identifier='ELLIPSE_THETA', name='Ellipse angle', description='Ellipse angle', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='rad'),\n", " 'ELLIPSE_ASPECTRATIO': Property(identifier='ELLIPSE_ASPECTRATIO', name='Ellipse aspect ratio', description='Ellipse aspect ratio', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None),\n", - " 'AREA': Property(identifier='AREA', name='Area', description='Area', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm^2'),\n", - " 'PERIMETER': Property(identifier='PERIMETER', name='Perimeter', description='Perimeter', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", + " 'AREA': Property(identifier='AREA', name='Area', description='Area', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer^2'),\n", + " 'PERIMETER': Property(identifier='PERIMETER', name='Perimeter', description='Perimeter', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", " 'CIRCULARITY': Property(identifier='CIRCULARITY', name='Circularity', description='Circularity', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None),\n", " 'SOLIDITY': Property(identifier='SOLIDITY', name='Solidity', description='Solidity', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None),\n", " 'SHAPE_INDEX': Property(identifier='SHAPE_INDEX', name='Shape index', description='Shape index', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit=None),\n", @@ -3176,54 +3176,54 @@ " 'SPOT_SOURCE_ID': Property(identifier='SPOT_SOURCE_ID', name='Source spot ID', description='Source spot ID', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None),\n", " 'SPOT_TARGET_ID': Property(identifier='SPOT_TARGET_ID', name='Target spot ID', description='Target spot ID', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None),\n", " 'LINK_COST': Property(identifier='LINK_COST', name='Edge cost', description='Edge cost', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit=None),\n", - " 'DIRECTIONAL_CHANGE_RATE': Property(identifier='DIRECTIONAL_CHANGE_RATE', name='Directional change rate', description='Directional change rate', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='rad/min'),\n", - " 'SPEED': Property(identifier='SPEED', name='Speed', description='Speed', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm/min'),\n", - " 'DISPLACEMENT': Property(identifier='DISPLACEMENT', name='Displacement', description='Displacement', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'EDGE_TIME': Property(identifier='EDGE_TIME', name='Edge time', description='Edge time', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='min'),\n", + " 'DIRECTIONAL_CHANGE_RATE': Property(identifier='DIRECTIONAL_CHANGE_RATE', name='Directional change rate', description='Directional change rate', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='rad/minute'),\n", + " 'SPEED': Property(identifier='SPEED', name='Speed', description='Speed', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer/minute'),\n", + " 'DISPLACEMENT': Property(identifier='DISPLACEMENT', name='Displacement', description='Displacement', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'EDGE_TIME': Property(identifier='EDGE_TIME', name='Edge time', description='Edge time', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='minute'),\n", " 'MANUAL_EDGE_COLOR': Property(identifier='MANUAL_EDGE_COLOR', name='Manual edge color', description='Manual edge color', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='int', unit=None),\n", " 'TRACK_INDEX': Property(identifier='TRACK_INDEX', name='Track index', description='Track index', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None),\n", - " 'DIVISION_TIME_MEAN': Property(identifier='DIVISION_TIME_MEAN', name='Mean cell division time', description='Mean cell division time', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min'),\n", - " 'DIVISION_TIME_STD': Property(identifier='DIVISION_TIME_STD', name='Std cell division time', description='Std cell division time', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min'),\n", + " 'DIVISION_TIME_MEAN': Property(identifier='DIVISION_TIME_MEAN', name='Mean cell division time', description='Mean cell division time', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute'),\n", + " 'DIVISION_TIME_STD': Property(identifier='DIVISION_TIME_STD', name='Std cell division time', description='Std cell division time', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute'),\n", " 'NUMBER_SPOTS': Property(identifier='NUMBER_SPOTS', name='Number of spots in track', description='Number of spots in track', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None),\n", " 'NUMBER_GAPS': Property(identifier='NUMBER_GAPS', name='Number of gaps', description='Number of gaps', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None),\n", " 'NUMBER_SPLITS': Property(identifier='NUMBER_SPLITS', name='Number of split events', description='Number of split events', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None),\n", " 'NUMBER_MERGES': Property(identifier='NUMBER_MERGES', name='Number of merge events', description='Number of merge events', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None),\n", " 'NUMBER_COMPLEX': Property(identifier='NUMBER_COMPLEX', name='Number of complex points', description='Number of complex points', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None),\n", " 'LONGEST_GAP': Property(identifier='LONGEST_GAP', name='Longest gap', description='Longest gap', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None),\n", - " 'TRACK_DURATION': Property(identifier='TRACK_DURATION', name='Track duration', description='Track duration', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min'),\n", - " 'TRACK_START': Property(identifier='TRACK_START', name='Track start', description='Track start', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min'),\n", - " 'TRACK_STOP': Property(identifier='TRACK_STOP', name='Track stop', description='Track stop', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='min'),\n", - " 'TRACK_DISPLACEMENT': Property(identifier='TRACK_DISPLACEMENT', name='Track displacement', description='Track displacement', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'TRACK_MEAN_SPEED': Property(identifier='TRACK_MEAN_SPEED', name='Track mean speed', description='Track mean speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min'),\n", - " 'TRACK_MAX_SPEED': Property(identifier='TRACK_MAX_SPEED', name='Track max speed', description='Track max speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min'),\n", - " 'TRACK_MIN_SPEED': Property(identifier='TRACK_MIN_SPEED', name='Track min speed', description='Track min speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min'),\n", - " 'TRACK_MEDIAN_SPEED': Property(identifier='TRACK_MEDIAN_SPEED', name='Track median speed', description='Track median speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min'),\n", - " 'TRACK_STD_SPEED': Property(identifier='TRACK_STD_SPEED', name='Track std speed', description='Track std speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min'),\n", + " 'TRACK_DURATION': Property(identifier='TRACK_DURATION', name='Track duration', description='Track duration', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute'),\n", + " 'TRACK_START': Property(identifier='TRACK_START', name='Track start', description='Track start', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute'),\n", + " 'TRACK_STOP': Property(identifier='TRACK_STOP', name='Track stop', description='Track stop', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='minute'),\n", + " 'TRACK_DISPLACEMENT': Property(identifier='TRACK_DISPLACEMENT', name='Track displacement', description='Track displacement', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'TRACK_MEAN_SPEED': Property(identifier='TRACK_MEAN_SPEED', name='Track mean speed', description='Track mean speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute'),\n", + " 'TRACK_MAX_SPEED': Property(identifier='TRACK_MAX_SPEED', name='Track max speed', description='Track max speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute'),\n", + " 'TRACK_MIN_SPEED': Property(identifier='TRACK_MIN_SPEED', name='Track min speed', description='Track min speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute'),\n", + " 'TRACK_MEDIAN_SPEED': Property(identifier='TRACK_MEDIAN_SPEED', name='Track median speed', description='Track median speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute'),\n", + " 'TRACK_STD_SPEED': Property(identifier='TRACK_STD_SPEED', name='Track std speed', description='Track std speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute'),\n", " 'TRACK_MEAN_QUALITY': Property(identifier='TRACK_MEAN_QUALITY', name='Track mean quality', description='Track mean quality', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None),\n", - " 'TOTAL_DISTANCE_TRAVELED': Property(identifier='TOTAL_DISTANCE_TRAVELED', name='Total distance traveled', description='Total distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'MAX_DISTANCE_TRAVELED': Property(identifier='MAX_DISTANCE_TRAVELED', name='Max distance traveled', description='Max distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm'),\n", + " 'TOTAL_DISTANCE_TRAVELED': Property(identifier='TOTAL_DISTANCE_TRAVELED', name='Total distance traveled', description='Total distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'MAX_DISTANCE_TRAVELED': Property(identifier='MAX_DISTANCE_TRAVELED', name='Max distance traveled', description='Max distance traveled', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", " 'CONFINEMENT_RATIO': Property(identifier='CONFINEMENT_RATIO', name='Confinement ratio', description='Confinement ratio', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None),\n", - " 'MEAN_STRAIGHT_LINE_SPEED': Property(identifier='MEAN_STRAIGHT_LINE_SPEED', name='Mean straight line speed', description='Mean straight line speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm/min'),\n", + " 'MEAN_STRAIGHT_LINE_SPEED': Property(identifier='MEAN_STRAIGHT_LINE_SPEED', name='Mean straight line speed', description='Mean straight line speed', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer/minute'),\n", " 'LINEARITY_OF_FORWARD_PROGRESSION': Property(identifier='LINEARITY_OF_FORWARD_PROGRESSION', name='Linearity of forward progression', description='Linearity of forward progression', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit=None),\n", - " 'MEAN_DIRECTIONAL_CHANGE_RATE': Property(identifier='MEAN_DIRECTIONAL_CHANGE_RATE', name='Mean directional change rate', description='Mean directional change rate', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='rad/min'),\n", + " 'MEAN_DIRECTIONAL_CHANGE_RATE': Property(identifier='MEAN_DIRECTIONAL_CHANGE_RATE', name='Mean directional change rate', description='Mean directional change rate', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='rad/minute'),\n", " 'lineage_name': Property(identifier='lineage_name', name='lineage name', description='Name of the track', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='string', unit=None),\n", " 'cell_ID': Property(identifier='cell_ID', name='cell ID', description='Unique identifier of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None),\n", - " 'cell_x': Property(identifier='cell_x', name='X', description='X coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'cell_y': Property(identifier='cell_y', name='Y', description='Y coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'cell_z': Property(identifier='cell_z', name='Z', description='Z coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", + " 'cell_x': Property(identifier='cell_x', name='X', description='X coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'cell_y': Property(identifier='cell_y', name='Y', description='Y coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'cell_z': Property(identifier='cell_z', name='Z', description='Z coordinate of the cell', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", " 'frame': Property(identifier='frame', name='Frame', description='Frame', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='int', unit=None),\n", - " 'ROI_coords': Property(identifier='ROI_coords', name='ROI coords', description='List of coordinates of the region of interest', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'link_x': Property(identifier='link_x', name='Edge X', description='X coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'link_y': Property(identifier='link_y', name='Edge Y', description='Y coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'link_z': Property(identifier='link_z', name='Edge Z', description='Z coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='µm'),\n", + " 'ROI_coords': Property(identifier='ROI_coords', name='ROI coords', description='List of coordinates of the region of interest', provenance='TrackMate', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'link_x': Property(identifier='link_x', name='Edge X', description='X coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'link_y': Property(identifier='link_y', name='Edge Y', description='Y coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'link_z': Property(identifier='link_z', name='Edge Z', description='Z coordinate of the link, i.e. mean coordinate of its two cells', provenance='TrackMate', prop_type='edge', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", " 'lineage_ID': Property(identifier='lineage_ID', name='Track ID', description='Unique identifier of the lineage', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None),\n", " 'FilteredTrack': Property(identifier='FilteredTrack', name='FilteredTrack', description='True if the track was not filtered out in TrackMate', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='int', unit=None),\n", - " 'lineage_x': Property(identifier='lineage_x', name='Track mean X', description='X coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'lineage_y': Property(identifier='lineage_y', name='Track mean Y', description='Y coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm'),\n", - " 'lineage_z': Property(identifier='lineage_z', name='Track mean Z', description='Z coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='µm'),\n", + " 'lineage_x': Property(identifier='lineage_x', name='Track mean X', description='X coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'lineage_y': Property(identifier='lineage_y', name='Track mean Y', description='Y coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", + " 'lineage_z': Property(identifier='lineage_z', name='Track mean Z', description='Z coordinate of the lineage, i.e. mean coordinate of its cells', provenance='TrackMate', prop_type='lineage', lin_type='CellLineage', dtype='float', unit='micrometer'),\n", " 'absolute_age': Property(identifier='absolute_age', name='absolute age', description='Age of the cell since the beginning of the lineage', provenance='pycellin', prop_type='node', lin_type='CellLineage', dtype='int', unit='frame'),\n", " 'relative_age': Property(identifier='relative_age', name='Relative age', description='Age of the cell since the beginning of the current cell cycle', provenance='pycellin', prop_type='node', lin_type='CellLineage', dtype='int', unit='frame'),\n", - " 'rod_length': Property(identifier='rod_length', name='Rod length', description='Length of the cell, for rod-shaped cells only', provenance='pycellin', prop_type='node', lin_type='CellLineage', dtype='float', unit='µm')}" + " 'rod_length': Property(identifier='rod_length', name='Rod length', description='Length of the cell, for rod-shaped cells only', provenance='pycellin', prop_type='node', lin_type='CellLineage', dtype='float', unit='micrometer')}" ] }, "execution_count": 9, @@ -3268,7 +3268,7 @@ " Type: edge\n", " Lineage type: CellLineage\n", " Data type: float\n", - " Unit: µm/min\n" + " Unit: micrometer/minute\n" ] } ], @@ -3305,12 +3305,12 @@ "evalue": "Cycle lineages have not been computed yet. Please compute the cycle lineages first with `model.add_cycle_data()`.", "output_type": "error", "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[11], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[43mmodel\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43madd_division_time\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mE:\\Code\\pycellin\\pycellin\\pycellin\\classes\\model.py:1237\u001b[0m, in \u001b[0;36mModel.add_division_time\u001b[1;34m(self, in_time_unit, custom_identifier)\u001b[0m\n\u001b[0;32m 1232\u001b[0m prop \u001b[38;5;241m=\u001b[39m tracking\u001b[38;5;241m.\u001b[39mcreate_division_time_property(\n\u001b[0;32m 1233\u001b[0m custom_identifier\u001b[38;5;241m=\u001b[39mcustom_identifier,\n\u001b[0;32m 1234\u001b[0m unit\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmodel_metadata[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtime_unit\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;28;01mif\u001b[39;00m in_time_unit \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mframe\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 1235\u001b[0m )\n\u001b[0;32m 1236\u001b[0m time_step \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmodel_metadata[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtime_step\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;28;01mif\u001b[39;00m in_time_unit \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;241m1\u001b[39m\n\u001b[1;32m-> 1237\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43madd_custom_property\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtracking\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mDivisionTime\u001b[49m\u001b[43m(\u001b[49m\u001b[43mprop\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtime_step\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mE:\\Code\\pycellin\\pycellin\\pycellin\\classes\\model.py:932\u001b[0m, in \u001b[0;36mModel.add_custom_property\u001b[1;34m(self, calculator)\u001b[0m\n\u001b[0;32m 912\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 913\u001b[0m \u001b[38;5;124;03mAdd a custom property to the model.\u001b[39;00m\n\u001b[0;32m 914\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 929\u001b[0m \u001b[38;5;124;03m have not been computed yet.\u001b[39;00m\n\u001b[0;32m 930\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 931\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m calculator\u001b[38;5;241m.\u001b[39mprop\u001b[38;5;241m.\u001b[39mlin_type \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCycleLineage\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdata\u001b[38;5;241m.\u001b[39mcycle_data:\n\u001b[1;32m--> 932\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[0;32m 933\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCycle lineages have not been computed yet. \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 934\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPlease compute the cycle lineages first with `model.add_cycle_data()`.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 935\u001b[0m )\n\u001b[0;32m 936\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mprops_metadata\u001b[38;5;241m.\u001b[39m_add_prop(calculator\u001b[38;5;241m.\u001b[39mprop)\n\u001b[0;32m 937\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_updater\u001b[38;5;241m.\u001b[39mregister_calculator(calculator)\n", - "\u001b[1;31mValueError\u001b[0m: Cycle lineages have not been computed yet. Please compute the cycle lineages first with `model.add_cycle_data()`." + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43mmodel\u001b[49m\u001b[43m.\u001b[49m\u001b[43madd_division_time\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/media/lxenard/data/Code/pycellin/pycellin/pycellin/classes/model.py:1237\u001b[39m, in \u001b[36mModel.add_division_time\u001b[39m\u001b[34m(self, in_time_unit, custom_identifier)\u001b[39m\n\u001b[32m 1232\u001b[39m prop = tracking.create_division_time_property(\n\u001b[32m 1233\u001b[39m custom_identifier=custom_identifier,\n\u001b[32m 1234\u001b[39m unit=\u001b[38;5;28mself\u001b[39m.model_metadata[\u001b[33m\"\u001b[39m\u001b[33mtime_unit\u001b[39m\u001b[33m\"\u001b[39m] \u001b[38;5;28;01mif\u001b[39;00m in_time_unit \u001b[38;5;28;01melse\u001b[39;00m \u001b[33m\"\u001b[39m\u001b[33mframe\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 1235\u001b[39m )\n\u001b[32m 1236\u001b[39m time_step = \u001b[38;5;28mself\u001b[39m.model_metadata[\u001b[33m\"\u001b[39m\u001b[33mtime_step\u001b[39m\u001b[33m\"\u001b[39m] \u001b[38;5;28;01mif\u001b[39;00m in_time_unit \u001b[38;5;28;01melse\u001b[39;00m \u001b[32m1\u001b[39m\n\u001b[32m-> \u001b[39m\u001b[32m1237\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43madd_custom_property\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtracking\u001b[49m\u001b[43m.\u001b[49m\u001b[43mDivisionTime\u001b[49m\u001b[43m(\u001b[49m\u001b[43mprop\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtime_step\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/media/lxenard/data/Code/pycellin/pycellin/pycellin/classes/model.py:932\u001b[39m, in \u001b[36mModel.add_custom_property\u001b[39m\u001b[34m(self, calculator)\u001b[39m\n\u001b[32m 912\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 913\u001b[39m \u001b[33;03mAdd a custom property to the model.\u001b[39;00m\n\u001b[32m 914\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 929\u001b[39m \u001b[33;03m have not been computed yet.\u001b[39;00m\n\u001b[32m 930\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 931\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m calculator.prop.lin_type == \u001b[33m\"\u001b[39m\u001b[33mCycleLineage\u001b[39m\u001b[33m\"\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m.data.cycle_data:\n\u001b[32m--> \u001b[39m\u001b[32m932\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 933\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mCycle lineages have not been computed yet. \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 934\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mPlease compute the cycle lineages first with `model.add_cycle_data()`.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 935\u001b[39m )\n\u001b[32m 936\u001b[39m \u001b[38;5;28mself\u001b[39m.props_metadata._add_prop(calculator.prop)\n\u001b[32m 937\u001b[39m \u001b[38;5;28mself\u001b[39m._updater.register_calculator(calculator)\n", + "\u001b[31mValueError\u001b[39m: Cycle lineages have not been computed yet. Please compute the cycle lineages first with `model.add_cycle_data()`." ] } ], @@ -4583,7 +4583,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "E:\\Code\\pycellin\\pycellin\\pycellin\\classes\\model.py:936: UserWarning:\n", + "/media/lxenard/data/Code/pycellin/pycellin/pycellin/classes/model.py:936: UserWarning:\n", "\n", "A Property 'division_time' already exists with the same type. Not overwriting the old Property.\n", "\n" @@ -4654,7 +4654,7 @@ " 'level': Property(identifier='level', name='level', description='Level of the cell cycle in the lineage, i.e. number of cell cycles upstream of the current one', provenance='pycellin', prop_type='node', lin_type='CycleLineage', dtype='int', unit=None),\n", " 'division_time': Property(identifier='division_time', name='Division time', description='Time between two successive divisions', provenance='pycellin', prop_type='node', lin_type='CycleLineage', dtype='int', unit='frame'),\n", " 'cycle_completeness': Property(identifier='cycle_completeness', name='Cycle completeness', description='Completeness of the cell cycle', provenance='pycellin', prop_type='node', lin_type='CycleLineage', dtype='bool', unit=None),\n", - " 'branch_total_displacement': Property(identifier='branch_total_displacement', name='Branch total displacement', description='Displacement of the cell during the cell cycle', provenance='pycellin', prop_type='node', lin_type='CycleLineage', dtype='float', unit='µm')}" + " 'branch_total_displacement': Property(identifier='branch_total_displacement', name='Branch total displacement', description='Displacement of the cell during the cell cycle', provenance='pycellin', prop_type='node', lin_type='CycleLineage', dtype='float', unit='micrometer')}" ] }, "execution_count": 19, @@ -4755,7 +4755,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "WARNING: Circular skeleton on node 9350 of track 2! The object is probably roundish and the radius is a better metric in that case. Setting the length and width to NaN.\n" + "WARNING: One pixel skeleton on node 9350! The object is probably roundish and the radius is a better metric in that case. Setting the length and width to NaN.\n" ] } ], @@ -8287,10 +8287,10 @@ "evalue": "'absolute_age'", "output_type": "error", "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mKeyError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[27], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[43mlin0\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mnodes\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m9013\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mabsolute_age\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\n", - "\u001b[1;31mKeyError\u001b[0m: 'absolute_age'" + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mKeyError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[27]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43mlin0\u001b[49m\u001b[43m.\u001b[49m\u001b[43mnodes\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m9013\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mabsolute_age\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\n", + "\u001b[31mKeyError\u001b[39m: 'absolute_age'" ] } ], @@ -8315,7 +8315,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.18" + "version": "3.11.13" } }, "nbformat": 4, diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index 6916d2d..3eda61d 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -11,66 +11,69 @@ from datetime import datetime from importlib.metadata import version from pathlib import Path +from tkinter import N from typing import Any import geff import networkx as nx -from pycellin.classes import Data, Feature, Model -from pycellin.custom_types import FeatureType +from pycellin.classes import Data, Property, Model +from pycellin.custom_types import PropertyType from pycellin.io.utils import ( _split_graph_into_lineages, _update_lineages_IDs_key, - _update_node_feature_key, + _update_node_prop_key, check_fusions, ) -def _extract_feats_metadata( +def _extract_props_metadata( md: dict[str, geff.metadata_schema.PropMetadata], - feats_dict: dict[str, Feature], - feat_type: FeatureType, + props_dict: dict[str, Property], + prop_type: PropertyType, ) -> None: for key, prop in md.items(): - if key not in feats_dict: - feats_dict[key] = Feature( - name=key, - description=prop.description if prop.description else key, + if key not in props_dict: + props_dict[key] = Property( + identifier=key, + name=prop.name or key, + description=prop.description or key, provenance="geff", - feat_type=feat_type, + prop_type=prop_type, lin_type="CellLineage", - data_type=prop.dtype, - unit=prop.unit, + dtype=prop.dtype, + unit=prop.unit or None, ) else: - if feats_dict[key].feat_type != feat_type: + if props_dict[key].prop_type != prop_type: # If the key is already taken, we rename with prefix. - if feat_type == "node": + if prop_type == "node": prefix = "cell" - elif feat_type == "edge": + elif prop_type == "edge": prefix = "link" else: raise ValueError( - f"Unsupported feature type: {feat_type}. Expected 'node' or 'edge'." + f"Unsupported property type: {prop_type}. Expected 'node' or 'edge'." ) - # TODO: should we rename both features? + # TODO: should we rename both properties? new_key = f"{prefix}_{key}" - feats_dict[new_key] = Feature( - name=new_key, - description=prop.description if prop.description else new_key, + props_dict[new_key] = Property( + identifier=new_key, + name=prop.name or new_key, + description=prop.description or new_key, provenance="geff", - feat_type=feat_type, + prop_type=prop_type, lin_type="CellLineage", - data_type=prop.dtype, - unit=prop.unit, + dtype=prop.dtype, + unit=prop.unit or None, ) else: raise KeyError( - f"Feature '{key}' already exists in feats_dict for nodes and edges. " - "Please ensure unique feature names." + f"Property '{key}' already exists in props_dict for nodes and edges. " + "Please ensure unique property names." ) # TODO: but then, what does the user do? They might not be able to rename - # the feature from the tool that generated the geff file. + # the property from the tool that generated the geff file. # Directly ask the user how to rename? @@ -123,23 +126,23 @@ def load_geff_file( # Extract and dispatch metadata # TODO: but for now we wait for the change in geff metadata specs - feats_dict = {} + props_dict = {} if geff_md.node_props_metadata is not None: # print(geff_md.node_props_metadata) - _extract_feats_metadata(geff_md.node_props_metadata, feats_dict, "node") + _extract_props_metadata(geff_md.node_props_metadata, props_dict, "node") # Split the graph into lineages lineages = _split_graph_into_lineages(geff_graph, lineage_ID_key=lin_id_key) print(f"Number of lineages: {len(lineages)}") - # Rename features to match pycellin conventions + # Rename properties to match pycellin conventions _update_lineages_IDs_key(lineages, lin_id_key) for lin in lineages: if cell_id_key is None: for node in lin.nodes: lin.nodes[node]["cell_ID"] = node else: - _update_node_feature_key(lin, old_key=cell_id_key, new_key="cell_ID") + _update_node_prop_key(lin, old_key=cell_id_key, new_key="cell_ID") # TODO: cells positions and edges positions (keys from axes) # Time? @@ -154,18 +157,16 @@ def load_geff_file( if __name__ == "__main__": - geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/reader_test_graph.geff" - # geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/mouse-20250719.zarr/tracks" - # geff_file = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" - geff_file = ( - "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" - ) - geff_file = "/mnt/data/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" + geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/reader_test_graph.geff" + # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/mouse-20250719.zarr/tracks" + # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" + geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" + geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" print(geff_file) model = load_geff_file(geff_file) # print(model) - print("feats_dict", model.feat_declaration.feats_dict) + print("props_dict", model.props_metadata.props) # lineages = model.get_cell_lineages() # print(f"Number of lineages: {len(lineages)}") # for lin in lineages: diff --git a/pycellin/io/trackmate/loader.py b/pycellin/io/trackmate/loader.py index 715a461..481c4c2 100644 --- a/pycellin/io/trackmate/loader.py +++ b/pycellin/io/trackmate/loader.py @@ -23,8 +23,8 @@ from pycellin.io.utils import ( _split_graph_into_lineages, check_fusions, - _update_node_feature_key, - _update_lineage_feature_key, + _update_node_prop_key, + _update_lineage_prop_key, _update_lineages_IDs_key, ) @@ -840,7 +840,7 @@ def _parse_model_tag( # into its connected components. lineages = _split_graph_into_lineages( graph, - lin_features=tracks_attributes, + lin_props=tracks_attributes, lineage_ID_key="TRACK_ID", ) @@ -856,8 +856,8 @@ def _parse_model_tag( ("FRAME", "frame"), # mandatory ("name", "cell_name"), # confusing ]: - _update_node_feature_key(lin, key_name, new_key) - _update_lineage_feature_key(lin, "name", "lineage_name") + _update_node_prop_key(lin, key_name, new_key) + _update_lineage_prop_key(lin, "name", "lineage_name") _update_location_related_props(lin) # Adding if each track was present in the 'FilteredTracks' tag diff --git a/pycellin/io/utils.py b/pycellin/io/utils.py index e19574a..f15ce6b 100644 --- a/pycellin/io/utils.py +++ b/pycellin/io/utils.py @@ -25,23 +25,23 @@ def check_fusions(model: Model) -> None: fusion_warning = ( f"Unsupported data, {len(all_fusions)} cell fusions detected. " "It is advised to deal with them before any other processing, " - "especially for tracking related features. Crashes and incorrect " + "especially for tracking related properties. Crashes and incorrect " "results can occur. See documentation for more details." ) warnings.warn(fusion_warning) -def _add_lineages_features( +def _add_lineage_props( lineages: list[CellLineage], - lin_features: list[dict[str, Any]], + lin_props: list[dict[str, Any]], lineage_ID_key: str | None = "lineage_ID", ) -> None: """ - Update each CellLineage in the list with corresponding lineage features. + Update each CellLineage in the list with corresponding lineage properties. This function iterates over a list of CellLineage objects, attempting to match each lineage with its corresponding lineage - features based on the 'lineage_ID_key' feature present in the + properties based on the 'lineage_ID_key' property present in the lineage nodes. It then updates the lineage graph with these attributes. @@ -49,8 +49,8 @@ def _add_lineages_features( ---------- lineages : list[CellLineage] A list of the lineages to update. - lin_features : list[dict[str, Any]] - A list of dictionaries, where each dictionary contains features + lin_props : list[dict[str, Any]] + A list of dictionaries, where each dictionary contains properties for a specific lineage, identified by a the given 'lineage_ID_key' key. lineage_ID_key : str | None, optional The key used to identify the lineage in the attributes. @@ -63,7 +63,7 @@ def _add_lineages_features( assignment. """ for lin in lineages: - # Finding the dict of features matching the lineage. + # Finding the dict of properties matching the lineage. tmp = set(t_id for _, t_id in lin.nodes(data=lineage_ID_key)) if not tmp: @@ -71,28 +71,28 @@ def _add_lineages_features( # Even if it can't be updated, we still want to return this graph. continue elif tmp == {None}: - # Happens when all the nodes do not have a 'lineage_ID_key' feature. + # Happens when all the nodes do not have a 'lineage_ID_key' property. continue elif None in tmp: # Happens when at least one node does not have a 'lineage_ID_key' - # feature, so we clean 'tmp' and carry on. + # property, so we clean 'tmp' and carry on. tmp.remove(None) elif len(tmp) != 1: raise ValueError("Impossible state: several IDs for one lineage.") current_lineage_id = list(tmp)[0] current_lineage_attr = [ - d_attr for d_attr in lin_features if d_attr[lineage_ID_key] == current_lineage_id + d_attr for d_attr in lin_props if d_attr[lineage_ID_key] == current_lineage_id ][0] - # Adding the features to the lineage. + # Adding the properties to the lineage. for k, v in current_lineage_attr.items(): lin.graph[k] = v def _split_graph_into_lineages( graph: nx.DiGraph, - lin_features: list[dict[str, Any]] | None = None, + lin_props: list[dict[str, Any]] | None = None, lineage_ID_key: str | None = "lineage_ID", ) -> list[CellLineage]: """ @@ -102,7 +102,7 @@ def _split_graph_into_lineages( ---------- lineage : nx.DiGraph The graph to split. - lin_features : list[dict[str, Any]] | None + lin_props : list[dict[str, Any]] | None A list of dictionaries, where each dictionary contains TrackMate attributes for a specific track, identified by a 'TRACK_ID' key. If None, no attributes will be added to the lineages. @@ -118,14 +118,14 @@ def _split_graph_into_lineages( CellLineage(graph.subgraph(c).copy()) for c in nx.weakly_connected_components(graph) ] del graph # Redondant with the subgraphs. - if not lin_features: + if not lin_props: # We need to create and add a lineage_ID key to each lineage. for i, lin in enumerate(lineages): lin.graph["lineage_ID"] = i else: - # Adding lineage features to each lineage. + # Adding lineage properties to each lineage. try: - _add_lineages_features(lineages, lin_features, lineage_ID_key) + _add_lineage_props(lineages, lin_props, lineage_ID_key) except ValueError as err: print(err) # The program is in an impossible state so we need to stop. @@ -134,7 +134,7 @@ def _split_graph_into_lineages( return lineages -def _update_node_feature_key( +def _update_node_prop_key( lineage: CellLineage, old_key: str, new_key: str, @@ -143,16 +143,16 @@ def _update_node_feature_key( default_value: Any | None = None, ) -> None: """ - Update the key of a feature in all the nodes of a lineage. + Update the key of a property in all the nodes of a lineage. Parameters ---------- lineage : CellLineage The lineage to update. old_key : str - The old key of the feature. + The old key of the property. new_key : str - The new key of the feature. + The new key of the property. enforce_old_key_existence : bool, optional If True, raises an error when the old key does not exist in a node. If False, the function will skip nodes that do not have the old key. @@ -181,22 +181,22 @@ def _update_node_feature_key( lineage.nodes[node][new_key] = default_value -def _update_lineage_feature_key( +def _update_lineage_prop_key( lineage: CellLineage, old_key: str, new_key: str, ) -> None: """ - Update the key of a feature in the graph of a lineage. + Update the key of a property in the graph of a lineage. Parameters ---------- lineage : CellLineage The lineage to update. old_key : str - The old key of the feature. + The old key of the property. new_key : str - The new key of the feature. + The new key of the property. """ if old_key in lineage.graph: lineage.graph[new_key] = lineage.graph.pop(old_key) diff --git a/tests/io/test_utils.py b/tests/io/test_utils.py index 55228cc..f5b2805 100644 --- a/tests/io/test_utils.py +++ b/tests/io/test_utils.py @@ -7,19 +7,19 @@ from pycellin.classes import CellLineage from pycellin.io.utils import ( - _add_lineages_features, + _add_lineage_props, _split_graph_into_lineages, - _update_node_feature_key, - _update_lineage_feature_key, + _update_node_prop_key, + _update_lineage_prop_key, _update_lineages_IDs_key, ) from pycellin.utils import is_equal -# _update_node_feature_key #################################################### +# _update_node_prop_key #################################################### -# def _update_node_feature_key( +# def _update_node_prop_key( # lineage: CellLineage, # old_key: str, # new_key: str, @@ -28,16 +28,16 @@ # default_value: Any | None = None, # ) -> None: # """ -# Update the key of a feature in all the nodes of a lineage. +# Update the key of a property in all the nodes of a lineage. # Parameters # ---------- # lineage : CellLineage # The lineage to update. # old_key : str -# The old key of the feature. +# The old key of the property. # new_key : str -# The new key of the feature. +# The new key of the property. # enforce_old_key_existence : bool, optional # If True, raises an error when the old key does not exist in a node. # If False, the function will skip nodes that do not have the old key. @@ -62,15 +62,15 @@ # lineage.nodes[node][new_key] = default_value -def test_update_node_feature_key(): - """Test updating a node feature key.""" +def test_update_node_prop_key(): + """Test updating a node property key.""" lineage = CellLineage() old_key_values = ["value1", "value2", "value3"] lineage.add_node(1, old_key=old_key_values[0]) lineage.add_node(2, old_key=old_key_values[1]) lineage.add_node(3, old_key=old_key_values[2]) - _update_node_feature_key(lineage, "old_key", "new_key") + _update_node_prop_key(lineage, "old_key", "new_key") for i, node in enumerate(lineage.nodes): assert "new_key" in lineage.nodes[node] @@ -78,14 +78,14 @@ def test_update_node_feature_key(): assert lineage.nodes[node]["new_key"] == old_key_values[i] -def test_update_node_feature_key_missing_old_key_skip(): +def test_update_node_prop_key_missing_old_key_skip(): """Test that nodes without old_key are skipped when enforce_old_key_existence=False.""" lineage = CellLineage() lineage.add_node(1, old_key="value1") lineage.add_node(2) # No old_key lineage.add_node(3, old_key="value3") - _update_node_feature_key(lineage, "old_key", "new_key") + _update_node_prop_key(lineage, "old_key", "new_key") assert lineage.nodes[1]["new_key"] == "value1" assert "old_key" not in lineage.nodes[1] @@ -95,28 +95,24 @@ def test_update_node_feature_key_missing_old_key_skip(): assert "old_key" not in lineage.nodes[3] -def test_update_node_feature_key_enforce_old_key_existence(): +def test_update_node_prop_key_enforce_old_key_existence(): """Test that missing old_key raises error when enforce_old_key_existence=True.""" lineage = CellLineage() lineage.add_node(1, old_key="value1") lineage.add_node(2) # No old_key - with pytest.raises( - ValueError, match="Node 2 does not have the required key 'old_key'" - ): - _update_node_feature_key( - lineage, "old_key", "new_key", enforce_old_key_existence=True - ) + with pytest.raises(ValueError, match="Node 2 does not have the required key 'old_key'"): + _update_node_prop_key(lineage, "old_key", "new_key", enforce_old_key_existence=True) -def test_update_node_feature_key_set_default_if_missing(): +def test_update_node_prop_key_set_default_if_missing(): """Test setting default value when old_key is missing and set_default_if_missing=True.""" lineage = CellLineage() lineage.add_node(1, old_key="value1") lineage.add_node(2) # No old_key lineage.add_node(3, old_key="value3") - _update_node_feature_key( + _update_node_prop_key( lineage, "old_key", "new_key", @@ -132,45 +128,45 @@ def test_update_node_feature_key_set_default_if_missing(): assert "old_key" not in lineage.nodes[3] -def test_update_node_feature_key_set_default_none(): +def test_update_node_prop_key_set_default_none(): """Test setting None as default value when old_key is missing.""" lineage = CellLineage() lineage.add_node(1, old_key="value1") lineage.add_node(2) # No old_key - _update_node_feature_key(lineage, "old_key", "new_key", set_default_if_missing=True) + _update_node_prop_key(lineage, "old_key", "new_key", set_default_if_missing=True) assert lineage.nodes[1]["new_key"] == "value1" assert lineage.nodes[2]["new_key"] is None -def test_update_node_feature_key_empty_lineage(): +def test_update_node_prop_key_empty_lineage(): """Test function with empty lineage (no nodes).""" lineage = CellLineage() # Should not raise an error and do nothing - _update_node_feature_key(lineage, "old_key", "new_key") + _update_node_prop_key(lineage, "old_key", "new_key") assert len(lineage.nodes) == 0 -def test_update_node_feature_key_same_key_name(): +def test_update_node_prop_key_same_key_name(): """Test updating a key to itself (should work without issues).""" lineage = CellLineage() lineage.add_node(1, test_key="value1") lineage.add_node(2, test_key="value2") - _update_node_feature_key(lineage, "test_key", "test_key") + _update_node_prop_key(lineage, "test_key", "test_key") assert lineage.nodes[1]["test_key"] == "value1" assert lineage.nodes[2]["test_key"] == "value2" -# _update_lineage_feature_key ################################################# +# _update_lineage_prop_key ################################################# -def test_update_lineage_feature_key(): +def test_update_lineage_prop_key(): lineage = CellLineage() lineage.graph["old_key"] = "old_value" - _update_lineage_feature_key(lineage, "old_key", "new_key") + _update_lineage_prop_key(lineage, "old_key", "new_key") assert "new_key" in lineage.graph assert lineage.graph["new_key"] == "old_value" @@ -278,10 +274,10 @@ def test_update_lineages_IDs_key_preserves_other_graph_attributes(): assert "TRACK_ID" not in lin1.graph -# _add_lineages_features ############################################################ +# _add_lineages_props ############################################################ -def test_add_lineages_features(): +def test_add_lineages_props(): g1_attr = {"name": "blob", "lineage_ID": 0} g2_attr = {"name": "blub", "lineage_ID": 1} @@ -289,7 +285,7 @@ def test_add_lineages_features(): g1_obt.add_node(1, lineage_ID=0) g2_obt = nx.DiGraph() g2_obt.add_node(2, lineage_ID=1) - _add_lineages_features([g1_obt, g2_obt], [g1_attr, g2_attr]) + _add_lineage_props([g1_obt, g2_obt], [g1_attr, g2_attr]) g1_exp = nx.DiGraph() g1_exp.graph["name"] = "blob" @@ -304,7 +300,7 @@ def test_add_lineages_features(): assert is_equal(g2_obt, g2_exp) -def test_add_lineages_features_different_lin_ID_key(): +def test_add_lineages_props_different_lin_ID_key(): g1_attr = {"name": "blob", "TRACK_ID": 0} g2_attr = {"name": "blub", "TRACK_ID": 1} @@ -312,9 +308,7 @@ def test_add_lineages_features_different_lin_ID_key(): g1_obt.add_node(1, TRACK_ID=0) g2_obt = nx.DiGraph() g2_obt.add_node(2, TRACK_ID=1) - _add_lineages_features( - [g1_obt, g2_obt], [g1_attr, g2_attr], lineage_ID_key="TRACK_ID" - ) + _add_lineage_props([g1_obt, g2_obt], [g1_attr, g2_attr], lineage_ID_key="TRACK_ID") g1_exp = nx.DiGraph() g1_exp.graph["name"] = "blob" @@ -329,7 +323,7 @@ def test_add_lineages_features_different_lin_ID_key(): assert is_equal(g2_obt, g2_exp) -def test_add_lineages_features_no_lin_ID_on_all_nodes(): +def test_add_lineages_props_no_lin_ID_on_all_nodes(): g1_attr = {"name": "blob", "lineage_ID": 0} g2_attr = {"name": "blub", "lineage_ID": 1} @@ -338,9 +332,7 @@ def test_add_lineages_features_no_lin_ID_on_all_nodes(): g1_obt.add_node(3) g2_obt = nx.DiGraph() g2_obt.add_node(2, lineage_ID=1) - _add_lineages_features( - [g1_obt, g2_obt], [g1_attr, g2_attr], lineage_ID_key="lineage_ID" - ) + _add_lineage_props([g1_obt, g2_obt], [g1_attr, g2_attr], lineage_ID_key="lineage_ID") g1_exp = nx.DiGraph() g1_exp.add_node(1) @@ -354,7 +346,7 @@ def test_add_lineages_features_no_lin_ID_on_all_nodes(): assert is_equal(g2_obt, g2_exp) -def test_add_lineages_features_no_lin_ID_on_one_node(): +def test_add_lineages_props_no_lin_ID_on_one_node(): g1_attr = {"name": "blob", "lineage_ID": 0} g2_attr = {"name": "blub", "lineage_ID": 1} @@ -365,7 +357,7 @@ def test_add_lineages_features_no_lin_ID_on_one_node(): g2_obt = nx.DiGraph() g2_obt.add_node(2, lineage_ID=1) - _add_lineages_features([g1_obt, g2_obt], [g1_attr, g2_attr]) + _add_lineage_props([g1_obt, g2_obt], [g1_attr, g2_attr]) g1_exp = nx.DiGraph() g1_exp.graph["name"] = "blob" @@ -382,7 +374,7 @@ def test_add_lineages_features_no_lin_ID_on_one_node(): assert is_equal(g2_obt, g2_exp) -def test_add_lineages_features_different_ID_for_one_track(): +def test_add_lineages_props_different_ID_for_one_track(): g1_attr = {"name": "blob", "lineage_ID": 0} g2_attr = {"name": "blub", "lineage_ID": 1} @@ -394,17 +386,17 @@ def test_add_lineages_features_different_ID_for_one_track(): g2_obt = nx.DiGraph() g2_obt.add_node(2, lineage_ID=1) with pytest.raises(ValueError): - _add_lineages_features([g1_obt, g2_obt], [g1_attr, g2_attr]) + _add_lineage_props([g1_obt, g2_obt], [g1_attr, g2_attr]) -def test_add_lineages_features_no_nodes(): +def test_add_lineages_props_no_nodes(): g1_attr = {"name": "blob", "lineage_ID": 0} g2_attr = {"name": "blub", "lineage_ID": 1} g1_obt = nx.DiGraph() g2_obt = nx.DiGraph() g2_obt.add_node(2, lineage_ID=1) - _add_lineages_features([g1_obt, g2_obt], [g1_attr, g2_attr]) + _add_lineage_props([g1_obt, g2_obt], [g1_attr, g2_attr]) g1_exp = nx.DiGraph() g2_exp = nx.DiGraph() @@ -455,9 +447,7 @@ def test_split_graph_into_lineages_different_lin_ID_key(): g.add_node(3, TRACK_ID=2) g.add_node(4, TRACK_ID=2) g.add_edge(3, 4) - obtained = _split_graph_into_lineages( - g, [g1_attr, g2_attr], lineage_ID_key="TRACK_ID" - ) + obtained = _split_graph_into_lineages(g, [g1_attr, g2_attr], lineage_ID_key="TRACK_ID") g1_exp = CellLineage(g.subgraph([1, 2])) g1_exp.graph["name"] = "blob" @@ -471,7 +461,7 @@ def test_split_graph_into_lineages_different_lin_ID_key(): assert is_equal(obtained[1], g2_exp) -def test_split_graph_into_lineages_no_lin_features(): +def test_split_graph_into_lineages_no_lin_props(): g = nx.DiGraph() g.add_edges_from([(1, 2), (3, 4)]) From 3eda3b45f84f72e2e28f2a14ac2b6ca4cd4fc22a Mon Sep 17 00:00:00 2001 From: lxenard Date: Thu, 11 Sep 2025 19:15:58 +0200 Subject: [PATCH 13/27] WIP extraction of lineage props metadata --- pycellin/io/geff/loader.py | 82 +++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index 3eda61d..e2493fe 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -17,7 +17,7 @@ import geff import networkx as nx -from pycellin.classes import Data, Property, Model +from pycellin.classes import Data, Property, PropsMetadata, Model from pycellin.custom_types import PropertyType from pycellin.io.utils import ( _split_graph_into_lineages, @@ -27,12 +27,41 @@ ) +def _recursive_dict_search(data: dict[str, Any], target_key: str) -> dict[str, Any] | None: + """ + Recursively search for a target key in a nested dictionary structure. + + Parameters + ---------- + data : dict + The dictionary to search through. + target_key : str + The key to search for. + + Returns + ------- + dict[str, Any] | None + The dict associated with the target key if found, None otherwise. + """ + if not isinstance(data, dict): + return None + if target_key in data: # does the current level contain the target key? + return data[target_key] + for value in data.values(): # recursively search in nested dictionaries + if isinstance(value, dict): + result = _recursive_dict_search(value, target_key) + if result is not None: + return result + return None + + def _extract_props_metadata( md: dict[str, geff.metadata_schema.PropMetadata], props_dict: dict[str, Property], prop_type: PropertyType, ) -> None: for key, prop in md.items(): + # print(key, prop) if key not in props_dict: props_dict[key] = Property( identifier=key, @@ -77,6 +106,27 @@ def _extract_props_metadata( # Directly ask the user how to rename? +def _extract_lin_props_metadata( + md: dict[str, Any], + props_dict: dict[str, Property], +) -> None: + for key, prop in md.items(): + if key not in props_dict: + props_dict[key] = Property( + identifier=key, + name=prop.get("name") or key, + description=prop.get("description") or key, + provenance="geff", + prop_type="lineage", + lin_type="CellLineage", + dtype=prop.get("dtype"), + unit=prop.get("unit") or None, + ) + else: + # TODO: deal with the case where the property already exists and needs to be renamed. + pass + + def load_geff_file( geff_file: Path | str, cell_id_key: str | None = None, @@ -103,6 +153,7 @@ def load_geff_file( geff_graph, geff_md = geff.read_nx(geff_file, validate=True) for node in geff_graph.nodes: print(f"Node {node}: {geff_graph.nodes[node]}") + break print(type(geff_graph)) print(geff_md.directed) @@ -111,6 +162,7 @@ def load_geff_file( else: lin_id_key = None print("lin_id_key:", lin_id_key) + # Determine axes # If no axes, need to have them as arguments...? Set a default to x, y, z, t...? print("Axes:", geff_md.axes) @@ -125,11 +177,29 @@ def load_geff_file( # int_graph = nx.relabel_nodes(geff_graph, {node: int(node) for node in geff_graph.nodes()}) # Extract and dispatch metadata - # TODO: but for now we wait for the change in geff metadata specs - props_dict = {} + # Generic metadata + metadata = {} # type: dict[str, Any] + + # Property metadata + props_dict: dict[str, Property] = {} if geff_md.node_props_metadata is not None: + # print(type(geff_md.node_props_metadata)) # print(geff_md.node_props_metadata) _extract_props_metadata(geff_md.node_props_metadata, props_dict, "node") + if geff_md.edge_props_metadata is not None: + _extract_props_metadata(geff_md.edge_props_metadata, props_dict, "edge") + # TODO: for now lineage properties are not associated to a specific tag but stored + # somewhere in the "extra" field. We need to check recurrently if there is a dict + # key called "lineage_props_metadata" in the "extra" field. + if geff_md.extra is not None: + # Recursive search for the "lineage_props_metadata" key through the "extra" + # field dict of dicts of dicts... + lin_props_metadata = _recursive_dict_search(geff_md.extra, "lineage_props_metadata") + if lin_props_metadata is not None: + # print(type(lin_props_metadata)) + # print(lin_props_metadata) + _extract_lin_props_metadata(lin_props_metadata, props_dict) + print("props_dict:", props_dict) # Split the graph into lineages lineages = _split_graph_into_lineages(geff_graph, lineage_ID_key=lin_id_key) @@ -146,9 +216,11 @@ def load_geff_file( # TODO: cells positions and edges positions (keys from axes) # Time? + # All the stuff in field extra is stored in the model meta + # Check for fusions data = Data({lin.graph["lineage_ID"]: lin for lin in lineages}) - model = Model(data=data) + model = Model(data=data, props_metadata=PropsMetadata(props=props_dict)) # print(model.data) # print(model.data.cell_data) check_fusions(model) # pycellin DOES NOT support fusion events @@ -166,7 +238,7 @@ def load_geff_file( print(geff_file) model = load_geff_file(geff_file) # print(model) - print("props_dict", model.props_metadata.props) + # print("props_dict", model.props_metadata.props) # lineages = model.get_cell_lineages() # print(f"Number of lineages: {len(lineages)}") # for lin in lineages: From 3ef02e5d98be663f3d840405de661dfa9e46e16d Mon Sep 17 00:00:00 2001 From: lxenard Date: Fri, 12 Sep 2025 15:53:46 +0200 Subject: [PATCH 14/27] Finalize extraction of properties metadata --- pycellin/io/geff/loader.py | 112 +++++++++++++++++++++++++------------ 1 file changed, 75 insertions(+), 37 deletions(-) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index e2493fe..8e92d56 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -47,7 +47,7 @@ def _recursive_dict_search(data: dict[str, Any], target_key: str) -> dict[str, A return None if target_key in data: # does the current level contain the target key? return data[target_key] - for value in data.values(): # recursively search in nested dictionaries + for value in data.values(): # recursive search in nested dictionaries if isinstance(value, dict): result = _recursive_dict_search(value, target_key) if result is not None: @@ -61,12 +61,11 @@ def _extract_props_metadata( prop_type: PropertyType, ) -> None: for key, prop in md.items(): - # print(key, prop) if key not in props_dict: props_dict[key] = Property( identifier=key, name=prop.name or key, - description=prop.description or key, + description=prop.description or prop.name or key, provenance="geff", prop_type=prop_type, lin_type="CellLineage", @@ -75,35 +74,42 @@ def _extract_props_metadata( ) else: if props_dict[key].prop_type != prop_type: - # If the key is already taken, we rename with prefix. + # The key must be unique but it already exists for nodes or edges, + # so it needs to be renamed. if prop_type == "node": - prefix = "cell" + current_prefix = "cell" + other_prefix = "link" elif prop_type == "edge": - prefix = "link" + current_prefix = "link" + other_prefix = "cell" else: raise ValueError( f"Unsupported property type: {prop_type}. Expected 'node' or 'edge'." ) - # TODO: should we rename both properties? - new_key = f"{prefix}_{key}" + # Rename the new property to be added. + new_key = f"{current_prefix}_{key}" props_dict[new_key] = Property( identifier=new_key, - name=prop.name or new_key, - description=prop.description or new_key, + name=prop.name or key, + description=prop.description or prop.name or key, provenance="geff", prop_type=prop_type, lin_type="CellLineage", dtype=prop.dtype, unit=prop.unit or None, ) + # Rename the other property as well for clarity. + other_key = f"{other_prefix}_{key}" + other_prop = props_dict.pop(key) + other_prop.identifier = other_key + props_dict[other_key] = other_prop else: + # GEFF ensure uniqueness of property keys for nodes and edges separately, + # so this should never happen. raise KeyError( - f"Property '{key}' already exists in props_dict for nodes and edges. " - "Please ensure unique property names." + f"Property '{key}' already exists in props_dict for {prop_type}s. " + "Please ensure unique property identifiers." ) - # TODO: but then, what does the user do? They might not be able to rename - # the property from the tool that generated the geff file. - # Directly ask the user how to rename? def _extract_lin_props_metadata( @@ -115,7 +121,7 @@ def _extract_lin_props_metadata( props_dict[key] = Property( identifier=key, name=prop.get("name") or key, - description=prop.get("description") or key, + description=prop.get("description") or prop.get("name") or key, provenance="geff", prop_type="lineage", lin_type="CellLineage", @@ -123,8 +129,58 @@ def _extract_lin_props_metadata( unit=prop.get("unit") or None, ) else: - # TODO: deal with the case where the property already exists and needs to be renamed. - pass + if props_dict[key].prop_type != "lineage": + # The key must be unique but it already exists for nodes or edges, + # so it needs to be renamed. + new_key = f"lin_{key}" + props_dict[new_key] = Property( + identifier=new_key, + name=prop.name or key, + description=prop.description or prop.get("name") or key, + provenance="geff", + prop_type="lineage", + lin_type="CellLineage", + dtype=prop.dtype, + unit=prop.unit or None, + ) + else: + raise KeyError( + f"Property '{key}' already exists in props_dict for lineages. " + "Please ensure unique property identifiers." + ) + + +def _read_props_metadata(geff_md: geff.metadata_schema.GeffMetadata) -> dict[str, Property]: + """ + Read and extract properties metadata from geff metadata. + + Parameters + ---------- + geff_md : geff.metadata_schema.GeffMetadata + The geff metadata object containing properties metadata. + + Returns + ------- + dict[str, Property] + Dictionary mapping property identifiers to Property objects. + """ + props_dict: dict[str, Property] = {} + if geff_md.node_props_metadata is not None: + _extract_props_metadata(geff_md.node_props_metadata, props_dict, "node") + if geff_md.edge_props_metadata is not None: + _extract_props_metadata(geff_md.edge_props_metadata, props_dict, "edge") + + # TODO: for now lineage properties are not associated to a specific tag but stored + # somewhere in the "extra" field. We need to check recurrently if there is a dict + # key called "lineage_props_metadata" in the "extra" field. + if geff_md.extra is not None: + # Recursive search for the "lineage_props_metadata" key through the "extra" + # field dict of dicts of dicts... + lin_props_metadata = _recursive_dict_search(geff_md.extra, "lineage_props_metadata") + if lin_props_metadata is not None: + _extract_lin_props_metadata(lin_props_metadata, props_dict) + + return props_dict def load_geff_file( @@ -181,25 +237,7 @@ def load_geff_file( metadata = {} # type: dict[str, Any] # Property metadata - props_dict: dict[str, Property] = {} - if geff_md.node_props_metadata is not None: - # print(type(geff_md.node_props_metadata)) - # print(geff_md.node_props_metadata) - _extract_props_metadata(geff_md.node_props_metadata, props_dict, "node") - if geff_md.edge_props_metadata is not None: - _extract_props_metadata(geff_md.edge_props_metadata, props_dict, "edge") - # TODO: for now lineage properties are not associated to a specific tag but stored - # somewhere in the "extra" field. We need to check recurrently if there is a dict - # key called "lineage_props_metadata" in the "extra" field. - if geff_md.extra is not None: - # Recursive search for the "lineage_props_metadata" key through the "extra" - # field dict of dicts of dicts... - lin_props_metadata = _recursive_dict_search(geff_md.extra, "lineage_props_metadata") - if lin_props_metadata is not None: - # print(type(lin_props_metadata)) - # print(lin_props_metadata) - _extract_lin_props_metadata(lin_props_metadata, props_dict) - print("props_dict:", props_dict) + props_dict = _read_props_metadata(geff_md) # Split the graph into lineages lineages = _split_graph_into_lineages(geff_graph, lineage_ID_key=lin_id_key) From 8025f0a748a65b76b13fea4121b4d3d28964bd61 Mon Sep 17 00:00:00 2001 From: lxenard Date: Fri, 12 Sep 2025 16:05:03 +0200 Subject: [PATCH 15/27] Add missing docstrings --- pycellin/io/geff/loader.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index 8e92d56..95fcc49 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -60,6 +60,26 @@ def _extract_props_metadata( props_dict: dict[str, Property], prop_type: PropertyType, ) -> None: + """ + Extract properties metadata from a given dictionary and update the props_dict. + + Parameters + ---------- + md : dict[str, geff.metadata_schema.PropMetadata] + The dictionary containing properties metadata. + props_dict : dict[str, Property] + The dictionary to update with extracted properties metadata. + prop_type : PropertyType + The type of property being extracted ('node' or 'edge'). + + Raises + ------ + ValueError + If an unsupported property type is provided. + KeyError + If a property identifier already exists in props_dict for the same property + type. + """ for key, prop in md.items(): if key not in props_dict: props_dict[key] = Property( @@ -116,6 +136,21 @@ def _extract_lin_props_metadata( md: dict[str, Any], props_dict: dict[str, Property], ) -> None: + """ + Extract lineage properties metadata from a given dictionary and update the props_dict. + + Parameters + ---------- + md : dict[str, Any] + The dictionary containing lineage properties metadata. + props_dict : dict[str, Property] + The dictionary to update with extracted lineage properties metadata. + + Raises + ------ + KeyError + If a property identifier already exists in props_dict for lineages. + """ for key, prop in md.items(): if key not in props_dict: props_dict[key] = Property( From 87ba1f82ece40d3ef1e734172e31e6c623b77bc5 Mon Sep 17 00:00:00 2001 From: lxenard Date: Fri, 12 Sep 2025 18:25:57 +0200 Subject: [PATCH 16/27] Raise Error when geff graph is undirected --- pycellin/io/geff/loader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index 95fcc49..6cde4f3 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -242,6 +242,10 @@ def load_geff_file( # Read the geff file geff_graph, geff_md = geff.read_nx(geff_file, validate=True) + if not geff_md.directed: + raise ValueError( + "The geff graph is undirected: pycellin does not support undirected graphs." + ) for node in geff_graph.nodes: print(f"Node {node}: {geff_graph.nodes[node]}") break @@ -270,6 +274,7 @@ def load_geff_file( # Extract and dispatch metadata # Generic metadata metadata = {} # type: dict[str, Any] + # All the stuff in field extra is stored in the model metadata # Property metadata props_dict = _read_props_metadata(geff_md) @@ -289,8 +294,6 @@ def load_geff_file( # TODO: cells positions and edges positions (keys from axes) # Time? - # All the stuff in field extra is stored in the model meta - # Check for fusions data = Data({lin.graph["lineage_ID"]: lin for lin in lineages}) model = Model(data=data, props_metadata=PropsMetadata(props=props_dict)) From c55fd895c50853338aab927421f6a64286f00c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Mon, 15 Sep 2025 21:21:13 +0200 Subject: [PATCH 17/27] WIP GEFF exporter --- pycellin/io/geff/exporter.py | 234 +++++++++++++++++++++++++++++++---- 1 file changed, 212 insertions(+), 22 deletions(-) diff --git a/pycellin/io/geff/exporter.py b/pycellin/io/geff/exporter.py index cf0d548..cc248a8 100644 --- a/pycellin/io/geff/exporter.py +++ b/pycellin/io/geff/exporter.py @@ -9,36 +9,226 @@ - geff Documentation: https://live-image-tracking-tools.github.io/geff/latest/ """ -import geff +import copy + +from geff.metadata_schema import Axis, DisplayHint, GeffMetadata +from geff import write_nx +import networkx as nx +from zmq import has + +from pycellin.classes import Model, CellLineage + + +def _find_node_overlaps(lineages: list[CellLineage]) -> dict[int, list[int]]: + """ + Find overlapping node IDs across lineages. + + Parameters + ---------- + lineages : list[CellLineage] + List of lineage graphs. + + Returns + ------- + dict[int, list[int]] + A dictionary mapping node IDs to the list of lineage indices they belong to. + """ + node_to_lineages: dict[int, int] = {} + overlaps: dict[int, list[int]] = {} + + for lin_index, lin in enumerate(lineages): + for nid in lin.nodes: + if nid in node_to_lineages: # Overlap found + if nid not in overlaps: + overlaps[nid] = [node_to_lineages[nid], lin_index] + else: + overlaps[nid].append(lin_index) + else: + node_to_lineages[nid] = lin_index + + return overlaps + + +def _get_next_available_id(lineages: list[CellLineage]) -> int: + """ + Get the next available node ID across all lineages. + + Parameters + ---------- + lineages : list[CellLineage] + List of lineage graphs to check. + + Returns + ------- + int + The next available node ID. + """ + next_ids = [lin._get_next_available_node_ID() for lin in lineages] + return max(next_ids) + + +def _relabel_nodes( + lineages: list[CellLineage], + overlaps: dict[int, list[int]], +) -> None: + """ + Relabel nodes in each lineage to ensure unique IDs across all lineages. + + Parameters + ---------- + lineages : list[CellLineage] + List of lineage graphs to relabel in place. + """ + next_available_id = _get_next_available_id(lineages) + for nid, lids in overlaps.items(): + for lid in lids[1:]: + mapping = {nid: next_available_id} + nx.relabel_nodes(lineages[lid], mapping, copy=False) + next_available_id += 1 + + +def _solve_node_overlaps(lineages: list[CellLineage]) -> None: + """ + Detect and resolve overlapping node IDs across lineages by reassigning unique IDs. + + Parameters + ---------- + lineages : list[CellLineage] + List of lineage graphs to check and modify in place. + """ + overlaps = _find_node_overlaps(lineages) + if overlaps: + print("Overlapping node IDs found:") + for nid, lids in overlaps.items(): + print(f" Node ID {nid} in lineages {lids}") + _relabel_nodes(lineages, overlaps) + + # Verify no more overlaps + # TODO: remove this, it's only for debug, or put in verbose + overlaps = _find_node_overlaps(lineages) + if overlaps: + print("Overlapping node IDs found after relabeling:") + for nid, lids in overlaps.items(): + print(f" Node ID {nid} in lineages {lids}") + else: + print("No overlapping node IDs found after relabeling.") + + else: + print("No overlapping node IDs found.") + + +def _build_geff_metadata(model: Model) -> GeffMetadata: + """ + Build GEFF metadata from a pycellin model. + + Parameters + ---------- + model : Model + The pycellin model to extract metadata from. + + Returns + ------- + GeffMetadata + The GEFF metadata object. + """ + # Generic metadata + axes = [] + has_x = model.has_property("cell_x") + has_y = model.has_property("cell_y") + has_z = model.has_property("cell_z") + has_t = model.has_property("frame") + # Axes + if has_x: + axes.append(Axis(name="cell_x", type="space", unit=model.get_space_unit())) + if has_y: + axes.append(Axis(name="cell_y", type="space", unit=model.get_space_unit())) + if has_z: + axes.append(Axis(name="cell_z", type="space", unit=model.get_space_unit())) + if has_t: + axes.append(Axis(name="frame", type="time", unit=model.get_time_unit())) + # Display hints + if has_x and has_y: + display_hints = DisplayHint(display_horizontal="cell_x", display_vertical="cell_y") + if has_z: + display_hints.display_depth = "cell_z" + if has_t: + display_hints.display_time = "frame" + + # Create metadata with minimal required parameters + # Note: Using empty lists/None for required but unused parameters + metadata = GeffMetadata( + directed=True, + axes=axes, + display_hints=display_hints, + ) + + # Property metadata + # TODO cf create_or_update_metadata() in io_utils and PropMetadata in metadata_schema + + return metadata + + +def export_GEFF(model: Model, geff_out: str) -> None: + """ + Export a pycellin model to GEFF format. + + Parameters + ---------- + model : Model + The pycellin model to export. + geff_out : str + Path to the output GEFF file. + """ + # We don't want to modify the original model. + model_copy = copy.deepcopy(model) + lineages = list(model_copy.data.cell_data.values()) + for graph in lineages: + print(len(graph.nodes), len(graph.edges)) + + # TODO: this is debug + model_copy.remove_property("ROI_coords") + # model_copy.add_cell(lid=0, cid=9510) + # model_copy.add_cell(lid=1, cid=9510) + # model_copy.add_cell(lid=1, cid=9509) + # model_copy.add_cell(lid=2, cid=9498) + + # For GEFF compatibility, we need to put all the lineages in the same graph, + # but some nodes can have the same identifier... + _solve_node_overlaps(lineages) + geff_graph = nx.compose_all(lineages) + print(len(geff_graph)) + + metadata = _build_geff_metadata(model_copy) + print(metadata) + + write_nx( + geff_graph, + geff_out, + metadata=metadata, + # axis_names=["cell_x", "cell_y", "cell_z", "frame"], + # axis_units=["um", "um", "um", "s"], + # zarr_format=2, + ) + + del model_copy if __name__ == "__main__": - # xml_in = "sample_data/Ecoli_growth_on_agar_pad.xml" + xml_in = "sample_data/Ecoli_growth_on_agar_pad.xml" + # xml_in = "sample_data/Celegans-5pc-17timepoints.xml" # xml_in = "sample_data/FakeTracks.xml" - ctc_in = "sample_data/FakeTracks_TMtoCTC.txt" + # ctc_in = "sample_data/FakeTracks_TMtoCTC.txt" # ctc_in = "sample_data/Ecoli_growth_on_agar_pad_TMtoCTC.txt" - geff_out = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" + # geff_out = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" + geff_out = "E:/Janelia_Cell_Trackathon/test_pycellin_geff/test.geff" from pycellin.io.trackmate.loader import load_TrackMate_XML from pycellin.io.cell_tracking_challenge.loader import load_CTC_file - # model = load_TrackMate_XML(xml_in) - # model.remove_feature("ROI_coords") - model = load_CTC_file(ctc_in) + model = load_TrackMate_XML(xml_in) + # model = load_CTC_file(ctc_in) print(model) - print(model.get_cell_lineage_features().keys()) + # print(model.get_cell_lineage_properties().keys()) print(model.data.cell_data.keys()) - lin1 = model.data.cell_data[1] - print(lin1) - print(lin1.nodes[77]) - # lin4 = model.data.cell_data[4] - # print(lin4) - - geff.write_nx( - model.data.cell_data[1], - # model.data.cell_data[4], - geff_out, - # axis_names=["cell_x", "cell_y", "cell_z", "frame"], - # axis_units=["um", "um", "um", "s"], - # zarr_format=2, - ) + + export_GEFF(model, geff_out) From e3c4da163bf140340458eb255926db5ce7b4f8cb Mon Sep 17 00:00:00 2001 From: lxenard Date: Tue, 16 Sep 2025 14:18:41 +0200 Subject: [PATCH 18/27] Update pyproject.toml with geff --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 990d497..10b59ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ description = "Graph-based framework to manipulate and analyze cell lineages fro readme = "README.md" requires-python = ">=3.10" dependencies = [ + "geff>=0.5.0", "igraph>=0.9", "lxml>=5", "matplotlib>=3", @@ -51,6 +52,7 @@ packages = [ "pycellin.graph.properties", "pycellin.io", "pycellin.io.cell_tracking_challenge", + "pycellin.io.geff", "pycellin.io.trackmate", "pycellin.io.trackpy", ] From ae7165696224658dcd50d45c511c40b0f98f82c1 Mon Sep 17 00:00:00 2001 From: lxenard Date: Tue, 16 Sep 2025 14:19:40 +0200 Subject: [PATCH 19/27] Add Model.has_cycle_data() method --- pycellin/classes/model.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pycellin/classes/model.py b/pycellin/classes/model.py index 2ba6499..8272c3b 100644 --- a/pycellin/classes/model.py +++ b/pycellin/classes/model.py @@ -1497,6 +1497,17 @@ def add_cycle_data(self) -> None: self.data._add_cycle_lineages() self.props_metadata._add_cycle_lineage_props() + def has_cycle_data(self) -> bool: + """ + Check if the model has cycle lineages. + + Returns + ------- + bool + True if the model has cycle lineages, False otherwise. + """ + return bool(self.data.cycle_data) + def _categorize_props(self, props: list[str] | None) -> tuple[list[str], list[str], list[str]]: """ Categorize properties by type (node, edge, lineage). From 28b99254958acd1acc4ecd292ef455b993a357ad Mon Sep 17 00:00:00 2001 From: lxenard Date: Tue, 16 Sep 2025 14:31:13 +0200 Subject: [PATCH 20/27] Working GEFF exporter --- pycellin/io/geff/exporter.py | 227 ++++++++++++++++++++++++++++------- 1 file changed, 184 insertions(+), 43 deletions(-) diff --git a/pycellin/io/geff/exporter.py b/pycellin/io/geff/exporter.py index cc248a8..121d003 100644 --- a/pycellin/io/geff/exporter.py +++ b/pycellin/io/geff/exporter.py @@ -4,6 +4,9 @@ """ exporter.py +This module is part of the pycellin package. +It provides functionality to export a pycellin model to the GEFF format. + References: - geff GitHub: https://github.com/live-image-tracking-tools/geff - geff Documentation: https://live-image-tracking-tools.github.io/geff/latest/ @@ -11,12 +14,11 @@ import copy -from geff.metadata_schema import Axis, DisplayHint, GeffMetadata -from geff import write_nx import networkx as nx -from zmq import has +from geff import write_nx +from geff.metadata_schema import Axis, DisplayHint, GeffMetadata, PropMetadata -from pycellin.classes import Model, CellLineage +from pycellin.classes import CellLineage, Model, Property def _find_node_overlaps(lineages: list[CellLineage]) -> dict[int, list[int]]: @@ -117,55 +119,183 @@ def _solve_node_overlaps(lineages: list[CellLineage]) -> None: print("No overlapping node IDs found.") -def _build_geff_metadata(model: Model) -> GeffMetadata: +def _build_axes( + has_x: bool, + has_y: bool, + has_z: bool, + has_t: bool, + space_unit: str | None, + time_unit: str | None, +) -> list[Axis]: """ - Build GEFF metadata from a pycellin model. + Build a list of Axis objects for GEFF metadata. Parameters ---------- - model : Model - The pycellin model to extract metadata from. + has_x : bool + Whether the x-axis is present. + has_y : bool + Whether the y-axis is present. + has_z : bool + Whether the z-axis is present. + has_t : bool + Whether the time axis is present. + space_unit : str | None + Unit for spatial axes (e.g., "micrometer"). + time_unit : str | None + Unit for time axis (e.g., "second"). Returns ------- - GeffMetadata - The GEFF metadata object. + list[Axis] + List of Axis objects representing spatial and temporal dimensions. """ - # Generic metadata axes = [] - has_x = model.has_property("cell_x") - has_y = model.has_property("cell_y") - has_z = model.has_property("cell_z") - has_t = model.has_property("frame") - # Axes if has_x: - axes.append(Axis(name="cell_x", type="space", unit=model.get_space_unit())) + axes.append(Axis(name="cell_x", type="space", unit=space_unit)) if has_y: - axes.append(Axis(name="cell_y", type="space", unit=model.get_space_unit())) + axes.append(Axis(name="cell_y", type="space", unit=space_unit)) if has_z: - axes.append(Axis(name="cell_z", type="space", unit=model.get_space_unit())) + axes.append(Axis(name="cell_z", type="space", unit=space_unit)) if has_t: - axes.append(Axis(name="frame", type="time", unit=model.get_time_unit())) - # Display hints + axes.append(Axis(name="frame", type="time", unit=time_unit)) + return axes + + +def _build_display_hints( + has_x: bool, + has_y: bool, + has_z: bool, + has_t: bool, +) -> DisplayHint | None: + """ + Build display hints for GEFF metadata. + + Parameters + ---------- + has_x : bool + Whether the x-axis is present. + has_y : bool + Whether the y-axis is present. + has_z : bool + Whether the z-axis is present. + has_t : bool + Whether the time axis is present. + + Returns + ------- + DisplayHint | None + DisplayHint object if x and y axes are present, otherwise None. + """ if has_x and has_y: display_hints = DisplayHint(display_horizontal="cell_x", display_vertical="cell_y") if has_z: display_hints.display_depth = "cell_z" if has_t: display_hints.display_time = "frame" + else: + display_hints = None + return display_hints - # Create metadata with minimal required parameters - # Note: Using empty lists/None for required but unused parameters - metadata = GeffMetadata( - directed=True, - axes=axes, - display_hints=display_hints, + +def _build_props_metadata( + properties: dict[str, Property], +) -> tuple[dict[str, PropMetadata], dict[str, PropMetadata]]: + """ + Build property metadata for GEFF from a pycellin model. + + Parameters + ---------- + properties : dict[str, Property] + Dictionary of property identifiers to Property objects. + + Returns + ------- + tuple[dict[str, PropMetadata], dict[str, PropMetadata]] + A tuple containing two dictionaries: + - Node properties metadata + - Edge properties metadata + + Raises + ------ + ValueError + If an unknown property type is encountered. + """ + node_props_md: dict[str, PropMetadata] = {} + edge_props_md: dict[str, PropMetadata] = {} + + for prop_id, prop in properties.items(): + prop_md = PropMetadata( + identifier=prop_id, + dtype=prop.dtype, + unit=prop.unit, + name=prop.name, + description=prop.description, + ) + match prop.prop_type: + case "node": + node_props_md[prop_id] = prop_md + case "edge": + edge_props_md[prop_id] = prop_md + case "lineage": + pass # not supported in GEFF 0.5.0 + case _: + raise ValueError(f"Unknown property type: {prop.prop_type}") + + return node_props_md, edge_props_md + + +def _build_geff_metadata(model: Model) -> GeffMetadata: + """ + Build GEFF metadata from a pycellin model. + + Parameters + ---------- + model : Model + The pycellin model to extract metadata from. + + Returns + ------- + GeffMetadata + The GEFF metadata object. + """ + # Generic metadata + has_x = model.has_property("cell_x") + has_y = model.has_property("cell_y") + has_z = model.has_property("cell_z") + has_t = model.has_property("frame") + axes = _build_axes( + has_x=has_x, + has_y=has_y, + has_z=has_z, + has_t=has_t, + space_unit=model.get_space_unit(), + time_unit=model.get_time_unit(), + ) + display_hints = _build_display_hints( + has_x=has_x, + has_y=has_y, + has_z=has_z, + has_t=has_t, ) # Property metadata - # TODO cf create_or_update_metadata() in io_utils and PropMetadata in metadata_schema + props = model.get_cell_lineage_properties() + node_props_md, edge_props_md = _build_props_metadata(props) - return metadata + # Define identifiers of lineage and cell cycle + track_node_props = {"lineage": "lineage_ID"} + if model.has_cycle_data(): + track_node_props["tracklet"] = "cycle_ID" + + return GeffMetadata( + directed=True, + axes=axes, + display_hints=display_hints, + track_node_props=track_node_props, + node_props_metadata=node_props_md, + edge_props_metadata=edge_props_md, + ) def export_GEFF(model: Model, geff_out: str) -> None: @@ -185,15 +315,12 @@ def export_GEFF(model: Model, geff_out: str) -> None: for graph in lineages: print(len(graph.nodes), len(graph.edges)) - # TODO: this is debug - model_copy.remove_property("ROI_coords") - # model_copy.add_cell(lid=0, cid=9510) - # model_copy.add_cell(lid=1, cid=9510) - # model_copy.add_cell(lid=1, cid=9509) - # model_copy.add_cell(lid=2, cid=9498) + # TODO: remove when GEFF can handle variable length properties + if model_copy.has_property("ROI_coords"): + model_copy.remove_property("ROI_coords") # For GEFF compatibility, we need to put all the lineages in the same graph, - # but some nodes can have the same identifier... + # but some nodes can have the same identifier across different lineages. _solve_node_overlaps(lineages) geff_graph = nx.compose_all(lineages) print(len(geff_graph)) @@ -205,9 +332,6 @@ def export_GEFF(model: Model, geff_out: str) -> None: geff_graph, geff_out, metadata=metadata, - # axis_names=["cell_x", "cell_y", "cell_z", "frame"], - # axis_units=["um", "um", "um", "s"], - # zarr_format=2, ) del model_copy @@ -219,16 +343,33 @@ def export_GEFF(model: Model, geff_out: str) -> None: # xml_in = "sample_data/FakeTracks.xml" # ctc_in = "sample_data/FakeTracks_TMtoCTC.txt" # ctc_in = "sample_data/Ecoli_growth_on_agar_pad_TMtoCTC.txt" - # geff_out = "C:/Users/lxenard/Documents/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" - geff_out = "E:/Janelia_Cell_Trackathon/test_pycellin_geff/test.geff" + # geff_out = "E:/Janelia_Cell_Trackathon/test_pycellin_geff/test.geff" + geff_out = ( + "/media/lxenard/data/Janelia_Cell_Trackathon/test_pycellin_geff/pycellin_to_geff.geff" + ) - from pycellin.io.trackmate.loader import load_TrackMate_XML + # Remove existing folder + import os + import shutil + + if os.path.exists(geff_out): + shutil.rmtree(geff_out) + + # Load data from pycellin.io.cell_tracking_challenge.loader import load_CTC_file + from pycellin.io.trackmate.loader import load_TrackMate_XML model = load_TrackMate_XML(xml_in) # model = load_CTC_file(ctc_in) + # model.add_cycle_data() print(model) - # print(model.get_cell_lineage_properties().keys()) + print(model.get_cell_lineage_properties().keys()) print(model.data.cell_data.keys()) + # To test overlapping node IDs + prop_values = {"cell_x": 10, "cell_y": 15, "cell_z": 20} + model.add_cell(lid=0, cid=9510, frame=0, prop_values=prop_values) + model.add_cell(lid=1, cid=9510, frame=0, prop_values=prop_values) + model.add_cell(lid=1, cid=9509, frame=0, prop_values=prop_values) + model.add_cell(lid=2, cid=9498, frame=0, prop_values=prop_values) export_GEFF(model, geff_out) From 8c80a1e62b7b3ce3f6d6d0ddc3774d8e814e427e Mon Sep 17 00:00:00 2001 From: lxenard Date: Tue, 16 Sep 2025 18:53:16 +0200 Subject: [PATCH 21/27] Add missing info in __init__.py files --- pycellin/__init__.py | 4 ++++ pycellin/classes/__init__.py | 15 +++++++++++++++ pycellin/graph/__init__.py | 9 +++++++++ pycellin/io/__init__.py | 13 +++++++++++++ pycellin/io/cell_tracking_challenge/__init__.py | 2 ++ pycellin/io/geff/__init__.py | 4 ++++ pycellin/io/trackmate/__init__.py | 2 ++ pycellin/io/trackpy/__init__.py | 2 ++ 8 files changed, 51 insertions(+) diff --git a/pycellin/__init__.py b/pycellin/__init__.py index f07fe57..bca9831 100644 --- a/pycellin/__init__.py +++ b/pycellin/__init__.py @@ -18,6 +18,8 @@ from .io.trackmate.exporter import export_TrackMate_XML from .io.trackpy.loader import load_trackpy_dataframe from .io.trackpy.exporter import export_trackpy_dataframe +from .io.geff.loader import load_GEFF +from .io.geff.exporter import export_GEFF from .graph.properties.utils import ( get_pycellin_cell_lineage_properties, @@ -44,6 +46,8 @@ "export_TrackMate_XML", "load_trackpy_dataframe", "export_trackpy_dataframe", + "load_GEFF", + "export_GEFF", "get_pycellin_cell_lineage_properties", "get_pycellin_cycle_lineage_properties", ] diff --git a/pycellin/classes/__init__.py b/pycellin/classes/__init__.py index b437478..af6fa92 100644 --- a/pycellin/classes/__init__.py +++ b/pycellin/classes/__init__.py @@ -13,3 +13,18 @@ EdgeGlobalPropCalculator, LineageGlobalPropCalculator, ) + +__all__ = [ + "Data", + "CellLineage", + "CycleLineage", + "Property", + "PropsMetadata", + "Model", + "NodeLocalPropCalculator", + "EdgeLocalPropCalculator", + "LineageLocalPropCalculator", + "NodeGlobalPropCalculator", + "EdgeGlobalPropCalculator", + "LineageGlobalPropCalculator", +] diff --git a/pycellin/graph/__init__.py b/pycellin/graph/__init__.py index e69de29..d687359 100644 --- a/pycellin/graph/__init__.py +++ b/pycellin/graph/__init__.py @@ -0,0 +1,9 @@ +from .properties.utils import ( + get_pycellin_cell_lineage_properties, + get_pycellin_cycle_lineage_properties, +) + +__all__ = [ + "get_pycellin_cell_lineage_properties", + "get_pycellin_cycle_lineage_properties", +] diff --git a/pycellin/io/__init__.py b/pycellin/io/__init__.py index 4aa1a30..aaf4a9d 100644 --- a/pycellin/io/__init__.py +++ b/pycellin/io/__init__.py @@ -4,3 +4,16 @@ from .trackmate.exporter import export_TrackMate_XML from .trackpy.loader import load_trackpy_dataframe from .trackpy.exporter import export_trackpy_dataframe +from .geff.loader import load_GEFF +from .geff.exporter import export_GEFF + +__all__ = [ + "load_CTC_file", + "export_CTC_file", + "load_TrackMate_XML", + "export_TrackMate_XML", + "load_trackpy_dataframe", + "export_trackpy_dataframe", + "load_GEFF", + "export_GEFF", +] diff --git a/pycellin/io/cell_tracking_challenge/__init__.py b/pycellin/io/cell_tracking_challenge/__init__.py index 6f4cb7a..e3ddb10 100644 --- a/pycellin/io/cell_tracking_challenge/__init__.py +++ b/pycellin/io/cell_tracking_challenge/__init__.py @@ -1,2 +1,4 @@ from .loader import load_CTC_file from .exporter import export_CTC_file + +__all__ = ["load_CTC_file", "export_CTC_file"] diff --git a/pycellin/io/geff/__init__.py b/pycellin/io/geff/__init__.py index e69de29..89fb0de 100644 --- a/pycellin/io/geff/__init__.py +++ b/pycellin/io/geff/__init__.py @@ -0,0 +1,4 @@ +from .loader import load_GEFF +from .exporter import export_GEFF + +__all__ = ["load_GEFF", "export_GEFF"] diff --git a/pycellin/io/trackmate/__init__.py b/pycellin/io/trackmate/__init__.py index a5347f2..33d8ce8 100644 --- a/pycellin/io/trackmate/__init__.py +++ b/pycellin/io/trackmate/__init__.py @@ -1,2 +1,4 @@ from .loader import load_TrackMate_XML from .exporter import export_TrackMate_XML + +__all__ = ["load_TrackMate_XML", "export_TrackMate_XML"] diff --git a/pycellin/io/trackpy/__init__.py b/pycellin/io/trackpy/__init__.py index b7bb51b..58cd1bf 100644 --- a/pycellin/io/trackpy/__init__.py +++ b/pycellin/io/trackpy/__init__.py @@ -1,2 +1,4 @@ from .loader import load_trackpy_dataframe from .exporter import export_trackpy_dataframe + +__all__ = ["load_trackpy_dataframe", "export_trackpy_dataframe"] From d91f806ea7fa4b0a7f2feb8eec08e6a5b6ca3b58 Mon Sep 17 00:00:00 2001 From: lxenard Date: Tue, 16 Sep 2025 19:01:50 +0200 Subject: [PATCH 22/27] Ugly WIP on GEFF loader --- pycellin/io/geff/loader.py | 205 +++++++++++++++++++++++++++++-------- 1 file changed, 163 insertions(+), 42 deletions(-) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index 6cde4f3..11c0cfb 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -3,21 +3,23 @@ """ loader.py +This module is part of the pycellin package. +It provides functionality to load a GEFF file into a pycellin model. + References: - geff GitHub: https://github.com/live-image-tracking-tools/geff - geff Documentation: https://live-image-tracking-tools.github.io/geff/latest/ """ from datetime import datetime -from importlib.metadata import version +import importlib.metadata from pathlib import Path -from tkinter import N from typing import Any import geff -import networkx as nx +from geff.metadata_schema import GeffMetadata -from pycellin.classes import Data, Property, PropsMetadata, Model +from pycellin.classes import Data, Model, Property, PropsMetadata from pycellin.custom_types import PropertyType from pycellin.io.utils import ( _split_graph_into_lineages, @@ -218,9 +220,101 @@ def _read_props_metadata(geff_md: geff.metadata_schema.GeffMetadata) -> dict[str return props_dict -def load_geff_file( +def _extract_units_from_axes(geff_md: GeffMetadata) -> dict[str, Any]: + """ + Extract and validate space and time units from geff metadata axes. + + Parameters + ---------- + geff_md : geff.metadata_schema.GeffMetadata + The geff metadata object containing axes information. + + Returns + ------- + dict[str, Any] + Dictionary containing space_unit and time_unit keys. + + Raises + ------ + ValueError + If multiple space units or time units are found in axes. + """ + units_metadata = {} + + if geff_md.axes is not None: + # Check unicity of space and time units + space_units = { + axis.unit for axis in geff_md.axes if axis.type == "space" and axis.unit is not None + } + units_metadata["space_unit"] = space_units.pop() if space_units else None + time_units = { + axis.unit for axis in geff_md.axes if axis.type == "time" and axis.unit is not None + } + units_metadata["time_unit"] = time_units.pop() if time_units else None + if len(space_units) > 1: + raise ValueError( + f"Multiple space units found in axes: {space_units}. " + f"Pycellin assumes a single space unit." + ) + if len(time_units) > 1: + raise ValueError( + f"Multiple time units found in axes: {time_units}. " + f"Pycellin assumes a single time unit." + ) + else: + units_metadata["space_unit"] = None + units_metadata["time_unit"] = None + + return units_metadata + + +def _set_generic_metadata( + geff_file: Path | str, geff_md: geff.metadata_schema.GeffMetadata +) -> dict[str, Any]: + """ + Set generic metadata for the model based on the geff file and its metadata. + + Parameters + ---------- + geff_file : Path | str + Path to the geff file. + geff_md : geff.metadata_schema.GeffMetadata + The geff metadata object. + + Returns + ------- + dict[str, Any] + Dictionary containing generic metadata. + + Raises + ------ + importlib.metadata.PackageNotFoundError + If the pycellin package is not found when trying to get its version. + """ + metadata = {} # type: dict[str, Any] + metadata["name"] = Path(geff_file).stem + metadata["file_location"] = geff_file + metadata["provenance"] = "geff" + metadata["date"] = str(datetime.now()) + try: + version = importlib.metadata.version("pycellin") + except importlib.metadata.PackageNotFoundError: + version = "unknown" + metadata["pycellin_version"] = version + metadata["geff_version"] = geff_md.geff_version + if geff_md.extra is not None: + metadata["geff_extra"] = geff_md.extra + + return metadata + + +def load_GEFF( geff_file: Path | str, cell_id_key: str | None = None, + cell_x_key: str | None = None, + cell_y_key: str | None = None, + cell_z_key: str | None = None, + time_key: str | None = None, ) -> Model: """ Load a geff file and return a pycellin model containing the data. @@ -232,13 +326,20 @@ def load_geff_file( cell_id_key : str | None, optional The key used to identify cells in the geff file. If None, the default key 'cell_ID' will be created and populated based on the node IDs. + cell_x_key : str | None, optional + The key used to identify the x-coordinate of cells in the geff file. + cell_y_key : str | None, optional + The key used to identify the y-coordinate of cells in the geff file. + cell_z_key : str | None, optional + The key used to identify the z-coordinate of cells in the geff file. + time_key : str | None, optional + The key used to identify the time point of cells in the geff file. Returns ------- Model A pycellin model containing the data from the geff file. """ - pass # Read the geff file geff_graph, geff_md = geff.read_nx(geff_file, validate=True) @@ -246,44 +347,57 @@ def load_geff_file( raise ValueError( "The geff graph is undirected: pycellin does not support undirected graphs." ) - for node in geff_graph.nodes: - print(f"Node {node}: {geff_graph.nodes[node]}") - break - - print(type(geff_graph)) - print(geff_md.directed) - if geff_md.track_node_props is not None and "lineage" in geff_md.track_node_props: - lin_id_key = geff_md.track_node_props["lineage"] + # for node in geff_graph.nodes: + # print(f"Node {node}: {geff_graph.nodes[node]}") + # break + print(geff_md) + + # Extract and dispatch metadata + metadata = _set_generic_metadata(geff_file, geff_md) + units_metadata = _extract_units_from_axes(geff_md) + metadata.update(units_metadata) + # print("Metadata:") + # for k, v in metadata.items(): + # print(f" {k}: {v}") + props_md = _read_props_metadata(geff_md) + if geff_md.track_node_props is not None: + lin_id_key = geff_md.track_node_props.get("lineage") else: lin_id_key = None print("lin_id_key:", lin_id_key) - # Determine axes - # If no axes, need to have them as arguments...? Set a default to x, y, z, t...? - print("Axes:", geff_md.axes) - # display_hints=DisplayHint( - # display_horizontal="POSITION_X", - # display_vertical="POSITION_Y", - # display_depth="POSITION_Z", - # display_time="POSITION_T", - # ), - - # Is int ID ensured in geff? YES - # int_graph = nx.relabel_nodes(geff_graph, {node: int(node) for node in geff_graph.nodes()}) + # Determine properties for x, y, z, t + # For now we assume that both display hints and axes are filled... + if geff_md.display_hints is not None: + prop_mapping = { + "cell_x": getattr(geff_md.display_hints, "display_horizontal", None), + "cell_y": getattr(geff_md.display_hints, "display_vertical", None), + "cell_z": getattr(geff_md.display_hints, "display_depth", None), + "time": getattr(geff_md.display_hints, "display_time", None), + } + print(prop_mapping) + else: + # We need to rely on axes only, or on inputs from the user. + pass - # Extract and dispatch metadata - # Generic metadata - metadata = {} # type: dict[str, Any] - # All the stuff in field extra is stored in the model metadata + # Identify the axes corresponding to the values of the prop_mapping. + # axes_mapping = {axis.name: axis for axis in geff_md.axes if axis.name is not None} + # print(axes_mapping) - # Property metadata - props_dict = _read_props_metadata(geff_md) + # Do we have axis related properties in props_md? + for axis in geff_md.axes: + if axis.name not in props_md: + print("Axis", axis.name, "not in props_md") + pass # Split the graph into lineages lineages = _split_graph_into_lineages(geff_graph, lineage_ID_key=lin_id_key) print(f"Number of lineages: {len(lineages)}") # Rename properties to match pycellin conventions + # In the properties metadata + pass + # In the actual data _update_lineages_IDs_key(lineages, lin_id_key) for lin in lineages: if cell_id_key is None: @@ -294,12 +408,14 @@ def load_geff_file( # TODO: cells positions and edges positions (keys from axes) # Time? - # Check for fusions - data = Data({lin.graph["lineage_ID"]: lin for lin in lineages}) - model = Model(data=data, props_metadata=PropsMetadata(props=props_dict)) + model = Model( + model_metadata=metadata, + props_metadata=PropsMetadata(props=props_md), + data=Data({lin.graph["lineage_ID"]: lin for lin in lineages}), + ) + check_fusions(model) # pycellin DOES NOT support fusion events # print(model.data) # print(model.data.cell_data) - check_fusions(model) # pycellin DOES NOT support fusion events return model @@ -308,20 +424,25 @@ def load_geff_file( geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/reader_test_graph.geff" # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/mouse-20250719.zarr/tracks" # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" - geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" - geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" + # geff_file = ( + # "/media/lxenard/data/Janelia_Cell_Trackathon/test_pycellin_geff/pycellin_to_geff.geff" + # ) + # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" + # Yohsuke's file for geffception + # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff" + geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff/linage_tree.geff" print(geff_file) - model = load_geff_file(geff_file) + model = load_GEFF(geff_file) # print(model) # print("props_dict", model.props_metadata.props) - # lineages = model.get_cell_lineages() + lineages = model.get_cell_lineages() # print(f"Number of lineages: {len(lineages)}") # for lin in lineages: # print(lin) - # lin0 = lineages[0] + lin0 = lineages[0] # print(lin0.nodes(data=True)) - # lin0.plot() + lin0.plot() # cell_id_key # lineage_id_key From dcf29a5206da30de0fbb7a2c7b3a8072228c1d07 Mon Sep 17 00:00:00 2001 From: lxenard Date: Wed, 17 Sep 2025 18:11:53 +0200 Subject: [PATCH 23/27] Less wonky GEFF loader --- pycellin/io/geff/loader.py | 575 ++++++++++++++++++++++++++++++++----- 1 file changed, 496 insertions(+), 79 deletions(-) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index 11c0cfb..b731322 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -11,15 +11,23 @@ - geff Documentation: https://live-image-tracking-tools.github.io/geff/latest/ """ -from datetime import datetime import importlib.metadata +import warnings +from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, Literal import geff +import networkx as nx from geff.metadata_schema import GeffMetadata -from pycellin.classes import Data, Model, Property, PropsMetadata +from pycellin.graph.properties.core import ( + create_cell_coord_property, + create_cell_id_property, + create_frame_property, + create_lineage_id_property, +) +from pycellin.classes import CellLineage, Data, Model, Property, PropsMetadata from pycellin.custom_types import PropertyType from pycellin.io.utils import ( _split_graph_into_lineages, @@ -47,7 +55,7 @@ def _recursive_dict_search(data: dict[str, Any], target_key: str) -> dict[str, A """ if not isinstance(data, dict): return None - if target_key in data: # does the current level contain the target key? + if target_key in data: # search in the current level return data[target_key] for value in data.values(): # recursive search in nested dictionaries if isinstance(value, dict): @@ -57,6 +65,290 @@ def _recursive_dict_search(data: dict[str, Any], target_key: str) -> dict[str, A return None +def _has_node_prop_key(graph: nx.Graph, key: str) -> bool: + """ + Check if all nodes in the graph have a specific property key. + + Parameters + ---------- + graph : nx.Graph + The graph to check. + key : str + The property key to look for. + + Returns + ------- + bool + True if all nodes have the property key, False otherwise. + """ + return all(key in graph.nodes[node] for node in graph.nodes) + + +def _set_lineage_id(lineages: list[CellLineage]) -> None: + """ + Set lineage IDs for the provided lineages. + + Parameters + ---------- + lineages : list[CellLineage] + List of CellLineage objects to set lineage IDs for. + + Raises + ------ + UserWarning + If no lineage identifier is found, a warning is issued and lineage IDs + are autogenerated based on weakly connected components. + """ + for lin_id, lin in enumerate(lineages): + lin.graph["lineage_ID"] = lin_id + for node in lin.nodes: + lin.nodes[node]["lineage_ID"] = lin_id + warnings.warn( + "No lineage identifier found. Lineage IDs have been autogenerated.", + stacklevel=3, + ) + + +def _identify_lin_id_key( + lineage_id_key: str | None, + geff_track_node_props: dict[Literal["lineage", "tracklet"], str] | None, + geff_graph: nx.Graph, +) -> str | None: + """ + Identify the lineage ID key from user input or geff metadata. + + If the lineage_id_key is provided by the user, the function will check + that it exists in the graph. If not provided, the function will try to infer it + from the geff track_node_props. If that fails, the function will create a new + 'lineage_ID' property. + + Parameters + ---------- + lineage_id_key : str | None + The key provided by the user to identify lineages. + geff_track_node_props : dict[Literal["lineage", "tracklet"], str] | None + The track_node_props from geff metadata. + geff_graph : nx.Graph + The geff graph. + + Returns + ------- + str | None + The identified lineage ID key, or None if not found or inferred. + """ + lin_id_key: str | None = None + if lineage_id_key is not None: + if _has_node_prop_key(geff_graph, lineage_id_key): + lin_id_key = lineage_id_key + else: + warnings.warn( + f"The provided lineage_id_key '{lineage_id_key}' is not found in the graph.", + stacklevel=3, + ) + lineage_id_key = None + + if lineage_id_key is None: + if geff_track_node_props is not None: + lin_id_key = geff_track_node_props.get("lineage") + else: + if _has_node_prop_key(geff_graph, "lineage_ID"): + lin_id_key = "lineage_ID" + else: + lin_id_key = None + + return lin_id_key + + +def _identify_time_key( + time_key: str | None, + geff_display_hints: geff.metadata_schema.DisplayHint | None, + geff_graph: nx.Graph, +) -> str: + """ + Identify the time key from user input or geff metadata. + + If the time_key is provided by the user, the function will check + that it exists in the graph. If not provided, it will try to infer it + from the geff display hints or common conventions. + + Parameters + ---------- + time_key : str | None + The key provided by the user to identify time points. + geff_display_hints : geff.metadata_schema.DisplayHint | None + The display_hints from geff metadata. + geff_graph : nx.Graph + The geff graph. + + Returns + ------- + str + The identified time key. + + Raises + ------ + ValueError + If the provided time_key is not found in the graph. + ValueError + If no time key is found or inferred. + NotImplementedError + If the identified time key cannot be matched to 'frame'. + Only 'frame' is currently supported. + """ + # If I end up checking for a lot of keys, there are probably better ways to do it. + # Use pattern matching? + if time_key is not None: + if not _has_node_prop_key(geff_graph, time_key): + raise ValueError(f"The provided time_key '{time_key}' is not found in the graph.") + else: + if geff_display_hints is not None: + time_key = getattr(geff_display_hints, "display_time", None) + elif _has_node_prop_key(geff_graph, "frame"): + time_key = "frame" + # elif _has_node_prop_key(geff_graph, "t"): + # time_key = "t" + # elif _has_node_prop_key(geff_graph, "time"): + # time_key = "time" + elif _has_node_prop_key(geff_graph, "FRAME"): + time_key = "FRAME" + elif _has_node_prop_key(geff_graph, "Frame"): + time_key = "Frame" + + if time_key is None: + raise ValueError( + "No time key found. Please provide a valid time_key argument or ensure " + "that the geff file contains a time display hint." + ) + + # TODO: to update when pycellin supports any time key + if time_key != "frame" and time_key.lower() == "frame": + time_key = "frame" + if time_key != "frame": + raise NotImplementedError( + f"Time key '{time_key}' cannot be matched to 'frame'. " + "Only 'frame' is currently supported." + ) + + return time_key + + +def _extract_space_key_from_display_hints( + hint_field: str, + geff_display_hints: geff.metadata_schema.DisplayHint | None, + geff_graph: nx.Graph, +) -> str | None: + """ + Extract a space key from geff display hints. + + Parameters + ---------- + hint_field : str + The field in the display hints to extract ('display_horizontal', 'display_vertical', + or 'display_depth'). + geff_display_hints : geff.metadata_schema.DisplayHint | None + The display_hints from geff metadata. + geff_graph : nx.Graph + The geff graph. + + Returns + ------- + str | None + The extracted space key, or None if not found. + + Raises + ------ + UserWarning + If the inferred space key is not found in the graph. In this case, + the property is ignored. + UserWarning + If no display hint is found and no key is provided. In this case, + the property is ignored. + """ + mapping = { + "display_horizontal": "x", + "display_vertical": "y", + "display_depth": "z", + } + + if geff_display_hints is not None: + space_key = getattr(geff_display_hints, hint_field, None) + if space_key is not None and not _has_node_prop_key(geff_graph, space_key): + warnings.warn( + f"The inferred space key '{space_key}' is not found in the graph. " + "Ignoring this property.", + stacklevel=4, + ) + else: + space_key = None + warnings.warn( + f"No cell_{mapping[hint_field]}_key provided and no display hint found. " + "Ignoring this property.", + stacklevel=4, + ) + + return space_key + + +def _identify_space_keys( + cell_x_key: str | None, + cell_y_key: str | None, + cell_z_key: str | None, + geff_display_hints: geff.metadata_schema.DisplayHint | None, + geff_graph: nx.Graph, +) -> tuple[str | None, str | None, str | None]: + """ + Identify the space keys (x, y, z) from user input or geff metadata. + + If the space keys are provided by the user, the function will check + that they exist in the graph. If not provided, it will try to infer them + from the geff display hints. + + Parameters + ---------- + cell_x_key : str | None + The key provided by the user to identify the x-coordinate. + cell_y_key : str | None + The key provided by the user to identify the y-coordinate. + cell_z_key : str | None + The key provided by the user to identify the z-coordinate. + geff_display_hints : geff.metadata_schema.DisplayHint | None + The display_hints from geff metadata. + geff_graph : nx.Graph + The geff graph. + + Returns + ------- + tuple[str | None, str | None, str | None] + The identified space keys (cell_x_key, cell_y_key, cell_z_key). + + Raises + ------ + UserWarning + If any of the provided space keys are not found in the graph. + """ + space_keys = [cell_x_key, cell_y_key, cell_z_key] + + # Validate provided keys and warn if they don't exist in the graph + for dim, key in zip(["x", "y", "z"], space_keys): + if key is not None: + if not _has_node_prop_key(geff_graph, key): + warnings.warn( + f"The provided cell_{dim}_key '{key}' is not found in the graph. " + "Ignoring this property.", + stacklevel=3, + ) + + # Update keys if they are None by extracting from display hints + hint_fields = ["display_horizontal", "display_vertical", "display_depth"] + for i, (hint_field, key) in enumerate(zip(hint_fields, space_keys)): + if key is None: + space_keys[i] = _extract_space_key_from_display_hints( + hint_field, geff_display_hints, geff_graph + ) + + return space_keys[0], space_keys[1], space_keys[2] + + def _extract_props_metadata( md: dict[str, geff.metadata_schema.PropMetadata], props_dict: dict[str, Property], @@ -172,13 +464,13 @@ def _extract_lin_props_metadata( new_key = f"lin_{key}" props_dict[new_key] = Property( identifier=new_key, - name=prop.name or key, - description=prop.description or prop.get("name") or key, + name=prop.get("name") or key, + description=prop.get("description") or prop.get("name") or key, provenance="geff", prop_type="lineage", lin_type="CellLineage", - dtype=prop.dtype, - unit=prop.unit or None, + dtype=prop.get("dtype"), + unit=prop.get("unit") or None, ) else: raise KeyError( @@ -187,7 +479,7 @@ def _extract_lin_props_metadata( ) -def _read_props_metadata(geff_md: geff.metadata_schema.GeffMetadata) -> dict[str, Property]: +def _build_props_metadata(geff_md: geff.metadata_schema.GeffMetadata) -> dict[str, Property]: """ Read and extract properties metadata from geff metadata. @@ -208,7 +500,7 @@ def _read_props_metadata(geff_md: geff.metadata_schema.GeffMetadata) -> dict[str _extract_props_metadata(geff_md.edge_props_metadata, props_dict, "edge") # TODO: for now lineage properties are not associated to a specific tag but stored - # somewhere in the "extra" field. We need to check recurrently if there is a dict + # somewhere in the "extra" field. We need to check recursively if there is a dict # key called "lineage_props_metadata" in the "extra" field. if geff_md.extra is not None: # Recursive search for the "lineage_props_metadata" key through the "extra" @@ -242,25 +534,28 @@ def _extract_units_from_axes(geff_md: GeffMetadata) -> dict[str, Any]: units_metadata = {} if geff_md.axes is not None: - # Check unicity of space and time units + # Check unicity of space time unit space_units = { axis.unit for axis in geff_md.axes if axis.type == "space" and axis.unit is not None } - units_metadata["space_unit"] = space_units.pop() if space_units else None - time_units = { - axis.unit for axis in geff_md.axes if axis.type == "time" and axis.unit is not None - } - units_metadata["time_unit"] = time_units.pop() if time_units else None if len(space_units) > 1: raise ValueError( f"Multiple space units found in axes: {space_units}. " f"Pycellin assumes a single space unit." ) + units_metadata["space_unit"] = space_units.pop() if space_units else None + + # Check unicity of time unit + time_units = { + axis.unit for axis in geff_md.axes if axis.type == "time" and axis.unit is not None + } if len(time_units) > 1: raise ValueError( f"Multiple time units found in axes: {time_units}. " f"Pycellin assumes a single time unit." ) + units_metadata["time_unit"] = time_units.pop() if time_units else None + else: units_metadata["space_unit"] = None units_metadata["time_unit"] = None @@ -268,11 +563,11 @@ def _extract_units_from_axes(geff_md: GeffMetadata) -> dict[str, Any]: return units_metadata -def _set_generic_metadata( +def _extract_generic_metadata( geff_file: Path | str, geff_md: geff.metadata_schema.GeffMetadata ) -> dict[str, Any]: """ - Set generic metadata for the model based on the geff file and its metadata. + Extract generic metadata for the model based on the geff file and its metadata. Parameters ---------- @@ -291,7 +586,7 @@ def _set_generic_metadata( importlib.metadata.PackageNotFoundError If the pycellin package is not found when trying to get its version. """ - metadata = {} # type: dict[str, Any] + metadata: dict[str, Any] = {} metadata["name"] = Path(geff_file).stem metadata["file_location"] = geff_file metadata["provenance"] = "geff" @@ -308,8 +603,133 @@ def _set_generic_metadata( return metadata +def _build_generic_metadata(geff_file: Path | str, geff_md: GeffMetadata) -> dict[str, Any]: + """ + Build and return a dictionary containing generic pycellin metadata. + + Parameters + ---------- + geff_file : Path | str + Path to the geff file. + geff_md : geff.metadata_schema.GeffMetadata + The geff metadata object to read from. + + Returns + ------- + dict[str, Any] + Dictionary containing generic pycellin metadata. + """ + metadata = _extract_generic_metadata(geff_file, geff_md) + units_metadata = _extract_units_from_axes(geff_md) + metadata.update(units_metadata) + + return metadata + + +def _normalize_properties_data( + lineages: list[CellLineage], + lin_id_key: str, + cell_x_key: str | None, + cell_y_key: str | None, + cell_z_key: str | None, + time_key: str, + cell_id_key: str | None, +) -> None: + """ + Normalize properties data in lineages to match pycellin conventions. + + This function updates the property keys in lineage node data to use + standardized pycellin naming conventions (e.g., 'cell_x', 'cell_y', 'frame'). + + Parameters + ---------- + lineages : list[CellLineage] + List of CellLineage objects to normalize. + lin_id_key : str + The current lineage ID key name. + cell_x_key : str | None + The current x-coordinate key name, if any. + cell_y_key : str | None + The current y-coordinate key name, if any. + cell_z_key : str | None + The current z-coordinate key name, if any. + time_key : str + The current time key name. + cell_id_key : str | None + The current cell ID key name, if any. + """ + if lin_id_key != "lineage_ID": + _update_lineages_IDs_key(lineages, lin_id_key) + for lin in lineages: + if cell_x_key is not None and cell_x_key != "cell_x": + _update_node_prop_key(lin, old_key=cell_x_key, new_key="cell_x") + if cell_y_key is not None and cell_y_key != "cell_y": + _update_node_prop_key(lin, old_key=cell_y_key, new_key="cell_y") + if cell_z_key is not None and cell_z_key != "cell_z": + _update_node_prop_key(lin, old_key=cell_z_key, new_key="cell_z") + if time_key != "frame": + _update_node_prop_key(lin, old_key=time_key, new_key="frame") + if cell_id_key is None: + for node in lin.nodes: + lin.nodes[node]["cell_ID"] = node + elif cell_id_key != "cell_ID": + _update_node_prop_key(lin, old_key=cell_id_key, new_key="cell_ID") + + +def _normalize_properties_metadata( + props_md: dict[str, Property], + cell_x_key: str | None, + cell_y_key: str | None, + cell_z_key: str | None, + space_unit: str | None, +) -> None: + """ + Normalize properties metadata to match pycellin conventions. + + This function ensures that standard pycellin properties exist in the metadata + and renames them to follow pycellin conventions. + + Parameters + ---------- + props_md : dict[str, Property] + The properties metadata dictionary to normalize. + cell_x_key : str | None + The current x-coordinate key name, if any. + cell_y_key : str | None + The current y-coordinate key name, if any. + cell_z_key : str | None + The current z-coordinate key name, if any. + space_unit : str | None + The space unit to use for coordinate properties. + """ + # Ensure standard pycellin properties exist + if "cell_ID" not in props_md: + props_md["cell_ID"] = create_cell_id_property(provenance="geff") + if "lineage_ID" not in props_md: + props_md["lineage_ID"] = create_lineage_id_property(provenance="geff") + if "frame" not in props_md: + props_md["frame"] = create_frame_property(provenance="geff") + + # Create or normalize coordinate property keys + for axis, geff_key in [ + ("x", cell_x_key), + ("y", cell_y_key), + ("z", cell_z_key), + ]: + pycellin_key = f"cell_{axis}" + if geff_key is not None: + if geff_key in props_md and geff_key != pycellin_key: + props_md[pycellin_key] = props_md.pop(geff_key) + props_md[pycellin_key].identifier = pycellin_key + else: + props_md[pycellin_key] = create_cell_coord_property( + unit=space_unit, axis=axis, provenance="geff" + ) + + def load_GEFF( geff_file: Path | str, + lineage_id_key: str | None = None, cell_id_key: str | None = None, cell_x_key: str | None = None, cell_y_key: str | None = None, @@ -323,6 +743,10 @@ def load_GEFF( ---------- geff_file : Path | str Path to the geff file to load. + lineage_id_key: str | None, optional + The key used to identify lineages in the geff file. If None, the function + will try to infer it from the geff metadata or autogenerate lineage IDs + based on weakly connected components. cell_id_key : str | None, optional The key used to identify cells in the geff file. If None, the default key 'cell_ID' will be created and populated based on the node IDs. @@ -347,75 +771,45 @@ def load_GEFF( raise ValueError( "The geff graph is undirected: pycellin does not support undirected graphs." ) - # for node in geff_graph.nodes: - # print(f"Node {node}: {geff_graph.nodes[node]}") - # break - print(geff_md) # Extract and dispatch metadata - metadata = _set_generic_metadata(geff_file, geff_md) - units_metadata = _extract_units_from_axes(geff_md) - metadata.update(units_metadata) - # print("Metadata:") - # for k, v in metadata.items(): - # print(f" {k}: {v}") - props_md = _read_props_metadata(geff_md) - if geff_md.track_node_props is not None: - lin_id_key = geff_md.track_node_props.get("lineage") - else: - lin_id_key = None - print("lin_id_key:", lin_id_key) - - # Determine properties for x, y, z, t - # For now we assume that both display hints and axes are filled... - if geff_md.display_hints is not None: - prop_mapping = { - "cell_x": getattr(geff_md.display_hints, "display_horizontal", None), - "cell_y": getattr(geff_md.display_hints, "display_vertical", None), - "cell_z": getattr(geff_md.display_hints, "display_depth", None), - "time": getattr(geff_md.display_hints, "display_time", None), - } - print(prop_mapping) - else: - # We need to rely on axes only, or on inputs from the user. - pass - - # Identify the axes corresponding to the values of the prop_mapping. - # axes_mapping = {axis.name: axis for axis in geff_md.axes if axis.name is not None} - # print(axes_mapping) - - # Do we have axis related properties in props_md? - for axis in geff_md.axes: - if axis.name not in props_md: - print("Axis", axis.name, "not in props_md") - pass + generic_md = _build_generic_metadata(geff_file, geff_md) + props_md = _build_props_metadata(geff_md) + + # Identify specific props keys + lin_id_key = _identify_lin_id_key(lineage_id_key, geff_md.track_node_props, geff_graph) + # print("lin_id_key:", lin_id_key) + time_key = _identify_time_key(time_key, geff_md.display_hints, geff_graph) + # print("time_key:", time_key) + cell_x_key, cell_y_key, cell_z_key = _identify_space_keys( + cell_x_key, cell_y_key, cell_z_key, geff_md.display_hints, geff_graph + ) + # print("cell_x_key:", cell_x_key) + # print("cell_y_key:", cell_y_key) + # print("cell_z_key:", cell_z_key) # Split the graph into lineages lineages = _split_graph_into_lineages(geff_graph, lineage_ID_key=lin_id_key) - print(f"Number of lineages: {len(lineages)}") + # print(f"Number of lineages: {len(lineages)}") + if lin_id_key is None: + _set_lineage_id(lineages) + lin_id_key = "lineage_ID" # Rename properties to match pycellin conventions - # In the properties metadata - pass - # In the actual data - _update_lineages_IDs_key(lineages, lin_id_key) - for lin in lineages: - if cell_id_key is None: - for node in lin.nodes: - lin.nodes[node]["cell_ID"] = node - else: - _update_node_prop_key(lin, old_key=cell_id_key, new_key="cell_ID") - # TODO: cells positions and edges positions (keys from axes) - # Time? + _normalize_properties_data( + lineages, lin_id_key, cell_x_key, cell_y_key, cell_z_key, time_key, cell_id_key + ) + _normalize_properties_metadata( + props_md, cell_x_key, cell_y_key, cell_z_key, generic_md["space_unit"] + ) + # Create the model model = Model( - model_metadata=metadata, + model_metadata=generic_md, props_metadata=PropsMetadata(props=props_md), data=Data({lin.graph["lineage_ID"]: lin for lin in lineages}), ) check_fusions(model) # pycellin DOES NOT support fusion events - # print(model.data) - # print(model.data.cell_data) return model @@ -429,20 +823,43 @@ def load_GEFF( # ) # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" # Yohsuke's file for geffception - # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff" - geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff/linage_tree.geff" + geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff" + # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff/linage_tree.geff" + + import plotly.io as pio + + # Plotly: set the default renderer to browser so I can visualize plots + pio.renderers.default = "browser" print(geff_file) - model = load_GEFF(geff_file) + model = load_GEFF(geff_file) # , cell_x_key="x", cell_y_key="y") # print(model) # print("props_dict", model.props_metadata.props) + # for k in model.props_metadata.props.keys(): + # print(f"{k}") lineages = model.get_cell_lineages() # print(f"Number of lineages: {len(lineages)}") # for lin in lineages: # print(lin) lin0 = lineages[0] + # lin7 = model.get_cell_lineage_from_ID(7) + # lin7.plot( + # node_hover_props=[ + # "cell_ID", + # "lineage_ID", + # "frame", + # "cell_x", + # "cell_y", + # "track_id", + # "seg_id", + # "tree_id", + # ] + # ) # print(lin0.nodes(data=True)) - lin0.plot() + for node in lin0.nodes(data=True): + print(node) + break + # lin0.plot() # cell_id_key # lineage_id_key From 505827746510fe9c953a8047206f5de37911149a Mon Sep 17 00:00:00 2001 From: lxenard Date: Wed, 17 Sep 2025 18:12:27 +0200 Subject: [PATCH 24/27] Fix incorrect typing --- pycellin/graph/properties/core.py | 4 +++- pycellin/io/utils.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pycellin/graph/properties/core.py b/pycellin/graph/properties/core.py index 8464dcf..1afddb1 100644 --- a/pycellin/graph/properties/core.py +++ b/pycellin/graph/properties/core.py @@ -43,7 +43,9 @@ def create_lineage_id_property(provenance: str = "pycellin") -> Property: ) -def create_cell_coord_property(unit: str, axis: str, provenance: str = "pycellin") -> Property: +def create_cell_coord_property( + unit: str | None, axis: str, provenance: str = "pycellin" +) -> Property: return Property( identifier=f"cell_{axis}", name=f"cell {axis}", diff --git a/pycellin/io/utils.py b/pycellin/io/utils.py index f15ce6b..d0a6611 100644 --- a/pycellin/io/utils.py +++ b/pycellin/io/utils.py @@ -91,7 +91,7 @@ def _add_lineage_props( def _split_graph_into_lineages( - graph: nx.DiGraph, + graph: nx.Graph | nx.DiGraph, lin_props: list[dict[str, Any]] | None = None, lineage_ID_key: str | None = "lineage_ID", ) -> list[CellLineage]: From 0e884d852d91f86050c16973848a1e0e1ac07d59 Mon Sep 17 00:00:00 2001 From: lxenard Date: Wed, 17 Sep 2025 18:33:11 +0200 Subject: [PATCH 25/27] Fix incorrect docstrings and imports order --- pycellin/io/geff/loader.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index b731322..2f27d8d 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -21,14 +21,14 @@ import networkx as nx from geff.metadata_schema import GeffMetadata +from pycellin.classes import CellLineage, Data, Model, Property, PropsMetadata +from pycellin.custom_types import PropertyType from pycellin.graph.properties.core import ( create_cell_coord_property, create_cell_id_property, create_frame_property, create_lineage_id_property, ) -from pycellin.classes import CellLineage, Data, Model, Property, PropsMetadata -from pycellin.custom_types import PropertyType from pycellin.io.utils import ( _split_graph_into_lineages, _update_lineages_IDs_key, @@ -93,8 +93,8 @@ def _set_lineage_id(lineages: list[CellLineage]) -> None: lineages : list[CellLineage] List of CellLineage objects to set lineage IDs for. - Raises - ------ + Warns + ----- UserWarning If no lineage identifier is found, a warning is issued and lineage IDs are autogenerated based on weakly connected components. @@ -135,6 +135,11 @@ def _identify_lin_id_key( ------- str | None The identified lineage ID key, or None if not found or inferred. + + Warns + ----- + UserWarning + If the provided lineage_id_key is not found in the graph. """ lin_id_key: str | None = None if lineage_id_key is not None: @@ -255,8 +260,8 @@ def _extract_space_key_from_display_hints( str | None The extracted space key, or None if not found. - Raises - ------ + Warns + ----- UserWarning If the inferred space key is not found in the graph. In this case, the property is ignored. @@ -321,8 +326,8 @@ def _identify_space_keys( tuple[str | None, str | None, str | None] The identified space keys (cell_x_key, cell_y_key, cell_z_key). - Raises - ------ + Warns + ----- UserWarning If any of the provided space keys are not found in the graph. """ From c05e31e702662ea4f10685c2ead348aaf5bb1762 Mon Sep 17 00:00:00 2001 From: lxenard Date: Wed, 17 Sep 2025 18:33:53 +0200 Subject: [PATCH 26/27] Small improvments on GEFF exporter --- pycellin/io/geff/exporter.py | 81 ++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/pycellin/io/geff/exporter.py b/pycellin/io/geff/exporter.py index 121d003..00136fd 100644 --- a/pycellin/io/geff/exporter.py +++ b/pycellin/io/geff/exporter.py @@ -65,8 +65,17 @@ def _get_next_available_id(lineages: list[CellLineage]) -> int: int The next available node ID. """ - next_ids = [lin._get_next_available_node_ID() for lin in lineages] - return max(next_ids) + if not lineages: + return 0 + + max_node_id = -1 + for lineage in lineages: + if lineage.nodes: + lineage_max = max(lineage.nodes) + if lineage_max > max_node_id: + max_node_id = lineage_max + + return max_node_id + 1 def _relabel_nodes( @@ -80,6 +89,8 @@ def _relabel_nodes( ---------- lineages : list[CellLineage] List of lineage graphs to relabel in place. + overlaps : dict[int, list[int]] + Dictionary mapping overlapping node IDs to the list of lineage indices they belong to. """ next_available_id = _get_next_available_id(lineages) for nid, lids in overlaps.items(): @@ -308,33 +319,49 @@ def export_GEFF(model: Model, geff_out: str) -> None: The pycellin model to export. geff_out : str Path to the output GEFF file. + + Raises + ------ + ValueError + If the model contains no lineage data. + OSError + If there are file I/O issues with the output path. + RuntimeError + If the GEFF export process fails. """ - # We don't want to modify the original model. - model_copy = copy.deepcopy(model) - lineages = list(model_copy.data.cell_data.values()) - for graph in lineages: - print(len(graph.nodes), len(graph.edges)) - - # TODO: remove when GEFF can handle variable length properties - if model_copy.has_property("ROI_coords"): - model_copy.remove_property("ROI_coords") - - # For GEFF compatibility, we need to put all the lineages in the same graph, - # but some nodes can have the same identifier across different lineages. - _solve_node_overlaps(lineages) - geff_graph = nx.compose_all(lineages) - print(len(geff_graph)) - - metadata = _build_geff_metadata(model_copy) - print(metadata) - - write_nx( - geff_graph, - geff_out, - metadata=metadata, - ) + # Validate that model has data to export + if not model.data.cell_data: + raise ValueError("Model contains no lineage data to export") + + try: + # We don't want to modify the original model. + model_copy = copy.deepcopy(model) + lineages = list(model_copy.data.cell_data.values()) + + for graph in lineages: + print(len(graph.nodes), len(graph.edges)) + + # TODO: remove when GEFF can handle variable length properties + if model_copy.has_property("ROI_coords"): + model_copy.remove_property("ROI_coords") + + # For GEFF compatibility, we need to put all the lineages in the same graph, + # but some nodes can have the same identifier across different lineages. + _solve_node_overlaps(lineages) + geff_graph = nx.compose_all(lineages) + print(len(geff_graph)) + + metadata = _build_geff_metadata(model_copy) + print(metadata) + + write_nx( + geff_graph, + geff_out, + metadata=metadata, + ) - del model_copy + except Exception as e: + raise RuntimeError(f"Failed to export GEFF file to '{geff_out}': {e}") from e if __name__ == "__main__": From d4b2738400ad37df11290d98d1eeee89c2f071bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20X=C3=A9nard?= Date: Thu, 18 Sep 2025 11:52:58 +0200 Subject: [PATCH 27/27] Add parameter to control validation of GEFF schema --- pycellin/io/geff/loader.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pycellin/io/geff/loader.py b/pycellin/io/geff/loader.py index 2f27d8d..8ad5545 100644 --- a/pycellin/io/geff/loader.py +++ b/pycellin/io/geff/loader.py @@ -225,13 +225,13 @@ def _identify_time_key( "that the geff file contains a time display hint." ) - # TODO: to update when pycellin supports any time key + # TODO: to update when pycellin can support any time key if time_key != "frame" and time_key.lower() == "frame": time_key = "frame" if time_key != "frame": raise NotImplementedError( f"Time key '{time_key}' cannot be matched to 'frame'. " - "Only 'frame' is currently supported." + "Pycellin currently only supports frame-like time keys." ) return time_key @@ -740,6 +740,7 @@ def load_GEFF( cell_y_key: str | None = None, cell_z_key: str | None = None, time_key: str | None = None, + validate_geff: bool = True, ) -> Model: """ Load a geff file and return a pycellin model containing the data. @@ -763,6 +764,10 @@ def load_GEFF( The key used to identify the z-coordinate of cells in the geff file. time_key : str | None, optional The key used to identify the time point of cells in the geff file. + If None, the function will try to infer it from the geff metadata. + validate_geff : bool, optional + Whether to validate the GEFF file against its schema, i.e. is the GEFF + file well-formed and compliant with the GEFF specification. Default is True. Returns ------- @@ -771,11 +776,14 @@ def load_GEFF( """ # Read the geff file - geff_graph, geff_md = geff.read_nx(geff_file, validate=True) + geff_graph, geff_md = geff.read_nx(geff_file, validate=validate_geff) if not geff_md.directed: raise ValueError( "The geff graph is undirected: pycellin does not support undirected graphs." ) + for node in geff_graph.nodes: + print(geff_graph.nodes[node]) + break # Extract and dispatch metadata generic_md = _build_generic_metadata(geff_file, geff_md) @@ -821,15 +829,21 @@ def load_GEFF( if __name__ == "__main__": geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/reader_test_graph.geff" + geff_file = "E:/Janelia_Cell_Trackathon/reader_test_graph.geff" # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/mouse-20250719.zarr/tracks" # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_pycellin_geff/test.zarr" # geff_file = ( # "/media/lxenard/data/Janelia_Cell_Trackathon/test_pycellin_geff/pycellin_to_geff.geff" # ) + # geff_file = "E:/Janelia_Cell_Trackathon/test_pycellin_geff/pycellin_to_geff.geff" # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" + # geff_file = "E:/Janelia_Cell_Trackathon/test_trackmate_to_geff/FakeTracks.geff" + # Yohsuke's file for geffception - geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff" + # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff" + # geff_file = "E:/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff" # geff_file = "/media/lxenard/data/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff/linage_tree.geff" + # geff_file = "E:/Janelia_Cell_Trackathon/cell_segmentation.zarr/tree.geff/linage_tree.geff" import plotly.io as pio @@ -864,7 +878,7 @@ def load_GEFF( for node in lin0.nodes(data=True): print(node) break - # lin0.plot() + lin0.plot() # cell_id_key # lineage_id_key