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¶
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:
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.