Skip to main content

Discretize continuous N-dimensional spatial environments into bins/nodes with connectivity graphs

Project description

neurospatial

Python 3.10+ License: MIT

neurospatial is a Python library for discretizing continuous N-dimensional spatial environments into bins/nodes with connectivity graphs. It provides tools for spatial analysis, particularly for neuroscience applications involving place fields, position tracking, and spatial navigation.

Whether you're analyzing animal navigation data, modeling place cells, or working with any spatial discretization problem, neurospatial gives you flexible, powerful tools to represent and analyze spatial environments.

Key Features

Core Capabilities

  • Multiple Layout Engines: Choose from regular grids, hexagonal tessellations, masked regions, polygon-bounded areas, triangular meshes, and 1D linearized tracks
  • Automatic Bin Detection: Infer active bins from data samples with morphological operations (dilation, closing, hole filling)
  • Connectivity Graphs: Built-in NetworkX graphs with mandatory node/edge metadata for spatial queries
  • 1D Linearization: Transform complex 2D environments into 1D linearized coordinates for track-based analysis
  • Region Support: Define and manage named regions of interest (ROIs) with immutable semantics
  • Environment Composition: Merge multiple environments with automatic bridge inference

What neurospatial does that others don't

  • generalizes analyses to 2D/3D and arbitrary shapes
  • Geodesic distance computations (distances are not just Euclidean, they respect environment topology)
  • Spatial kernels that respect connectivity graphs (smoothing is not just Gaussian, it respects environment topology)
  • Interactive and static visualization of environments and spatial fields
  • Comprehensive simulation subpackage for generating synthetic trajectories, neural activity, and spikes with ground truth
  • unified analyses (the field reimplements many common neuroscience spatial analyses), this is designed to be a one-stop shop for spatial environment discretization and analysis so that the field is using consistent methods
  • python-native with no matlab dependencies
  • gpu acceleration

Spatial Analysis Operations

  • Trajectory Analysis: Convert trajectories to bin sequences, compute empirical transition matrices with adjacency filtering
  • Occupancy Mapping: Time-in-bin computation with speed filtering, gap handling, and optional kernel smoothing (including linear time allocation for accurate boundary handling)
  • Field Smoothing: Diffusion kernel smoothing on graphs with volume correction for continuous fields
  • Interpolation: Evaluate bin-valued fields at arbitrary points (nearest neighbor or bilinear/trilinear for grids)
  • Distance Fields: Compute geodesic and Euclidean distances, k-hop neighborhoods, connected components
  • Field Utilities: Normalize, clamp, combine fields; compute KL/JS divergence and cosine distance
  • Environment Operations: Subset/crop environments by regions or polygons, rebin grids, copy with cache management

Field Animation

  • Multi-Backend Animation: Visualize spatial fields over time with 4 specialized backends
    • Napari: GPU-accelerated interactive viewer with lazy loading (100K+ frames)
    • Video: Parallel MP4/WebM export with ffmpeg (unlimited frames)
    • HTML: Standalone player with instant scrubbing (up to 500 frames)
    • Jupyter Widget: Notebook integration with play/pause controls
  • Auto-Selection: Intelligent backend selection based on file extension, dataset size, and environment
  • Large-Scale Support: Memory-mapped arrays, LRU caching, frame subsampling for hour-long sessions
  • Trajectory Overlays: Overlay animal trajectories on animated fields (Napari backend)

Installation

From PyPI

pip install neurospatial

Or with uv:

uv pip install neurospatial

For Development

# Clone the repository
git clone https://github.com/edeno/neurospatial.git
cd neurospatial

# Install with uv (recommended)
uv sync

# Or with pip
pip install -e ".[dev]"

Note: This project uses uv for package management. If you have uv installed, all commands should be prefixed with uv run (e.g., uv run pytest).

Tested Dependency Versions

neurospatial v0.4.0 has been tested with the following dependency versions:

