Skip to content

Analysis & Algorithms

Ossify provides computational methods for analyzing neuromorphological structures, with a focus on tree topology, synapse analysis, and signal processing on neuronal graphs.

Overview

Algorithm Category Functions Purpose
Morphological Analysis strahler_number Tree structure characterization
Synapse Analysis synapse_betweenness, label_axon_from_*, segregation_index Connectivity and compartmentalization
Smoothing & Filtering smooth_features Signal processing on graphs

Morphological Analysis

strahler_number

strahler_number

strahler_number(cell: Union[Cell, SkeletonLayer]) -> ndarray

Compute Strahler number on a skeleton, starting at 1 for each tip. Returns a feature suitable for a SkeletonLayer.

Parameters:

  • cell (Union[Cell, SkeletonLayer]) –

    The skeleton to compute the Strahler number on. For convenience, you can pass a Cell object, but note that the return feature is always for the skeleton.

Returns:

  • ndarray

    The Strahler number for each vertex in the skeleton.

The Strahler number provides a hierarchical ordering of tree branches, starting from 1 at terminal branches and incrementing when branches of equal order merge.

Usage Example

import ossify as osy

# Load a cell with skeleton
cell = osy.load_cell("neuron.osy")

# Calculate morphological properties
strahler = osy.algorithms.strahler_number(cell)

# Add as feature to skeleton
cell.skeleton.add_feature(strahler, "strahler_order")

# Visualize with color coding
fig, ax = osy.plot.plot_morphology_2d(
    cell, 
    color="strahler_order",
    palette="viridis"
)

#### Interpretation

- **Order 1**: Terminal branches (end segments)
- **Order 2**: Branches formed by merging two Order 1 branches  
- **Order n**: Branches formed by merging two Order (n-1) branches
- **Higher orders** indicate more "central" or "primary" branches

---

## Synapse Analysis {: .doc-heading}

### synapse_betweenness  

::: ossify.algorithms.synapse_betweenness
    options:
        heading_level: 4
        show_root_heading: true
        show_root_full_path: false
        show_signature_annotations: true
        separate_signature: true
        show_source: false

**Calculates [synapse flow betweenness], measuring how many presynapticpostsynaptic paths pass through each vertex.**

    [synapse flow betweenness]: https://doi.org/10.7554/eLife.12059

#### Usage Example

