Skip to content

Commit

Permalink
new support function: navis.graph.simplify_graph
Browse files Browse the repository at this point in the history
  • Loading branch information
schlegelp committed Sep 7, 2024
1 parent bbe6877 commit 408959f
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 25 deletions.
1 change: 1 addition & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ Functions to convert between neurons graph representation (networkx or iGraph).
| [`navis.rewire_skeleton()`][navis.rewire_skeleton] | {{ autosummary("navis.rewire_skeleton") }} |
| [`navis.insert_nodes()`][navis.insert_nodes] | {{ autosummary("navis.insert_nodes") }} |
| [`navis.remove_nodes()`][navis.remove_nodes] | {{ autosummary("navis.remove_nodes") }} |
| [`navis.graph.simplify_graph()`][navis.graph.simplify_graph] | {{ autosummary("navis.graph.simplify_graph") }} |

### Connectivity metrics

Expand Down
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ This version contains a major internal rework of both [`navis.plot2d`][] and [`n
##### Additions
- Added [Octarine](https://github.com/schlegelp/octarine) as the default backend for plotting from terminal
- New function: [`navis.graph.skeleton_adjacency_matrix`][] computes the node adjacency for skeletons
- New function: [`navis.graph.simplify_graph`][] simplifies skeleton graphs to only root, branch and leaf nodes while preserving branch length (i.e. weights)
- New [`NeuronList`][navis.NeuronList] method: [`get_neuron_attributes`][navis.NeuronList.get_neuron_attributes] is analagous to `dict.get`
- [`NeuronLists`][navis.NeuronList] now implemented the `|` (`__or__`) operator which can be used to get the union of two [`NeuronLists`][navis.NeuronList]
- [`navis.Volume`][] now have an (optional) `.units` property similar to neurons
Expand Down
77 changes: 58 additions & 19 deletions navis/graph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,63 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

from .converters import (network2nx, network2igraph, neuron2igraph, nx2neuron,
neuron2nx, neuron2KDTree, neuron2tangents)
from .graph_utils import (classify_nodes, cut_skeleton, longest_neurite,
split_into_fragments, reroot_skeleton, distal_to,
dist_between, find_main_branchpoint,
generate_list_of_childs, geodesic_matrix,
node_label_sorting, _break_segments,
_generate_segments, segment_length,
_connected_components, rewire_skeleton,
connected_subgraph, insert_nodes, remove_nodes,
dist_to_root, skeleton_adjacency_matrix)
from .clinic import (health_check)
from .converters import (
network2nx,
network2igraph,
neuron2igraph,
nx2neuron,
neuron2nx,
neuron2KDTree,
neuron2tangents,
simplify_graph,
)
from .graph_utils import (
classify_nodes,
cut_skeleton,
longest_neurite,
split_into_fragments,
reroot_skeleton,
distal_to,
dist_between,
find_main_branchpoint,
generate_list_of_childs,
geodesic_matrix,
node_label_sorting,
_break_segments,
_generate_segments,
segment_length,
_connected_components,
rewire_skeleton,
connected_subgraph,
insert_nodes,
remove_nodes,
dist_to_root,
skeleton_adjacency_matrix,
)
from .clinic import health_check


__all__ = ['cut_skeleton', 'longest_neurite', 'split_into_fragments',
'reroot_skeleton', 'distal_to', 'dist_between', 'segment_length',
'find_main_branchpoint', 'geodesic_matrix',
'rewire_skeleton', 'insert_nodes', 'remove_nodes', 'health_check',
'graph_utils', 'network2nx', 'network2igraph', 'neuron2igraph',
'nx2neuron', 'neuron2nx', 'neuron2KDTree', 'neuron2tangents',
'dist_to_root']
__all__ = [
"cut_skeleton",
"longest_neurite",
"split_into_fragments",
"reroot_skeleton",
"distal_to",
"dist_between",
"segment_length",
"find_main_branchpoint",
"geodesic_matrix",
"rewire_skeleton",
"insert_nodes",
"remove_nodes",
"health_check",
"graph_utils",
"network2nx",
"network2igraph",
"neuron2igraph",
"nx2neuron",
"neuron2nx",
"neuron2KDTree",
"neuron2tangents",
"dist_to_root",
]
87 changes: 83 additions & 4 deletions navis/graph/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
logger = config.get_logger(__name__)

__all__ = sorted(['network2nx', 'network2igraph', 'neuron2igraph', 'nx2neuron',
'neuron2nx', 'neuron2KDTree', 'neuron2tangents'])
'neuron2nx', 'neuron2KDTree', 'neuron2tangents', "simplify_graph"])


def neuron2tangents(x: 'core.NeuronObject') -> 'core.Dotprops':
Expand Down Expand Up @@ -217,13 +217,17 @@ def network2igraph(x: Union[pd.DataFrame, Iterable],
return g


def neuron2nx(x: 'core.NeuronObject') -> nx.DiGraph:
def neuron2nx(x: 'core.NeuronObject', simplify=False) -> nx.DiGraph:
"""Turn Tree-, Mesh- or VoxelNeuron into an NetworkX graph.
Parameters
----------
x : TreeNeuron | MeshNeuron | VoxelNeuron | NeuronList
Uses simple 6-connectedness for voxels.
simplify : bool
For TreeNeurons only: simplify the graph by keeping only roots,
leaves and branching points. Preserves the original
branch lengths (i.e. weights).
Returns
-------
Expand Down Expand Up @@ -253,6 +257,9 @@ def neuron2nx(x: 'core.NeuronObject') -> nx.DiGraph:
G.add_nodes_from(x.nodes.node_id.values)
# Add edges
G.add_weighted_edges_from(elist)

if simplify:
simplify_graph(G, inplace=True)
elif isinstance(x, core.MeshNeuron):
G = nx.Graph()
G.add_nodes_from(np.arange(x.n_vertices))
Expand Down Expand Up @@ -285,6 +292,70 @@ def neuron2nx(x: 'core.NeuronObject') -> nx.DiGraph:
return G


def simplify_graph(G, inplace=False):
"""Simplify graph (networkX or igraph).
This function will simplify the graph by keeping only roots, leaves and
branching points. Preserves branch lengths (i.e. weights).
"""
if not inplace:
G = G.copy()

if isinstance(G, nx.Graph):
# Find all leaf and branch points
leafs = {n for n in G.nodes if G.in_degree(n) == 0 and G.out_degree(n) != 0}
branches = {n for n in G.nodes if G.in_degree(n) > 1 and G.out_degree(n) != 0}
roots = {n for n in G.nodes if G.out_degree(n) == 0}

stop_nodes = roots | leafs | branches

# Walk from each leaf/branch point to the next leaf, branch or root
to_remove = []
for start_node in leafs | branches:
dist = 0
node = start_node
while True:
parent = next(G.successors(node))
dist += G.edges[node, parent]['weight']

if parent in stop_nodes:
G.add_weighted_edges_from([(start_node, parent, dist)])
break

to_remove.append(parent)
node = parent

G.remove_nodes_from(to_remove)
else:
# Find all leaf and branch points
leafs = G.vs.select(_indegree=0, _outdegree_ne=0)
branches = G.vs.select(_indegree_gt=1, _outdegree_ne=0)
roots = G.vs.select(_outdegree=0)

stop_nodes = np.concatenate((roots.indices, leafs.indices, branches.indices))

# Walk from each leaf/branch point to the next leaf, branch or root
to_remove = []
for start_node in np.concatenate((leafs.indices, branches.indices)):
dist = 0
node = start_node
while True:
parent = G.successors(node)[0]
dist += G.es[G.get_eid(node, parent)]['weight']

if parent in stop_nodes:
G.add_edge(start_node, parent, weight=dist)
break

to_remove.append(parent)
node = parent

G.delete_vertices(to_remove)

return G


def _voxels2edges(x, connectivity=18):
"""Turn VoxelNeuron into an edges.
Expand Down Expand Up @@ -344,7 +415,8 @@ def _voxels2edges(x, connectivity=18):


def neuron2igraph(x: 'core.NeuronObject',
connectivity=18,
simplify: bool = False,
connectivity: int = 18,
raise_not_installed: bool = True) -> 'igraph.Graph':
"""Turn Tree-, Mesh- or VoxelNeuron(s) into an iGraph graph.
Expand All @@ -354,8 +426,12 @@ def neuron2igraph(x: 'core.NeuronObject',
----------
x : TreeNeuron | MeshNeuron | VoxelNeuron | NeuronList
Neuron(s) to convert.
simplify : bool
For TreeNeurons only: simplify the graph by keeping only roots,
leaves and branching points. Preserves the original branch
lengths (i.e. weights).
connectivity : 6 | 18 | 26
Connectedness for VoxelNeurons:
For VoxelNeurons only. Defines the connectedness:
- 6 = faces
- 18 = faces + edges
- 26 = faces + edges + vertices
Expand Down Expand Up @@ -425,6 +501,9 @@ def neuron2igraph(x: 'core.NeuronObject',

w = np.sqrt(np.sum((tn_coords - parent_coords) ** 2, axis=1))
G.es['weight'] = w

if simplify:
simplify_graph(G, inplace=True)
elif isinstance(x, core.MeshNeuron):
elist = x.trimesh.edges_unique
G = igraph.Graph(elist, n=x.n_vertices, directed=False)
Expand Down
5 changes: 3 additions & 2 deletions navis/graph/graph_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1912,8 +1912,9 @@ def node_label_sorting(
# Get starting points (i.e. branches off the root) and sort by longest
# path to a terminal (note we're operating on the simplified version
# of the skeleton)
G = graph.simplify_graph(x.graph)
curr_points = sorted(
list(x.simple.graph.predecessors(x.root[0])),
list(G.predecessors(x.root[0])),
key=lambda n: dist_mat[n].max() + dist_mat.loc[n, x.root[0]],
reverse=True,
)
Expand All @@ -1927,7 +1928,7 @@ def node_label_sorting(
pass
else:
new_points = sorted(
list(x.simple.graph.predecessors(nodes_walked[-1])),
list(G.predecessors(nodes_walked[-1])),
# Use distance to the farthest terminal + distance to current node as sorting key
key=lambda n: dist_mat[n].max() + dist_mat.loc[n, nodes_walked[-1]],
reverse=True,
Expand Down

0 comments on commit 408959f

Please sign in to comment.