Package Tested Version
Python 3.13.5
numpy 2.3.4
pandas 2.3.3
matplotlib 3.10.7
networkx 3.5
scipy 1.16.3
scikit-learn 1.7.2
shapely 2.1.2
track-linearization 2.4.0

These versions represent the tested configuration. neurospatial likely works with a range of versions for each dependency, but these specific versions have full test coverage.

Optional Dependencies

For animation features, install optional dependencies:

# Napari backend (GPU-accelerated interactive viewer)
pip install "napari[all]>=0.4.18,<0.6"

# Jupyter widget backend (notebook integration)
pip install "ipywidgets>=8.0,<9.0"

# Video backend (requires system ffmpeg installation)
# macOS
brew install ffmpeg

# Ubuntu/Debian
sudo apt-get install ffmpeg

# Windows (via chocolatey)
choco install ffmpeg

# Conda
conda install -c conda-forge ffmpeg

Note: HTML backend requires no additional dependencies. Video backend performance scales with CPU cores (use n_workers parameter for parallel rendering).

Quickstart

Here's a minimal example showing how to create an environment from spatial data:

import numpy as np
from neurospatial import Environment

# Generate some 2D position data (e.g., from animal tracking)
# Shape: (n_samples, 2) for x, y coordinates in centimeters
position_data = np.array([
    [0.0, 0.0],
    [5.0, 5.0],
    [10.0, 10.0],
    [15.0, 5.0],
    [20.0, 0.0],
    # ... more positions
])

# Create an environment with 2 cm bins
env = Environment.from_samples(
    positions=position_data,
    bin_size=2.0,  # 2 cm bins
    name="OpenField"
)

# Query the environment
print(f"Environment has {env.n_bins} bins")
print(f"Dimensions: {env.n_dims}D")
print(f"Extent: {env.dimension_ranges}")

# Map a point to its bin
point = np.array([[10.5, 10.2]])
bin_idx = env.bin_at(point)
print(f"Point {point[0]} is in bin {bin_idx[0]}")

# Find neighbors of a bin
neighbors = env.neighbors(bin_idx[0])
print(f"Bin {bin_idx[0]} has {len(neighbors)} neighbors")

# Visualize the environment
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
env.plot(ax=ax)
plt.show()

Your First Place Field

A neuroscientist's first task is usually: "I have an animal moving around and spikes from a neuron — show me where the cell fires." Here is the end-to-end pipeline using simulated data so you can run it right now without any setup:

import numpy as np
import matplotlib.pyplot as plt

from neurospatial import Environment
from neurospatial.encoding import compute_spatial_rate
from neurospatial.simulation import generate_population_spikes
from neurospatial.simulation.models import PlaceCellModel
from neurospatial.simulation.trajectory import simulate_trajectory_ou

# 1. Build a square open-field environment (60 × 60 cm, 2 cm bins).
xx, yy = np.meshgrid(np.linspace(0, 60, 31), np.linspace(0, 60, 31))
arena_corners = np.column_stack([xx.ravel(), yy.ravel()])
env = Environment.from_samples(arena_corners, bin_size=2.0)
env.units = "cm"

# 2. Simulate ten minutes of foraging.
positions_t, times = simulate_trajectory_ou(
    env, duration=600.0, speed_units="cm", seed=42,
)

# 3. Build a place cell tuned to the middle of the arena.
cell = PlaceCellModel(env, center=np.array([30.0, 30.0]), width=8.0,
                      max_rate=15.0, seed=42)

# 4. Generate the spike train from the cell's firing model + trajectory.
spike_times = generate_population_spikes(
    [cell], positions_t, times, seed=42, show_progress=False,
)[0]

# 5. Recover the place field from spikes + trajectory.
result = compute_spatial_rate(
    env, spike_times, times, positions_t,
    smoothing_method="diffusion_kde", bandwidth=5.0,
)

# 6. Plot.
fig, ax = plt.subplots()
result.plot(ax=ax)
ax.set_title("Place field recovered from simulated spikes")
plt.show()