```python
# Get synapse locations mapped to skeleton
pre_syn_ids = cell.annotations["pre_syn"].map_index_to_layer(
    "skeleton", as_positional=True
)
post_syn_ids = cell.annotations["post_syn"].map_index_to_layer(
    "skeleton", as_positional=True
)

# Calculate synapse betweenness
betweenness = ossify.synapse_betweenness(
    cell.skeleton, 
    pre_syn_ids, 
    post_syn_ids
)

# Add as skeleton feature
cell.skeleton.add_feature(betweenness, "syn_betweenness")

# Find vertices with highest betweenness (potential branch points)
high_betweenness = betweenness > np.percentile(betweenness, 95)

label_axon_from_synapse_flow

label_axon_from_synapse_flow

label_axon_from_synapse_flow(cell: Union[Cell, SkeletonLayer], pre_syn: Union[str, ndarray] = 'pre_syn', post_syn: Union[str, ndarray] = 'post_syn', extend_feature_to_segment: bool = False, ntimes: int = 1, return_segregation_index: bool = False, segregation_index_threshold: float = 0, as_postitional: bool = False) -> Union[ndarray, Tuple[ndarray, float]]

Split a neuron into axon and dendrite compartments using synapse locations.

Parameters:

  • cell (Union[Cell, SkeletonLayer]) –

    The neuron to split.

  • pre_syn (Union[str, ndarray], default: 'pre_syn' ) –

    The annotation associated with presynaptic sites or a list of skeleton vertex ids (see as_postitional).

  • post_syn (Union[str, ndarray], default: 'post_syn' ) –

    The annotation associated with postsynaptic sites or a list of skeleton vertex ids (see as_postitional).

  • how (Literal[synapse_flow, spectral]) –

    The method to use for splitting.

  • n_splits (int) –

    The number of splits to perform. Only applies to the "synapse_flow" method.

  • extend_feature_to_segment (bool, default: False ) –

    Whether to propagate the is_axon feature to the whole segment, rather than a specific vertex. This is likely more biologically accurate, but potentially a less optimal split.

  • segregation_index_threshold (float, default: 0 ) –

    The minimum segregation index required to accept a split. If the best split has a segregation index below this threshold, no split is performed and all vertices are featureed as dendrite.

  • as_positional (bool) –

    If True, assumes the pre_syn and post_syn arrays are positional indices into the skeleton vertex array. If False, assumes they are the vertex indices (i.e. cell.skeleton.vertex_indices).

Returns:

  • is_axon ( Union[ndarray, Tuple[ndarray, float]] ) –

    A boolean array on Skeleton vertices with True for the axon compartments, False for the dendrite compartments.

Identifies axon vs dendrite compartments based on synaptic connectivity patterns using flow-based analysis.

Usage Example

# Basic axon identification
is_axon = ossify.label_axon_from_synapse_flow(
    cell,
    pre_syn="pre_syn",      # Annotation layer with presynaptic sites
    post_syn="post_syn",    # Annotation layer with postsynaptic sites
    extend_feature_to_segment=True,  # Extend to full segments
    return_segregation_index=True  # Return quality metric
)

if isinstance(is_axon, tuple):
    axon_mask, segregation = is_axon
    print(f"Segregation quality: {segregation:.3f}")
else:
    axon_mask = is_axon

# Add compartment features
compartment = np.where(axon_mask, "axon", "dendrite")
cell.skeleton.add_feature(compartment, "compartment")

# Iterative refinement for complex morphologies
is_axon_refined = ossify.label_axon_from_synapse_flow(
    cell,
    pre_syn="pre_syn",
    post_syn="post_syn", 
    ntimes=3,  # Multiple iterations
    segregation_index_threshold=0.5  # Quality threshold
)

label_axon_from_spectral_split

label_axon_from_spectral_split

label_axon_from_spectral_split(cell: Union[Cell, SkeletonLayer], pre_syn: str = 'pre_syn', post_syn: str = 'post_syn', aggregation_distance: float = 1, smoothing_alpha: float = 0.99, axon_bias: float = 0, raw_split: bool = False, extend_feature_to_segment: bool = True, max_times: Optional[int] = None, segregation_index_threshold: float = 0.5, return_segregation_index: bool = False) -> ndarray

Alternative axon identification using spectral analysis of smoothed synapse density.

Usage Example

# Spectral method with density smoothing
is_axon_spectral = ossify.label_axon_from_spectral_split(
    cell,
    pre_syn="pre_syn",
    post_syn="post_syn",
    aggregation_distance=2000,      # Synapse aggregation radius (nm)
    smoothing_alpha=0.95,           # Smoothing strength
    axon_bias=0.1,                  # Bias towards axon classification
    segregation_index_threshold=0.6
)

# Compare with flow-based method
agreement = (is_axon_spectral == axon_mask).mean()
print(f"Method agreement: {agreement:.2%}")

segregation_index

segregation_index

segregation_index(axon_pre: int, axon_post: int, dendrite_pre: int, dendrite_post: int) -> float

Compute the segregation index between pre and post-synaptic compartments relative a compartment-free neuron. Values close to 1 indicate strong segregation, values close to 0 indicate no segregation.

Parameters:

  • axon_pre (int) –

    The number of pre-synaptic axon compartments.

  • axon_post (int) –

    The number of post-synaptic axon compartments.

  • dendrite_pre (int) –

    The number of pre-synaptic dendrite compartments.

  • dendrite_post (int) –

    The number of post-synaptic dendrite compartments.

Returns:

  • float

    The segregation index, between 0 and 1.

Quantifies the segregation of presynaptic and postsynaptic sites between compartments.

Usage Example

# Calculate segregation metrics
axon_vertices = cell.skeleton.vertex_index[axon_mask]
dendrite_vertices = cell.skeleton.vertex_index[~axon_mask]

# Count synapses by compartment
pre_axon = cell.annotations["pre_syn"].map_mask_to_layer("skeleton", axon_mask).sum()
pre_dendrite = len(cell.annotations["pre_syn"]) - pre_axon
post_axon = cell.annotations["post_syn"].map_mask_to_layer("skeleton", axon_mask).sum()  
post_dendrite = len(cell.annotations["post_syn"]) - post_axon

# Calculate segregation index
seg_index = ossify.segregation_index(
    axon_pre=pre_axon,
    axon_post=post_axon,
    dendrite_pre=pre_dendrite, 
    dendrite_post=post_dendrite
)

print(f"Segregation index: {seg_index:.3f}")
print(f"Pre/post ratio (axon): {pre_axon/(post_axon+1):.2f}")
print(f"Pre/post ratio (dendrite): {pre_dendrite/(post_dendrite+1):.2f}")

Interpretation

  • 0.0: No segregation (random distribution)
  • 1.0: Perfect segregation (complete separation)
  • >0.7: Strong segregation (typical for mature neurons)
  • <0.3: Poor segregation (may indicate immature or damaged neurons)

Smoothing & Filtering

smooth_features

smooth_features

smooth_features(cell: Union[Cell, SkeletonLayer], feature: ndarray, alpha: float = 0.9) -> ndarray

Computes a smoothed feature spreading that is akin to steady-state solutions to the heat equation on the skeleton graph.

Parameters:

  • cell (Cell) –

    Neuron object

  • feature (ndarray) –

    The initial feature array. Must be Nxm, where N is the number of skeleton vertices

  • alpha (float, default: 0.9 ) –

    A neighborhood influence parameter between 0 and 1. Higher values give more influence to neighbors, by default 0.90.

Returns:

  • ndarray

    The smoothed feature array

Applies graph-based smoothing to features using a heat equation approach.

Usage Example

# Create noisy feature data
np.random.seed(42)
noisy_signal = np.random.randn(cell.skeleton.n_vertices)

# Apply different levels of smoothing
smoothed_light = ossify.smooth_features(cell.skeleton, noisy_signal, alpha=0.5)
smoothed_heavy = ossify.smooth_features(cell.skeleton, noisy_signal, alpha=0.95)

# Add to skeleton for comparison
cell.skeleton.add_feature(noisy_signal, "original")
cell.skeleton.add_feature(smoothed_light, "smoothed_light") 
cell.skeleton.add_feature(smoothed_heavy, "smoothed_heavy")

# Multi-channel smoothing
synapse_density = np.column_stack([
    cell.skeleton.map_annotations_to_feature("pre_syn", distance_threshold=1000, agg="count"),
    cell.skeleton.map_annotations_to_feature("post_syn", distance_threshold=1000, agg="count")
])

smoothed_density = ossify.smooth_features(
    cell.skeleton, 
    synapse_density, 
    alpha=0.9
)

# Extract smoothed channels
pre_density_smooth = smoothed_density[:, 0]
post_density_smooth = smoothed_density[:, 1]

Parameters

  • alpha: Smoothing strength (0=no smoothing, 1=maximum smoothing)
  • Input shape: Can be 1D (single feature) or 2D (multiple features)
  • Output: Same shape as input with smoothed values

Workflow Examples

Complete Compartment Analysis Pipeline

def analyze_compartmentalization(cell, min_segregation=0.5):
    \"\"\"Complete pipeline for axon-dendrite analysis\"\"\"

    # 1. Calculate synapse betweenness
    pre_ids = cell.annotations["pre_syn"].map_index_to_layer("skeleton", as_positional=True)
    post_ids = cell.annotations["post_syn"].map_index_to_layer("skeleton", as_positional=True) 

    betweenness = ossify.synapse_betweenness(cell.skeleton, pre_ids, post_ids)

    # 2. Flow-based compartmentalization
    axon_flow, seg_flow = ossify.label_axon_from_synapse_flow(
        cell, 
        return_segregation_index=True,
        segregation_index_threshold=min_segregation
    )

    # 3. Spectral alternative
    axon_spectral = ossify.label_axon_from_spectral_split(
        cell,
        segregation_index_threshold=min_segregation
    )

    # 4. Method comparison
    agreement = (axon_flow == axon_spectral).mean()

    # 5. Add all results as features
    cell.skeleton.add_feature(betweenness, "syn_betweenness")
    cell.skeleton.add_feature(axon_flow.astype(str), "axon_flow")
    cell.skeleton.add_feature(axon_spectral.astype(str), "axon_spectral")

    results = {
        'segregation_flow': seg_flow,
        'method_agreement': agreement,
        'axon_fraction_flow': axon_flow.mean(),
        'axon_fraction_spectral': axon_spectral.mean()
    }

    return results

# Run analysis
results = analyze_compartmentalization(cell)
print(f"Flow segregation: {results['segregation_flow']:.3f}")
print(f"Method agreement: {results['method_agreement']:.2%}")

Morphological Feature Extraction

def extract_morphology_features(cell):
    \"\"\"Extract comprehensive morphological features\"\"\"

    skeleton = cell.skeleton

    # Basic metrics
    total_length = skeleton.cable_length()
    n_branches = len(skeleton.branch_points)
    n_tips = len(skeleton.end_points)

    # Strahler analysis
    strahler = ossify.strahler_number(skeleton)
    max_order = strahler.max()
    primary_branches = (strahler == max_order).sum()

    # Compartment analysis (if synapses available)
    if "pre_syn" in cell.annotations.names:
        axon_mask, segregation = ossify.label_axon_from_synapse_flow(
            cell, return_segregation_index=True
        )
        axon_length = skeleton.cable_length(skeleton.vertex_index[axon_mask])
        dendrite_length = total_length - axon_length
    else:
        segregation = None
        axon_length = None
        dendrite_length = None

    # Distance metrics
    distances = skeleton.distance_to_root()
    max_distance = distances.max()
    mean_distance = distances.mean()

    features = {
        'total_length': total_length,
        'n_branches': n_branches, 
        'n_tips': n_tips,
        'max_strahler_order': max_order,
        'primary_branches': primary_branches,
        'max_distance_to_root': max_distance,
        'mean_distance_to_root': mean_distance,
        'segregation_index': segregation,
        'axon_length': axon_length,
        'dendrite_length': dendrite_length
    }

    return features

# Extract features
features = extract_morphology_features(cell)
for name, value in features.items():
    if value is not None:
        print(f"{name}: {value:.2f}")

Algorithm Design Principles

Graph-Based: All algorithms leverage the graph structure of neuronal trees for efficient computation.

Biologically Informed: Methods incorporate known principles of neuronal organization (e.g., synaptic segregation).

Flexible Input: Functions accept both Cell objects and individual SkeletonLayer instances.

Quality Metrics: Many functions return quality/confidence measures alongside results.

Performance & Usage Tips

  • Preprocessing: Use smooth_features() to reduce noise in raw synapse data
  • Parameter Tuning: Adjust segregation_index_threshold based on data quality
  • Method Comparison: Use multiple algorithms and compare results for robustness
  • Batch Processing: Process multiple cells using the same parameters for consistency