1414
1515import copy
1616
17+ import geff
18+ import geff_spec
1719import networkx as nx
18- from geff import write_nx
19- from geff .metadata_schema import Axis , DisplayHint , GeffMetadata , PropMetadata
2020
2121from pycellin .classes import CellLineage , Model , Property
22+ from pycellin .io .utils import _ensure_data_metadata_consistency
2223
2324
2425def _find_node_overlaps (lineages : list [CellLineage ]) -> dict [int , list [int ]]:
@@ -40,7 +41,7 @@ def _find_node_overlaps(lineages: list[CellLineage]) -> dict[int, list[int]]:
4041
4142 for lin_index , lin in enumerate (lineages ):
4243 for nid in lin .nodes :
43- if nid in node_to_lineages : # Overlap found
44+ if nid in node_to_lineages : # overlap found
4445 if nid not in overlaps :
4546 overlaps [nid ] = [node_to_lineages [nid ], lin_index ]
4647 else :
@@ -134,10 +135,10 @@ def _build_axes(
134135 has_x : bool ,
135136 has_y : bool ,
136137 has_z : bool ,
137- has_t : bool ,
138+ time_prop : str ,
138139 space_unit : str | None ,
139140 time_unit : str | None ,
140- ) -> list [Axis ]:
141+ ) -> list [geff_spec . Axis ]:
141142 """
142143 Build a list of Axis objects for GEFF metadata.
143144
@@ -149,36 +150,36 @@ def _build_axes(
149150 Whether the y-axis is present.
150151 has_z : bool
151152 Whether the z-axis is present.
152- has_t : bool
153- Whether the time axis is present .
153+ time_prop : str
154+ Name of the time property .
154155 space_unit : str | None
155156 Unit for spatial axes (e.g., "micrometer").
156157 time_unit : str | None
157158 Unit for time axis (e.g., "second").
158159
159160 Returns
160161 -------
161- list[Axis]
162+ list[geff_spec. Axis]
162163 List of Axis objects representing spatial and temporal dimensions.
163164 """
164165 axes = []
165166 if has_x :
166- axes .append (Axis (name = "cell_x" , type = "space" , unit = space_unit ))
167+ axes .append (geff_spec . Axis (name = "cell_x" , type = "space" , unit = space_unit ))
167168 if has_y :
168- axes .append (Axis (name = "cell_y" , type = "space" , unit = space_unit ))
169+ axes .append (geff_spec . Axis (name = "cell_y" , type = "space" , unit = space_unit ))
169170 if has_z :
170- axes .append (Axis (name = "cell_z" , type = "space" , unit = space_unit ))
171- if has_t :
172- axes . append ( Axis ( name = "frame" , type = "time" , unit = time_unit ))
171+ axes .append (geff_spec . Axis (name = "cell_z" , type = "space" , unit = space_unit ))
172+ axes . append ( geff_spec . Axis ( name = time_prop , type = "time" , unit = time_unit ))
173+
173174 return axes
174175
175176
176177def _build_display_hints (
177178 has_x : bool ,
178179 has_y : bool ,
179180 has_z : bool ,
180- has_t : bool ,
181- ) -> DisplayHint | None :
181+ time_prop : str ,
182+ ) -> geff_spec . DisplayHint | None :
182183 """
183184 Build display hints for GEFF metadata.
184185
@@ -190,28 +191,29 @@ def _build_display_hints(
190191 Whether the y-axis is present.
191192 has_z : bool
192193 Whether the z-axis is present.
193- has_t : bool
194- Whether the time axis is present .
194+ time_prop : str
195+ Name of the time property .
195196
196197 Returns
197198 -------
198- DisplayHint | None
199+ geff_spec. DisplayHint | None
199200 DisplayHint object if x and y axes are present, otherwise None.
200201 """
201202 if has_x and has_y :
202- display_hints = DisplayHint (display_horizontal = "cell_x" , display_vertical = "cell_y" )
203+ display_hints = geff_spec .DisplayHint (
204+ display_horizontal = "cell_x" , display_vertical = "cell_y"
205+ )
203206 if has_z :
204207 display_hints .display_depth = "cell_z"
205- if has_t :
206- display_hints .display_time = "frame"
208+ display_hints .display_time = time_prop
207209 else :
208210 display_hints = None
209211 return display_hints
210212
211213
212214def _build_props_metadata (
213215 properties : dict [str , Property ],
214- ) -> tuple [dict [str , PropMetadata ], dict [str , PropMetadata ]]:
216+ ) -> tuple [dict [str , geff_spec . PropMetadata ], dict [str , geff_spec . PropMetadata ]]:
215217 """
216218 Build property metadata for GEFF from a pycellin model.
217219
@@ -222,7 +224,7 @@ def _build_props_metadata(
222224
223225 Returns
224226 -------
225- tuple[dict[str, PropMetadata], dict[str, PropMetadata]]
227+ tuple[dict[str, geff_spec. PropMetadata], dict[str, geff_spec. PropMetadata]]
226228 A tuple containing two dictionaries:
227229 - Node properties metadata
228230 - Edge properties metadata
@@ -232,13 +234,18 @@ def _build_props_metadata(
232234 ValueError
233235 If an unknown property type is encountered.
234236 """
235- node_props_md : dict [str , PropMetadata ] = {}
236- edge_props_md : dict [str , PropMetadata ] = {}
237+ node_props_md : dict [str , geff_spec . PropMetadata ] = {}
238+ edge_props_md : dict [str , geff_spec . PropMetadata ] = {}
237239
238240 for prop_id , prop in properties .items ():
239- prop_md = PropMetadata (
241+ if prop .dtype .lower () == "string" :
242+ dtype = "str"
243+ else :
244+ dtype = prop .dtype
245+
246+ prop_md = geff_spec .PropMetadata (
240247 identifier = prop_id ,
241- dtype = prop . dtype ,
248+ dtype = dtype ,
242249 unit = prop .unit ,
243250 name = prop .name ,
244251 description = prop .description ,
@@ -256,7 +263,7 @@ def _build_props_metadata(
256263 return node_props_md , edge_props_md
257264
258265
259- def _build_geff_metadata (model : Model ) -> GeffMetadata :
266+ def _build_geff_metadata (model : Model ) -> geff . GeffMetadata :
260267 """
261268 Build GEFF metadata from a pycellin model.
262269
@@ -267,39 +274,38 @@ def _build_geff_metadata(model: Model) -> GeffMetadata:
267274
268275 Returns
269276 -------
270- GeffMetadata
277+ geff. GeffMetadata
271278 The GEFF metadata object.
272279 """
273- # Generic metadata
280+ # Generic metadata.
274281 has_x = model .has_property ("cell_x" )
275282 has_y = model .has_property ("cell_y" )
276283 has_z = model .has_property ("cell_z" )
277- has_t = model .has_property ("frame" )
278284 axes = _build_axes (
279285 has_x = has_x ,
280286 has_y = has_y ,
281287 has_z = has_z ,
282- has_t = has_t ,
288+ time_prop = model . reference_time_property ,
283289 space_unit = model .get_space_unit (),
284290 time_unit = model .get_time_unit (),
285291 )
286292 display_hints = _build_display_hints (
287293 has_x = has_x ,
288294 has_y = has_y ,
289295 has_z = has_z ,
290- has_t = has_t ,
296+ time_prop = model . reference_time_property ,
291297 )
292298
293- # Property metadata
299+ # Property metadata.
294300 props = model .get_cell_lineage_properties ()
295301 node_props_md , edge_props_md = _build_props_metadata (props )
296302
297- # Define identifiers of lineage and cell cycle
303+ # Define identifiers of lineage and cell cycle.
298304 track_node_props = {"lineage" : "lineage_ID" }
299305 if model .has_cycle_data ():
300306 track_node_props ["tracklet" ] = "cycle_ID"
301307
302- return GeffMetadata (
308+ return geff . GeffMetadata (
303309 directed = True ,
304310 axes = axes ,
305311 display_hints = display_hints ,
@@ -329,19 +335,23 @@ def export_GEFF(model: Model, geff_out: str) -> None:
329335 RuntimeError
330336 If the GEFF export process fails.
331337 """
332- # Validate that model has data to export
338+ # Validate that model has data to export.
333339 if not model .data .cell_data :
334- raise ValueError ("Model contains no lineage data to export" )
340+ raise ValueError ("Model contains no lineage data to export. " )
335341
336342 try :
337343 # We don't want to modify the original model.
338344 model_copy = copy .deepcopy (model )
339345 lineages = list (model_copy .data .cell_data .values ())
340346
347+ # For GEFF compatibility, we need to ensure consistency between
348+ # metadata and data.
349+ model_copy = _ensure_data_metadata_consistency (model_copy )
350+
341351 for graph in lineages :
342352 print (len (graph .nodes ), len (graph .edges ))
343353
344- # TODO: remove when GEFF can handle variable length properties
354+ # TODO: remove when GEFF can handle variable length properties.
345355 if model_copy .has_property ("ROI_coords" ):
346356 model_copy .remove_property ("ROI_coords" )
347357
@@ -352,16 +362,16 @@ def export_GEFF(model: Model, geff_out: str) -> None:
352362 print (len (geff_graph ))
353363
354364 metadata = _build_geff_metadata (model_copy )
355- print (metadata )
365+ # print(metadata)
356366
357- write_nx (
367+ geff . write (
358368 geff_graph ,
359369 geff_out ,
360370 metadata = metadata ,
361371 )
362372
363373 except Exception as e :
364- raise RuntimeError (f"Failed to export GEFF file to '{ geff_out } ': { e } " ) from e
374+ raise RuntimeError (f"Failed to export GEFF file to '{ geff_out } ': { e } . " ) from e
365375
366376
367377if __name__ == "__main__" :
@@ -389,14 +399,14 @@ def export_GEFF(model: Model, geff_out: str) -> None:
389399 model = load_TrackMate_XML (xml_in )
390400 # model = load_CTC_file(ctc_in)
391401 # model.add_cycle_data()
392- print (model )
393- print (model .get_cell_lineage_properties ().keys ())
394- print (model .data .cell_data .keys ())
402+ # print(model)
403+ # print(model.get_cell_lineage_properties().keys())
404+ # print(model.data.cell_data.keys())
395405 # To test overlapping node IDs
396406 prop_values = {"cell_x" : 10 , "cell_y" : 15 , "cell_z" : 20 }
397- model .add_cell (lid = 0 , cid = 9510 , frame = 0 , prop_values = prop_values )
398- model .add_cell (lid = 1 , cid = 9510 , frame = 0 , prop_values = prop_values )
399- model .add_cell (lid = 1 , cid = 9509 , frame = 0 , prop_values = prop_values )
400- model .add_cell (lid = 2 , cid = 9498 , frame = 0 , prop_values = prop_values )
407+ model .add_cell (lid = 0 , cid = 9510 , time_value = 0 , prop_values = prop_values )
408+ model .add_cell (lid = 1 , cid = 9510 , time_value = 0 , prop_values = prop_values )
409+ model .add_cell (lid = 1 , cid = 9509 , time_value = 0 , prop_values = prop_values )
410+ model .add_cell (lid = 2 , cid = 9498 , time_value = 0 , prop_values = prop_values )
401411
402412 export_GEFF (model , geff_out )
0 commit comments