Skip to content

Complete Workflows

This page demonstrates end-to-end analysis workflows that integrate multiple neurospatial features.

Workflow 1: Place Field Analysis

A complete workflow for analyzing spatial firing patterns of neurons during navigation.

Overview

Goal: Compute spatial firing rate maps from position tracking and spike data

Steps: Load data → Create environment → Compute occupancy → Process spikes → Calculate firing rates → Visualize

Complete Example

import numpy as np
import matplotlib.pyplot as plt
from neurospatial import Environment

# Step 1: Load experimental data
# Position data: (n_timepoints, 2) array of x, y coordinates in cm
# Spike times: timestamps when neuron fired
position_data = load_position_data()  # Your data loading function
spike_times = load_spike_times()      # Your spike loading function
sampling_rate = 30.0  # Hz

# Step 2: Create environment with appropriate parameters
env = Environment.from_samples(
    positions=position_data,
    bin_size=2.5,  # 2.5 cm bins for 100x100 cm arena
    infer_active_bins=True,
    bin_count_threshold=5,  # Require at least 5 samples per bin
    dilate=True,  # Expand active region slightly
    fill_holes=True,  # Fill interior gaps
    name="OpenFieldSession1"
)

print(f"Created environment with {env.n_bins} active bins")
print(f"Spatial extent: {env.dimension_ranges}")

# Step 3: Compute occupancy (time spent in each bin)
position_bin_indices = env.bin_at(position_data)
occupancy_counts, _ = np.histogram(
    position_bin_indices,
    bins=np.arange(env.n_bins + 1)
)
occupancy_time = occupancy_counts / sampling_rate  # Convert to seconds

# Sanity check
total_time = len(position_data) / sampling_rate
print(f"Total session time: {total_time:.1f} seconds")
print(f"Time accounted for: {occupancy_time.sum():.1f} seconds")
assert np.isclose(occupancy_time.sum(), total_time, rtol=0.01)

# Step 4: Map spikes to bins
# Find position at each spike time (requires interpolation in real data)
spike_positions = interpolate_position(position_data, spike_times)
spike_bin_indices = env.bin_at(spike_positions)

# Count spikes per bin
spike_counts, _ = np.histogram(
    spike_bin_indices,
    bins=np.arange(env.n_bins + 1)
)

# Step 5: Calculate firing rate map
# Only compute firing rate for bins with sufficient occupancy
min_occupancy = 0.1  # seconds
firing_rate = np.full(env.n_bins, np.nan)
valid_bins = occupancy_time >= min_occupancy

firing_rate[valid_bins] = (
    spike_counts[valid_bins] / occupancy_time[valid_bins]
)

print(f"Peak firing rate: {np.nanmax(firing_rate):.2f} Hz")
print(f"Mean firing rate: {np.nanmean(firing_rate):.2f} Hz")
print(f"Bins with valid firing rate: {np.sum(valid_bins)}/{env.n_bins}")

# Step 6: Smooth firing rate map (optional)
from scipy.ndimage import gaussian_filter

# Reshape to 2D grid for smoothing
if hasattr(env.layout, 'grid_shape'):
    grid_shape = env.layout.grid_shape
    active_mask = env.layout.active_mask

    # Create full grid with NaN for inactive bins
    firing_rate_grid = np.full(grid_shape, np.nan)
    firing_rate_grid[active_mask] = firing_rate

    # Smooth (only affects active regions)
    smoothed_grid = gaussian_filter(
        np.nan_to_num(firing_rate_grid),
        sigma=1.0
    )
    firing_rate_smoothed = smoothed_grid[active_mask]
else:
    firing_rate_smoothed = firing_rate  # Can't smooth non-grid layouts

# Step 7: Visualize results
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# Plot 1: Trajectory with environment
ax1 = axes[0]
env.plot(ax=ax1)
ax1.plot(position_data[:, 0], position_data[:, 1],
         'r-', alpha=0.3, linewidth=0.5)
ax1.set_title('Trajectory')

# Plot 2: Occupancy map
ax2 = axes[1]
scatter = ax2.scatter(
    env.bin_centers[:, 0],
    env.bin_centers[:, 1],
    c=occupancy_time,
    s=50,
    cmap='viridis'
)
plt.colorbar(scatter, ax=ax2, label='Time (s)')
ax2.set_title('Occupancy')
ax2.set_xlabel('X (cm)')
ax2.set_ylabel('Y (cm)')