The recovered field will be a Gaussian-like blob centered near (30, 30) — the same location we put the simulated cell. From here you can swap in real spike times, change the trajectory, add more cells, or detect place fields with detect_place_fields. See example 11 for the full tutorial.

Core Concepts

Bins and Active Bins

neurospatial discretizes continuous space into bins (also called nodes). Each bin represents a region of space with a center coordinate and (optionally) a size.

Active bins are bins that contain actual data or are considered part of the environment. When creating an environment from data samples, neurospatial can automatically infer which bins should be active based on:

  • Data occupancy (bins with enough samples)
  • Morphological operations (filling gaps, connecting nearby regions)
  • Explicit masks or polygons

This is essential for neuroscience applications where you want to focus on visited areas while excluding walls, obstacles, or unvisited regions.

Connectivity Graphs

Each environment includes a connectivity graph (NetworkX Graph) defining which bins are neighbors. This graph powers spatial queries like:

  • Finding shortest paths between locations
  • Computing geodesic (manifold) distances
  • Determining local neighborhoods

The connectivity graph includes mandatory metadata on all nodes and edges (positions, distances, vectors, indices) for robust spatial operations.

Layout Engines

Layout engines define how space is discretized. Available engines include:

  • RegularGridLayout: Standard rectangular/cuboid grids
  • HexagonalLayout: Hexagonal tessellations (more uniform neighbor distances)
  • GraphLayout: 1D linearized tracks for maze/track experiments
  • MaskedGridLayout: Grids with arbitrary active/inactive regions
  • ImageMaskLayout: Binary image-based layouts
  • ShapelyPolygonLayout: Polygon-bounded grids
  • TriangularMeshLayout: Triangular tessellations

You typically don't interact with layout engines directly; instead, use the Environment factory methods which select the appropriate engine for you.

Common Use Cases

1. Analyzing Animal Position Data

import numpy as np
from neurospatial import Environment

# In a real analysis, replace these three lines with your own loader:
#   times = load_timestamps()   # shape (n_timepoints,) in seconds
#   position = load_tracking()  # shape (n_timepoints, 2)
#   speeds = load_speeds()      # shape (n_timepoints,)
# Here we synthesize a 1 Hz, 60 s random walk in a 100x100 cm arena so the
# block is runnable end-to-end.
rng = np.random.default_rng(0)
times = np.linspace(0.0, 60.0, 60)
position = np.cumsum(rng.normal(0, 3.0, size=(60, 2)), axis=0) + 50.0
speeds = np.linalg.norm(np.diff(position, axis=0, prepend=position[:1]), axis=1) / np.gradient(times)

# Create environment with 5 cm bins, auto-detect active areas
env = Environment.from_samples(
    positions=position,
    bin_size=5.0,  # cm
    infer_active_bins=True,
    dilate=True,  # Expand active region
    fill_holes=True,  # Fill small gaps
    name="Experiment1_OpenField"
)

# Compute occupancy with speed filtering
occupancy = env.occupancy(
    times=times,
    positions=position,
    speed=speeds,
    min_speed=2.5,  # cm/s - filter slow periods
    bandwidth=10.0  # cm - smooth the occupancy map
)

# Analyze movement patterns
transitions = env.transitions(times=times, positions=position, normalize=True)
bin_sequence = env.bin_sequence(times=times, positions=position, dedup=True)

2. Creating Masked Environments

from shapely.geometry import Polygon

# Define a circular arena (80 cm diameter)
theta = np.linspace(0, 2*np.pi, 100)
boundary = np.column_stack([40 * np.cos(theta), 40 * np.sin(theta)])
polygon = Polygon(boundary)

# Create environment bounded by polygon
env = Environment.from_polygon(
    polygon=polygon,
    bin_size=2.5,  # cm
    name="CircularArena"
)

3. Linearizing Track Mazes

import networkx as nx

