Source code for neuron_morphology.features.branching.bifurcations
from typing import Optional, List, Dict
import numpy as np
from neuron_morphology.feature_extractor.marked_feature import marked
from neuron_morphology.feature_extractor.mark import (
RequiresRoot,
BifurcationFeatures,
Geometric,
)
from neuron_morphology.feature_extractor.data import (
MorphologyLike, get_morphology)
from neuron_morphology.morphology import Morphology
__all__ = [
"num_outer_bifurcations",
"calculate_outer_bifs",
"mean_bifurcation_angle_local",
"mean_bifurcation_angle_remote"
]
[docs]def calculate_outer_bifs(
morphology: Morphology,
soma: Dict,
node_types: Optional[List[int]]
) -> int:
"""
Counts the number of bifurcation points beyond the a sphere
with 1/2 the radius from the soma to the most distant point
in the morphology, with that sphere centered at the soma.
Parameters
----------
morphology: Describes the structure of a neuron
soma: Must have keys "x", "y", and "z", describing the position of this
morphology's soma in
node_types: Restrict included nodes to these types. See
neuron_morphology.constants for avaiable node types.
Returns
-------
the number of bifurcations
"""
nodes = morphology.get_node_by_types(node_types)
far = 0
for node in nodes:
dist = morphology.euclidean_distance(soma, node)
if dist > far:
far = dist
count = 0
rad = far / 2.0
for node in nodes:
if len(morphology.children_of(node)) > 1:
dist = morphology.euclidean_distance(soma, node)
if dist > rad:
count += 1
return count
@marked(Geometric)
@marked(BifurcationFeatures)
@marked(RequiresRoot)
def num_outer_bifurcations(
data: MorphologyLike,
node_types: Optional[List[int]] = None
) -> int:
""" Feature Extractor interface to calculate_outer_bifurcations. Returns
the number of bifurcations (branch points), excluding those too close to
the root (threshold is 1/2 the max distance from the root to any node).
Parameters
----------
data : Holds a morphology object. No additional data is required
node_types : Restrict included nodes to these types. See
neuron_morphology.constants for avaiable node types.
"""
return calculate_outer_bifs(
data.morphology,
data.morphology.get_root(),
node_types
)
def angle_between(v1, v2):
"""Helper function to get angle between two numpy vectors"""
v1 = np.asarray(v1)
v2 = np.asarray(v2)
v1_u = v1 / np.linalg.norm(v1)
v2_u = v2 / np.linalg.norm(v2)
return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
@marked(Geometric)
@marked(BifurcationFeatures)
def mean_bifurcation_angle_local(
data: MorphologyLike,
node_types: Optional[List[int]] = None
) -> float:
"""
Compute the average angle between child segments at
bifurcations throughout the morphology.
Trifurcations are ignored. Note: this introduces possible segmentation
artifacts if trifurcations are due to large segment sizes.
Parameters
----------
data: The reconstruction whose max euclidean distance will be
calculated
node_types: restrict consideration to these types
Returns
-------
Scalar value
"""
morphology = get_morphology(data)
total_angle = 0.0
n = 0
nodes = morphology.get_node_by_types(node_types)
for node in nodes:
if len(morphology.children_of(node)) == 2:
node_vec = np.asarray([node['x'], node['y'], node['z']])
a = morphology.children_of(node)[0]
a_vec = np.asarray([a['x'], a['y'], a['z']])
b = morphology.children_of(node)[1]
b_vec = np.asarray([b['x'], b['y'], b['z']])
total_angle += angle_between(a_vec - node_vec, b_vec - node_vec)
n += 1
if n == 0:
return float('nan')
return total_angle / n
@marked(Geometric)
@marked(BifurcationFeatures)
def mean_bifurcation_angle_remote(
data: MorphologyLike,
node_types: Optional[List[int]] = None
) -> float:
"""
Compute the average angle between the next branch point or terminal
tip of child segments at each bifurcation.
Trifurcations are ignored. Note: this introduces possible segmentation
artifacts if trifurcations are due to large segment sizes.
Parameters
----------
Parameters
----------
data: The reconstruction whose max euclidean distance will be
calculated
node_types: restrict consideration to these types
Returns
-------
Scalar value, nan if no nodes
"""
morphology = get_morphology(data)
total_angle = 0.0
n = 0
nodes = morphology.get_node_by_types(node_types)
for node in nodes:
if len(morphology.children_of(node)) == 2:
node_vec = np.asarray([node['x'], node['y'], node['z']])
# find the point to measure to, whether it be the next
# branch point or tip
a = morphology.children_of(node)[0]
while len(morphology.children_of(a)) == 1:
a = morphology.children_of(a)[0]
a_vec = np.asarray([a['x'], a['y'], a['z']])
b = morphology.children_of(node)[1]
while len(morphology.children_of(b)) == 1:
b = morphology.children_of(b)[0]
b_vec = np.asarray([b['x'], b['y'], b['z']])
total_angle += angle_between(a_vec - node_vec, b_vec - node_vec)
n += 1
if n == 0:
return float('nan')
return total_angle / n