# Plot 3: Firing rate map
ax3 = axes[2]
scatter = ax3.scatter(
    env.bin_centers[:, 0],
    env.bin_centers[:, 1],
    c=firing_rate_smoothed,
    s=50,
    cmap='hot',
    vmin=0
)
plt.colorbar(scatter, ax=ax3, label='Firing Rate (Hz)')
ax3.set_title('Place Field')
ax3.set_xlabel('X (cm)')
ax3.set_ylabel('Y (cm)')

plt.tight_layout()
plt.savefig('place_field_analysis.png', dpi=300)
plt.show()

# Step 8: Export results
results = {
    'environment': env,
    'occupancy_time': occupancy_time,
    'spike_counts': spike_counts,
    'firing_rate': firing_rate,
    'firing_rate_smoothed': firing_rate_smoothed,
    'bin_centers': env.bin_centers,
}

# Save for later analysis
np.savez('place_field_results.npz', **results)

Key Considerations

Bin Size Selection: - Too large: Lose spatial resolution - Too small: Insufficient occupancy, noisy firing rates - Rule of thumb: 2-5 cm for rat open field (100x100 cm arena)

Occupancy Threshold: - Exclude bins with low occupancy to avoid division by zero - Typical: 0.1-1.0 seconds minimum - Trade-off: Coverage vs. reliability

Smoothing: - Reduces noise but blurs fine spatial structure - Gaussian filter with sigma=1-2 bins is typical - Consider not smoothing if bin size already large

Workflow 2: Region-Based Analysis

Analyzing behavior across experimentally-defined spatial zones.

Overview

Goal: Compare neural activity and behavior across different regions of the environment

Steps: Define regions → Compute metrics per region → Statistical comparison

Complete Example

from neurospatial import Environment
from shapely.geometry import Point
import numpy as np

# Create environment from position data
env = Environment.from_samples(position_data, bin_size=3.0)

# Define experimental regions
# Center zone (15 cm radius circle)
center_point = Point(50.0, 50.0)  # Arena center
env.regions.add("Center", polygon=center_point.buffer(15.0))

# Corner zones (10x10 cm squares)
corners = {
    "TopLeft": [(0, 90), (10, 90), (10, 100), (0, 100)],
    "TopRight": [(90, 90), (100, 90), (100, 100), (90, 100)],
    "BottomLeft": [(0, 0), (10, 0), (10, 10), (0, 10)],
    "BottomRight": [(90, 0), (100, 0), (100, 10), (90, 10)],
}

for name, coords in corners.items():
    from shapely.geometry import Polygon
    env.regions.add(name, polygon=Polygon(coords))

# Find which bins belong to each region
region_bins = {}
for region_name in env.regions.list_names():
    region_polygon = env.regions[region_name].polygon
    bins_in_region = []

    for bin_idx in range(env.n_bins):
        bin_point = Point(env.bin_centers[bin_idx])
        if region_polygon.contains(bin_point):
            bins_in_region.append(bin_idx)

    region_bins[region_name] = np.array(bins_in_region)
    print(f"{region_name}: {len(bins_in_region)} bins")

# Compute occupancy per region
position_bins = env.bin_at(position_data)
sampling_rate = 30.0  # Hz

region_occupancy = {}
for region_name, bins in region_bins.items():
    time_in_region = np.sum(np.isin(position_bins, bins)) / sampling_rate
    region_occupancy[region_name] = time_in_region
    print(f"Time in {region_name}: {time_in_region:.2f} seconds")

# Compute firing rate per region
spike_positions = interpolate_position(position_data, spike_times)
spike_bins = env.bin_at(spike_positions)

region_firing_rates = {}
for region_name, bins in region_bins.items():
    spikes_in_region = np.sum(np.isin(spike_bins, bins))
    time_in_region = region_occupancy[region_name]

    if time_in_region > 0.5:  # Require 0.5s minimum
        firing_rate = spikes_in_region / time_in_region
        region_firing_rates[region_name] = firing_rate
    else:
        region_firing_rates[region_name] = np.nan

    print(f"{region_name} firing rate: {firing_rate:.2f} Hz")

# Statistical comparison
# Example: Is firing rate higher in center vs. corners?
center_rate = region_firing_rates["Center"]
corner_rates = [region_firing_rates[name] for name in corners.keys()]
corner_rates = [r for r in corner_rates if not np.isnan(r)]

print(f"\nCenter: {center_rate:.2f} Hz")
print(f"Corners: {np.mean(corner_rates):.2f} ± {np.std(corner_rates):.2f} Hz")

# Visualize regions
fig, ax = plt.subplots(figsize=(8, 8))
env.plot(ax=ax)