# Define track graph (e.g., plus maze)
graph = nx.Graph()
graph.add_node(0, pos=(0.0, 0.0))    # center
graph.add_node(1, pos=(0.0, 50.0))   # north arm
graph.add_node(2, pos=(50.0, 0.0))   # east arm
graph.add_node(3, pos=(0.0, -50.0))  # south arm
graph.add_node(4, pos=(-50.0, 0.0))  # west arm

graph.add_edge(0, 1, edge_id=0, distance=50.0)
graph.add_edge(0, 2, edge_id=1, distance=50.0)
graph.add_edge(0, 3, edge_id=2, distance=50.0)
graph.add_edge(0, 4, edge_id=3, distance=50.0)

# Create 1D linearized environment
env = Environment.from_graph(
    graph=graph,
    edge_order=[(4, 0), (0, 1), (0, 2), (0, 3)],  # traversal order
    edge_spacing=0.0,  # no gaps between edges
    bin_size=2.0,  # cm
    name="PlusMaze"
)

# Convert 2D positions to 1D linearized coordinates
position_2d = np.array([[25.0, 0.0]])  # halfway down east arm
position_1d = env.to_linear(position_2d)

# And back
position_2d_reconstructed = env.linear_to_nd(position_1d)

4. Defining Regions of Interest

from shapely.geometry import Point

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

# Define reward zones as circular regions (buffered points)
reward1_polygon = Point(10.0, 10.0).buffer(5.0)  # 5 cm radius circle
reward2_polygon = Point(30.0, 30.0).buffer(5.0)

env.regions.add("RewardZone1", polygon=reward1_polygon)
env.regions.add("RewardZone2", polygon=reward2_polygon)

# Or add a point region
env.regions.add("StartLocation", point=(0.0, 0.0))

# Access region information
print(f"Number of regions: {len(env.regions)}")
print(f"Region names: {env.regions.list_names()}")

# Get region statistics
area = env.regions.area("RewardZone1")
center = env.regions.region_center("RewardZone1")
print(f"RewardZone1 area: {area:.2f}, center: {center}")

Simulation

neurospatial includes a comprehensive simulation subpackage for generating synthetic spatial data, neural activity, and spike trains. This is essential for testing analysis pipelines, validating algorithms against ground truth, and creating educational examples.

Quick Example

import numpy as np
from neurospatial import Environment
from neurospatial.simulation import (
    simulate_trajectory_ou,
    PlaceCellModel,
    generate_poisson_spikes,
)

# Sample positions used to infer the active region (e.g., a 100x100 cm arena).
# In real use, replace this with your own tracking data.
arena_data = np.random.default_rng(0).uniform(0, 100, size=(2000, 2))

# Create environment
env = Environment.from_samples(arena_data, bin_size=2.0)
env.units = "cm"  # Required for trajectory simulation

# Generate realistic trajectory using Ornstein-Uhlenbeck process.
# speed_units must match env.units exactly (no auto-conversion in v0.4).
positions, times = simulate_trajectory_ou(
    env,
    duration=120.0,  # seconds
    speed_units="cm",
    speed_mean=8.0,  # cm/s
    coherence_time=0.7,  # smoothness parameter
    seed=42
)

# Create place cell with known ground truth
place_cell = PlaceCellModel(
    env,
    center=[50.0, 75.0],  # field center in cm
    width=10.0,  # field width (Gaussian std)
    max_rate=25.0  # peak firing rate in Hz
)

# Generate spikes
firing_rates = place_cell.firing_rate(positions, times)
spike_times = generate_poisson_spikes(firing_rates, times, seed=42)

# Validate with neurospatial analysis
from neurospatial.encoding import compute_spatial_rate
result = compute_spatial_rate(env, spike_times, times, positions)
detected_field = result.firing_rate

# Compare detected field to ground truth
true_center = place_cell.ground_truth['center']
print(f"True field center: {true_center}")
print(f"Detected peak: {env.bin_centers[detected_field.argmax()]}")

