Skip to content

Working with Graphs

AI-Generated Documentation

This documentation was generated with assistance from AI. While we strive for accuracy, errors may be present. If you find issues, unclear explanations, or have suggestions for improvement, please report them on GitHub.

Graph layers represent general network structures with vertices and edges. Unlike skeletons, graphs can have cycles, multiple connected components, and any connectivity pattern. They're commonly used for level-2 graph data from connectomics pipelines.

Shared Features

Graphs inherit many common features from the PointMixin class. For information about features, masking, transformations, spatial queries, and cross-layer mapping, see Shared Layer Features.

Graphs vs Skeletons

Graphs are general network structures that can have cycles and multiple components. Skeletons are specialized tree structures (no cycles, single component, with a defined root). See Working with Skeletons for tree-specific analysis.

What is a Graph Layer?

A GraphLayer contains: - Vertices: 3D coordinates of network nodes - Edges: Connections between vertices (can form cycles) - Network properties: Connectivity analysis, distance calculations between arbitrary vertices

Inspecting Graph Layers

Quick Overview with describe()

The describe() method provides a comprehensive summary of graph layers, showing vertex/edge counts, features, and connections to other layers:

# Individual graph layer
cell.graph.describe()

Output:

# Cell: my_neuron
# Layer: graph (GraphLayer)  
├── 45 vertices, 67 edges
├── features: [node_type, confidence]
└── Links: []

The output shows: - Cell context: Which cell this graph belongs to - Layer type: Confirms this is a GraphLayer - Metrics: Vertex and edge counts
- features: Available data columns beyond spatial coordinates - Links: Connections to other layers (<-> = bidirectional, = unidirectional)

Layer Manager Overview

You can also inspect all morphological layers at once using cell.layers.describe() to see how your graph fits with other layers like skeletons and meshes.

Creating Graph Layers

Basic Graph Creation

import numpy as np
import ossify

# Create a graph with a cycle
vertices = np.array([
    [0, 0, 0],    # Vertex 0
    [1, 0, 0],    # Vertex 1
    [2, 0, 0],    # Vertex 2
    [1, 1, 0],    # Vertex 3
])

edges = np.array([
    [0, 1],  # Edge from 0 to 1
    [1, 2],  # Edge from 1 to 2
    [2, 3],  # Edge from 2 to 3
    [3, 0],  # Edge from 3 to 0 (creates cycle)
])

# Add to cell
cell = ossify.Cell(name="graph_example")
cell.add_graph(vertices=vertices, edges=edges)

print(f"Graph has {cell.graph.n_vertices} vertices and {len(cell.graph.edges)} edges")

Graph with features

# Add vertex features during creation
compartment_features = np.array([0, 1, 1, 0])  # Different compartments

cell.add_graph(
    vertices=vertices,
    edges=edges,
    features={"compartment": compartment_features}
)

Multiple Connected Components

# Graph with disconnected components
vertices = np.array([
    [0, 0, 0], [1, 0, 0],    # Component 1
    [5, 0, 0], [6, 0, 0],    # Component 2 (disconnected)
])

edges = np.array([
    [0, 1],  # Component 1
    [2, 3],  # Component 2
    # No edges between components
])

cell.add_graph(vertices=vertices, edges=edges)

Graph-Specific Features

Edge Access and Connectivity

graph = cell.graph

# Access edge data
edges_array = graph.edges               # Edges with vertex indices
edges_positional = graph.edges_positional  # Edges with positional indices
edge_df = graph.edge_df                # Edges as DataFrame

print(f"Number of edges: {len(graph.edges)}")

Graph Representation as Sparse Matrices

# Different sparse matrix representations for analysis
csgraph = graph.csgraph                    # Weighted by Euclidean distance (directed)
csgraph_binary = graph.csgraph_binary      # Unweighted/binary (directed)
csgraph_undirected = graph.csgraph_undirected  # Weighted (undirected)
csgraph_binary_undirected = graph.csgraph_binary_undirected  # Binary (undirected)

# Use with scipy.sparse.csgraph functions
from scipy.sparse.csgraph import connected_components
n_components, features = connected_components(graph.csgraph_undirected)
print(f"Number of connected components: {n_components}")

Distance Calculations Between Arbitrary Vertices

# Calculate shortest path distances between any vertices
sources = graph.vertex_index[:2]  # First two vertices
targets = graph.vertex_index[2:4]  # Next two vertices

distances = graph.distance_between(
    sources=sources,
    targets=targets,
    as_positional=False,    # Using vertex indices
    limit=10.0             # Maximum distance to consider
)
print(f"Distance matrix shape: {distances.shape}")
print(f"Distances:\n{distances}")

# Find shortest path between two vertices
path = graph.path_between(
    source=graph.vertex_index[0],
    target=graph.vertex_index[2],
    as_positional=False,
    as_vertices=False      # Return vertex indices, not coordinates
)
print(f"Path from vertex {graph.vertex_index[0]} to {graph.vertex_index[2]}: {path}")

Network Analysis with External Libraries

# Convert to NetworkX for advanced network analysis
import networkx as nx

# Create NetworkX graph from edges
G = nx.from_edgelist(graph.edges)

# Network properties
print(f"Number of connected components: {nx.number_connected_components(G)}")
print(f"Is connected: {nx.is_connected(G)}")
print(f"Average clustering: {nx.average_clustering(G)}")

# Centrality measures
betweenness = nx.betweenness_centrality(G)
degree_centrality = nx.degree_centrality(G)

