Skip to content

Commit 2f28845

Browse files
committed
SWC io: DFS-sort nodes, optionally do not reindex
Previously, nodes were sorted by parent index, which would ensure that root nodes appear before non-roots but did not guarantee that all children are defined after their parent. Now, nodes are sorted in depth-first order (addressing their children in order of their index). Additionally, added the option of return_node_map=None, which does not re-index nodes before writing. This is useful when exporting a SWC files of several neurons along with where node IDs must persist their global uniqueness.
1 parent da2f777 commit 2f28845

File tree

1 file changed

+80
-25
lines changed

1 file changed

+80
-25
lines changed

navis/io/swc_io.py

+80-25
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,12 @@ def write_swc(x: 'core.NeuronObject',
453453
this might drop synapses (i.e. in case of multiple
454454
pre- and/or postsynapses on a single node)! ``labels``
455455
must be ``True`` for this to have any effect.
456-
return_node_map : bool
456+
return_node_map : bool, optional
457457
If True, will return a dictionary mapping the old node
458458
ID to the new reindexed node IDs in the file.
459+
If False (default), will reindex nodes but not return the mapping.
460+
If None, will not reindex nodes (i.e. their current IDs will be written).
461+
459462
460463
Returns
461464
-------
@@ -535,7 +538,7 @@ def _write_swc(x: 'core.TreeNeuron',
535538
write_meta: Union[bool, List[str], dict] = True,
536539
labels: Union[str, dict, bool] = True,
537540
export_connectors: bool = False,
538-
return_node_map: bool = False) -> None:
541+
return_node_map: Optional[bool] = False) -> None:
539542
"""Write single TreeNeuron to file."""
540543
# Generate SWC table
541544
res = make_swc_table(x,
@@ -589,6 +592,45 @@ def _write_swc(x: 'core.TreeNeuron',
589592
return node_map
590593

591594

595+
def sort_swc(df: pd.DataFrame, roots, sort_children=True, inplace=False):
596+
"""Depth-first search tree to ensure parents are always defined before children."""
597+
children = defaultdict(list)
598+
node_id_to_orig_idx = dict()
599+
for row in df.itertuples():
600+
child = row.node_id
601+
parent = row.parent_id
602+
children[parent].append(child)
603+
node_id_to_orig_idx[child] = row.index
604+
605+
if sort_children:
606+
to_visit = sorted(roots, reverse=True)
607+
else:
608+
to_visit = list(roots)[::-1]
609+
610+
idx = 0
611+
order = np.full(len(df), np.nan)
612+
count = 0
613+
while to_visit:
614+
node_id = to_visit.pop()
615+
order[node_id_to_orig_idx[node_id]] = count
616+
cs = children.pop(order[-1], [])
617+
if sort_children:
618+
to_visit.append(sorted(sort_children, reverse=True))
619+
else:
620+
to_visit.append(cs[::-1])
621+
count += 1
622+
623+
# undefined behaviour if any nodes are not reachable from the given roots
624+
625+
if not inplace:
626+
df = df.copy()
627+
628+
df["_order"] = order
629+
df.sort_values("_order", inplace=True)
630+
df.drop(columns=["_order"])
631+
return df
632+
633+
592634
def make_swc_table(x: 'core.TreeNeuron',
593635
labels: Union[str, dict, bool] = None,
594636
export_connectors: bool = False,
@@ -606,17 +648,20 @@ def make_swc_table(x: 'core.TreeNeuron',
606648
607649
str : column name in node table
608650
dict: must be of format {node_id: 'label', ...}.
609-
bool: if True, will generate automatic labels, if False all nodes have label "0".
651+
bool: if True, will generate automatic labels for branches ("5") and ends ("6"),
652+
soma where labelled ("1"), and optionally connectors (see below).
653+
If False (or for all nodes not labelled as above) all nodes have label "0".
610654
611655
export_connectors : bool, optional
612-
If True, will label nodes with pre- ("7") and
613-
postsynapse ("8"). Because only one label can be given
614-
this might drop synapses (i.e. in case of multiple
615-
pre- or postsynapses on a single node)! ``labels``
616-
must be ``True`` for this to have any effect.
617-
return_node_map : bool
656+
If True, will label nodes with only presynapses ("7"),
657+
only postsynapses ("8"), or both ("9").
658+
This overrides branch/end/soma labels.
659+
``labels`` must be ``True`` for this to have any effect.
660+
return_node_map : bool, optional
618661
If True, will return a dictionary mapping the old node
619662
ID to the new reindexed node IDs in the file.
663+
If False, will remap IDs but not return the mapping.
664+
If None, will not remap IDs.
620665
621666
Returns
622667
-------
@@ -625,8 +670,8 @@ def make_swc_table(x: 'core.TreeNeuron',
625670
Only if ``return_node_map=True``.
626671
627672
"""
628-
# Work on a copy
629-
swc = x.nodes.copy()
673+
# Work on a copy sorted in depth-first order
674+
swc = sort_swc(x.nodes, x.root, inplace=False)
630675

631676
# Add labels
632677
swc['label'] = 0
@@ -642,31 +687,41 @@ def make_swc_table(x: 'core.TreeNeuron',
642687
if not isinstance(x.soma, type(None)):
643688
soma = utils.make_iterable(x.soma)
644689
swc.loc[swc.node_id.isin(soma), 'label'] = 1
690+
645691
if export_connectors:
646692
# Add synapse label
647693
pre_ids = x.presynapses.node_id.values
648694
post_ids = x.postsynapses.node_id.values
649-
swc.loc[swc.node_id.isin(pre_ids), 'label'] = 7
650-
swc.loc[swc.node_id.isin(post_ids), 'label'] = 8
651-
652-
# Sort such that the parent is always before the child
653-
swc.sort_values('parent_id', ascending=True, inplace=True)
654695

655-
# Reset index
656-
swc.reset_index(drop=True, inplace=True)
696+
is_pre = swc["node_id"].isin(pre_ids)
697+
swc.loc[is_pre, 'label'] = 7
657698

658-
# Generate mapping
659-
new_ids = dict(zip(swc.node_id.values, swc.index.values + 1))
699+
is_post = swc["node"].isin(post_ids)
700+
swc.loc[is_post, 'label'] = 8
660701

661-
swc['node_id'] = swc.node_id.map(new_ids)
662-
# Lambda prevents potential issue with missing parents
663-
swc['parent_id'] = swc.parent_id.map(lambda x: new_ids.get(x, -1))
702+
is_both = np.logical_and(is_pre, is_post)
703+
swc.loc[is_both, 'label'] = 9
664704

665-
# Get things in order
705+
# Order columns
666706
swc = swc[['node_id', 'label', 'x', 'y', 'z', 'radius', 'parent_id']]
667707

668-
# Make sure radius has no `None`
708+
# Make sure radius has no `None` or negative
669709
swc['radius'] = swc.radius.fillna(0)
710+
swc['radius'][swc['radius'] < 0] = 0
711+
712+
if return_node_map is not None:
713+
# remap IDs
714+
715+
# Reset index
716+
swc.reset_index(drop=True, inplace=True)
717+
718+
# Generate mapping
719+
new_ids = dict(zip(swc.node_id.values, swc.index.values + 1))
720+
721+
swc['node_id'] = swc.node_id.map(new_ids)
722+
# Lambda prevents potential issue with missing parents
723+
swc['parent_id'] = swc.parent_id.map(lambda x: new_ids.get(x, -1))
724+
670725

671726
# Adjust column titles
672727
swc.columns = ['PointNo', 'Label', 'X', 'Y', 'Z', 'Radius', 'Parent']

0 commit comments

Comments
 (0)