Available Features

  • Trajectory Simulation

    • Ornstein-Uhlenbeck process for realistic exploration
    • Structured trajectories (laps, alternation tasks)
    • Boundary handling (reflect, periodic, stop)
  • Neural Models

    • Place cells (Gaussian fields, direction-selective, speed-gated)
    • Boundary/border cells (distance-tuned)
    • Grid cells (hexagonal patterns)
    • All models expose .ground_truth for validation
  • Spike Generation

    • Inhomogeneous Poisson process
    • Refractory period constraints
    • Population spike generation with progress tracking
  • High-Level API

    • Pre-configured sessions: open_field_session(), linear_track_session(), etc.
    • Automated validation: validate_simulation() compares detected vs true parameters
    • One-call workflow: simulate_session() handles trajectory + models + spikes

Learn More

See the comprehensive tutorial: Simulation Workflows Notebook for complete examples including:

  • Quick start with pre-configured sessions
  • Low-level API for custom workflows
  • All cell types (place, boundary, grid)
  • Validation and visualization
  • Performance tips and customization

Animation

Visualize how spatial fields evolve over time with multi-backend animation support.

Quick Example

import numpy as np

from neurospatial import Environment
from neurospatial.animation import subsample_frames
from neurospatial.encoding import compute_spatial_rate

# Assumes you already have:
#   positions: shape (n_samples, 2) animal trajectory
#   times:     shape (n_samples,)   timestamps in seconds
#   spikes:    list of length 30, each entry is a 1-D array of spike
#              times for one cell (see "Your First Place Field" above
#              for an end-to-end simulation that produces these arrays)
env = Environment.from_samples(positions, bin_size=2.5)
fields = [
    compute_spatial_rate(env, spikes[i], times, positions).firing_rate
    for i in range(30)
]

# frame_times is required: one timestamp per field (seconds)
frame_times = np.arange(len(fields)) / 30.0  # 30 Hz

# Interactive Napari viewer (best for exploration)
env.animate_fields(fields, frame_times=frame_times, backend="napari")

# Video export with parallel rendering (best for presentations)
env.clear_cache()  # Required for parallel rendering
env.animate_fields(
    fields, frame_times=frame_times,
    save_path="animation.mp4", n_workers=4,
)

# HTML standalone player (best for sharing)
env.animate_fields(fields, frame_times=frame_times, save_path="animation.html")

# Jupyter widget (best for notebooks)
env.animate_fields(fields, frame_times=frame_times, backend="widget")

Backend Selection Guide

Backend Best For Max Frames Output
Napari Large datasets (100K+), interactive exploration Unlimited* Live viewer
Video Presentations, publications Unlimited .mp4, .webm
HTML Sharing, web embedding 500 .html
Widget Jupyter notebooks ~1000 Interactive widget

* Limited only by disk space (lazy loading with LRU cache)

Large-Scale Datasets

For sessions with 100K+ frames (e.g., 1-hour recording at 250 Hz):

import numpy as np
from neurospatial.animation import subsample_frames

# Use memory-mapped arrays (doesn't load into RAM)
fields = np.memmap('fields.dat', dtype='float32', mode='w+',
                   shape=(900_000, env.n_bins))

# Napari lazy-loads from disk (no data loading); frame_times is required
frame_times = np.arange(len(fields)) / 250.0  # 250 Hz acquisition
env.animate_fields(fields, frame_times=frame_times, backend="napari")

# Or subsample for video export (250 Hz → 30 fps)
subsampled = subsample_frames(fields, source_fps=250, target_fps=30)
sub_times = np.arange(len(subsampled)) / 30.0
env.clear_cache()
env.animate_fields(
    subsampled, frame_times=sub_times,
    save_path="replay.mp4", n_workers=4,
)

Learn More

Documentation

Project Structure

