Pipeline Details#
This document explains how the spatial neural activity analysis pipeline works.
Overview#
Both ArenaDataset (2D) and MazeDataset (1D) implement the same abstract pipeline defined by BaseCaMAPDataset. Each step depends on the previous one.
from_yaml(config, data_path)— parse configs, auto-select ArenaDataset or MazeDatasetload()— load neural traces, behavior positions, visualization assetspreprocess_behavior()— geometric corrections (Hampel, perspective, clipping, unit conversion)deconvolve()— OASIS AR(2) deconvolution → per-unit spike trains at neural fpsmatch_events()— interpolate behavior onto neural timestamps, compute speed, build canonical tablecompute_occupancy()— spatial occupancy map (2D histogram or 1D bins)analyze_units()— per-unit rate map, spatial information, stability, place fieldssave_bundle()— write.camapdirectory with config, arrays, parquets, figures, and ametadata.jsonthat records both the bundle schema version and thecamappackage version (viahatch-vcs, so it includes the git SHA of the build).
Steps 3–4 are gated on which blocks the data config carries: preprocess_behavior() is a no-op when no behavior: block is present, deconvolve() is a no-op when no neural: block is present, and the place-cell steps 5–7 require both. This makes neural-only (load → deconvolve → save) and behavior-only (load → preprocess → save) workflows valid out of the box.
The canonical neural-rate table (ds.canonical) is the central artifact: one row per neural frame with columns frame_index, neural_time, x, y, speed, [pos_1d, arm_index, ...], s_unit_<id>.... Behavior is linearly interpolated onto neural timestamps inside match_events() (or, for the maze pipeline, inside detect-zones), so every analysis downstream operates on a single common clock. The long-format event_place table is derived from canonical for the per-unit spatial analysis functions.
Data Files#
Neural inputs (in neural.path directory):
{trace_name}.zarr: calcium traces (frames × units). Name set bytrace_namein the analysis config (e.g.C.zarr,C_lp.zarr)A.zarr: spatial footprints for cell contour overlay (optional)max_proj.zarr: max projection image for visualization background (optional)neural.timestampCSV: neural frame timestamps (frame, timestamp_first, timestamp_last)
Behavior inputs:
behavior.positionCSV: animal position per frame (DeepLabCut format with bodypart columns)behavior.timestampCSV: behavior frame timestampsbehavior.video(optional): behavior video file used for the calibration overlay frame
Intermediate DataFrames (generated during pipeline):
canonical: per-neural-frame table withx, y, speed, [pos_1d, ...], s_unit_<id>.... Single source of truth for all downstream analysis.trajectory_filtered/trajectory_1d_filtered: speed-filtered view ofcanonical(withframe_indexaliased so existing analysis helpers consume it directly).event_place: long-format event table (unit_id, frame_index, x, y, s, speed, ...) derived from the speed-filtered canonical view. Each row is one above-zero spike sample from one unit.event_index: long-format event table over the unfiltered canonical view, used by the notebook browser.
Processing Steps#
ds.load()#
Load calcium traces from
{trace_name}.zarr(skipped when noneural:block is configured)Load behavior position and timestamps (skipped when no
behavior:block is configured; speed is computed later at neural rate inmatch_events)Load visualization assets (max_proj, footprints, behavior video frame)
For the maze pipeline, also auto-runs zone detection if the cached
zone_trackingCSV is missing
ds.preprocess_behavior()#
Saves a copy of the raw trajectory in trajectory_raw, then applies geometric corrections (Hampel, perspective, clipping, unit conversion). Speed is computed later at the neural sample rate inside match_events(). No-op when the data config has no behavior: block.
ArenaDataset (2D)#
When arena_bounds is configured (full pipeline):
Hampel jump removal: per-frame outlier detection on the (x, y) trajectory using a centered rolling-median centroid (window=
behavior.hampel_window_frames) and the MAD-scaled deviationbehavior.hampel_n_sigmas * 1.4826 * MAD. Flagged frames are linearly interpolated.Perspective correction: correct for camera angle using
camera_height_mmandtracking_height_mmBoundary clipping: clip positions to
arena_boundsRecompute speed: recalculate speed in mm/s from corrected positions, using a centered window of
behavior.speed_window_seconds
When arena_bounds is not configured (fallback with warnings):
Spatial corrections are skipped (Hampel jump removal, perspective correction, boundary clipping)
Speed and position remain in pixels
MazeDataset (1D)#
MazeDataset.load() reads a zone_tracking CSV that maps each neural frame to a zone label and an arm-relative position. If the CSV is missing, load() runs Zone Detection automatically; pass --force-redetect to camap analysis (or force_redetect=True to MazeDataset.load()) to refresh it.
preprocess_behavior() then operates on the already-neural-rate trajectory and:
Serialize to 1D: convert
(zone, arm_position)into a single concatenatedpos_1daxis using physical arm lengths frombehavior_graph.yaml.Optionally split by direction: each contiguous arm traversal becomes
Arm_X_fwdorArm_X_revbased on its starting position (split_by_direction).Optionally drop incomplete traversals: keep only room-to-room arm crossings (
require_complete_traversal).Compute 1D speed: centered-window (
behavior.speed_window_seconds) finite-difference onpos_1dat the neural sample rate, with a same-arm guard so cross-arm transitions never inflate the speed estimate.
ds.match_events()#
Requires both a neural: and a behavior: block — raises with a targeted message if either side is missing. Builds the canonical neural-rate table:
Load neural timestamps (
timestamp_firstper neural frame), validate with Hampel filter, exclude anomalous frames.Arena: linearly interpolate
(x, y)from the behavior-rate trajectory onto neural timestamps, then compute speed at neural rate. Maze: the trajectory is already at neural rate (interpolation happened inside zone detection), so this step is a direct join.Stack the deconvolved per-unit spike trains (
S_list) intos_unit_<id>columns.Drop neural frames with no behavior coverage (outside behavior time window).
Derive the speed-filtered view (
trajectory_filtered/trajectory_1d_filtered) and the long-formatevent_place/event_indextables.
The hard-error guardrail in :func:camap.behavior.interpolate_behavior_onto_neural refuses to upsample when neural_fps > 5 * behavior_fps, since per-frame jitter would dominate.
Zone Detection (maze only)#
Zone detection projects raw DLC (x, y) onto the maze graph at the neural sample rate. It runs automatically from MazeDataset.load() when the zone_tracking CSV is missing, and can also be invoked directly via camap detect-zones -d data_paths.yaml (which additionally exports a validation video).
Load raw DLC
(x, y)frombehavior_position.csv.Hampel jump removal on the raw trajectory at behavior rate, using
zone_detection.hampel_window_framesandzone_detection.hampel_n_sigmas.Linearly interpolate the cleaned
(x, y)onto the neural timestamp grid (timestamp_firstfromneural_timestamp.csv).Compute per-zone soft-membership probability and run a state machine at the neural sample rate (
min_confidence,min_seconds_same,min_seconds_forbidden, graph adjacency) to assign a zone label per neural frame.For arm-zone frames, project the cleaned
(x, y)onto the arm polyline to getarm_position(0–1) and pinned point(x_pinned, y_pinned).Write the resulting per-neural-frame table to the
zone_trackingCSV with columnsx, y, x_pinned, y_pinned, zone, arm_position, neural_timeindexed by neural frame.
zone_tracking defaults to zone_tracking_{data_path.stem}.csv next to the data config when not set.
ds.deconvolve()#
Run OASIS AR2 deconvolution on each unit’s calcium trace
Parameters:
g(AR coefficients),baseline,penalty,s_minOutput:
good_unit_ids, per-unit spike trains (S_list)No-op when the data config has no
neural:block
ds.compute_occupancy()#
Compute 2D occupancy histogram from
trajectory_filteredSmooth with
spatial_sigma, mask bins belowmin_occupancyOutput:
occupancy_time,valid_mask, bin edges
ds.analyze_units() — per unit#
Four independent computations from the same inputs (events, filtered trajectory, occupancy):
Rate map: event weights / occupancy time, smoothed with
spatial_sigma. Stored onUnitResultasrate_map_smoothed(authoritative, firing-rate units). Arate_map_peak_normalizedproperty is derived on demand for display.Spatial information + shuffle test: Skaggs SI with circular-shift shuffle → SI p-value
Shuffled rate percentile: per-bin percentile of shuffled rate maps → used for place field seed detection
Stability + shuffle tests (one per configured block count in
stability_splits): correlation between odd/even block rate maps with circular-shift shuffle → one stability p-value per split. Each split runs with an independent RNG stream so the tests are statistically independent.Minimum-events gate (
min_events): units with fewer speed-filtered events are assignedp_val=1.0for both SI and stability, so they cannot be classified as place cells. Their rate map is still computed for inspection.
Place cell classification: units with SI p-value < p_value_threshold AND UnitResult.is_stable(p_value_threshold) (every configured split’s stability p-value is below the threshold).
Place field detection (Guo et al. 2023):
Seed detection: bins where rate exceeds the shuffled rate percentile. Only contiguous seed regions with ≥
place_field_min_binsbins are keptExtension: each seed region extends to contiguous bins with rate ≥
place_field_threshold× (seed’s peak rate)
Results#
Coverage map: sum of place field masks across place cells
Coverage curve: cells sorted by field size (largest first), cumulative fraction of environment covered
Interactive browser: max projection overlay, trajectory with events, rate map with place field contour, SI histogram, stability maps, trace view
Summary Figures#
save_bundle() exports these PDFs to the figures/ directory inside the bundle. Both arena and maze pipelines produce a consistent set.
Shared (both pipelines):
diagnostics.pdf— SI and stability distributions across all unitssummary_scatter.pdf— SI vs stability scatter plot with place cell classificationfootprints.pdf— spatial footprints of all recorded unitsbehavior_preview.pdf— 2D trajectory density (additive alpha), speed-filtered trajectory, and speed histogramspeed_traces.pdf— animal speed over time with place cell neural traces (top 20 by SI)occupancy.pdf— trajectory and full-session occupancy on the top row, then one row of odd/even half-occupancies per configured entry instability_splits(same block scheme as the stability test). Below-threshold bins (min_occupancy, on the smoothed half-occupancy) are outlined in cyan.
Arena only:
arena_calibration.pdf— raw trajectory overlaid on arena bounds and video framepreprocess_steps.pdf— trajectory at each correction stage (Hampel, perspective, clipping)coverage.pdf— place field coverage map and coverage curve
Maze only:
speed_histogram.pdf— 1D arm speed distribution with threshold linegraph_overlay.pdf— maze graph polylines on behavior video framepopulation_rate_map.pdf— all unit 1D rate maps tiledglobal_pvo_matrix.pdf— population vector overlap matrix across arms. Usesrate_map_smoothed(firing-rate units), so the cosine similarity reflects true rate magnitudes across cells rather than being a shape-only metric.
Data Integrity#
The pipeline flags and excludes problematic data rather than silently repairing it. Every exclusion is logged with a count.
Timestamp validation (neural timestamps):
Outliers from the local trend (Hampel filter, window=11, 3σ) → excluded
Residual backward jumps after Hampel → excluded
Large forward gaps (recording stalls, >1s or >10× median dt) → warned, NOT excluded (valid timestamps; interpolated positions within the gap may be unreliable)
Only
timestamp_firstis used;timestamp_lastis ignored (occasionally noisy)
Temporal alignment (behavior ↔ neural):
Zero overlap between recordings → hard error
Partial overlap (neural starts before or ends after behavior) → logged, uncovered frames dropped
Neural fps > 5× behavior fps → hard error (upsampled jitter would dominate)
Position filtering (behavior trajectory):
Hampel filter on raw (x, y) at behavior rate → outliers interpolated
Out-of-arena positions → clipped to boundary (intentional for arena calibration)
Non-numeric values in data columns → logged if any coerced to NaN
Speed filtering:
Computed at neural rate via centered window (
speed_window_seconds)Zero-dt frames (from timestamp exclusion) → NaN speed, not zero
NaN speeds → logged and dropped by the speed threshold
Analysis methods:
Rate maps: independent numerator/denominator Gaussian smoothing (Skaggs et al. 1996)
Spatial information: Skaggs et al. 1993, with +1-corrected rank p-value (Phipson & Smyth 2010)
Shuffle null: circular shift excluding zero-shift, independent RNG seeds for SI, per-split stability, and rate-percentile streams (each split in
stability_splitsalso gets its own seed offset)Stability: one test per entry in
stability_splits. Each uses interleaved odd/even blocks at that block count and a Fisher-z-transformed correlation;block_shiftoffsets the block boundaries so “first half” isn’t always frame 0.Place field detection: Guo et al. 2023 seed-and-grow algorithm
Place cell classification: dual criterion (SI p < threshold AND stability p < threshold)
Key Parameters#
speed_threshold: minimum speed to include data (mm/s)min_occupancy: minimum time per bin to be valid (seconds)bins: spatial resolution (number of bins per axis)spatial_sigma: Gaussian smoothing sigma for occupancy and rate maps (in bins)n_shuffles: number of circular-shift shuffle iterationsmin_shift_seconds: minimum circular shift for shuffle test (seconds)p_value_threshold: p-value threshold for both SI and stability significancesi_weight_mode:"amplitude"(event amplitudes) or"binary"(event counts)stability_splits: list of block counts for the stability tests (e.g.[2, 10]).n=2is a classic first/second half;n>=4interleaves odd/even blocks so session-long drift averages out.block_shift: fractional offset applied to the block boundaries of all stability splits (0 = no shift)min_events: minimum speed-filtered event count to run SI + stability tests. Units below the threshold keep a rate map but getp_val=1.0. Set to 0 to disable; typical calcium-imaging values are 20–50.place_field_threshold: fraction of peak rate for place field extensionplace_field_min_bins: minimum contiguous bins for a place field seedplace_field_seed_percentile: percentile of shuffled rates for seed detection
Configuration Reference#
Data Paths Config#
A per-session data config has two optional top-level blocks: neural: and behavior:. At least one must be present. Omit neural: for behavior-only sessions or behavior: for neural-only sessions; the pipeline gates each step on which side is configured.
arena data_paths.yaml
neural:
path: path/to/neural
timestamp: path/to/neural_timestamp.csv
behavior:
type: arena # 'arena' for 2D open-field, 'maze' for 1D arm analysis
fps: 20.0 # Behavior camera sampling rate (Hz)
position: path/to/behavior_position.csv
timestamp: path/to/behavior_timestamp.csv
bodypart: LED # DLC bodypart name for position tracking
maze data_paths.yaml
neural:
path: path/to/neural
timestamp: path/to/neural_timestamp.csv
behavior:
type: maze
fps: 20.0
position: path/to/behavior_position.csv # raw DLC output (input to detect-zones)
timestamp: path/to/behavior_timestamp.csv
bodypart: LED
x_col: x_pinned
y_col: y_pinned
mm_per_pixel: 1.0
behavior_graph: path/to/behavior_graph.yaml # zone polygons + adjacency graph
zone_tracking: path/to/zone_tracking.csv # zone-detected output (input to MazeDataset.load)
arm_order: [Arm_1, Arm_2, Arm_3, Arm_4]
zone_column: zone
arm_position_column: arm_position
zone_detection:
hampel_window_frames: 7 # Centered window for raw-position Hampel filter (in detect-zones)
hampel_n_sigmas: 3.0 # MAD-scaled threshold (~99.7% Gaussian band)
arm_max_distance: 60.0 # Max px from arm centerline for arm classification
min_confidence: 0.5 # Min zone probability for transition
neural-only data_paths.yaml
neural:
path: path/to/neural
timestamp: path/to/neural_timestamp.csv
# Only load() and deconvolve() run; match_events / compute_occupancy / analyze_units
# require a behavior block and will raise if called.
behavior-only data_paths.yaml
behavior:
type: arena
fps: 20.0
position: path/to/behavior_position.csv
timestamp: path/to/behavior_timestamp.csv
bodypart: LED
# Only load() and preprocess_behavior() run; deconvolve() is a no-op without neural data.
camap reads the DLC scorer name from the CSV header and does not require a fixed scorer string such as 3DMazeTrack or FuzzyTrack.
Analysis Config#
arena_config.yaml
neural:
fps: 20.0
oasis:
g: [1.60, -0.63] # AR(2) coefficients (required, usually overridden by data config)
baseline: p10
penalty: 0.8 # Sparsity penalty (higher = fewer events). Default 0.
s_min: 0 # Minimum event size threshold. Default 0.
trace_name: C_lp
behavior:
type: arena
speed_threshold: 10.0 # mm/s
speed_window_seconds: 0.25
hampel_window_frames: 7 # Centered window for raw-position Hampel filter (arena only)
hampel_n_sigmas: 3.0 # MAD-scaled threshold (~99.7% Gaussian band)
spatial_map_2d:
bins: 50
min_occupancy: 0.025 # Minimum occupancy (in seconds) to include a bin
spatial_sigma: 3 # Gaussian smoothing (in bins) for occupancy and rate maps
n_shuffles: 1000
random_seed: 1
event_threshold_sigma: 0 # Event threshold in SDs above mean (for trajectory plot only)
p_value_threshold: 0.05 # P-value threshold for SI and every stability split
min_shift_seconds: 20 # Minimum circular shift (seconds) for shuffle test
si_weight_mode: amplitude # 'amplitude' or 'binary'
stability_splits: [2, 10] # One stability test per entry (n=2: classic halves; n>=4: interleaved blocks)
block_shift: 0.0 # Fractional offset of block boundaries (0 = first block starts at frame 0)
min_events: 0 # Skip SI + stability for units with fewer events (p_val=1.0). Typical: 20-50.
place_field_threshold: 0.35 # Fraction of peak rate for place field boundary
place_field_min_bins: 5 # Minimum contiguous bins for a place field
place_field_seed_percentile: 95 # Percentile of shuffled rates for seed detection
maze_config.yaml
neural:
fps: 20.0
oasis:
g: [1.60, -0.63]
baseline: p10
penalty: 0.8
trace_name: C_lp
behavior:
type: maze
speed_threshold: 25 # mm/s (note: maze Hampel lives in data config zone_detection block)
speed_window_seconds: 0.25
spatial_map_1d:
bin_width_mm: 10
min_occupancy: 0.025
spatial_sigma: 2
n_shuffles: 1000
p_value_threshold: 0.05
min_shift_seconds: 20
si_weight_mode: amplitude
stability_splits: [2, 10] # One stability test per entry (n=2: classic halves; n>=4: interleaved blocks)
block_shift: 0.0
min_events: 0
split_by_direction: true
require_complete_traversal: false