# Add centralities as features
graph.add_feature(list(betweenness.values()), name="betweenness")
graph.add_feature(list(degree_centrality.values()), name="degree_centrality")

Annotation Aggregation

# Aggregate point annotations to graph vertices
# (requires annotations in the cell)
if "synapses" in cell.annotations.names:
    synapse_counts = graph.map_annotations_to_feature(
        annotation="synapses",
        distance_threshold=1.0,     # Distance threshold for aggregation
        agg="count",                # Count synapses near each vertex
        chunk_size=1000,            # Process in chunks for efficiency
        validate=False,             # Skip mapping validation
        agg_direction="undirected"  # Consider all nearby edges
    )

    # Add as feature
    graph.add_feature(synapse_counts, name="synapse_count")

    # Custom aggregation functions
    synapse_stats = graph.map_annotations_to_feature(
        annotation="synapses",
        distance_threshold=2.0,
        agg={
            "mean_confidence": ("confidence", "mean"),
            "max_size": ("size", "max"),
            "synapse_density": ("size", lambda x: len(x) / 2.0)  # Custom function
        }
    )

Working with Level-2 Graph Data

# Common pattern for connectomics L2 graphs
# L2 graphs often have many vertices and represent spatial connectivity

# Large graph from connectomics data
import pandas as pd

# Typical L2 vertex data
l2_data = pd.DataFrame({
    'l2_id': [100, 101, 102, 103, 104],
    'rep_coord_nm_x': [1000, 2000, 3000, 1500, 2500],
    'rep_coord_nm_y': [1000, 1000, 1000, 2000, 2000],
    'rep_coord_nm_z': [500, 500, 500, 500, 500],
    'size_nm3': [1e6, 1.5e6, 0.8e6, 1.2e6, 0.9e6]
})

# L2 edges (fewer edges than complete graph)
l2_edges = np.array([
    [100, 101],
    [101, 102], 
    [101, 103],
    [103, 104]
])

cell.add_graph(
    vertices=l2_data,
    edges=l2_edges,
    spatial_columns=['rep_coord_nm_x', 'rep_coord_nm_y', 'rep_coord_nm_z'],
    vertex_index='l2_id',
    features={'volume': 'size_nm3'}
)

print(f"L2 graph vertex indices: {cell.graph.vertex_index}")

Graph Analysis Patterns

Connectivity Analysis

# Find isolated vertices (no edges)
degrees = np.array([len(nx.neighbors(G, v)) for v in G.nodes()])
isolated_vertices = graph.vertex_index[degrees == 0]
print(f"Isolated vertices: {isolated_vertices}")

# Find high-degree vertices (hubs)
high_degree_threshold = np.percentile(degrees, 90)
hub_vertices = graph.vertex_index[degrees > high_degree_threshold]
print(f"Hub vertices: {hub_vertices}")

Subgraph Extraction

# Extract subgraph around specific vertices
center_vertices = graph.vertex_index[:3]
subgraph_mask = graph.vertex_index.isin(center_vertices)

# Expand to include neighbors
for center in center_vertices:
    neighbors = list(G.neighbors(center))
    neighbor_mask = graph.vertex_index.isin(neighbors)
    subgraph_mask = subgraph_mask | neighbor_mask

# Create masked subgraph
subgraph = graph.apply_mask(subgraph_mask, as_positional=False)
print(f"Subgraph has {subgraph.n_vertices} vertices")

Distance-based Queries

# Find all vertices within a certain graph distance
center_vertex = graph.vertex_index[0]
max_distance = 3

# Calculate distances from center
distances = graph.distance_between(
    sources=[center_vertex],
    targets=graph.vertex_index,
    as_positional=False,
    limit=max_distance
)

# Find vertices within distance
nearby_mask = distances[0] <= max_distance
nearby_vertices = graph.vertex_index[nearby_mask]
print(f"Vertices within distance {max_distance}: {nearby_vertices}")

Key Graph-Specific Methods

Graph Creation

  • cell.add_graph(vertices, edges, features=None, spatial_columns=None, vertex_index=None) - Add graph to cell

Graph-Specific Properties

  • graph.edges - Edge indices with vertex indices
  • graph.edges_positional - Edge indices with positional indices
  • graph.edge_df - Edges as DataFrame
  • graph.csgraph - Weighted sparse graph (directed)
  • graph.csgraph_binary - Binary sparse graph (directed)
  • graph.csgraph_undirected - Weighted sparse graph (undirected)
  • graph.csgraph_binary_undirected - Binary sparse graph (undirected)

Network Analysis

  • graph.distance_between(sources, targets, as_positional=False, limit=None) - Calculate shortest path distances
  • graph.path_between(source, target, as_positional=False, as_vertices=False) - Find shortest path
  • graph.map_annotations_to_feature(annotation, distance_threshold, agg="count", agg_direction="undirected", ...) - Aggregate annotations

Integration with External Libraries

  • Convert edges to NetworkX: nx.from_edgelist(graph.edges)
  • Use with scipy.sparse.csgraph functions: connected_components(graph.csgraph_undirected)

Additional Features

For comprehensive information about vertex access, features, masking, transformations, spatial queries, and cross-layer mapping, see Shared Layer Features.

When to Use Graphs

  • Level-2 connectomics data with spatial connectivity
  • Network structures that may have cycles or multiple components
  • General connectivity analysis between arbitrary vertex pairs
  • Integration with network analysis libraries like NetworkX