neurospatial/
├── src/neurospatial/
│   ├── environment/            # Main Environment class (modular package)
│   │   ├── core.py            # Core dataclass with state and properties
│   │   ├── factories.py       # Factory classmethods (from_samples, from_graph, etc.)
│   │   ├── queries.py         # Spatial query methods
│   │   ├── trajectory.py      # Trajectory analysis (occupancy, transitions)
│   │   ├── transforms.py      # Rebin/subset operations
│   │   ├── fields.py          # Spatial field operations (smooth, interpolate)
│   │   ├── metrics.py         # Environment metrics and properties
│   │   ├── serialization.py   # Save/load methods
│   │   ├── regions.py         # Region operations
│   │   ├── visualization.py   # Plotting methods (includes animate_fields)
│   │   └── decorators.py      # check_fitted decorator
│   ├── animation/              # Field animation
│   │   ├── core.py            # Main dispatcher and subsample_frames utility
│   │   ├── rendering.py       # Rendering utilities (colormap, RGB conversion)
│   │   ├── _parallel.py       # Parallel frame rendering for video backend
│   │   └── backends/          # Backend implementations
│   │       ├── napari_backend.py   # GPU-accelerated interactive viewer
│   │       ├── video_backend.py    # Parallel MP4/WebM export with ffmpeg
│   │       ├── html_backend.py     # Standalone HTML player
│   │       └── widget_backend.py   # Jupyter widget integration
│   ├── composite.py            # CompositeEnvironment for multi-env merging
│   ├── alignment.py            # Probability distribution transforms
│   ├── transforms.py           # 2D affine transformations
│   ├── layout/
│   │   ├── base.py            # LayoutEngine protocol
│   │   ├── factories.py       # Layout factory functions
│   │   └── engines/           # Concrete layout implementations
│   └── regions/
│       ├── core.py            # Region and Regions classes
│       └── serialization.py   # JSON I/O for regions
└── tests/                      # Comprehensive test suite (1,185+ tests)

Requirements

  • Python 3.10+
  • numpy >= 1.24.0
  • pandas >= 2.0.0
  • matplotlib >= 3.7.0
  • networkx >= 3.0
  • scipy >= 1.10.0
  • scikit-learn >= 1.2.0
  • shapely >= 2.0.0
  • track-linearization >= 2.4.0

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes with tests
  4. Run the test suite (uv run pytest)
  5. Run code quality checks (uv run ruff check . && uv run ruff format .)
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

See CLAUDE.md for detailed development guidelines.

Citation

If you use neurospatial in your research, please cite:

@software{neurospatial2026,
  author = {Denovellis, Eric},
  title = {neurospatial: Spatial environment discretization for neuroscience},
  year = {2026},
  url = {https://github.com/edeno/neurospatial},
  version = {0.4.0}
}

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

Contact

Eric Denovellis


Status: Alpha - API may change. Contributions and feedback welcome!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

neurospatial-0.5.0.tar.gz (25.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

neurospatial-0.5.0-py3-none-any.whl (1.1 MB view details)

Uploaded Python 3

File details

Details for the file neurospatial-0.5.0.tar.gz.

File metadata

  • Download URL: neurospatial-0.5.0.tar.gz
  • Upload date:
  • Size: 25.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for neurospatial-0.5.0.tar.gz
Algorithm Hash digest
SHA256 ade0ca92739e1cb728e3d40711402b55a82f603fa1150d3b21fa67af14a8ffca
MD5 5357a1a4eec614cb293416db72610561
BLAKE2b-256 7d4202b7b433853dc703bb0ab2fbbbf33ac614492256c46cddd5c4ecb788abf3

See more details on using hashes here.

Provenance

The following attestation bundles were made for neurospatial-0.5.0.tar.gz:

Publisher: publish.yml on edeno/neurospatial

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file neurospatial-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: neurospatial-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 1.1 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for neurospatial-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bb62f3eee31c99d60f4eefee18d154ca99ac1478b854cc3a92fe9fe5a3657abc
MD5 5821727c9c9815c36c6d7b57f74ed077
BLAKE2b-256 a0133f8f919f68990994b033d0ce9a32685776300a718a3f4a5c7f914767bd9b

See more details on using hashes here.

Provenance

The following attestation bundles were made for neurospatial-0.5.0-py3-none-any.whl:

Publisher: publish.yml on edeno/neurospatial

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page