Skip to content

Quickstart

In a few lines, neurospatial turns spike times and a trajectory into a place field — a smoothed map of where a neuron fires in space. This page walks through that end-to-end analysis on simulated data, then explains the pieces.

Install

pip install neurospatial

See the installation guide for optional extras (napari animation, NWB, JAX, xarray) and uv/conda instructions.

Your first place field

The block below is complete and runnable top to bottom. It creates an environment, simulates a foraging trajectory and one place cell's spikes with neurospatial's built-in simulators, and estimates the cell's firing-rate map.

import matplotlib.pyplot as plt
import numpy as np

from neurospatial import Environment
from neurospatial.simulation import (
    PlaceCellModel,
    generate_poisson_spikes,
    simulate_trajectory_ou,
)
from neurospatial import compute_spatial_rate

rng = np.random.default_rng(1)

# 1. Create an Environment: a 100 x 100 cm open-field arena binned at 2 cm.
arena_samples = rng.uniform(0, 100, size=(2000, 2))
env = Environment.from_samples(arena_samples, bin_size=2.0, units="cm")

# 2. Simulate 5 minutes of foraging, plus one place cell tuned to arena center.
positions, times = simulate_trajectory_ou(
    env, duration=300.0, speed_units="cm", seed=1
)
place_cell = PlaceCellModel(
    env, center=np.array([50.0, 50.0]), width=15.0, max_rate=30.0, seed=1
)
firing_rate_hz = place_cell.firing_rate(positions, times)
spike_times = generate_poisson_spikes(firing_rate_hz, times, seed=1)

# 3. Estimate the place field: a boundary-aware, smoothed firing-rate map.
result = compute_spatial_rate(env, spike_times, times, positions, bandwidth=8.0)

Bringing your own data

Replace the simulation in step 2 with your own arrays: spike_times (shape (n_spikes,), seconds), times (shape (n_samples,), seconds), and positions (shape (n_samples, 2), same units as bin_size). Build env from your recorded trajectory instead of arena_samples. Nothing else changes.

Required tracking pairs always put timestamps before positions: compute_spatial_rate(env, spike_times, times, positions), and behavior uses (env, times, positions) or (times, positions). For pynapple tracking, pass tsd.t, tsd.values explicitly. Spatial rate functions require the positions argument; a tracking object cannot replace the timestamp array.

Inspect and plot the result

compute_spatial_rate returns a SpatialRateResult. Ask it for headline numbers, then plot the map:

# Scalar headline metrics include peak_firing_rate, spatial_info and sparsity.
print(result.summary())
# -> a dict of scalars, e.g. ~1378 bins, peak firing rate ~9.3 Hz,
#    spatial_info in bits/spike and ~300 s shared occupancy (exact values vary).

# Where the cell fires most, and how spatially informative it is.
print("Peak firing location (cm):", result.peak_location())
print("Spatial information (bits/spike):", result.spatial_information())
print("Place candidate (information screen):", result.is_place_cell(criterion="spatial_info"))
print("Has a detected field:", result.has_place_field())

# One-row table has the same columns as a population rate table.
table = result.summary_table()
print(table)
print("Metric units:", table.attrs["units"])
print("Heuristic thresholds:", table.attrs["classification_thresholds"])

# Plot the firing-rate map; returns a Matplotlib Axes you can further style.
ax = result.plot()
ax.set_title("Simulated place field")
plt.show()

The peak sits near (50, 50) cm — right where the place cell was tuned — and the map is a smooth bump over the arena. That is the core loop: environment + spikes + trajectory → firing-rate map you can measure and plot.

Cell classification requires choosing a criterion: "spatial_info" is a fast screen biased upward at low spike counts; "shuffle" compares information with circularly shifted spike trains. Call is_place_cell(env, spike_times, times, positions, criterion="shuffle", rng=0) for that verdict using the raw arrays. It costs about 1000 map recomputes by default. Results offer screens and field detection; they keep no raw arrays.

Native 1D fields

Native 1D grids and graph tracks both support rate plots. A native grid plots firing rate in Hz over physical bin coordinates. Pass simulator arrays and labels explicitly, then choose a population row or its singular result:

from neurospatial import compute_spatial_rates
from neurospatial.simulation import linear_track_session

sim = linear_track_session(duration=60, n_place_cells=3, seed=0)
track_rates = compute_spatial_rates(
    sim.env, sim.spike_times, sim.times, sim.positions, unit_ids=sim.unit_ids,
)
ax = track_rates.plot(idx=0)  # Same field as track_rates[0].plot().
ax.set_title(f"Unit {track_rates.unit_ids[0]}: native 1D firing rate")
plt.show()

What just happened

Four concepts carried that analysis. They are worth a minute now; the Core Concepts page goes deeper.

Environment. Environment.from_samples(...) discretized the continuous arena into a grid of bins (small square tiles), keeping only the bins your samples actually covered. env.n_bins reports how many there are, and every later step — occupancy, smoothing, plotting — is defined over those bins:

print(f"{env.n_bins} active bins, {env.n_dims}D, units = {env.units!r}")

Bins. Each bin has an integer index and a center coordinate. A position is mapped to the bin whose tile contains it, so positions (continuous cm) become per-bin occupancy (seconds spent in each tile), and spike_times become per-bin spike counts. Firing rate is spikes ÷ occupancy, smoothed.

Connectivity graph. The environment also carries a graph linking neighboring bins. That is what makes the smoothing boundary-aware: probability mass flows between adjacent, reachable bins instead of leaking across walls or gaps — the difference between a physically meaningful map and a blurry one. The same graph powers geodesic distances and shortest paths:

center_bin = env.bin_at([[50.0, 50.0]])[0]
print("Neighbors of the center bin:", env.neighbors(center_bin))

Recording gaps and time windows. Gaps longer than max_gap=0.5 seconds are detected from times and excluded automatically. Use epochs= to select analysis windows and spike_window= when ephys started late or stopped early. Without it, spikes are assumed recorded whenever position was; result.spike_window_assumed makes that assumption visible.

Next steps

  • Core Concepts — bins, connectivity graphs, layout engines, and 1D linearized tracks, in depth.
  • Place Field Analysis example — the same workflow on a fuller dataset, with a whole population of cells.
  • User Guide — decoding, trajectory and behavioral analysis, egocentric frames, animation, and more.
  • API Reference — every function and result class.