# Color-code regions
colors = plt.cm.Set3(np.linspace(0, 1, len(env.regions)))
for idx, region_name in enumerate(env.regions.list_names()):
    region = env.regions[region_name]
    if region.polygon:
        x, y = region.polygon.exterior.xy
        ax.fill(x, y, alpha=0.3, color=colors[idx], label=region_name)

ax.legend()
ax.set_title('Experimental Regions')
plt.show()

Workflow 3: Multi-Session Alignment

Comparing environments across recording sessions.

Overview

Goal: Align spatial representations from different sessions to track stability

Steps: Create environments for each session → Align using transforms → Compare firing patterns

Complete Example

from neurospatial import Environment
from neurospatial.alignment import map_probabilities
import numpy as np

# Session 1 (reference)
env1 = Environment.from_samples(
    session1_position,
    bin_size=2.5,
    name="Session1"
)
firing_rate1 = compute_firing_rate(env1, session1_position, session1_spikes)

# Session 2 (may have slight camera shift or animal positioning differences)
env2 = Environment.from_samples(
    session2_position,
    bin_size=2.5,
    name="Session2"
)
firing_rate2 = compute_firing_rate(env2, session2_position, session2_spikes)

# Align session 2 to session 1 coordinate frame
firing_rate2_aligned = map_probabilities(
    source_env=env2,
    target_env=env1,
    source_probabilities=firing_rate2
)

# Compute spatial correlation
valid_bins = ~np.isnan(firing_rate1) & ~np.isnan(firing_rate2_aligned)
correlation = np.corrcoef(
    firing_rate1[valid_bins],
    firing_rate2_aligned[valid_bins]
)[0, 1]

print(f"Spatial correlation: {correlation:.3f}")

# Visualize comparison
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# Session 1
axes[0].scatter(env1.bin_centers[:, 0], env1.bin_centers[:, 1],
                c=firing_rate1, s=50, cmap='hot')
axes[0].set_title('Session 1')

# Session 2 (aligned)
axes[1].scatter(env1.bin_centers[:, 0], env1.bin_centers[:, 1],
                c=firing_rate2_aligned, s=50, cmap='hot')
axes[1].set_title('Session 2 (aligned)')

# Difference
difference = firing_rate2_aligned - firing_rate1
axes[2].scatter(env1.bin_centers[:, 0], env1.bin_centers[:, 1],
                c=difference, s=50, cmap='RdBu_r',
                vmin=-np.nanmax(np.abs(difference)),
                vmax=np.nanmax(np.abs(difference)))
axes[2].set_title(f'Difference (r={correlation:.3f})')

plt.tight_layout()
plt.show()

Workflow 4: Track Linearization

Analyzing maze experiments with branching structures.

Overview

Goal: Convert 2D maze positions to 1D linearized coordinates for sequential analysis

Steps: Define track graph → Create 1D environment → Map positions → Analyze

See the complete example in examples/05_track_linearization.ipynb.

Common Patterns

Pattern: Handling Edge Cases

# Always check for valid bins
bin_indices = env.bin_at(positions)
valid = bin_indices != -1  # -1 indicates point outside environment

# Use only valid data
valid_positions = positions[valid]
valid_bins = bin_indices[valid]

# Or handle invalid gracefully
firing_rate = np.full(env.n_bins, np.nan)
valid_occupancy = occupancy_time > min_threshold
firing_rate[valid_occupancy] = spike_counts[valid_occupancy] / occupancy_time[valid_occupancy]

Pattern: Batch Processing

# Process multiple neurons efficiently
neurons = load_all_neurons()
firing_rate_maps = []

for neuron_id, spike_times in neurons.items():
    spike_positions = interpolate_position(position_data, spike_times)
    spike_bins = env.bin_at(spike_positions)
    spike_counts, _ = np.histogram(spike_bins, bins=np.arange(env.n_bins + 1))

    firing_rate = spike_counts / occupancy_time
    firing_rate[occupancy_time < min_occupancy] = np.nan

    firing_rate_maps.append(firing_rate)

firing_rate_maps = np.array(firing_rate_maps)  # Shape: (n_neurons, n_bins)

Pattern: Progressive Refinement

# Start with coarse binning for quick overview
env_coarse = Environment.from_samples(positions, bin_size=10.0)
# ... analyze ...

# Refine in regions of interest
env_fine = Environment.from_samples(
    positions,
    bin_size=2.0,
    infer_active_bins=True,
    dilate=True
)
# ... detailed analysis ...

See Also