Skip to content

Commit 7b974d7

Browse files
committed
Debug geff exporter
1 parent 34dde88 commit 7b974d7

2 files changed

Lines changed: 235 additions & 50 deletions

File tree

pycellin/io/geff/exporter.py

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414

1515
import copy
1616

17+
import geff
18+
import geff_spec
1719
import networkx as nx
18-
from geff import write_nx
19-
from geff.metadata_schema import Axis, DisplayHint, GeffMetadata, PropMetadata
2020

2121
from pycellin.classes import CellLineage, Model, Property
22+
from pycellin.io.utils import _ensure_data_metadata_consistency
2223

2324

2425
def _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

176177
def _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

212214
def _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

367377
if __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

Comments
 (0)