Changelog¶
Changelog¶
Unreleased¶
Added¶
-
Give singular rate results the same metric tables and labeled xarray Dataset export as population results, including GAM slices, unit identity, frame and recording-window metadata. Shared table builders/classifiers retain the established statistics and thresholds without wrapping singular GAM results in artificial populations.
-
Preserve NWB units' per-unit
obs_intervalsand their shared acquisitionspike_window, including subset selection and lazy-spike reads. Population analyses can use this intersection explicitly; absent coverage stays None and disjoint coverage stays an empty window. -
Build fitted spatial decoders from
SpatialRatesResultwithBayesianDecoder.from_rates, carrying the environment, precision, unit identity and training spike-window metadata. Generated labels remain positional; caller-supplied labels align by identity, including direct constructors, indexing and dataclass replacement. Prediction preserves the existing warning and likelihood behavior for non-finite maps. -
Accept
BayesianDecoder.fit(unit_ids=)and preserve caller-supplied unit identity for prediction alignment. Labels supplied alongside a spike group must match its labels and order; the shared resolver rejects duplicates. -
Snapshot all public namespace names and function signatures in one sorted, reviewable file, with a unified diff and explicit regeneration command for intentional API changes.
-
Give lazy root exports concrete static types for IDEs and type checkers, and verify each headline class/function with a CI typing contract without eager runtime imports.
-
Expose spatial rate computation/results, position decoding/results and peri-event histogram/results lazily at the root, alongside core spatial types and domain namespaces.
-
Add opt-in
criterion="shuffle"to all five raw-array cell predicates, with explicit stream labels and p-value levels. Mode-specific keywords raise together when they would be ignored; result methods and batch classifiers remain threshold screens without retained inputs. -
Add five raw-array circular-shift significance functions for place, head-direction, spatial-view and both object-vector frames. Shared valid-window masks, copied inputs and label-keyed random streams preserve recording coverage and seeded single/population agreement.
-
Add seeded circular spike-time shifts over joined valid recording windows. Shifts preserve retained spike counts and compressed-clock circular spacings without placing spikes in gaps.
-
Add
is_egocentric_object_vector_cellfor heading-relative tuning. Breaking:is_object_vector_cellnow measures allocentric direction and no longer acceptsheadings; callis_egocentric_object_vector_cellfor the previous heading-relative verdict; each frame predicate forwards its encoder options and retains the existing information threshold and error behavior. -
Add
compute_object_vector_rate(s)for allocentric animal-to-object direction alongside the egocentric encoders. Both use the shared polar binning, smoothing and recording-window rules, and record a requireddirection_frameon singular and population results.
Changed¶
-
Breaking: video and HTML animation exports default to
overwrite=False, refusing existing outputs before rendering or dry-run estimates. HTML checks its default filename and nonempty frames directory before creating directories; ffmpeg uses-nunless overwrite is explicit. Environment saves share the same paired-target check and corrected-call wording. -
Breaking: rate summary tables lead with rate/information, place classification last, and carry estimator, physical units, direction frame and resolved classification thresholds in
DataFrame.attrs. Singular/population columns match; constantmethodis no longer a summary column, and labels are documented as heuristic screens. -
Breaking: population rate summaries name
n_unitsandmax_peak_firing_rateexplicitly. Shared occupancy remains seconds observed once; singular summaries add cheap family metrics and stay safe for all-NaN maps without computing grid/border scores. -
Breaking: NWB position, head-direction and units readers return frozen, non-iterable
NWBPosition,NWBHeadDirectionandNWBUnitsholders. Position holders expose normalized declared physical units without a second scaling step or an assumed unit; eager/lazy reads retain their existing conversion and lifetime contracts. -
Breaking: simulation validation and summary plotting each take a required
SimulationSessionwith keyword-onlyunit_idsselection by label. The raw keyword validation form and row-index selection keywords are removed; unknown labels explain the available labels and corrected call. -
Breaking: align
SimulationSessionattributes with analysis inputs:spike_times, integerunit_ids, and ground truth keyed by unit label. The frozen holder checks that spikes, labels and models align one-to-one; seeded trajectory, spike and duration behavior is unchanged. -
Breaking: require tracking positions in spatial rate computation, decoder
fit/score, anddecode_session(_summary). Session decoding always encodes tracking and no longer acceptsencoding_models=; estimator predictions take only spikes and array timestamps and reuse the extracted gap-aware full/streamed model decoders. -
Breaking: standardize required trajectory pairs as
(times, positions)inapproach_rate,compute_decision_analysis,compute_goal_directed_metrics,compute_path_efficiency,compute_pre_decision_metrics,compute_vte_session,compute_vte_trial,extract_pre_decision_window,goal_bias,head_sweep_from_positions,instantaneous_goal_alignment,mean_square_displacement,pre_decision_heading_stats,pre_decision_speed_stats,segment_by_velocity,time_efficiency,heading_from_velocity,visibility_occupancyandgenerate_population_spikes. Placeenvfirst incompute_region_coverage,field_shape_metrics,rate_map_coherence,map_points_to_bins,shuffle_place_fields_circular_2dandcalibrate_video. Rename the spike parameters onbin_spikes_in_time,population_peri_event_histogram,restrict_spike_trainsandvalidate_simulationtospike_times. Analysis entry points take explicit arrays rather than aPositionLikeadapter. -
Breaking: rename the phase-precession callable to
compute_phase_precession, preserving the siblingencoding.phase_precessionmodule for normal imports. -
Breaking: rename place-field detection to
has_place_field;is_place_cellnow requires a classification criterion. Align free predicates, result methods and batch screens on read-only threshold defaults and inclusive cutoffs, make method thresholds keyword-only, rename spatialclassify(min_spatial_info=)tomin_info, and propagate invalid-input errors. -
Breaking: default
ObjectVectorCellModelto allocentric direction tuning without headings, which changes simulated spikes, without an error, for code that relied on heading-relative tuning. Setdirection_frame="egocentric"to retain heading-relative behavior; model ground truth records the frame and missing-heading errors explain both choices. -
Plot object-vector tuning in its recorded frame: allocentric zero points East and positive angles turn toward North; the corrected egocentric ahead/left orientation is retained. Frame-specific literature and direction conventions are explicit.
-
Rename object-vector results to
ObjectVectorRateResultandObjectVectorRatesResult, and their information accessor tospatial_information(). Removed names have no aliases; indexed population results preserve their frame.
Fixed¶
- Unit labels of mixed types keep their types:
unit_ids=[1, "u"]no longer turns1into the string"1". Result keys, indexed results, labelled spike groups and shuffle random streams all use the label as given, so a unit's seeded null is the same in single and population calls. - Behavior change: spatial rates use only intervals whose two tracking samples both lie inside the environment. An interval ending in a dropout (NaN) or outside was previously kept in the occupancy while its spikes were dropped, biasing rates low (20% isolated NaN frames read a 5 Hz unit as 3.97 Hz); it is now excluded from spikes and occupancy alike, which recovers 5 Hz without imputing positions.
- The all-intervals-excluded, spikes-outside-tracking, out-of-bin and
min_occupancy-masks-all warnings point at the user's calling line instead of a line inside neurospatial, for every rate family. object_vector_cell_significanceandegocentric_object_vector_cell_significancebuild the polar grid and each frame's polar bin once per call instead of once per shuffle: about 3x faster (0.24 s to 0.08 s for 10 units, 20 shuffles, 60,000 samples) with identical results.- Kinematic speeds treat a zero-length interval (duplicate timestamps) as
undefined (NaN) instead of infinite, so
pre_decision_speed_stats, goal approach rates and heading labels no longer absorb aninfor a divide-by-zero warning. - Kinematic speeds from 1-D (linear-track) positions are the track speed.
pre_decision_speed_statspreviously broadcast a 1-D difference against the time steps and returned an unrelated number. - Breaking:
Environment.bin_center_ofraisesBinIndexOutOfRangeErrorfor indices outside[0, n_bins), like the graph queries. Negative indices previously wrapped silently to the last bins, and indices past the end raised NumPy'sIndexError. DecodingSummary.plotbreaks its entropy and MAP lines at recording gaps instead of drawing a straight segment across the pause.- neurospatial exceptions survive pickling, so errors raised in worker processes reach the caller with their original message and attributes; previously several failed to unpickle or repeated their message.
SimulationSessionchecks thattimesandpositionshave one row per sample and thatground_truthis keyed by exactly theunit_ids, so relabeling units withdataclasses.replacemust re-keyground_truthtoo. Duplicateunit_idsare rejected, since two cells sharing a label would both be validated against one ground truth.NWBUnitschecks thatunit_idsandobs_intervalsalign withspike_times.ObjectVectorRateResultandObjectVectorRatesResultreject adirection_frameother than"allocentric"or"egocentric"; plots previously drew any other value as egocentric.spatial_view_cell_significancecomputes the gaze geometry once per call instead of once per shuffle. Withgaze_model="ray_cast"on 3,000 samples, 10 shuffles drop from 6.95 s to 0.64 s with identical observed scores, null scores and p-values.- Object-vector and egocentric
method="binned"rates keep bins whose occupancy equalsmin_occupancy, as every other rate path does, and warn whenmin_occupancymasks every occupied bin. ShuffleTestResultstoresnull_scoresas a read-only copy, and its p-value documentation names the finite-null denominator the significance functions use.DecodingResult(...)checks that the posterior is 2-D with one column perenvbin and thattimeshas one entry per posterior row, instead of silently mapping columns to the wrong bin centers. Thetimeschecks thatto_xarray()used to apply now fire at construction.- Smoothing,
gradient,divergenceandheat_kernel_wavelet_basison a non-grid environment read from NWB explain that the round trip does not restore cell geometry and name the original layout, instead of advising a factory method the user never skipped. BayesianDecoder.fit(and the session decoders that build an encoding model) raiseValueErrorwhen the speed, gap, window andmin_occupancygates leave no occupied bin, instead of returning a model that decodes every time bin as a uniform posterior.-
read_unitsreads files in which a unit has noobs_intervalsrows (a unit that was never observed); that unit's coverage is an empty(0, 2)array, so the sharedspike_windowis empty unless it is excluded withunit_ids=. -
Center continuous posterior image columns on decoder timestamps, preserving MAP/actual overlays, gapped index clocks and explicit extent overrides. Two-column plots use their timestamp spacing; a single column has a documented 1-second display width, with no changes to posterior arrays or clocks.
-
Plot native 1D rate maps as labeled lines over physical bin coordinates, including singular/population results and NaN breaks. Existing graph-track and 2D plotting paths are retained; rate arrays are unchanged.
-
Name the current
spike_timeskeyword in simulation validation's missing-input diagnostic. -
Name nonnumeric times/positions together in shared conversion errors, retaining shape/order problems from the convertible argument and providing Why/Fix guidance without changing valid arrays.
-
Share one swap-aware times/positions validator across encoding and environment trajectory APIs. Timestamp shape, finiteness, ordering and sample-count problems are reported together with a corrected call; missing position samples remain supported.
-
Explain invalid circular-shift windows with a valid
windows=call, preserving all row/shape problems from shared interval normalization. -
Resolve heading interpretation thresholds through
HEAD_DIRECTION_THRESHOLDS, matching the predicate and batch screen while preserving the default text and positional option. -
Align place shuffle windows with inferred speed gates and 1-D trajectory normalization, preserving retained null spike counts. Validate object-vector metric choices before coordinate construction in both frames, with shared public diagnostics.
-
Validate required environments before constructing place/view shuffle windows, so missing environments receive the encoder diagnostic and a corrected call. Object-vector Euclidean significance continues to accept
env=None. -
Normalize allocentric object bearings exactly like zero-heading egocentric bearings at direction-bin boundaries, and make invalid-environment guidance omit headings for allocentric calls. Result help consistently describes the recorded frame.
-
Require the documented spatial-information threshold for every place, grid and border label. In the seeded 10-minute noise example, erroneous border labels fall from 19/20 to 0/20; label precedence and threshold values are unchanged.
-
Honor requested duration in lap-based sessions, including linear-track and T-maze conveniences. Keep every one-way traversal and fixed pause on a shared half-open recording clock, derive traversal speeds from available time, and reject infeasible durations with guidance. Direct speed-driven lap trajectories and other simulation methods are unchanged.
Fixed — documentation¶
-
Document singular/population table parity, readable column order, units/threshold metadata and singular xarray identity. The quickstart covers native 1D rate plots; animation guidance explains explicit overwrite opt-in and the current HTML numeric-colorbar limitation with an executable static-scale/timestamp-label recovery. Posterior plotting help records the centered continuous and short-clock edge conventions.
-
Include required unit labels in the manual
SimulationSessionconstructor example. -
Teach explicit simulator/NWB holder attributes and a complete file → selected epochs → population fields → decoder → summary/overlay workflow. A synthetic HDF5 example executes the published NWB recipe after file close with physical units, nondefault unit IDs and two analysis epochs; synchronized tutorials preserve the simulation-duration contract.
-
Preserve the existing shared-validator NumPy examples and parameter sections while adding timestamp/position diagnostics.
-
Teach explicit tracking arrays, typed precomputed-rate and count-array decoding handoffs, and one retained event cohort across gap-aware PSTHs, rasters, regressors and positioned tables. Keep event identifiers and selection masks visible, explain each helper's existing edge closure, and synchronize the updated tutorials.
-
Correct both decoder tutorial posterior overlays to use spatial bins and the MAP line's plotted time coordinates. Synchronized examples and an executable continuous/gapped-clock recipe preserve physical coordinates for accuracy metrics and separate position plots.
-
Explain graph linearization as geometric coordinates rather than direction separation. The synchronized track tutorial and an executable graph/trial recipe use explicit direction labels and recover separate planted field centers within one bin.
-
Distinguish standardized assembly activity and EV/REV effect sizes from calibrated significance, document controlled REV correctly, and explain that selected dimensions can have no thresholded core members. Statistical calculations are unchanged.
-
Fix the README's simulated-field peak using a complete 5 cm sampling grid and the result's NaN-aware peak lookup. Reference examples use canonical graph, visibility, region, and immutable-data APIs; expected gotcha errors are checked with markers.
Documentation¶
-
Fix renamed phase-precession imports in executable examples and move the spatial adapter test normalizer import to its owning private module and distinguish independently standardized assembly activation from reactivation strength on a shared template-normalized projection scale.
-
Update root/domain imports and hard-break migration guidance, and make population covariance APIs discoverable from the API index. Add an executable simulation → encoding recipe with separate decoding and population-statistics branches, one shared unit selection and explicit baseline-controlled EV/REV interpretation.
-
Document recording-window metadata on the spatial result classes now exposed at the root; their existing GLM diagnostics and constructor fields are covered by the public docstring checks.
-
Clarify that the head-direction MVL screen weights firing rates and its Rayleigh test weights spike counts, with a rate-times-occupancy fallback. The numerical Rayleigh calculation is unchanged.
-
Update cell-criterion migration, README/quickstart, API references, glossary and neuroscience guidance. Execute six new flagship docstrings and synchronize the view, heading and object-vector tutorials; the object-vector tutorial reports its estimator/criterion and adds seeded shuffle results alongside an explicit place-cell control.
-
Document finite-count information bias and the measured noise-screen results in every cell predicate, field detector and batch classifier. Explain occupancy bias in Rayleigh tests weighted by spike counts, circular-shift assumptions and the need for calibrated publication verdicts; retain the uniform-spike example and show its shuffle rejection.
-
Teach allocentric and egocentric object-vector analysis separately in the guides, migration notes, glossary and synchronized tutorial. Public examples record the frame and use the appropriate heading-free or heading-required call; executable docstrings cover the new APIs.
-
Document marker-based pytest checks and current first-run calls. Publish the changelog from one canonical source, preserving copy-only notes and consolidating duplicate release headings.
Removed¶
-
Breaking: remove
Session,load_sessionandneurospatial.recording, plus the unusedPositionLiketrajectory adapter andvalidate_simulation's raw keyword form. NWB component readers and simulator holders expose arrays for explicit analysis calls; pynapple conversion still returns(times, positions). -
Breaking: remove the image-mask
bin_sizekeyword shim and both view-resultpeak_view_locationmethods. Usepixel_sizeand the sharedpeak_location()/peak_locations()accessors. -
Breaking: remove root decoder/binner, spike-container and epoch-selection re-exports; import them from their domains. Drop public normalizers, event validators, animation internals/protocols, duplicate calibration and renderer helpers, and cache plumbing. Delete the probability-space Poisson wrapper,
integrated_absolute_rotation,Affine3D,SpikeOverlayand surrogate compatibility imports. -
Remove the superseded public API curation plans and the test pinning the old root export list; the public API snapshot replaces that export contract.
-
Remove the deprecated
detect_cell_types,detect_hd_cells,detect_view_cellsanddetect_ovcsmethods; uselabel_cell_typesorclassify. -
Remove the deprecated region-crossing positional order. Use
detect_region_crossings(position_bins, times, env, region_name="home");region_nameis required and keyword-only.
Added — executable documentation¶
-
Require function inputs to be documented under Parameters or Other Parameters; only class constructors may also use Attributes.
-
Install widget support in the headless module-doctest CI job so the newly executable widget example runs there too.
-
Execute Markdown examples using adjacent
docs-testmarkers, cumulative reader state for README/quickstart, fresh seeded blocks for reference fragments, and restored animation setup patches. Replace the indexed snippet manifest and subprocess helper with default pytest coverage. -
Execute flagship docstrings, including NWB geometry and position examples, with only display/video calls skipped. Module doctests write outputs in temporary directories; the NWB workflow enforces its examples separately.
-
Check public docstring sections and constructor inputs, and complete the missing examples and parameter documentation.
Changed — errors that teach¶
-
Corrected-call guidance includes each encoder's and decoder's required arguments and all animation backends. Population egocentric validation reports coordinate and length problems together, and malformed peri-event window bounds fail with guidance before alignment.
-
Region fixes show list-valued
end_regionsand handle missing names without a conversion error. Zero-rate posterior errors distinguish all-negative-infinity likelihoods from corrupt NaN inputs. -
Warn visibly when
bin_sizecovers every data axis or a peri-event window exceeds 60 seconds, with units and corrected values. Large-grid warnings are nowUserWarninginstead of hiddenResourceWarning; the existing hard memory ceiling is unchanged. Length-1 bin-index arrays explain how to unwrapbin_atoutput before a graph query. -
First-run factory, graph-query, encoding, decoding, peri-event, animation and file errors give concrete corrected calls. Segmentation and boundary regressors raise
RegionNotFoundErrorwith the caller's region argument and available names; NWB readers and direction-label lookups name their corrected arguments. -
Replace
HOW:labels withFix:in diagnostic messages. Shared timestamp, spike and trajectory validators name their caller, collect all input problems, detect swapped arrays, and check coordinate dimensions before binning. A non-Environment first argument now raisesTypeErrorwith a corrected call. -
Add
NeurospatialErroras the common base for library exceptions while retaining their standard Python bases.RegionNotFoundErroralso supportsexcept ValueErrorand prints an unquotedFix:line with a suggested region name. - Breaking: raise
BinIndexOutOfRangeErrorfor graph-query bin indices (formerlyIndexError; it subclassesValueError, notIndexError, soexcept IndexErrorno longer catches it),LayoutNotBuiltErrorfor unbuilt layouts, andIncompatibleEnvironmentErrorfor mismatched environment dimensions or decoding bins.
Changed — behavior analyses respect recording gaps¶
- Timestamp-based heading rejects malformed position shapes before velocity division, preventing a 1-D trajectory from broadcasting into a square matrix. The error names the shape and supplies an aligned x/y-coordinate fix.
- Timed home ranges omit bins with zero observed dwell, including at
percentile=100, so rounding in cumulative percentages cannot include an isolated sample's bin. Untimed visit counts retain their previous meaning. - The 13 kinematic/event entry points expose
max_gap=0.5andepochs=None:heading_from_velocity,pre_decision_heading_stats,pre_decision_speed_stats,head_sweep_from_positions,heading_direction_labels,compute_path_efficiency,instantaneous_goal_alignment,goal_bias,approach_rate,compute_goal_directed_metrics,compute_trajectory_curvature,compute_home_range, andevents.add_positions. Pre-decision and VTE composites use their existing window keywords for kinematics too. Examples and notebook pairs now pass timestamps toheading_from_velocity. - Speed statistics, approach rates, direction labels, head sweeps, trajectory
curvature and home-range dwell respect
max_gapandepochs; goal and VTE composites forward the same windows. IdPhi sums runs without joining their headings, and the two-epoch dwell totals 200 seconds instead of charging the pause to a bin. Entirely excluded speed windows return NaN without reduction warnings. Samples outside runs have NaN curvature and stationary labels.compute_path_efficiencyreports NaN traveled length, efficiency and angular efficiency when any interval is unobserved. Shortest length and wall-clock time efficiency/time-to-goal retain their existing meanings. - Breaking:
heading_from_velocity(times, positions)replaces scalardt, validates finite, strictly increasing aligned timestamps, and computes velocity, smoothing and circular interpolation separately per observed run. The measured 1,911.44 cm/s teleport no longer produces a heading across the pause. Isolated samples and runs without moving anchors have NaN headings. Shared heading statistics, goal alignment and simulation callers use actual timestamps. Smoothing bandwidth retains its existing units of samples. events.add_positionsacceptsmax_gapandepochs, interpolating only inside closed observed-run spans. Events in pauses, on isolated samples or outside the tracked span now receive NaN coordinates; extrapolation ends. Events exactly at either observed run endpoint retain that sample's position.- Kinematic interval velocity masks unobserved intervals with NaN, preserving interval alignment and position-units-per-second measurements.
- All 16 segmentation and sequence entry points now expose keyword-only
max_gap=0.5andepochs=None:detect_region_crossings,detect_runs_between_regions,segment_by_velocity,detect_laps,segment_trials,detect_goal_directed_runs,running_direction_labels,detect_boundary_crossings,extract_pre_decision_window,compute_pre_decision_metrics,compute_decision_analysis,compute_vte_trial,compute_vte_session, andEnvironment.bin_sequence,bin_sequence_with_runs,transitions. Usemax_gap=Noneto disable the gap gate for intentionally coarse sampling;epochsstill applies. - Position-only analyses warn when
max_gaporepochsexclude every interval, for example tracking sampled more slowly than 2 Hz under the defaultmax_gap=0.5. The warning names the active gate, reports the median sampling interval and points at the calling line, so an empty or all-NaN result is no longer indistinguishable from a measurement. - Pre-decision extraction, metrics, composite decision analysis and trial/session
VTE accept
max_gapandepochs. Pre-decision samples come only from the observed run containing the entry time; entries outside every run return an empty window. The pause example retains five post-resumption samples instead of ten spanning 1000.9 seconds. Session VTE still respects the trial start. - Runs, trials, velocity epochs, laps, goal-directed candidates and decision
boundary crossings accept
max_gapandepochsand analyze each observed run separately. The measured 98.5→1100 s run and 0→1100 s trial are no longer marked successful across a pause, the 98.1→1100 s movement epoch is clipped, and the 88.6→1108.6 s region lap and 599.95 s boundary crossing disappear. Auto laps keep the first 10% of the whole input as their template. Running direction labels forward the same windows and retain one label per sample. detect_region_crossingsacceptsmax_gapandepochs, and detects entries and exits separately within observed runs. A first post-pause target sample no longer invents an entry at 1100 s. The contiguous detector keeps its existing crossing timestamps and region-boundary handling.Environment.bin_sequenceandbin_sequence_with_runsacceptmax_gapandepochs, drop samples that touch no observed interval (including singleton inputs), and split same-bin runs and deduplication at recording breaks. Original run indices and lengths stay aligned with the input. Empiricaltransitionsgates every intervening interval in a lagged pair; the two-epoch fixture gives 9,998 one-step and 9,994 lag-three pairs. Raw bin-only transitions retain their previous behavior without a time gate.- Empirical transitions return a zero CSR matrix when every pair is excluded by gaps or epochs, including lagged pairs under the default adjacency filter.
Changed — decoding and peri-event histograms respect recording gaps¶
- Peri-event counts use original spike times against absolute event-shifted edges, preserving inclusive starts and exclusive stops at nonzero and Unix timestamps. The shared precision guard rejects widths that the absolute clock cannot represent, with an origin-shift fix for spike/event times.
- Peri-event histograms share the decoding edge and count helpers: every bin
is half-open and stays inside its requested window. Spikes at the last
whole-bin edge are excluded, and large timestamp offsets use relative
rounding instead of an absolute
1e-9epsilon. - Conflicting
epochsand explicit bounds inbin_spikes_in_timereport the argument conflict with separateWhy:andFix:lines. - Single-unit and population peri-event histograms accept
epochsandspike_window, keeping only events whose entire analysis window is observed. Means and SEM use retained events, and results, child results, plots and summaries carryn_events_dropped. With neither window supplied, all events remain eligible; partial drops add no warning. - Full and streamed decoding results record
spike_windowandspike_window_assumed, including summaries and NetCDF-compatible xarray attributes. Attaching session metadata preserves the single posterior allocation; public construction and replacement still copy input arrays. - Posterior heatmaps with detected recording breaks use time-bin indices and dashed gap markers, preserving visible separation instead of stretching bins across a pause. Contiguous and single-bin results keep time axes; two timestamps alone cannot distinguish a gap from a larger bin width.
- Breaking:
BayesianDecoder.fit(epoch=...)is replaced by keyword-onlyepochsandspike_window. Training masks the original arrays instead of concatenating epoch slices, preserving aligned speed samples and unit identities.predict,predict_summary, andscoreforward the same window keywords and the decoder's configuredmax_gapto observed-run decoding. decode_sessionand its streamed summary acceptepochsandspike_window, and form bins only within observed sample runs. No posterior is invented inside a tracking pause. Decode bins retain immobility and out-of-bounds periods; speed/bounds restrict encoding only. Full and streamed paths use the same half-open edges and counters, including run breaks.bin_spikes_in_timeacceptsepochs=and tiles each normalized window independently. Shared decoding helpers count only half-open bins and clamp their edges to window stops. Final-edge spikes are no longer counted, and floating-point rounding uses timestamp-relative slack instead of an absolute1e-9epsilon (which lost a bin near1e9seconds). Partial trailing bins are dropped, and unrepresentable bin widths raise with an origin-shift fix.
Changed — recording gaps and time windows in rate maps¶
- The three frame-family cell predicates propagate invalid
epochsandspike_windowdiagnostics, including combined argument problems andWhy:/Fix:guidance, instead of silently classifying malformed inputs asFalse. Each analysis still normalizes the windows once. - Directional, view and egocentric encoders record the normalized
spike_windowon singular, population and zero-unit results. Population indexing/iteration preserves it, and xarray exports include the assumption as an integer and explicit windows as a flat array for NetCDF round-trips. compute_directional_rates,compute_view_rates, andcompute_egocentric_ratesshare the population-silence heuristic: with nospike_window, at least five units silent for at least 60 seconds within a tracked run produce one warning. The scan respects gaps and epochs, includes leading and trailing silence, and ignores invalid directional, view or polar bins; silence is not proof of recording coverage.compute_directional_rate(s),compute_view_rate(s),compute_egocentric_rate(s),is_head_direction_cell,is_spatial_view_cell, andis_object_vector_cellaccept keyword-onlymax_gap,epochs, andspike_window. They parse windows once and apply one shared analysis mask across units, including empty populations. Directional, view and egocentric rates now recover a 5 Hz synthetic rate across a 1000-second recording pause instead of charging the pause to a single bin. In an unsmoothed example, pooled directional/egocentric rates rise from 0.833 Hz to 5.001 Hz and view rates from 0.717 Hz to 4.916 Hz; no bin absorbs the 1000-second pause.- Directional, view and object-vector rates warn, like spatial rates, when
the gates exclude every interval (for example
epochsin milliseconds) or when more than half of the spikes fall outside the tracked time (for examplespike_timesin milliseconds), instead of silently returning an empty or truncated map. - Directional, view and egocentric binning helpers use one interval mask for
both spike counts and occupancy. Their default
max_gap=0.5excludes recording pauses instead of charging them to one bin, and their normalizedepochsandspike_windowkeywords restrict the same intervals on both sides. Behavior change: a spike attimes[-1]is no longer counted; the final sample starts no half-open interval. - Malformed numeric attributes on IntervalSet-like windows now report the
window argument, conversion problem,
Why:andFix:together, without hiding invalid values in the other time-window argument. compute_spatial_rateswarns once when at least five units are all silent for at least 60 seconds of continuously tracked time and nospike_windowwas supplied. This recording-outage heuristic includes leading and trailing silence and rest, but never combines silence across untracked pauses; an explicit window suppresses it. Its absence does not prove coverage.- Every rate result exposes
spike_windowand the read-onlyspike_window_assumedflag insummary(). Spatial encoders record the normalized acquisition windows, preserving them when indexing a population and in direction-conditioned place fields. Spatial population xarray exports store the assumption as an integer and supplied windows as a flat array, so both round-trip through NetCDF. Frame-family encoders now record their acquisition windows as well. - Breaking:
behavior.in_epochs,restrict, andrestrict_spike_trainsshare the same window parser as spatial analyses. Nested sequences always describe(n, 2)rows; parallel(starts, ends)arrays are no longer an input form, and a tuple or list of two 1-D NumPy arrays raises (namingnp.column_stack([starts, stops])) instead of being read as two rows. Overlapping or touching rows merge, zero-width and empty window sets raise with a fix, andepochs=Noneleaves data unrestricted. Point membership still uses the requestedclosed=setting on the normalized windows. - Spatial rate functions,
is_place_cell, andcompute_directional_place_fieldsacceptepochs=andspike_window=. One shared analysis mask excludes the same intervals from spike counts and occupancy, including GLM and empty populations. Direction-labelled runs use epochs instead of concatenated samples, removing artificial occupancy across breaks (1.6 s instead of 1.9 s in a repeated-label example). - Behavior change: with
max_gap=None, intervals starting outside the environment now exclude their spikes as well as their occupancy. A spike exactly at the last position timestamp,times[-1], lies outside all half-open sampling intervals and is no longer counted; it is reported as time-dropped. Environment.occupancyacceptsepochs=as half-open windows in seconds. It excludes intervals that cross window boundaries for both start and linear time allocation, interval counts, and smoothed occupancy.
Fixed — numerical and I/O corrections¶
-
Behavior change:
pre_decision_heading_statsnow excludes samples belowmin_speedusing the same uninterpolated velocity and speed as goal alignment. On an east/stop/north path, circular variance and resultant length are 0.2929 and 0.7071 (formerly 0.1849 and 0.8151 from stationary headings). The all-stationary result remains(0.0, 1.0, 0.0). -
Behavior change:
instantaneous_goal_alignmentnow returns NaN for samples belowmin_speed, andgoal_biasaverages moving samples only. On an east/stop/north path, all 30 stopped samples are excluded andgoal_biasis 0.5055 (formerly 0.5862 from interpolated stationary headings). Fully stationary trajectories still return NaN without raising. -
Behavior change:
heading_from_velocityandheading_from_body_orientationinterpolate missing or low-speed headings along the shorter arc, uniformly in angle. Near a 180° turn, filled headings formerly snapped toward the endpoints (errors up to 0.785 rad) because interpolation followed the chord. Antipodal turns follow the sign of the stored angle difference; unmasked NaN headings no longer contaminate neighboring filled samples. -
Behavior change:
heat_kernel_wavelet_basisnow uses the finite-volume diffusion generator, scaled by squared bin spacing. On uniform Cartesian grids its standard deviation issqrt(2 * scale)bins at every bin size (scale 1 formerly spread 2.77, 3.91, and 6.17 bins at sizes 1, 2, and 5). Layouts without finite-volume geometry now raiseNotImplementedErrorwith a fix.chebyshev_filter_basiskeeps its graph-hop locality and distance-weighted, spectrally rescaled operator. -
Behavior change:
gradient,divergence, andcompute_differential_operatornow use the finite-volume cell geometry ofenv.smooth: gradient divides by edge length, and divergence weights face flux by cell volume with positive values at sources. Forf=x, gradients are 1 at bin spacings 1, 2, and 4 (formerly 1, 2.83, and 8);div(grad(x²+y²))is 4 in grid interiors (formerly -15.31, -122.51, and -980.08). The Laplacian has the continuum sign and units Hz/cm² for rates in Hz and positions in cm. Layouts without finite-volume geometry, including NWB-reconstructed environments, now raiseNotImplementedErrorwith a fix instead of returning physically inconsistent derivatives. decode_position(and every decoder built onnormalize_to_posterior) floored the prior at1e-10, so a bin with zero prior could still win the posterior given a large enough likelihood (for example a MAP in an excluded bin with posterior0.999998). Zero-prior bins now get exactly zero posterior, small positive priors are no longer floored, and thehandle_degenerate="uniform"fallback spreads mass only over bins with positive prior (NaN when the prior has none).theta_phaseused a transfer-function Butterworth band-pass that is numerically unstable for a narrow theta band at typical LFP sampling rates: on a clean 8 Hz sine the phase was off by ~1.56 rad at 2-5 kHz and all-NaN at 10 kHz and above. It now filters with second-order sections (sosfiltfilt); the phase error is below 0.01 rad at 2-30 kHz.circular_linear_correlation,circular_circular_correlationandcircular_basis_metricscomputed p-values as1 - cdf, which cancels to exactly0.0for strong effects (below about1e-16). They now use survival functions, so a strong effect reports its true tiny p-value (for exampleexp(-500)for a perfect circular-linear correlation over 1000 samples).Environment.from_grid_mask(and everyMaskedGridLayout, includingenv.subset) accepted nonuniform, decreasing, duplicate or non-finitegrid_edges, then reported wrong bin sizes (for example[-1, -1]or[0, 0]) and used one width per axis for cell volumes and diffusion. It now raisesValueErrornaming the axis unless the edges are finite, strictly increasing and uniformly spaced (to within float rounding), and raises when coordinates are so large that float64 cannot resolve the bin width, with a fix to subtract an origin offset.- Behavior change:
BayesianDecoder.predict,predict_summaryandscorepassed spike trains to the encoding models by position even when both the fit and the predict input were labelled, so a reordered or different pynappleTsGroupsilently paired each unit's spikes with another unit's model. When both the fit (a labelled group, orunit_ids=at construction) and the predict input carry labels, trains are now matched by label in any order, and a label mismatch raisesValueErrorlisting the missing and unexpected labels. Otherwise trains are still paired by position, and a unit-count mismatch now raises aValueErrorthat says so (previously a Poisson likelihood shape error). A predict input that repeats a label raises. compute_directional_rates,compute_view_ratesandcompute_egocentric_ratesmis-read a pynappleTsGroup(it was coerced as one train, giving one unit labelled0). They now accept it likecompute_spatial_rates: one rate map per unit, with the group's index asunit_ids.- Behavior change: passing
unit_ids=together with a labelledTsGroupsilently replaced the group's labels, so incompute_spatial_ratesa group keyed[10, 20]withunit_ids=[20, 10]attached unit 10's spikes to label - All four population encoders now raise
ValueErrorlisting both label sets unlessunit_idsequals the group's index. - Behavior change: repeated
unit_idswere accepted (for examplecompute_spatial_rates(..., unit_ids=[5, 5])). Every place that takesunit_idsnow raisesValueErrornaming the repeated labels: the population encoders,population_peri_event_histogram, theBayesianDecoderconstructor andfit(fitted labels) andpredictinput labels, and population result constructors (includingsummary_table(unit_ids=)relabelling).SpikeTrainsalready rejected them. - Behavior change:
to_pynapplepassed unsorted timestamps to pynapple, which sorts the times without reordering the values, soto_pynapple([0, 2, 1], [10, 20, 30])returnedt=[0, 1, 2],d=[10, 20, 30](each value paired with another sample's time). It now raisesValueErrorfor unsorted, repeated or non-finite timestamps, naming the first offending indices, and for acolumnslist whose length does not match the value columns (pynapple silently renamed them0, 1, ...). These checks run before pynapple is imported. DirectionalRatesResult.to_xarray()wroteattrs["bandwidth"] = Nonefor unsmoothed results (bandwidth=None), soDataset.to_netcdf()raisedTypeError. The attribute is now omitted when no smoothing was applied, as for spatial results.read_environmentestimated the bin sizes of reloaded non-grid environments (graph tracks, hexagonal, triangular mesh) from the nearest neighbour's index instead of its distance: a Y-track with 2.94 cm² bins came back with 1640.25. The estimate now uses the median nearest-neighbour spacing.- Writing a non-grid environment (graph track, hexagonal, triangular mesh) to
NWB and reading it back lost its geometry: bin sizes were re-estimated
(2.94 became 8.68 on a Y-track),
grid_edgescame backNone, and every edge's directionvectorwas reversed (81 of 81 on the Y-track). The NWB environment schema is now 1.1: it stores the exact bin sizes and graph grid edges, and writes each edge so its vector round-trips. Schema 1.0 files still read, with estimated bin sizes. - Behavior change:
DecodingResultkept a reference to the caller's posterior and cachedmap_estimateand the other derived properties, so editing that array in place (or reassigningresult.posterior) left the cached values stale (for example a MAP of bin 0 whileposterior.argmaxwas bin 1).DecodingResultis now frozen, and its constructor stores read-only copies ofposteriorandtimes. Usedataclasses.replace(result, posterior=new)for a modified result.decode_positionhands over its freshly computed posterior without a copy. - Behavior change:
to_linearreturned wrong positions on graph environments whoseedge_orderdiffers fromgraph.edges()order, such asEnvironment.maze("w", ...): on a W maze with 50 cm segments,(75, 0)on the base mapped to 175 instead of 75 and(0, 40)on the left arm to 114.03 instead of 140, and about 38% of on-track points disagreed withbin_at.track_linearizationlooks a point's nearest segment up by itsedge_idattribute but finds it by its position ingraph.edges(). Graph layouts now linearize a copy numbered ingraph.edges()order, soto_linearreturns along-track distance forfrom_graph,maze,linear_trackand reloaded environments. Anyedge_idon an input graph is ignored, and the caller's graph is not modified. - Behavior change: polar tuning plots were drawn mirrored relative to the
library's angle conventions.
plot_head_direction_tuningandplot_circular_basis_tuningdrew 0 at the top and clockwise, so a North-preferring (π/2) cell pointed East; they now draw angles as in the arena, 0 = East (right), π/2 = North (up), counter-clockwise, and reset a caller-supplied polar axis to that orientation.plot_object_vector_tuningdrew +π/2 (left of the animal) on the right; it keeps "ahead" at the top and now draws left on the left. - Behavior change:
read_position,read_head_directionandread_posereturned an NWB series' stored values and ignored itsconversionandoffset, which NWB defines as the map tounit(data * conversion + offset). A pixel series stored 0-500 withunit="meters",conversion=0.002,offset=0.1came back as 0-500 instead of 0.1-1.1 m, andenvironment_from_positionbuilt a 500 × 500 environment. The readers now return values in the series'unit(head direction converts before the degree-to-radian step), andlazy=TrueraisesValueErrorfor a series whose scaling is not the identity rather than returning unconverted handles. The NWB overlays inherit the fix.environment_from_positionmaps NWB unit names to theEnvironment.unitsregistry, so pynwb's default"meters"becomes"m"(no unrecognized-units warning), and likewisecm,mmandpx. - Behavior change:
environment_from_positionsilently setenv.units = "cm"when the position series declared no unit (unit=""). It now emits aUserWarningnaming the series and saying to passunits=, then still assumes"cm". Passingunits=skips the lookup and the warning. - Behavior change:
population_peri_event_histogramraisedAxisError: axis -1 is out of boundsfor a pynappleTsGroup, because it iterated the group (which yields unit keys, not trains). It now accepts a labelled group like the population encoders: one PSTH per unit, with the group's index asunit_ids. Aunit_ids=passed with a labelled group must equal its index, or the call raisesValueErrorlisting both. - Behavior change:
compute_vte_sessioncut each pre-decision window from the whole session, so a window reached back past the trial start into the inter-trial interval or the previous trial. A trial starting at 5.0 s that entered the decision region at 5.267 s got the window[4.267, 5.267]and a head sweep of 12.57 rad, all from movement before the trial; the trial's own straight run has 0. Windows are now clipped at the trial start, andwindow_startreports the clipped start. A trial left with fewer than 3 samples before its entry is skipped, now with aUserWarning(it was skipped silently).compute_vte_trialis unchanged. - Behavior change:
method="diffusion_kde"(the default forcompute_spatial_rate(s)andsmooth_rate_map(s)) reported huge rates in bins far from all occupancy: a simulated 50 Hz place cell in a 40 s open-field session got 20,554 Hz in a corner the animal never approached, so the map's peak (and anything using it) landed there. Far from occupancy both smoothed densities fall to the diffusion operator's numerical accuracy (1e-6 of the largest value when its eigenbasis is truncated, float64 roundoff otherwise), and the ratio was noise over noise. Bins whose smoothed occupancy is within 1000 times that accuracy of zero, relative to the largest in their connected component (typically 4-7 bandwidths from any occupancy), are now NaN, like other unsupported bins (fill_value=replaces them). The remaining rates match the exact dense kernel to within 1e-4 of the peak rate at bandwidths 5-20. - Behavior change: the simulators' default place-field width was
3 * mean(env.bin_sizes), butbin_sizesholds per-bin volumes (areas in 2-D), so a 2-D grid with 2 cm bins got 12 cm fields and one with 5 cm bins got 75 cm fields.PlaceCellModel(width=None), and through itsimulate_session,open_field_session,linear_track_sessionandtmaze_alternation_session, now default to 3 × the bin spacing (the median bin length along a track, otherwise the median distance between neighbouring bin centres, which isbin_sizeon a regular grid): 6 cm at 2 cm bins, 15 cm at 5 cm bins. A one-bin environment now raisesValueErrorasking forwidth=. Likewisevalidate_simulation's defaultmax_center_erroris 2 × the bin spacing (4 cm at 2 cm bins, previously 8 cm). - Behavior change:
method="binned"withbandwidth > 0had the same far-field problem: on a corridor visited only at x < 40 cm, with raw rates of at most 50 Hz, it reported 295,658 Hz at x = 104 cm (bandwidth 10) and 237,467 Hz at x = 190 cm (bandwidth 20). Its smoothed rate is a ratio of two diffused sums, and far from every visited bin both are noise. Those bins are now NaN under the same rule asdiffusion_kde; the remaining rates match the exact dense average to within 1e-4 of the peak rate.
Added — method="glm": penalized-Poisson GAM estimator (spatial only)¶
compute_spatial_rate and compute_spatial_rates gain a fourth estimator,
method="glm": a batched penalized-Poisson generalized additive model that uses
the cached finite-volume eigenbasis as its smoothness-penalty basis. Occupancy
enters as a log-offset (never a denominator) and the smoothness penalty λ
is chosen by REML, so the fit returns finite firing rates everywhere —
including the low-occupancy and unvisited bins where the ratio estimators
(diffusion_kde / gaussian_kde / binned) produce NaN.
- New keyword-only params on
compute_spatial_rate(s):penalty(fixedλ ≥ 0;None→ choose by REML) andrank(requested basis rank; out-of-range values are clamped, not rejected, to the effective rank reported viaresult.rank). These are mutually exclusive with the ratio-method paramsbandwidth/min_occupancy/fill_value, which now default toNone(resolving to their historical5.0/0.0/ preserve-NaNbehavior). Combining a param with the wrongmethodraisesValueError. - New result fields on
SpatialRateResult/SpatialRatesResult:coefficients,penalty,penalty_weights,rank,deviance,converged,n_iter,reml_objective(allNonefor the ratio methods).bandwidthisNonefor glm.summary_table()gains the GAM scalar columns for glm results. - Spatial only.
compute_view_rate(s),compute_egocentric_rate(s), and the directional encoders keep their ratio-onlymethodset (no"glm"). - The default
method="diffusion_kde"path is unchanged (byte-for-byte).
Added — method="glm" through NWB persistence and the decoder¶
method="glm" is now usable end-to-end (encode → persist → decode), not just as
a standalone estimator.
- NWB round-trip.
write_spatial_rates/read_place_fieldnow persist a glmSpatialRatesResultfield-for-field: the scalarspenalty/rank/n_iter/converged/reml_objectiveand the(rank,)penalty_weightsvector in the table's JSON metadata blob, anddeviance(n_units,)pluscoefficients(stored(n_units, rank), transposed back to(rank, n_units)on read) as per-unit table columns.bandwidthis stored nullable (Nonefor glm). Ratio-method results round-trip unchanged, with the GAM fieldsNone. The spatial-rates schema version bumps2.0 → 2.1(glm-diagnostics addition); ratio tables are byte-identical to2.0apart from the version string. This removes the earlierNotImplementedErrorwrite_spatial_ratesraised on a glm result. The clean-break contract still holds: pre-0.9"smoothing_method"-keyed tables do not read. - Decoder.
decode_session,decode_session_summary, andBayesianDecoderacceptmethod="glm"and forward the glm knobspenalty/rankinto the encoding step.bandwidthandmin_occupancyare now nullable on all three (defaultNone, resolving to the encoder defaults5.0/0.0for the ratio methods) so they can stay unset for glm. All three validate the method-specific params with the same validator the encoder uses, so a ratio param combined withmethod="glm"(or a glm param with a ratio method, or an out-of-domainpenalty/rank) raises the identicalValueErrorat the decoder boundary. The ratio decode path is unchanged.
Added — method="glm" optional float32 JAX acceleration¶
compute_spatial_rate(s)(method="glm", backend="jax") now routes the
penalized-Poisson fit + REML λ selection through an optional float32 JAX
mirror of the NumPy/SciPy core (installed via the jax extra; the base install
and backend="numpy" continue to run the float64 core). The float64 core remains
the correctness reference: the JAX path is a faithful mirror — same box
constraint |B·γ| ≤ η_clip, out-of-domain guard, and REML objective — cast back
to NumPy float64 at the boundary, so SpatialRatesResult diagnostics stay
float64 and the public return contract is unchanged.
- Parity. At a fixed
penaltythe float32 fit matches the float64 core to ~1e-6on the firing-rate map (relative L2 and relative error above a rate threshold). Under automatic REML it is a touch looser (float32 selects a slightly differentλnear the broad objective minimum), approximately~1e-3in measured rate/λ comparisons and still scientifically identical. - Speed. Markedly faster on populations — on a 400-bin, 30-unit, rank-60 problem the warm JAX REML fit is ~40–60× faster than the NumPy path on the CPU backend (fixed reference workload, fixed seed). The first call on an unseen shape pays JIT compilation, so a one-off fixed-penalty fit can be slower than NumPy; the win is on warm/repeated fits and grows on GPU.
- Kernel cleanup. The constant-field warm start is constructed exactly from the MRF basis's leading component intercepts and shared across both backends and every REML evaluation, eliminating the basis-wide least-squares SVD (including its repeated NumPy REML cost). The JAX Newton step uses the SPD Hessian's Cholesky factorization and triangular solves rather than generic LU.
- No new API. Dispatch is driven entirely by the existing
backend=keyword ("numpy"/"jax"/"auto"); there is no new user flag. JAX stays an optional extra (Linux/macOS).
Added — method="glm": per-neuron smoothing (pooled=False)¶
compute_spatial_rate(s)(method="glm") gain a keyword-only pooled flag
(default True, unchanged behavior). With pooled=False REML selects an
independent smoothing penalty λ_k per neuron instead of one shared λ, so
a sharp field is smoothed less and a broad one more. This is purely additive: the
pooled=True default is byte-for-byte identical to before apart from one new
scalar diagnostic (reml_at_boundary).
- Cost.
pooled=Falseruns the REML search once per informative unit (~one REML per neuron), reusing the existing batched Newton fit per distinctλ; there is no separate per-neuron fast path. - Result fields become per-unit vectors. On the automatic-REML path
(
penalty=None, a penalized basis, ≥1 spiking unit)penalty/reml_objective/reml_at_boundarywiden to(n_units,)and a newpenalty_selected_by_reml(n_units,)bool mask records provenance. A single-unitcompute_spatial_ratecall unwraps these back to scalars.summary_table()surfaces them; indexing a population result slices them. - Fixed penalty beats
pooled. Supplyingpenalty=<float>skips REML and records one scalarλregardless ofpooled.pooledis likewise a no-op atpenalty_rank == 0(a shared-basis property, soλstays a scalarNone) and when no unit spikes (the all-zero-spike degenerate path, with no pooled REML run). - Zero-spike units (whose per-unit
λis statistically unidentified) take the pooledλover the informative units as a fallback, flaggedpenalty_selected_by_reml=Falsewithreml_objective=nan(a documented sentinel); their field floors to the rate floor. - Boundary diagnostic. The new
reml_at_boundaryfield (scalar forpooled=True,(n_units,)forpooled=False,Nonewhen REML did not run) flags aλselected near a REML search bound (within5·xatol):λitself is weakly identified — its optimum may lie at or beyond the bound. The fitted field remains finite, but its sensitivity toλshould be checked. A single warning names the boundary side and the affected units (by theirunit_ids); the appliedλis the finite value returned near the bound (the interval is never expanded). - Validation.
pooledis a strictbool(rejects"true"/1/0/ arrays /None) and glm-only (pooled=Falsewith a ratio method raises;pooled=Truewith one is the harmless default). - NWB. The spatial-rates schema bumps
2.1 → 2.2:pooledand a scalarreml_at_boundaryjoin the GAM metadata blob, and the per-unitpenalty/reml_objective/reml_at_boundary/penalty_selected_by_remlvectors are persisted as table columns when present. A schema-2.1file with nopooledkey reads back method-conditionally —pooled=Truefor a glm table,pooled=Nonefor a ratio table.
Changed — BREAKING: smoothing_method renamed to method (estimator axis)¶
The smoothing/estimator keyword is now uniformly named method across every
smoothing encoder, result class, the decoder, and NWB metadata — one name, one
meaning. This is a hard rename with no alias (per project policy); callers
that named smoothing_method= or read .smoothing_method must update. Callers
that never touched it see identical behavior — the default stays
"diffusion_kde" (and "binned" for the egocentric encoders), and no numerics
change.
- Renamed keyword
smoothing_method→methodoncompute_spatial_rate,compute_spatial_rates,compute_view_rate,compute_view_rates,compute_egocentric_rate,compute_egocentric_rates,is_place_cell,compute_directional_place_fields,decode_session,decode_session_summary,BayesianDecoder, andsimulation.validate_simulation. Each encoder keeps its existing method value set and default. - Renamed field
smoothing_method→methodonSpatialRateResult,SpatialRatesResult,ViewRateResult, andViewRatesResult. - NWB:
write_spatial_ratesnow stores the estimator under the metadata key"method"(was"smoothing_method"); the spatial-rates schema version bumps to2.0. This is a clean break with no back-compatibility shim — spatial-rates tables written by earlier versions (schema1.x) no longer read. decode_session_summary: becausemethodnow names the smoothing estimator, it is no longer accepted as a per-block decode kwarg. The Poisson observation model (the only supported likelihood) is applied unconditionally.method="glm"(a penalized-Poisson GAM estimator,compute_spatial_rate(s)only) lands on this renamed axis — see the "Added —method="glm"" entry above.
Added — env.diffuse: matrix-free diffusion smoothing (0.8.0, performance)¶
Smoothing no longer materializes the dense (n_bins, n_bins) diffusion kernel on
its hot paths. A new method
applies the finite-volume heat operator H = exp(-t L) without building an
(n, n) matrix, using a cached, per-component, bandwidth-aware truncated
symmetric eigenbasis of S = M^{-1/2}(D − W) M^{-1/2}. Time and memory scale
with n_bins × rank (rank ~ measure(domain)/σ^d), not n_bins², so smoothing
now scales to large/fine grids. The eigenbasis depends only on geometry, so it is
built once and reused across every bandwidth, mode, and neuron (auto-invalidated
when the environment is mutated). fields may be 1-D (n_bins,) or a 2-D batch
(n_bins, n_fields).
env.diffuseis a pure linear operator — no output clip, no renormalization. The always-retained per-component null mode makes mass conservation exact under truncation (mode="transition"preservessum;mode="average"preserves a constant field), so smoothing a signed field is preserved. Positivity, where required, is the consumer's job.env.smoothnow routes throughenv.diffuseand gains a documented approximation contract: it reproduces the dense kernel to within a near-lossless truncation tolerance (dropped modes contribute ≤tol(default1e-6) in the M-weighted norm; the raw per-bin error carries a volume-conditioning factorκ(M) = sqrt(max vol / min vol), worst on polarr→0/ skewed mesh). On a non-negative input the linear apply may leave tolerance-level negatives (bounded relative to the dense output) instead of the dense kernel's exact 0-floor — clip the result yourself if you need a strict floor.backend="jax"runs the apply in JAX (the cached NumPy eigenbasis is cast tojnp), sojit/grad/ GPU work through the smoothing. The JAXdiffusion_kdeencoders no longer round-trip through NumPy.- The
diffusion_kde,binned, and diffuse-resample_fieldconsumers route throughenv.diffuse; strict> 0support gates (binned, resample) derive their support from the W-component structure (exact, truncation-proof), not the smoothed denominator's sign, so truncation cannot emit a spuriousNaN.diffusion_kdeclips its own output≥ 0(decode nonnegativity).
Breaking: the kernel= parameter is removed from
neurospatial.encoding._smoothing.smooth_rate_map and smooth_rate_maps_batch.
Passing a precomputed dense kernel is obsolete — the cached eigenbasis now
provides the cross-neuron reuse the parameter existed for. There is no
backward-compat shim (per the project default); callers passing kernel= should
simply drop it. env.compute_kernel is unchanged (it still returns the dense
(n, n) matrix via the expm path, byte-identical, for callers that genuinely
need the matrix), as is transitions(method="diffusion").
Changed — diffusion smoothing bandwidth is now the true physical σ (breaking, correctness)¶
The diffusion_kde / diffusion-kernel bandwidth is now the true physical
standard deviation (σ) on every environment layout, independent of bin size or
resolution. Previously the effective smoothing width scaled with bin size (2D
place fields at bin_size=1 oversmoothed by ~70% for the same bandwidth), so
place-field widths and spatial information were not comparable across
resolutions or with other tools. This is a behavior-changing correctness fix:
smoothed values change even on uniform grids — that difference is the
correction.
- New finite-volume operator. The kernel is now the finite-volume heat
operator
H = exp(-t L),t = σ²/2,L = M⁻¹(D − W)withW[i, j] = A[i, j] / d[i, j](A= shared-face measure,d= center distance) andM= per-bin cell volumes. On any K-orthogonal layout this has the continuum limit−∇², so a point source smooths to physical σ exactly. - Impact on existing results. For the same
bandwidth, 2D place fields now smooth ~1.5–1.7× less. To approximate the old amount of smoothing, scalebandwidthby roughly√(bin_size)(rough, mode-dependent — prefer re-choosingbandwidthas a physical σ in your units). - Correct orientation per consumer.
compute_kernel(mode="transition")returnsHᵀ(column-stochastic, mass-conserving smoothing of extensive quantities);mode="density"returnsH·M⁻¹(count → density, integrates to 1 under bin volumes);transitions(method="diffusion")now returns the row-stochasticHvia transpose (correct on non-uniform bin volumes — polar, mesh — where it no longer assumes symmetry). - Breaking: public
neurospatial.ops.compute_diffusion_kernelssignature. The old Gaussian-weight formcompute_diffusion_kernels(graph, bandwidth_sigma, bin_sizes=..., mode=...)is replaced bycompute_diffusion_kernels(graph, *, volumes, sigma, mode). The face measure is read from a per-edge"A"attribute (single source of truth, like"distance"); a missing"A"on an edge raises, andA == 0means no diffusion across that edge.modenow also accepts"average"(row-stochasticH) at the low level. min_occupancycaveat.min_occupancyis documented in seconds, but the KDE paths compare it tooccupancy_density = K @ occupancy, which under the new density kernel is seconds-per-cell-volume on non-uniform grids — a pre-existing unit mismatch that this change neither fixes nor worsens for uniform grids, and that lies outside the grid-invariance guarantee. Tracked as a follow-up.- Out of scope (unchanged): nonuniform-Cartesian custom
grid_edgesinherit the uniform-cell approximation and are excluded from the physical-σ guarantee; performance (denseexpm) is unchanged.
Added — mode="average" intensive-field smoother¶
- New public
mode="average"onEnvironment.compute_kernelandEnvironment.smooth(and the low-levelcompute_diffusion_kernels): the row-stochastic heat operatorHthat averages an intensive field (a rate map or probability density). Unlikedensity(H·M⁻¹), it carries no cell-volume bias on non-uniform bin volumes (polar, mesh), and unliketransitionit is the correct choice for a rate map rather than a total. Discrete probability mass (a posterior summing to 1) still usestransition.env.smooth's default staysdensity(unchanged behavior for existing mode-less calls);averageis recommended for intensive rate maps. smooth_rate_map(method="binned")andresample_field(method="diffuse")now smooth their intensive fields through the row-stochastic average (masked / valid-bin-normalized), removing the volume bias on non-uniformM. Forresample_field, this also fixes a down-bias where covered bins adjacent to an uncovered region were pulled toward zero, and makes a sourceNaNinterpolate from valid neighbours instead of propagating. Uniform-grid results are unchanged (the per-cell volume factor cancels in the rate/weight ratio).
Corrected guidance¶
method="binned"is not a dense-kernel memory mitigation. Earlier notes (through v0.6.0) recommendedbinnedto avoid the dense(n_bins, n_bins)kernel, butbinnedsmooths its rate map through the same diffusion kernel (viaenv.smooth), so all smoothing methods build a dense kernel. The only memory mitigation is reducing the bin count (a largerbin_size). Docstrings and the high-bin warnings are corrected accordingly.
UX hardening — fail-loud errors, one count convention, docs & viewer polish (0.8.0)¶
A broad usability pass: silent failures now raise or warn with actionable messages, a few APIs were made consistent, and the docs/viewers were polished.
Breaking changes¶
add_positionsis nowadd_positions(events, *, times, positions)— reordered to the canonical(data, times, positions)order and made keyword-only. A bare 1-D trajectory makes both arrays 1-D, so a positional swap was shape-indistinguishable and silently mis-interpolated; keyword-only removes the ambiguity. Migration:add_positions(events, times=times, positions=positions).- Assembly functions take
(n_time_bins, n_neurons)—detect_assemblies,assembly_activation,pairwise_correlations, andreactivation_strengthnow use the same time-first count convention asdecode_positionandbin_spikes_in_time, so a binned count matrix feeds them directly. Migration: pass the defaultbin_spikes_in_time(...)output as-is, or transpose an existing(n_neurons, n_time_bins)matrix. - Behavior result
summary()returns adictof scalar metrics (was a formatted string) on everybehaviorresult class. Migration: usestr(result)for the human-readable form. - Fail-loud instead of silent sentinels.
bin_aton a graph/track environment raises on wrong-dimension points (was all-1);heading_from_velocityraises when every sample is belowmin_speed(opt out withallow_all_nan=True); graph queries reject a multi-point coordinate batch.
Added¶
units=andframe=keyword args on the factory methods (from_samples,open_field,linear_track,maze) set the metadata at construction:Environment.from_samples(pos, bin_size=2.0, units="cm").- Graph queries accept coordinates, not just bin indices —
neighbors,path_between, andreachable_frommap a coordinate viabin_at(integers are still treated as indices). ResultMixinon every result class — concise__repr__/_repr_html_(summary metrics, not array dumps) and a scalarsummary()dict.dir(neurospatial.ops)now surfaces the lazily-exported ops (autocomplete).
Fixed¶
min_occupancythresholds raw occupancy seconds, not smoothed density — fixes silent all-zero place fields on the documented golden path; applied identically acrossdiffusion_kde/gaussian_kde/binned(single and batch).min_occupancy=0.0(the default) is unchanged.- Grid allocation is preflighted — a transposed
(2, N)trajectory (read as N dimensions) raises in ~1 ms instead of OOMing inhistogramdd/meshgrid; a likely-transposed array now warns rather than false-rejecting a valid low-sample N-D environment. - Non-finite query rows map to the
-1"outside" sentinel per row (one NaN no longer collapses the whole occupancy/rate map on graph/masked envs). to_filerefuses to overwrite by default (overwrite=Trueto allow);occupancy()checks shape before monotonicity (clear swapped-argument error);from_graphvalidates the edgedistanceattribute.SpatialRateResult.plot()documents the realcolorbar/colorbar_labelkwargs and labels the colorbar "Firing Rate (Hz)"; batchplot()raises an actionable error when no unit index is given.- Interactive-viewer accessibility — the standalone HTML player ignores
global keyboard shortcuts when an interactive control has focus, adds
:focus-visiblestyles, and stops announcing every autoplay frame to screen readers; the napari region dock and track builder gain a compact layout, visible labels, and accessible controls.
Documentation¶
- Docs version bumped to 0.8.0; dead links and the fork-clone step fixed; a
value-first, runnable quickstart; grouped/collapsible nav; a new advanced
architecture page; colorblind-safe
viridisin examples; example-notebook links normalized with an internal link-check step in docs CI.
Additional release notes¶
-
BREAKING:
smoothing_methodrenamed tomethod. The smoothing/estimator keyword is now uniformly namedmethodacross every smoothing encoder (compute_spatial_rate(s),compute_view_rate(s),compute_egocentric_rate(s),is_place_cell,compute_directional_place_fields), the four rate result classes (SpatialRateResult/SpatialRatesResult,ViewRateResult/ViewRatesResult), the decoder (decode_session,decode_session_summary,BayesianDecoder), andsimulation.validate_simulation. This is a hard rename with no alias; defaults and numerics are unchanged ("diffusion_kde", or"binned"for the egocentric encoders). NWBwrite_spatial_ratesstores the estimator under the key"method"(schema bumped to2.0); this is a clean break with no back-compatibility shim, so spatial-rates tables written by earlier versions (schema1.x) no longer read. Ondecode_session_summary,methodnow names the smoothing estimator, so it is no longer accepted as a per-block decode kwarg (Poisson is the only likelihood and is applied unconditionally). -
method="binned"is not a dense-kernel memory mitigation. Earlier notes (through 0.6.0) recommendedbinnedto avoid the dense(n_bins, n_bins)kernel, butbinnedsmooths its rate map through the same diffusion kernel (viaenv.smooth), so all smoothing methods build a dense kernel. The only memory mitigation is fewer bins (a largerbin_size). -
add_positionsis nowadd_positions(events, *, times, positions)— reordered to the canonical(data, times, positions)order and made keyword-only (a bare 1-D trajectory made a positional swap shape-indistinguishable). Migration:add_positions(events, times=times, positions=positions). -
Assembly functions take
(n_time_bins, n_neurons)—detect_assemblies,assembly_activation,pairwise_correlations,reactivation_strengthnow matchdecode_position/bin_spikes_in_time. Migration: feed the defaultbin_spikes_in_time(...)output as-is, or transpose an old(n_neurons, n_time_bins)matrix. -
Behavior result
summary()returns adict(was a formatted string). Migration: usestr(result)for the human-readable form. -
Fail-loud instead of silent sentinels —
bin_atraises on wrong-dimension points on a graph/track env;heading_from_velocityraises when every sample is belowmin_speed(opt out withallow_all_nan=True); graph queries reject a multi-point coordinate batch. -
units=/frame=keyword args onfrom_samples,open_field,linear_track, andmazeset the metadata at construction. -
Graph queries accept coordinates (not just bin indices) —
neighbors,path_between,reachable_frommap a coordinate viabin_at. -
ResultMixinon every result class — concise repr/HTML and a scalarsummary()dict. -
dir(neurospatial.ops)surfaces the lazily-exported ops (autocomplete). -
min_occupancythresholds raw occupancy seconds, not smoothed density — fixes silent all-zero place fields; consistent across all smoothing methods (single and batch);min_occupancy=0.0unchanged. -
Grid allocation is preflighted — a transposed
(2, N)trajectory raises fast instead of OOMing; a likely-transposed array warns rather than false-rejecting a valid low-sample N-D environment. -
Non-finite query rows map to the
-1sentinel per row (one NaN no longer empties the whole map). -
to_fileno-overwrite default;occupancy()shape-before-monotonicity;from_graphedge-distancevalidation. -
SpatialRateResult.plot()colorbar docs corrected + "Firing Rate (Hz)" label; actionable batchplot()error. -
Interactive-viewer accessibility — HTML player keyboard-focus guard,
:focus-visible, aria-live toggled off during autoplay; napari region dock and track-builder layout/labels. -
Version bumped to 0.8.0; dead links and fork-clone fixed; value-first runnable quickstart; grouped/collapsible nav; new advanced architecture page;
viridisdefaults; example-notebook links normalized + internal link-check in docs CI.
0.6.0 - 2026-07-03¶
Commit highlights¶
Features¶
- feat(io/nwb): SpatialRatesResult round-trip (write_spatial_rates/read_place_field) + lazy reads (92cb14a)
- feat(decoding): add immutable BayesianDecoder (fit/predict/predict_summary/score) over the decode core (5ec00da)
- feat(recording): add frozen Session bundle + load_session (from_arrays/from_nwb, with_environment/restrict) (5edf393)
- feat(behavior): add restrict/in_epochs/restrict_spike_trains (array or IntervalSet epochs) (9b940fa)
- feat(encoding): add SpikeTrains ragged-spike container (label access, filter, unit_table) (1eba7ca)
- feat(typing): add PositionLike/SpikeTrainsLike/EnvironmentLike Protocols + pynapple ingress (optional) (b35330c)
- feat(decoding): honor float32 end-to-end through decode_session(_summary) via a dtype knob (fd58811)
- feat(encoding): speed filtering masks both spikes and occupancy in encode path (c514061)
- feat(decoding): add memory-safe summary decode + dtype/time_chunk on decode_position (afc1d68)
- feat: Update documentation and code to reflect 'unit' terminology for v0.6 API consistency (a5520ab)
- feat: Add ux-reviewer agent for evaluating user experience in scientific software (b2d3cdb)
- feat: Update API for detect_region_crossings and enhance documentation (0c0488e)
- feat(environment): experiment-shaped factory presets (open_field, linear_track, maze) (928a921)
- feat(encoding,behavior,decoding): enforce naming contract (classify/label_cell_types/is_place_cell/peak_location; decode_position result arg; deprecation aliases) (e3c9c94)
- feat(results)!: split terminal verbs — dense to_dataframe + new summary_table; PSTH ResultMixin (breaking, D1) (ccc72a6)
- feat(results)!: to_xarray returns a labeled xr.Dataset (breaking, D1) (14fe4f3)
- feat(encoding,events): thread unit_ids through population results and compute functions (c3e29e9)
- feat(decoding): add decode_session() one-call encode->bin->decode golden path (b1fabc4)
Bug Fixes¶
- Merge pull request #25 from edeno/phase-3-pynapple-validation (6d8f7e2)
- fix(io/pynapple): validate shapes in to_pynapple (ValueError, not raw AssertionError) (241ed10)
- Merge pull request #24 from edeno/phase-3-trajectory-extra (3d635b1)
- fix(decoding): drop nonexistent neurospatial[trajectory] hints (scikit-image is core) (51e1ac5)
- Merge pull request #23 from edeno/phase-3-nwb-extra-msgs (8f4ac4d)
- fix(io/nwb): ndx-pose/ndx-events ImportError hints name the real [nwb] extra (14a8d93)
- Merge pull request #22 from edeno/phase-3-bugfixes (e5c11d9)
- fix(interop): restrict speed under fit(epoch=); mixed-type unit_ids; correct nwb extra name (56d0b51)
- fix(io/nwb): persist+validate env on rates round-trip (no degenerate env); atomic write; occupancy integrity; lazy length checks + unit caching; add nwb CI job (fefe6c3)
- fix(decoding): score warns/raises on undecodable bins; validate BayesianDecoder config+fitted state; add is_fitted, warn_on_drop, score distance=; error provenance (ff606f3)
- fix(recording): raise on malformed NWB env (not silent None); add environment_name; warn on empty restrict; Position self-validates (822ebc4)
- fix(behavior): raise on ambiguous nested-list epochs; validate closed on empty epochs; doc NaN exclusion (a3d6ee2)
- fix(typing): extract TsGroup trains by unit-id index (not iterate-keys); narrow EnvironmentLike; fix pynapple tests (9fd8d86)
- fix(decoding): route log_poisson_likelihood/poisson_likelihood dt through shared validate_dt (9b939a5)
- fix(decoding): validate dt in decode_position_summary too; test decode_position(_summary) dt guards; unify dt message; fix examples README sync wording (917ad4b)
- fix(decoding): unify dt validation (reject str/bool) via shared helper; clean ValueError for unparseable dtype (726ed71)
- fix(decoding): clean error for non-numeric/bool dt; restrict sync prune to numbered examples (23501df)
- fix(decoding): validate dt (finite, >0) in decode_session(_summary) before grid math (2766022)
- fix(decoding): preserve float32 in result-object handoff; validate time_chunk as positive int (not bool/float/str) (20b0e63)
- fix(decoding): reject time_chunk=None in summary decoders (it defeats the never-materialize-full-posterior contract) (eeb3d80)
- fix(encoding): warn when interval filtering empties the rate map (max_gap/out-of-bounds, not just min_speed) (7536d53)
- fix(decoding): validate prior shape in decode_position_summary (was silently truncating over-long 2D priors) (8b18f59)
- fix(encoding): align spike counts with occupancy on the FULL interval mask (max_gap, out-of-bounds, speed) (87b2164)
- fix(encoding): raise on speed-without-min_speed; warn on all-excluded speed filter; tighten final-sample gate (d13c968)
- fix(decoding): robust scaling test, precise float32 summary reductions, quiet degenerate-row warning (0847aa7)
- fix: address Phase 1 PR review (linear_track NaN guard, unit_table validation, all-NaN peaks, decode_position error, maze kind, honest annotations, docs) (92067e7)
- fix(encoding,docs): add peak_locations() to batch results; correct CLAUDE.md peak/cell-type contract; dedup CHANGELOG (aee29bf)
- fix(environment): order-based (coordinate-independent) maze topology; no track_graph mutation (9d9cc69)
- fix(results): raise (not silently skip) on xarray coord shape mismatch; omit None units (eeac275)
- fix(decoding): validate times up front in decode_session (beginner-grade non-finite error) (ec427eb)
- fix(decoding): warn on out-of-window spike drop in decode_session (units footgun) (09b1dea)
- fix: address Phase 0 PR review findings (E1006 code, docs accuracy, test gaps) (3f779e7)
- fix(decoding): keep decode_session export in neurospatial.decoding only (e6b2030)
- fix(environment): distance_to raises RegionNotFoundError for missing region (KeyError-compatible) (85410c0)
- fix(errors): remove internal-doc (CLAUDE.md) references from user-facing errors (63c8062)
- fix(environment): correct factory examples + error code in bare-Environment() message (3eaa207)
- fix(environment): beginner-grade error for bare Environment() pointing to factories (80d6ae7)
- fix(events): reject Inf event_times in align_spikes_to_events (matches docstring) (a364db1)
- fix(behavior): normalize array-likes at trajectory public boundaries before .ndim (2d4b5f8)
- fix(encoding): address review findings on spike-drop warning (4b3dfa5)
- fix(encoding): warn when spikes are dropped outside trajectory window or inactive bins (0d04d01)
Documentation¶
- docs(interop): add Interoperability guide (session-first-optional: pynapple/NWB/Session/SpikeTrains/BayesianDecoder) (7848436)
- docs(plan): add Phase 3 interop/ergonomics design RFC (locks Protocols, Session, BayesianDecoder) (a0dc512)
- docs(decoding): widen likelihood Raises docstrings to match new dt validation (65b0e2c)
- docs(changelog): sync hosted changelog with latest v0.6 fixes (dt/dtype validation, float32 result handoff, time_chunk) (bdf2afd)
- docs(animation): fix broken per-frame memmap rate-map example; clarify jupytext vs docs-mirror sync; soften index parity claim (06f5e91)
- docs(examples): add missing example-23 row to index; correct CI sync guidance; prune stale mirrors in sync script (ebf0397)
- docs: surface decode_session_summary/scale APIs (api index, workflow long-session callout, batch processing, snippet CI) (15dd728)
- docs(changelog): sync hosted changelog with v0.6 scale API (dtype end-to-end, time_chunk hybrid + None rejection, summary speed forwarding) (e3eb03c)
- docs(encoding): correct compute_spatial_rates dtype note for R8 (decode_session now honors float32 end-to-end); test blockwise prior validation (cc7c498)
- docs: teach compute_spatial_rates batch path in README/animation/overlays/metrics (not per-neuron loops) (fe83cc4)
- docs(changelog): smoothing kernels warn and proceed (no hard gate / allow_large) (4a099e2)
- docs(examples): re-sync stale bayesian-decoding mirror from source; add CI sync guard (961ea4c)
- docs(decoding): teach batch/decode_session path not the per-neuron loop; add Phase 2 entries to docs changelog (241a8d5)
- docs/fix(decoding): correct time_chunk memory claim; DecodingSummary map_estimate + post-init validation; test gaps (59b3a56)
- docs(encoding): clarify float32 dtype decode-memory scope; fix batch return annotations (4dbfbc9)
- docs(encoding): teach compute_spatial_rates batch path, not the per-neuron loop (952b861)
- docs(encoding): note joblib warning-swallowing in population_coverage; coerce label_cell_types score dtype (27be8cf)
- docs: v0.6 naming contract in CLAUDE.md + migration guide + breaking-change CHANGELOG (1.6, 1.7) (c91184a)
- docs(encoding): note is_place_cell (field-based) vs classify (info-threshold) divergence (review nit) (d725d9f)
- docs,fix(encoding,events): document unit_ids/unit_table/unit_id; guard ndim; honest unit_id scalar (2a4d1ac)
- docs: sync Phase 0 plan §0.7 to shipped decode_session; archive re-review (d5cb4eb)
- docs(reviews): archive Phase 0 independent review with resolution note (d296b58)
- docs,ci: bump installation.md to v0.5.0; execute nb-20 in notebook CI (0abba24)
- docs: align decode_session example with manual path + execute notebook (review fixes) (b0b1e55)
- docs: teach decode_session as the one-call decode golden path (720547f)
- docs: fix degenerate Workflow 1 example + review nits (Phase 0.6) (4bb7636)
- docs: teach one canonical beginner path; fix broken API snippets (Phase 0.6) (da3ba4c)
- fix(errors): remove internal-doc (CLAUDE.md) references from user-facing errors (63c8062)
- docs(plan): apply independent + maintainer review findings to v0.6 plan (8cf9c92)
- docs(plan): add v0.6 UX implementation plan + review synthesis (9ccf0f7)
- docs: update CHANGELOG.md for v0.5.0 (9552a27)
Other Changes¶
- Merge pull request #27 from edeno/chore/release-0.6.0 (ce62eff)
- chore(release): 0.6.0 — bump version, cut CHANGELOG 0.6.0, sync docs; fix stale doc refs (aa3e80f)
- chore(reviews): strip trailing blank line at EOF (git diff --check hygiene) (b393fd3)
- Merge pull request #21 from edeno/phase-3-simplify (19beb30)
- refactor(interop): dedup pynapple adapters, collapse Session spike coercion, avoid double spike/interval normalization (35af797)
- refactor(encoding): store SpikeTrains.trains as an immutable tuple; fix lazy-export comment (229135e)
- test(io): cover to_pynapple TypeError guard for non-result + values=None (3d27e81)
- chore(deps): lock pynapple optional extra (quantities, tabulate) (dd06e7e)
- test(decoding): structural throughput guard for summary decoders (per-block decode count = ceil(n_time/time_chunk), independent of n_neurons) (b0da2c1)
- test(encoding): enforce batch throughput contract — occupancy/kernel computed once, not per-neuron (deterministic call-count) (2aa7c21)
- test(decoding): cover decode_position time_chunk<1 guard; route summary prior-shape check through shared helper (f0d40d7)
- refactor(smoothing): warn (don't hard-gate) on high-bin dense kernels; drop allow_large/_KERNEL_HARD_LIMIT_BINS (a3e74eb)
- refactor(decoding): make decode_session_summary block-alignment check unconditional (survive -O) (08568b9)
- test(encoding): guard population_coverage docstring arg order (55c6bee)
- refactor(environment): sync compute_kernel protocol signature with allow_large (63c6d09)
- refactor: Remove type ignore comments for clarity in various encoding functions (fe31432)
- refactor: Rename methods and update docstrings for clarity in encoding tests (64d30ab)
- feat(encoding,behavior,decoding): enforce naming contract (classify/label_cell_types/is_place_cell/peak_location; decode_position result arg; deprecation aliases) (e3c9c94)
- test(encoding): rename stale to_dataframe/neuron_id test names to summary_table/unit_id (review nit) (7e882c0)
- refactor(decoding): let encoder own the drop warning in decode_session None branch (891a5be)
- refactor(encoding): rename normalize_spike_times -> as_spike_trains (f4cf99a)
- refactor(decoding): polish decode_session per review (times guard, ArrayLike, tests) (9bd454d)
- test: let CLAUDE.md backstop raise on unreadable file (review nit) (5ae370c)
- fix(environment): beginner-grade error for bare Environment() pointing to factories (80d6ae7)
- test(events): strengthen finite-event_times assertion (review nit) (8cad979)
- test(behavior): add list-input normalization tests for traveled_path_length (b81d78b)
- chore(worktrees): ignore .claude/worktrees/ for per-task subagent worktrees (994e796)
Full Changelog: https://github.com/edeno/neurospatial/compare/v0.5.0...v0.6.0
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. While the project is pre-1.0, minor releases may still include breaking changes; these are called out under a dedicated Breaking changes heading.
Changed¶
- The internal
_build_encoding_model(shared bydecode_session/decode_session_summary/BayesianDecoder.fit) gained an optionalcontextparameter that names the caller in its up-front timestamp-validation error.BayesianDecoder.fitpassescontext="BayesianDecoder.fit", so anepochthat selects too-few training samples now raises"At least 2 samples required for BayesianDecoder.fit, got N"instead of the misleadingdecode_sessionprovenance. The default preserves every existing message. decode_session/decode_session_summarynow acceptpositions=Nonewhenencoding_modelsis supplied (the passthrough decode never uses a position track). Previously this raised"as_times_positions received a timestamp array but no positions"; now the position track is normalized only when actually needed (the encode step, or aPositionLiketimes). Every existing caller that passespositionsis byte-for-byte unchanged. This enables the fitted-model decode path used byBayesianDecoder.predict/predict_summary.
Fixed¶
- The public likelihood functions
log_poisson_likelihoodandpoisson_likelihoodnow validatedtvia the sharedvalidate_dthelper, consistent with the decode entry points. A non-numericdt(including a numeric string like"0.1"), abool(dt=True), and a non-finitedt(nan/inf) now raise a clearValueErrorinstead of the weakerdt <= 0guard (which leaked a rawTypeErroron strings, silently accepteddt=Trueas1, and letnan/infslip through). The error message changed from"dt must be positive"to `"dt must be a finite number0"`.
bin_spikes_in_timenow validatesdtconsistently withdecode_session/decode_session_summaryvia a sharedvalidate_dthelper: a non-numericdt(including a numeric string like"0.1") and abool(dt=True) now raise a clearValueError("dt must be a finite number > 0, ..."). Previously a numeric string leaked a rawTypeErrorfrom"0.1" <= 0anddt=Truewas silently accepted and used as a chunk size of1.decode_positionroutes through the same helper, so an invaliddtthere also raises the clean message instead of a cryptic downstream error.- An unparseable
dtype(e.g.dtype="bogus") now raises a clearValueErrornamingdtypeacross the decode/encode entry points (decode_position,decode_position_summary,compute_spatial_rates,decode_session,decode_session_summary) instead of a raw NumPyTypeError: data type 'bogus' not understood. decode_sessionanddecode_session_summarynow validatedt(finite,> 0) up front with a clearValueError, matchingbin_spikes_in_time's wording ("dt must be finite and > 0, got ..."). Previously the shared_build_encoding_modelbuilt the decode time grid directly, bypassingbin_spikes_in_time's guard, so an invaliddtleaked a cryptic error:dt=0→ZeroDivisionError,dt=NaN→ "cannot convert float NaN to integer",dt<0→ a misleading "span smaller than one bin" message, anddt=inf→ a similar cryptic failure.decode_positionnow preservesfloat32when handed a rate-result object (anything exposing.firing_rates). Previously the friendly object path promoted afloat32.firing_ratestofloat64, silently losing part of thedtype=np.float32memory win that the raw-array path already delivered. The object path now matches the raw-array path byte-for-byte:float32staysfloat32,float64staysfloat64, an integer rate map is promoted tofloat64, and aNone/ dict / non-2-D.firing_ratesstill raises the same clearValueError.time_chunkis now validated as a positive integer (notbool) acrossnormalize_to_posterior,decode_position,decode_position_summary, anddecode_session_summary, via a shared validator that raises a clearValueErrornaming the value and its type. Previously a float (1.5) or string ("2") leaked a rawTypeErrorfromrange(...)/comparison, andTruewas silently accepted as a chunk size of1.- The summary decoders
decode_position_summaryanddecode_session_summarynow rejecttime_chunk=None(raising a clearValueError). Previously aNonevalue set the streaming block to the full session length, materializing the full(n_time, n_bins)posterior transiently and defeating the memory-safe "never materialize the full posterior" contract these functions promise.time_chunkmust be a positive integer (default1024); usedecode_position/decode_sessionif you want the full posterior.
Added¶
- NWB
SpatialRatesResultround-trip + lazy reads (neurospatial.io.nwb).write_spatial_rates(nwbfile, result, *, name="spatial_rates", overwrite=False)stores a population result on a unit axis — aDynamicTable(one row per unit) inanalysis/with aunit_idcolumn, a 2-Dfiring_ratecolumn of shape(n_units, n_bins), and one column perunit_tablefield; occupancy is stored once as a companionTimeSeries,smoothing_method/bandwidth/ bin counts ride in the table description, and bin centers are shared via the existingbin_centersdataset. The fullEnvironmentis now persisted alongside the rates (viawrite_environmentunder the derived namef"{name}_environment"), so it round-trips with its connectivity edges and geometry intact. The write is atomic: all name collisions (the table,f"{name}_occupancy",f"{name}_environment") and shape validation are resolved before the first object is added, so a duplicate withoutoverwriteraises before any mutation andoverwrite=Truecleans every companion; a later add failure rolls back the partial write.firing_rates/occupancyare defensively copied so the NWB containers never alias the live result, and aunit_tablecolumn namedunit_id/firing_rate(reserved) raises a clearValueError.read_place_field(nwbfile, *, name="spatial_rates", env=None)is the inverse, reconstructing aSpatialRatesResultwithfiring_rates/occupancy/unit_ids/unit_table/smoothing_method/bandwidthall preserved (unit_idsandunit_tablelinks survive non-default ids and non-trivial tables). Whenenv=is omitted it restores the persisted environment with full connectivity (read_place_field(nwb).env.neighbors(i)matches the original; there is no connectivity-less bin-centers fabrication), and it raises if neither anenv=nor a persisted env is present. A mismatched or staleenv=(itsn_binsdisagreeing with the stored rates) now raises a clearValueErrorinstead of silently attaching, the companion occupancy length is validated at read against the table's recordedn_bins, and anamepointing at a non-spatial-rates table raises a clearValueError. Shape mismatches raise param-namedValueErrors and duplicate names honoroverwrite.read_position/read_pose/read_unitsgain a keyword-onlylazy=False:lazy=Truereturns h5py-backed handles (per-unit spike-time handles forread_units) that materialize only when sliced /np.asarray-ed — valid only while the backingNWBFileis open — while the default eager path stays byte-for-byte unchanged. The lazyread_position/read_posepaths now validate lengths via the handles'.shape[0](no materialization), so a positions/timestamps or per-bodypart mismatch raises exactly as the eager path (lazy pose no longer silently misaligns bodyparts of differing length), and the lazy per-unit spike-time handle caches its materialized array (the ragged slice is read and sorted at most once). Atest_nwb.ymlCI job installs thenwbextra and runs the NWB tests (tests/nwb+tests/test_recording.py), which the defaultdev-only job skips.neurospatialstill never imports pynwb (loaded lazily inio/nwb). BayesianDecoder— an immutable (frozen)fit/predict/predict_summary/scorewrapper over the decode core (neurospatial.decoding, also exported at the top level).fit(spike_times, times, positions, *, speed=None, min_speed=None, epoch=None)builds encoding models by reusingdecode_session's internal encoder and returns a new fitted decoder (never mutates the original); an optionalepochrestricts the training data first (viabehavior.restrict/restrict_spike_trains) for train/test splits.predict/predict_summarydelegate todecode_session/decode_session_summarywith the fitted models, so a fitted decoder's posterior is byte-exact withdecode_sessionon the same inputs;scorereports decode error ("median_error"/"mean_error", lower is better) viaDecodingResult.error_against.scoregained adistance=option ("euclidean"default or"geodesic", forwarded toerror_against) so scoring can use the environment's graph distance, not only straight-line error, and now does not silently drop undecodable bins: it warns (naming the excluded fraction) when any decode time bin is undecodable (all-non-finite posterior row, whichnanmedian/nanmeanwould otherwise ignore) and raises a clearValueError— instead of returningnan— when no bin is decodable (naming the likely degenerate-encoding-model or seconds-vs-milliseconds unit-mismatch cause); the all-decodable path stays warning-free. Invalidmetric/distanceare validated before decoding so a typo does not cost a full decode. Construction now validates config and fitted state:dt(via the sharedvalidate_dt) anddtypeare checked at build time, and a directly-injected fitted decoder (BayesianDecoder(env, encoding_models=..., unit_ids=...)) is checked for fitted-state coupling (unit_idspresent, 2-D models, and a bin/unit-count match againstenv) instead of detonating later inside the core. A new read-onlyis_fittedproperty lets callers branch without catchingRuntimeError, and awarn_on_dropconfig field (defaultTrue) threads to both thefitencode step and thepredict/predict_summarydecode steps as a single knob to silence the spikes-out-of-window warnings. AcceptsSpikeTrainsLike/PositionLikeinputs, and decodes through theEnvironment, so geodesic / linearized-track / graph-based decoding works (unlike pynappledecode_1d/decode_2d). Unfittedpredict/predict_summary/scoreraise a clearRuntimeError.Session— a frozen discoverability bundle (neurospatial.recording, also exported at the top level) groupingenv/position/spikes/epochs/metadata. It is not a god-object: it exposes the raw arrays (session.times/session.positions/session.spikes) but carries no heavy analysis methods — compute stays functional, e.g.compute_spatial_rates(session.env, session.spikes, session.times, session.positions).positionuniformly exposes.t/.values(arrays are wrapped in a small internalPositionholder; a pynappleTsd/TsdFramealready conforms). Constructors:Session.from_arrays(*, env=None, times, positions, spike_times, unit_ids=None, unit_table=None, epochs=None, metadata=None)(accepts arrays or aPositionLike, and a list / 2-D array /SpikeTrains/ pynappleTsGroupfor spikes, threadingunit_ids/unit_table) andSession.from_nwb(path_or_file, *, environment_name=None, unit_ids=None, **read_kwargs). Frozen / immutable:with_environment(env)andrestrict(epochs)return a newSessionand never mutate the original.restrictslices the position and the spikes to the epochs and is identity-preserving — restriction trims spikes per unit (never drops units), so it rebuilds aSpikeTrainscarrying the originalunit_ids/unit_tableunchanged, and records the epochs on the new session. Arestrictthat keeps zero position samples (epochs that miss the session entirely — often a seconds-vs-milliseconds unit mismatch) emits aUserWarningnaming the likely cause and returns the (empty) session. A mismatchedposition.t/position.valueslength (also self-enforced by the internalPositionholder, which additionally requires a 1-Dt), apositionthat is notPositionLike(missing.t/.values), or a non-EnvironmentLikeenvraises a clearValueError.load_session(source, **kwargs)— dispatches an NWB file path (str/os.PathLike) or an open pynwbNWBFiletoSession.from_nwb; any othersourceraises a clearTypeErrordirecting you toSession.from_arrays.Session.from_nwbbuilds the bundle via the existing lazyneurospatial.io.nwbreaders (read_units/read_position/read_environment), soneurospatial.recordingnever imports pynwb / pynapple andimport neurospatialstays cheap. The environment is read by presence (membership innwbfile.scratch), not by catching an error: a genuinely-absent environment maps toenv=None, while an environment that is present but unreadable (malformed / wrong schema) now raises instead of being silently swallowed toNone. The newenvironment_name=selector reads the standardspatial_environmentscratch entry by default and can select an environment written under a custom name. (The lazily-materializedlazy=TrueNWB read path is intentionally deferred to a later phase.)restrict/in_epochs/restrict_spike_trains— array-native epoch selection ("give me my running periods / trial N") in a newneurospatial.behavior.epochs.epochsaccepts(start, end)scalars, two(starts, ends)1-D arrays, an(n, 2)array, or a pynappleIntervalSet— theIntervalSetis duck-typed (.start/.end), so this stays array-first and never imports pynapple. The one genuinely ambiguous form — a length-2 pair of length-2 sequences ([[0, 5], [10, 15]]), which could mean two(start, end)interval rows or two parallel(starts, ends)arrays — raisesValueError; pass an(n, 2)NumPy array (interval rows) or explicit 1-Dstart/endarrays to disambiguate. Endpoints are inclusive by default (closed="both", matchingbehavior.segmentationand pynapple; also"left"/"right"/"neither").restrict(times, *arrays, epochs=...)slicestimesand any arrays aligned totimes(e.g.positions) by the same in-epoch mask, order preserved (t, pos = restrict(times, positions, epochs=run_epochs)); with no extra arrays it restricts an event-time array by its own timestamps (restrict(spike_train, epochs=...)).restrict_spike_trains(trains, epochs)restricts ragged per-unit spikes, each train by its own timestamps, and accepts a plain sequence or aSpikeTrains.restrictis also exported at the top level (neurospatial.restrict).SpikeTrains— a frozen ragged-spike-train container (label accessst[unit_id],filter("region == 'CA1'"), optional per-unitunit_table), the one justified new container (ragged per-unit spike times genuinely don't fit a rectangular array). Exported from bothneurospatial(top level) andneurospatial.encoding. It duck-types asSpikeTrainsLike— it is not aMapping, exposes a non-callable.indexproperty (the unit ids), and its__iter__yields the per-unit train arrays — so it flows through the Phase 3.1 spike-input adapter's iterate branch and intocompute_spatial_rates(and the other batch encoders/decoders) withunit_idspreserved. Label access (st[unit_id]) and the adapter's positional iteration coexist because they use different dunders (__getitem__vs__iter__/.index). Frozen and immutable:trainsis stored as atupleso in-place mutation raises,filterreturns a new container and never mutates the original; duplicateunit_idsraiseValueError.- Input Protocol surface + pynapple ingress/egress (optional). A new
neurospatial/_typing.pydefines the structural Protocols that let third-party objects flow into the array-first scientific core without the core ever importing orisinstance-checking them: PositionLike— a.t/.valuestime-series (pynappleTsd/TsdFrameconform).compute_spatial_rate/compute_spatial_ratesanddecode_session/decode_session_summarynow accept aPositionLikein thetimesslot (withpositionsomitted) and normalize it to plainfloat64arrays at the boundary viaas_times_positions. The plain-array path is unchanged byte-for-byte.SpikeTrainsLike— the accepted spike-input union, plus a real pynappleTsGroup. ATsGroupis acollections.UserDict, so iterating it yields the unit-id keys, not the per-unit trains;SpikeTrainsLikeis therefore the indexable-by-id surface (.indexof unit ids +group[uid]returning a per-unitTswith.t). A newencoding.as_spike_trains_with_idsextracts trains by indexing each id (never by iterating), so a rawTsGroupflows correctly intocompute_spatial_rates/decode_session; it surfaces the ids without changingas_spike_trains'slist[NDArray]contract, and when a group carries ids and the caller passes nounit_ids=, they now flow intoSpatialRatesResult.unit_idsinstead of being silently dropped.EnvironmentLike— a public re-export ofEnvironmentProtocol. The internalisinstance(env, Environment)check inCompositeEnvironmentis replaced by a duck-typedis_environment_likecheck, fixing the surprise that the siblingEgocentricPolarEnvironment(not anEnvironmentsubclass) was rejected even though it is a legitimate environment.- pynapple I/O shim behind a new optional
pynappleextra:neurospatial.io.from_pynapple(TsGroup→(trains, unit_ids);Tsd/TsdFrame→(times, positions);IntervalSet→(start, end)) andneurospatial.io.to_pynapple(a decoded MAP track /times+values→Tsd/TsdFrame).import pynappleis lazy inside these functions, so the package and the array path import and run with pynapple absent; calling them without it raises a clearImportErrornamingneurospatial[pynapple]. The scientific modules never import pynapple. - Speed filtering on the encode path.
compute_spatial_rateandcompute_spatial_ratesgain keyword-onlyspeed/min_speedparameters (also forwarded bydecode_session/decode_session_summary). Whenmin_speedis set, low-speed periods are excluded using one shared per-interval speed gate applied to both the spike numerator and the occupancy denominator, so amin_speedknob can never silently bias firing rates by filtering only one side. The spike gate matches the occupancy gate exactly: occupancy keeps intervalkiffspeed[k] >= min_speed, and a spike at timetis kept iffspeed[searchsorted(times, t, "right") - 1] >= min_speed— the same per-interval criterion, so identical intervals drop on both sides. Whenspeedis omitted it is auto-derived with a forward difference (speed[k] = ||positions[k+1] - positions[k]|| / (times[k+1] - times[k]), withspeed[n-1] = speed[n-2]) to matchenv.occupancy'stime_allocation="start"interval semantics; pass an explicitspeedfor geodesic / linearized-track environments.min_speed=None(the default) applies no filtering and leaves output byte-for-byte unchanged. - Passing
speedwithoutmin_speednow raisesValueError(instead of silently ignoringspeed), mirroringenv.occupancy, which raises onmin_speedwithoutspeed. - When the interval filter excludes all trajectory intervals — whether
via
min_speed,max_gap, or the out-of-bounds-start rule (e.g. a wrong-units threshold) —compute_spatial_rate/compute_spatial_ratesnow emit oneUserWarningnaming the active gate(s) and suggesting fixes (check units; passmax_gap=None), instead of silently returning an empty rate map. The warning fires once per call (not per neuron in the batch path) and is suppressed bywarn_on_drop=False. decode_sessionanddecode_session_summarygain a keyword-onlydtypeparameter (np.float32/np.float64, defaultnp.float64). "Decode in this dtype": a singledecode_session(dtype=np.float32)controls both the encoding-model working set and the posterior dtype, end-to-end — the functions no longer force-promote encoding models back tofloat64. Ondecode_session_summaryit is now an explicit parameter rather than adecode_kwargsentry. Defaultnp.float64leaves every existing caller byte-for-byte unchanged; any other dtype raisesValueError.
Performance¶
decode_session(dtype=np.float32)/decode_session_summary(dtype=np.float32)now actually halve the decode working set on the beginner golden path: thefloat32rate-map dtype is honored end-to-end (encoding-model working set + posterior) instead of being silently promoted back tofloat64inside the session helpers. Values matchfloat64withinfloat32tolerance (the rate computation is done infloat64and only the result is cast, percompute_spatial_rates).
Breaking changes¶
to_xarray()now returns a labeledxarray.Datasetinstead of anxarray.DataArraywith integer coordinates. This is a clean break: there is noDataArrayshim and noto_dataset()alias. Two distinct shapes are produced:- Population rate results (
SpatialRatesResult,DirectionalRatesResult,ViewRatesResult,EgocentricRatesResult) return aDatasetwith dims("unit_id", "bin").unit_idis the index coordinate holding the real per-unit identity labels (result.unit_ids), so units are selected by label. Thebindimension carries non-indexbin_center_x/bin_center_y(/bin_center_z) coordinates for Cartesian environments, orbin_center_distance/bin_center_anglefor the polar egocentric result. The rate matrix is thefiring_ratedata var;occupancyis a("bin",)data var.attrscarryunits,bandwidth(where applicable), anenvfingerprint, andsoftware_version. Duplicateunit_idsnow raiseValueError(label-based selection requires uniqueness). -
Decode results (
DecodingResult) return aDatasetwith dims("time", "bin")(a posterior over space per time bin; nounit_idaxis). Theposteriordata var holds the posterior, with the samebin_center_*coordinate logic andunits/env/software_versionattrs.Before → after:
-
The two terminal verbs now mean one thing on every result class.
to_dataframe()on the batch (plural) encoding results —SpatialRatesResult,DirectionalRatesResult,ViewRatesResult,EgocentricRatesResult— is now dense tidy: one row per(unit, bin)(single-unit results: one row perbin), always carrying aunit_idcolumn plus the bin-center coordinate columns (bin_center_x/y/zfor Cartesian,bin_center_distance/anglefor polar egocentric,bin_center_anglefor directional),firing_rate, andoccupancy. The per-unit summary thatto_dataframe()used to return (one row per neuron withpeak_x,peak_rate,spatial_info,sparsity,grid_score,border_score,cell_type, etc.) has moved to the newsummary_table(), which isunit_id-indexed. This is a clean break: there is no mode flag and no transition shim. Theneuron_ids=keyword on the old per-unitto_dataframe()is replaced byunit_ids=onsummary_table()(defaulting to the result's ownunit_ids).Before → after:
# before — to_dataframe() returned one row per neuron with metric columns df = result.to_dataframe() # columns: neuron_id, peak_x, ... place = df[df["cell_type"] == "place"] # after — summary_table() is the per-unit summary; to_dataframe() is dense summary = result.summary_table() # one row per unit, unit_id-indexed place = summary[summary["cell_type"] == "place"] dense = result.to_dataframe() # one row per (unit, bin), carries unit_id
Added¶
-
Memory-safe summary decoding for long sessions.
decode_position_summaryis a new sibling ofdecode_positionthat streams over time, computing the posterior one time-block at a time and reducing each block to per-time scalars/vectors (map_position/map_bin,mean_position,posterior_entropy,peak_prob) without ever materializing the full(n_time, n_bins)posterior. It returns a newDecodingSummaryfrozen dataclass (alongsideDecodingResult) carryingResultMixinand the standard terminal verbs —to_dataframe()(one row per time bin),summary(),plot(), andto_xarray()(a trackDatasetwith atimedim and nobinposterior axis) — sharing accessor names and column conventions withDecodingResultso user code ports between the two.decode_session_summaryis the matching one-call encode→bin→decode wrapper (sibling ofdecode_session). The summary reductions are bit-for-bit identical to reducing the full posterior formap_position/map_bin, and match to floating-point tolerance formean_position/posterior_entropy/peak_prob. -
Experiment-shaped factory presets on
Environmentthat speak experiment vocabulary and delegate to the existingfrom_*factories: Environment.open_field(positions, bin_size, ...)— the only positions-based preset; delegates tofrom_sampleswithfill_holes=Trueflipped on (a sensible open-arena default that fills interior gaps).Environment.linear_track(*, endpoints=..., node_positions=..., bin_size)— builds a 1D track graph (is_linearized_track == True) from an explicit topology (two endpoints for a straight track, or waypoints for a piecewise-linear track) and delegates tofrom_graph.Environment.maze(kind, *, track_graph=..., node_positions=..., bin_size)— assembles the standard W / plus / T track-graph topology (or accepts a readynetworkxgraph) and delegates tofrom_graph.
Track/maze presets require an explicit topology spec; raw positions cannot
infer a linear/W/plus/T graph, and calling them without a topology raises a
clear ValueError.
-
to_xarray()onDirectionalRatesResult,ViewRatesResult, andEgocentricRatesResult(the directional/view/egocentric population results previously had no xarray export). Each returns the labeledxr.Datasetdescribed under Breaking changes above. -
Durable unit identity on encoding/events results. Every population result (
SpatialRatesResult,DirectionalRatesResult,ViewRatesResult,EgocentricRatesResult,PopulationPeriEventResult) now carries aunit_ids: np.ndarrayfield (plus an optionalunit_table: pd.DataFrame | None), and every single-unit result (SpatialRateResult,DirectionalRateResult,ViewRateResult,EgocentricRateResult,PeriEventResult) carries a singularunit_id. Indexing or iterating a population result stamps the per-unit label onto the child (rates[i].unit_id == rates.unit_ids[i], iteration preserves order and labels). The batch compute functions (compute_spatial_rates,compute_directional_rates,compute_view_rates,compute_egocentric_rates,population_peri_event_histogram) gained a keyword-onlyunit_ids=parameter that threads onto the result; a wrong-length value raises a clearValueError. Fully back-compatible:unit_idsdefaults tonp.arange(n_units)and the new fields arecompare=False, so existing callers and equality/hash behavior are unchanged. -
summary_table()— the per-unit summary terminal verb — on every batch encoding result (SpatialRatesResult,DirectionalRatesResult,ViewRatesResult,EgocentricRatesResult) and onPopulationPeriEventResult. Returns one row per unit,unit_id-indexed, with that result's scalar metric columns (peak location/rate, spatial info, grid/border score, cell type, preferred direction/distance, etc.). Accepts an optionalunit_ids=to relabel the index. -
PSTH results now carry the uniform result surface.
PeriEventResultandPopulationPeriEventResultinherit the canonicalResultMixinand implement the terminal verbs:to_dataframe()(dense — one row per time bin for the single-unit result, one row per(unit, time-bin)for the population result, always carryingunit_id),summary()(flat dict of headline scalars — peak rate/latency, baseline rate; population addsmean_peak_rate/population_peak_latency),PopulationPeriEventResult.summary_table()(one row per unit withpeak_rate/peak_latency/baseline_rate), andplot()(delegates toplot_peri_event_histogram, returns the axis). -
decode_session(env, spike_times, times, positions, *, dt, ...)— one-call encode→bin→decode golden path inneurospatial.decoding.session. Gluescompute_spatial_rates,bin_spikes_in_time, anddecode_positioninto a single function so beginners can decode position in ≤10 lines. Exported fromneurospatial.decoding(from neurospatial.decoding import decode_session). Accepts an optionalencoding_models=array to bypass the encoding step entirely. Extra keyword arguments are forwarded verbatim todecode_position. -
as_spike_trains— public helper that coerces the various spike-input formats (1D array, NaN-padded 2D array, list of scalars, list of 1D arrays) into the canonical list of per-neuron spike-time arrays. Exported fromneurospatial.encoding(and present in__all__); the implementation lives inneurospatial.encoding._spikes. It standardizes the container shape only — spike-time values are never shifted, rescaled, or aligned. (This is the previously-internalnormalize_spike_times, renamed before any public release so the name reads as a structural conversion, not a value transform; no deprecated alias is kept since the public name was never released.) -
classify(*, ...)— a single-type boolean cell-type predicate (returnsNDArray[np.bool_]) on every batch encoding result:EgocentricRatesResult(OVC,min_info=0.3),ViewRatesResult(view cell,min_info=0.5),DirectionalRatesResult(HD cell,min_mvl/alpha), andSpatialRatesResult(place cell,min_spatial_info=0.5). These replace the per-domaindetect_ovcs/detect_view_cells/detect_hd_cellsdetectors (now deprecated aliases). -
label_cell_types(...)onSpatialRatesResult— the multi-class string labeler ("place"/"grid"/"border"/"unclassified", returnsNDArray[np.str_]). This is the renameddetect_cell_typesand is kept deliberately SEPARATE from the booleanclassify(different return type). -
is_place_cell(...)— a single-neuron place-cell predicate, both as a free function inneurospatial.encoding.spatial(exported fromneurospatial.encoding) and as aSpatialRateResult.is_place_cell()method. Mirrorsis_spatial_view_cell/is_object_vector_celland agrees withdetect_place_fields(returnsTrueiff that detector finds ≥1 field). -
decode_position(env, spike_counts, encoding_models, dt, ...)now accepts a population rate result object (anything exposing afiring_ratesattribute, e.g.SpatialRatesResult) directly in place of the raw(n_neurons, n_bins)array, removing thenp.stack([r.firing_rate ...])glue between the encoding and decoding steps. -
New keyword-only parameter
warn_on_drop: bool = Trueonbin_spike_train,bin_spike_trains(encoding/_binning.py),compute_spatial_rate, andcompute_spatial_rates(encoding/spatial.py). Set toFalseto intentionally silence all spike-drop warnings (e.g. when the caller handles the diagnostic themselves).
Performance¶
-
decode_session_summarynow streams the time-binning so the full(n_time, n_neurons)spike-count matrix is never materialized. It builds the encoding model once over the whole session (small,(n_neurons, n_bins)), then bins spikes block-by-block against a contiguous slice of the global time grid and decodes + reduces each block via the same shared inner-loop helper asdecode_position_summary. Peak memory is nowO(time_chunk × max(n_neurons, n_bins))plus the(n_neurons, n_bins)encoding model — independent of session length — meeting the 1 hr / 25 ms / 5000-bin / <500 MB summary-decode DoD golden path (the dense(144000, 1000)count matrix alone would be ~1.15 GB). The result is byte-for-byte identical to the prior materialize-then-stream path; per-block counts are binned against the precomputed global edges so they match a single global histogram exactly, and boundary spikes are counted exactly once.decode_session(the full-posterior path) is unchanged. -
decode_positiongains two keyword-only memory knobs with no change to its return contract (.posteriorstays a fully-materializedndarray): dtype=np.float32stores and computes the posterior in single precision, halving stored and transient memory (parity with float64 to ~1e-6 relative). EveryDecodingResultmethod works unchanged on a float32 posterior.time_chunkis now hybrid.time_chunk=None(the default) keeps the full-matmul path byte-for-byte unchanged — the Poisson log-likelihood is computed once over the whole window and normalized at once (transient peak ~3× the stored posterior). An explicittime_chunk=know computes the Poisson log-likelihood blockwise directly into the preallocated posterior, so the full-size log-likelihood and its working copy are never materialized — cutting the transient peak to ~1× over the returned posterior (the posterior itself is unavoidably 1×, sincedecode_positionreturns the full dense array). The opt-in path is tolerance-equal, not byte-exact, to the full path: the per-block likelihood matmul is a different BLAS shape than the full matmul, so it differs by ~1e-15 (MAP/argmax identical; every row sums to 1). For a path that never holds even the full posterior, usedecode_position_summary.
For sessions where even the stored dense posterior is too large to hold, use
the new decode_position_summary / decode_session_summary (see Added),
which never materialize the full (n_time, n_bins) posterior.
-
compute_spatial_ratesgains a keyword-onlydtype(np.float32/np.float64, defaultnp.float64).dtype=np.float32halves the stored(n_units, n_bins)rate-map array. The rate computation (GEMM / division) is still performed in float64 and only the final result is cast, so float32 values match the float64 default within float32 tolerance.decode_session/decode_session_summarynow accept their owndtypeparameter (default float64) that honors float32 end-to-end — the encoding-model working set and the posterior — sodecode_session(dtype=np.float32)halves the decode working set on the golden path (see thedecode_session/decode_session_summarydtypeentry above). Defaultnp.float64leaves every existing caller byte-for-byte unchanged; any other dtype raisesValueError. -
Documented the dense diffusion-kernel O(n²) memory cost and added a loud high-bin memory warning. The heat kernel
exp(-tL)of a connected graph is dense by construction (every entry > 0), so it always costsn_bins**2 * 8bytes of float64 memory (≈ 3.2 GB at 20,000 bins).compute_diffusion_kernelsandenv.compute_kernelnow emit a loudUserWarning(with a GB estimate) above 3,000 bins and then proceed — there is no hard limit and noallow_largeopt-out. The warning names the size, the GB estimate, the dense O(n²) reason, and the fixes (reduce bins or usesmoothing_method="binned"). No numerical results change — this is documentation plus a warn-and-proceed guard. The reliable scale wins this release remain float32 rate maps and the memory-safe summary decode (above). A faster, lower-peakexpm_multiply/ Chebyshev rewrite of the kernel is a deferred stretch goal and is intentionally not part of this release. -
Warned on the dense Gaussian-KDE high-bin path the same way as the diffusion kernel.
smoothing_method="gaussian_kde"builds a dense(n_bins, n_bins)weight matrix (exp(-d²/2σ²), every entry > 0), so it carries the same O(n_bins²) memory cost; it now emits a loudUserWarning(with a GB estimate) above the shared_LARGE_KERNEL_THRESHOLD(3,000 bins) and proceeds — no hard limit, noallow_large. One warn threshold is shared by both dense smoothing paths instead of a divergent copy. Also corrected staleencoding/_smoothing.pydocs that wrongly described the diffusion kernel as "sparse" /O(n_bins)— it is dense(n_bins, n_bins), O(n_bins²) per neuron, with a one-time O(n_bins³) matrix-exponential build. No numerical results change. -
SpatialRatesResult.summary_table()no longer double-computes grid and border scores. It previously ran the expensive per-neuron grid/border score computation twice per call (once for thegrid_score/border_scorecolumns and again insidelabel_cell_types()); it now computes each once and forwards them.label_cell_types()gains optional keyword-onlygrid_scores=/border_scores=parameters to accept precomputed score arrays (validated to be 1-D and lengthn_neurons); when omitted it recomputes as before, so existing callers are unchanged. -
population_coverage()gains a keyword-onlyn_jobsparameter (default1) to parallelize per-neuron place-field detection via joblib (-1uses all CPUs).n_jobs=1keeps the sequential path with no joblib overhead; results are byte-for-byte identical regardless ofn_jobs(returned data identical; per-neuron exclusion warnings are not surfaced undern_jobs != 1).
Changed¶
-
Default rate-map output changed for gappy / out-of-bounds data. With the new
max_gapdefault of0.5 s,compute_spatial_rate/compute_spatial_rates(anddecode_session/decode_session_summary, which forwardmax_gap) now exclude spikes inside large tracking gaps and out-of-bounds excursions from the numerator so it matches the occupancy denominator. See the Fixed entry "Firing-rate numerator/denominator alignment on the FULL interval mask" for the full rationale and themax_gap=Noneopt-out. -
detect_region_crossingsargument order changed to follow the behavioral-segmentation convention:(position_bins, times, env, *, region_name, direction=...). The old positional order(position_bins, times, region_name, env, ...)is still accepted for one release (transitional dispatch emits aDeprecationWarning) and will be removed in 0.7. -
Docs/examples now teach
decode_sessionas the one-call decode golden path; the manual 3-call path (compute_spatial_rates→bin_spikes_in_time→decode_position) is kept as an "Advanced: manual three-call path" section.examples/20_bayesian_decodingnow leads withdecode_session; a new "Workflow 2: Bayesian Decoding (one call)" section was added todocs/user-guide/workflows.md(with aworkflows_decode_session_golden_pathCI snippet indocs/snippets.yml); andREADME.mdpoints tofrom neurospatial.decoding import decode_sessionas the position-decoding entry point. -
Docs (Phase 0.6 sweep): rewrote Workflow 1 in
docs/user-guide/workflows.mdto use the canonicalcompute_spatial_rate(env, spike_times, times, positions, ...).firing_rateidiom withsimulate_trajectory_ou+PlaceCellModelfixtures, replacing the hand-rollednp.histogram/scipy.ndimage.gaussian_filterapproach. Added a CI snippet entry (workflows_place_field_canonical) todocs/snippets.yml.
Deprecated¶
All of the following emit a DeprecationWarning and are scheduled for removal
in 0.7. Each old name forwards to its replacement with unchanged behavior.
EgocentricRatesResult.detect_ovcs→EgocentricRatesResult.classify.ViewRatesResult.detect_view_cells→ViewRatesResult.classify.DirectionalRatesResult.detect_hd_cells→DirectionalRatesResult.classify.SpatialRatesResult.detect_cell_types→SpatialRatesResult.label_cell_types(the multi-class string labeler; not folded intoclassify).ViewRateResult.peak_view_location→ViewRateResult.peak_location.ViewRatesResult.peak_view_location→ViewRatesResult.peak_locations.detect_region_crossingsold positional order(position_bins, times, region_name, env, ...)→ new order(position_bins, times, env, *, region_name, ...).
Fixed¶
-
decode_position_summarynow validatespriorshape (was silently truncating over-long 2-D priors), matchingdecode_position. A 1-D prior must be(n_bins,)and a 2-D time-varying prior must be exactly(n_time, n_bins); an over-long or short 2-D prior, a wrong-length 1-D prior, or a non-1-D/2-D prior now raisesValueErrorbefore streaming instead of silently slicing the prior to the decoded time range. -
Firing-rate numerator/denominator alignment on the FULL interval mask. A firing-rate map is
spike_counts (numerator) / occupancy (denominator)per bin.env.occupancydrops a trajectory intervalkfor three reasons —dt[k] > max_gap(large tracking gap),speed[k] < min_speed(low speed), andstart_bin[k] < 0(interval's start sample out of the active environment) — but the spike binner previously only filtered by the time window and (since the speed-filter work) by speed. A spike inside a dropped interval (e.g. a 1 s tracking gap, or an out-of-bounds excursion) was therefore counted in the numerator while occupancy excluded that interval's time from the denominator, inflating/biasing the rate. The spike numerator and the occupancy denominator now drop the identical set of intervals via one sharedinterval_valid_maskhelper (the single source of truth thatenv.occupancyalso consumes).compute_spatial_rate/compute_spatial_ratesgain a keyword-onlymax_gap: float | None = 0.5(matchingenv.occupancy's default, so occupancy behavior is unchanged) that gates both sides identically. - Behavior change: rate maps now differ for sessions that contain large
tracking gaps (intervals longer than
max_gap) or out-of-bounds samples — previously those rates were inflated. This is a correctness fix. Passmax_gap=Noneto disable gap gating on both sides (restoring the pre-fix, no-gap-gating behavior while keeping numerator and denominator aligned). -
No more silent empty rate maps. When the interval filter excludes every trajectory interval (so occupancy is all-zero and the rate map is all-NaN/0),
compute_spatial_rate/compute_spatial_ratesnow emit a singleUserWarningnaming the active gate(s) (max_gap,min_speed, and/or the out-of-bounds-start possibility) and suggesting fixes (check units; passmax_gap=None). Previously only themin_speedgate warned — a too-largemax_gap(or a wrong-units one) and the out-of-bounds-start rule emptied the map silently. Gated bywarn_on_drop=True(the default); fires once per call (not per neuron in the batch path). -
decode_sessionnow warns loudly when most spikes fall outside the decode time window[times.min(), times.max()]instead of silently dropping them. Previously, whenencoding_models=was passed (which skipscompute_spatial_rates), spikes outside the window were silently discarded bybin_spikes_in_time'snp.histogram, so a milliseconds-vs-seconds unit mismatch produced an all-zero count matrix and a plausible-but-wrong posterior with no warning.decode_sessionnow owns a singleUserWarning(naming the dropped count/total, percentage, decode window, spike range, and units hypothesis) that covers both theencoding_models-provided andencoding_models=Nonebranches; the encoder's now-redundant duplicate is suppressed. A new keyword-onlywarn_on_drop: bool = Trueparameter silences it for genuinely sparse sessions. -
The bare-
Environment()error now uses a unique code[E1006](was[E1001], which collided with "No active bins found") and points at the correct docs host (https://edeno.github.io/neurospatial/). Documented indocs/errors.md. -
Environment.distance_to(region_name)now raisesRegionNotFoundError(fromneurospatial._exceptions) instead of a bareKeyErrorwhen the named region is absent.RegionNotFoundErrorsubclassesKeyError, so all existingexcept KeyErrorblocks keep working without change. -
Removed stale
compute_firing_rate(...)calls (that function does not exist publicly) fromdocs/user-guide/workflows.md(Workflow 3) anddocs/user-guide/spatial-analysis.md; replaced with the correctcompute_spatial_rate(env, spike_times, times, positions, ...).firing_rateAPI and argument order. -
Replaced 11 broken
env.plot(field, ax=...)calls indocs/user-guide/spike-field-primitives.mdanddocs/user-guide/rl-primitives.mdwithenv.plot_field(field, ax=...). Theenv.plot()method's first positional argument isax, not a field array, so passing a field there was silently broken. -
Updated
examples/20_bayesian_decoding.py(and the jupytext-paired.ipynb) to use the batchcompute_spatial_rates(env, spike_times_list, times, positions, ...).firing_ratesfor encoding models and the canonicalbin_spikes_in_time(spike_trains, dt, t_start, t_stop)helper for time-binned spike counts; regenerated the notebook viajupytext --sync. -
Bumped stale version strings from
v0.4.0tov0.5.0indocs/index.md(status line and BibTeX entry) andREADME.md(dependency table header and BibTeX entry). -
GraphValidationErrormessages inlayout/validation.pyand the wrappingValueErrorinenvironment/core.pyno longer reference the internal developer guideCLAUDE.md. Error text now points to the public issue tracker (https://github.com/edeno/neurospatial/issues) instead. A permanent repo-wide backstop test (tests/test_no_internal_doc_refs.py) asserts that no source file undersrc/neurospatial/contains this string, preventing the leak from regressing. -
Environment.__init__now raises a beginner-gradeValueErrorwhen called without arguments (i.e. bareEnvironment()). The old message "layout parameter is required" gave no actionable guidance; the new message explains thatEnvironmentmust be created through a factory method, shows a concrete correct example (Environment.from_samples(data, bin_size=2.0)), lists the other available factories (from_polygon,from_graph,from_grid_mask,from_pixel_mask), and links to the online docs — matching the style already used byEnvironmentNotFittedError. The exception type remainsValueErrorso existingexcept ValueErrorcallers are unaffected. -
align_spikes_to_eventsinevents/alignment.pynow rejectsevent_timescontainingInfvalues with a descriptiveValueError, matching the existingspike_timesInf check and fulfilling the docstring promise. Previously anInfevent time silently produced an empty-or-wrong result. -
behavior.trajectorypublic functions (compute_turn_angles,compute_step_lengths,mean_square_displacement) andbehavior.navigation.traveled_path_lengthnow coercepositions(andtimesfor MSD) withnp.asarrayat the public boundary before any.ndim/.shapeaccess. Passing a plain Python list no longer raises a confusingAttributeError; valid list-of-lists inputs succeed, and malformed inputs raise a descriptiveTypeErrororValueError. -
bin_spike_trainandbin_spike_trainsinencoding/_binning.pyno longer silently drop spikes outside the trajectory time window or spikes that map to inactive environment bins. When the dropped fraction exceeds 50 % (or all spikes are dropped), aUserWarningis emitted naming the dropped count, total, both time ranges, and a units-hypothesis hint (e.g. spike_times in milliseconds vs. times in seconds). Two separate messages cover the two drop causes (time-window and inactive-bin). The batch path (bin_spike_trains,compute_spatial_rates) warns exactly once per cause in the main process — never from joblib worker processes where warnings are commonly swallowed. Default behaviour for in-window spikes is byte-for-byte unchanged.
Documentation¶
population_coveragedocstring example now shows the vectorized batch path (compute_spatial_rates(...).firing_rates) instead of the per-neuroncompute_spatial_rateloop; the pre-existing arg-order bug (population_coverage(firing_rates, env)) has been corrected topopulation_coverage(env, firing_rates).population_coverageshape-mismatchValueErrormessage now steers users tocompute_spatial_rates(the batch function) rather than the slowcompute_spatial_rate+np.stackrecipe.compute_spatial_rate(singular) docstring now includes a short note pointing many-neuron users tocompute_spatial_rates(plural), which shares occupancy and smoothing-kernel work across the whole population.
Additional release notes¶
to_xarray()now returns a labeledxarray.Datasetinstead of anxarray.DataArray.- Population rate results (
SpatialRatesResult,DirectionalRatesResult,ViewRatesResult,EgocentricRatesResult) use dims("unit_id", "bin"). The rate matrix lives in thefiring_ratedata variable andunit_idstores real per-unit identity labels. - Decode results (
DecodingResult) use dims("time", "bin"); they have nounit_idaxis. -
Duplicate
unit_idsraiseValueErrorbecause label-based xarray selection requires unique labels. -
Batch encoding
to_dataframe()is now dense tidy: one row per(unit, bin), always carryingunit_id, bin-center coordinates,firing_rate, andoccupancy. -
Per-unit metric tables moved to
summary_table(), which is indexed byunit_id. The oldneuron_ids=relabeling keyword is replaced byunit_ids=onsummary_table(). -
Real unit identity on population results via
unit_ids, plus singularunit_idon indexed/iterated single-unit results. -
summary_table()on batch encoding results and population PSTH results. -
Experiment-shaped environment presets:
Environment.open_field(...),Environment.linear_track(...), andEnvironment.maze(...). -
SpatialRatesResult.label_cell_types()for multi-class labels, withSpatialRatesResult.classify()reserved for the boolean place-cell predicate. -
decode_position()accepts population rate result objects directly, and preserves their dtype (afloat32rate result decodes infloat32rather than being promoted back tofloat64). -
Memory-safe summary decoding for long sessions.
decode_position_summaryreturns a newDecodingSummaryresult that streams over time and reduces each block to per-time scalars/vectors (map_position/map_bin,mean_position,posterior_entropy,peak_prob) without ever materializing the full(n_time, n_bins)posterior.decode_session_summaryis the matching one-call encode -> bin -> decode wrapper, and now also streams the time-binning so the full count matrix is never materialized either.DecodingSummarycarries the standard terminal verbs (to_dataframe(),summary(),plot(),to_xarray()). -
decode_session()anddecode_session_summary()gain a keyword-onlydtypeparameter (np.float32/np.float64, defaultnp.float64) that is honored end-to-end — a singledtype=np.float32controls both the encoding-model working set and the posterior, halving the decode working set on the golden path (no silent promotion back tofloat64). Defaultnp.float64leaves every existing caller byte-for-byte unchanged; any other dtype raisesValueError. -
decode_position()gains keyword-onlydtype(np.float32/np.float64, defaultnp.float64) and a hybridtime_chunk.time_chunk=None(the default) keeps the full-matmul path byte-for-byte unchanged;decode_position(time_chunk=N)computes the Poisson likelihood blockwise directly into the preallocated posterior, cutting the transient peak to ~1× over the returned posterior (tolerance-equal to the default, not byte-exact).decode_session()forwards both. -
The summary decoders (
decode_position_summary/decode_session_summary) rejecttime_chunk=None(raising a clearValueError): aNonevalue would set the streaming block to the full session length and materialize the full(n_time, n_bins)posterior, defeating their never-materialize contract.time_chunkmust be a positive integer (default1024); usedecode_position/decode_sessionif you want the full posterior. -
compute_spatial_rates()gains a keyword-onlydtype(np.float32/np.float64, defaultnp.float64) to halve the memory of stored rate maps. -
Speed filtering on the encode path:
compute_spatial_rate/compute_spatial_ratesgain keyword-onlyspeed/min_speed(forwarded by bothdecode_sessionanddecode_session_summary). Whenmin_speedis set, one shared per-interval speed gate filters both the spike numerator and the occupancy denominator, so amin_speedknob can never bias firing rates by filtering only one side. -
population_coverage()gains a keyword-onlyn_jobsparameter to parallelize per-neuron coverage; results are identical regardless ofn_jobs. -
decode_session/decode_session_summarynow validatedt(finite,> 0; non-numeric andboolrejected) up front with a clearValueError, matchingbin_spikes_in_time. Previously the shared decode-grid builder bypassed that guard, so an invaliddtleaked a cryptic downstream error (dt=0->ZeroDivisionError,dt=NaN-> "cannot convert float NaN to integer",dt<0-> a misleading "span smaller than one bin" message). -
bin_spikes_in_timenow validatesdtconsistently via the same shared helper: a non-numericdt(including a numeric string like"0.1") and abool(dt=True) now raise a clearValueError("dt must be a finite number > 0, ..."). Previously a numeric string leaked a rawTypeErroranddt=Truewas silently accepted as a chunk size of1. -
decode_positionnow preservesfloat32when handed a rate-result object (anything exposing.firing_rates). Previously the friendly object path promoted afloat32.firing_ratestofloat64, silently losing part of thedtype=np.float32memory win the raw-array path already delivered. The object path now matches the raw-array path byte-for-byte:float32staysfloat32,float64staysfloat64, an integer rate map is promoted tofloat64, and aNone/ dict / non-2-D.firing_ratesstill raises the same clearValueError. -
time_chunkis now validated as a positive integer (notbool) acrossnormalize_to_posterior,decode_position,decode_position_summary, anddecode_session_summary, raising a clearValueErrornaming the value and its type. Previously a float (1.5) or string ("2") leaked a rawTypeError, andTruewas silently accepted as a chunk size of1. -
Firing-rate numerator/denominator alignment (behavior change for gappy / out-of-bounds data).
compute_spatial_rate/compute_spatial_ratesgain a keyword-onlymax_gap(default0.5 s); spikes inside large tracking gaps and out-of-bounds excursions are now excluded from the numerator so it drops the identical set of intervals thatenv.occupancydrops from the denominator. Rate maps now differ (and are more correct) for sessions with large tracking gaps or out-of-bounds samples; passmax_gap=Noneto disable gap gating on both sides. -
SpatialRatesResult.summary_table()no longer double-computes grid and border scores (single pass, faster for large populations). -
The dense smoothing kernels (
diffusion_kdeandgaussian_kde) areO(n_bins²)memory by construction. For very large bin counts they now emit a loudUserWarning(with the estimated size) and proceed — there is no hard limit and no opt-out parameter. To reduce memory, usesmoothing_method="binned"(or fewer bins / a largerbin_size). -
EgocentricRatesResult.detect_ovcs(...)->classify(...) -
ViewRatesResult.detect_view_cells(...)->classify(...) -
DirectionalRatesResult.detect_hd_cells(...)->classify(...) -
SpatialRatesResult.detect_cell_types(...)->label_cell_types(...) -
ViewRateResult.peak_view_location()->peak_location() -
ViewRatesResult.peak_view_location()->peak_locations() -
detect_region_crossings(position_bins, times, region_name, env, ...)->detect_region_crossings(position_bins, times, env, *, region_name, ...)
[v0.5.0] - 2026-06-04¶
What's Changed¶
Features¶
Bug Fixes¶
- fix(io,regions): atomic writes, NWB metadata round-trip, deep immutability (4307973)
- fix(animation): grid artist reuse, frame_times, figure/capture leaks (8f862d3)
- fix(decoding,stats,events): PSTH window, weighted Rayleigh, posterior NaN (301d2b0)
- fix(simulation): RNG independence, exact Poisson, mixed-mode kwargs (f00c048)
- fix(encoding): bearing wrap, occupancy masking, phase-precession optimizer (1525ffd)
- fix(environment,behavior,annotation): invariants, segmentation, leaks (d5c9f34)
- fix(ops,layout): correctness + safety fixes in core primitives (cefe030)
- docs(plan): mark test-suite remediation complete (9f18cc6)
- fix(encoding): recover egocentric distance tuning in binned rate (ef5090a)
- fix(simulation): place uniform field centers in the interior, not on the boundary (9872487)
Documentation¶
- docs(plan): mark test-suite remediation complete (9f18cc6)
- docs: address review follow-up on the fix-all changeset (a2ca036)
- test: reword two docstrings to drop implementation-plan references (28105fe)
- docs: refresh UX-facing onboarding docs (4080f49)
- docs: update CHANGELOG.md for v0.4.0 (c03df84)
Other Changes¶
- chore(release): 0.5.0 (#4) (d1afe9d)
- chore: remove completed test-suite remediation plan (8a7a04b)
- test(animation): drive visual regression through production renderer (8e5ed03)
- test: dedup shared test helpers into shared modules (review follow-up) (215c971)
- test: reword two docstrings to drop implementation-plan references (28105fe)
- test: tighten thresholds, strengthen sim recovery, hygiene (Phase 7) (fad0dcb)
- test: replace mocks with real-path tests where they hid integration (Phase 8) (ad3d105)
- test(ops,layout,environment): core-primitives behavioral coverage (Phase 5) (e23365d)
- test(nwb): add NWBHDF5IO disk round-trip tests for writers (Phase 6) (628acb2)
- test(animation): verify real cross-backend color-mapping parity (Phase 4) (b72dc61)
- test(decoding,stats,events): add closed-form + analytic-reference correctness tests (Phase 3) (d4f8da4)
- test(encoding): add end-to-end recovery + NumPy/JAX parity tests (Phase 2) (a8b216e)
- test(encoding): add object_vector_score tests; fix OVC convention doc + CLAUDE.md drift (d73cdc8)
Full Changelog: https://github.com/edeno/neurospatial/compare/v0.4.0...v0.5.0
0.4.0 - 2026-05-26¶
This release is the v0.4 UX cleanup: a wide-ranging consolidation of
public-API names, argument orders, return types, error semantics, and
example coverage. There are no deprecations; every change is a
clean delete-and-replace. Pin to <0.4.0 if you need the old surface.
Breaking changes¶
Parameter renames¶
distance_metric/distance_type/use_geodesic→metricacross all physical-distance APIs. Legal values are{"euclidean", "geodesic"}. AffectsEnvironment.distance_to,compute_egocentric_rate(s),compute_egocentric_distance,compute_spatial_rate(s),ObjectVectorCellModel,PlaceCellModel,BoundaryCellModel, and the boundary / border modules.smoothing_sigma/kernel_bandwidth→bandwidthacross smoothing APIs (compute_spatial_rate(s),compute_view_rate(s),compute_egocentric_rate(s),Environment.smooth, KDE helpers).velocity_threshold/speed_threshold/threshold→min_speedacross velocity-based behaviour segmentation (segment_by_velocity,heading_from_velocity, etc.).- Overlay
data=→ semantic name.PositionOverlay(data=...)→PositionOverlay(positions=...);HeadDirectionOverlay(data=...)→HeadDirectionOverlay(headings=...).
Result-class field renames¶
EgocentricRateResult.ego_env→env;ViewRateResult.view_occupancy→occupancy.PeriEventResult.firing_rateis now a cached attribute, not a method. Replaceresult.firing_rate()withresult.firing_rate.DecodingResult.uncertainty→posterior_entropy(matches the free function indecoding/estimates.py).- Singular vs plural method/attribute normalization on result classes
(single-neuron results use singular methods; batch results use plural).
Renames:
SpatialRateResult.peak_firing_rates()→peak_firing_rate(),SpatialRateResult.peak_locations()→peak_location(),ViewRateResult.peak_view_locations()→peak_view_location(). is_X_cellmethod names normalized to match the free-function names (is_object_vector_cell,is_spatial_view_cell,is_head_direction_cell).
Argument-order canonicalization¶
- Encoding functions are now
env-first, with the canonical order(env, spike_times, times, positions, headings?, object_positions?, *, ...). Affectscompute_spatial_rate(s),compute_egocentric_rate(s),compute_view_rate(s),detect_place_fields,is_spatial_view_cell,is_object_vector_cell,is_border_cell, and friends.
# v0.3
compute_spatial_rate(spike_times, times, positions, env)
compute_egocentric_rate(spike_times, times, positions, headings,
object_positions, env=env)
# v0.4
compute_spatial_rate(env, spike_times, times, positions)
compute_egocentric_rate(env, spike_times, times, positions,
headings, object_positions)
compute_directional_rate/is_head_direction_cellkeep the heading-domain-native(spike_times, times, headings, *, ...)signature — this is the documented exception to the env-first rule (heading is a circular angular variable, not a spatial position). See the function docstrings andCLAUDE.md"Canonical Argument Order".- Egocentric ops
allocentric_to_egocentric/egocentric_to_allocentricreorder to(positions, headings, targets). - Behavioural segmentation functions reordered to
(position_bins, times, env, *, ...). distance_to_rewardinevents.regressorsreordered to(env, times, positions, reward_times, ...).fit_isotonic_trajectory/fit_linear_trajectoryreordered to(env, posterior, times, *, ...)with a standardizedmethodkeyword.*keyword-only separator added consistently across the public API. Numerical parameters and verbose flags become keyword-only.
Coordinate / convention changes¶
Environment.is_1d→Environment.is_linearized_track. Same semantics (a 1-D graph track embedded in 2-D world coordinates); the new name resolves the historical "is this n_dims==1 or a 2-D track?" ambiguity. Serialized environment metadata uses the new key — pre-v0.4 saved environments will need to be re-saved.GridProperties.peak_coordsis now(x_offset, y_offset)instead of(row_offset, col_offset). Swappeak_coords[:, 0](was row) forpeak_coords[:, 1](now y) when reading the second component.simulate_trajectory_ou(speed_units=...)is now required (was defaulted). Speed defaults switch from m/s to cm/s. Mismatch betweenspeed_unitsandenv.unitsraises rather than silently rescaling.
Removed (no aliases, no deprecation)¶
Environment.save/Environment.load. The pickle path is gone. UseEnvironment.to_file/Environment.from_file(JSON metadata plus npz arrays).Environment.mask_for_region. UseEnvironment.region_mask.from_image/from_maskfactory aliases. Replaced byfrom_pixel_mask(image_mask, pixel_size, ...)andfrom_grid_mask(active_mask, grid_edges, ...).path_efficiency(float-returning). Usecompute_path_efficiencywhich returns aPathEfficiencyResult.- Cross-domain re-exports. Each public symbol now has exactly one
canonical import path; the top-level
neurospatialnamespace no longer re-exports symbols fromencoding,decoding, etc.
Added¶
PlaceFieldsResultdataclass.detect_place_fieldsreturns a frozen dataclass withfields,excluded_reason, andn_excludedfields. Still iterable / sized / indexable, so existingfor f in detect_place_fields(...)andlen(...)patterns keep working. Closes the "silent drop when mean rate too high" failure mode.BinSequenceWithRunsdataclass + new method.Environment.bin_sequencealways returns anndarray;Environment.bin_sequence_with_runsreturns a dataclass withbins,run_starts,run_lengths.MSDResultand friends. Misc result-type cleanup in trajectory analysis:MSDResult,SpatialAutocorrelationResult,PathEfficiencyResult.Environment.is_polarproperty andcoordinate_kindattribute.from_polar_egocentricsetscoordinate_kind="polar". Methods that assume Cartesian (distance_to,distance_between,Environment.contains,apply_transform,bin_aton(x, y)input) raise on polar environments with a clear error.plot_fieldswitches axis labels and skips the equal-aspect call so egocentric polar firing fields still render correctly.- Custom exception classes.
EnvironmentNotFittedError(already existed) now has a free-function variant; addedRegionNotFoundError,RegionAlreadyExistsError, and three more in_exceptions.py. Environment.from_pixel_maskandEnvironment.from_grid_maskfactories (replacingfrom_image/from_mask).Environment._state_versioninvalidation token. Cached properties verify the version on access; subset / transform / rebin bump it, so stale caches are surfaced loudly instead of returning silently-wrong results.Environment.__str__returnsinfo()for quick inspection.- Glossary page at docs/glossary.md defining 14
core terms. Linked from
docs/getting-started/core-concepts.mdand the README. docs/api/index.mdexpansion. Structured sections forencoding,decoding,behavior,events,ops.egocentric,ops.visibility,ops.basis,stats,animation,io.nwb.docs/examples/index.mdrewrite. Goal → notebook table plus full per-notebook entries with Time + Prerequisites.- Notebooks 24–27. Object-vector cells, head-direction tuning, peri-event PSTH, and NWB loading round-trip.
- README "Your First Place Field" front-door example. Canonical
pattern using
simulate_trajectory_ou,PlaceCellModel,generate_population_spikes, andcompute_spatial_rate. - CI doc-snippet test.
scripts/test_doc_snippets.pyplus.github/workflows/test_docs.ymlre-executes a curated manifest of doc snippets on every PR. - CI notebook regen test.
.github/workflows/test_notebooks.ymlre-executes11_place_field_analysis.ipynbper PR to catch silent regressions in the example surface. - Shared example styling.
examples/_style.pyWong / Okabe-Ito palette and fixed figure sizes. Wired into every tutorial notebook that previously set matplotlib rcParams inline (01-08, 19, 20, 22-27). Notebooks 09-18 and 21 had no rcParams blocks and intentionally keep matplotlib defaults.
Changed¶
- Silent failures replaced with loud failures.
subset()round-trip now returns aMaskedGridinstead of a one-offsubsetlayout kind, so the result is fully serializable.bin_atvsmap_points_to_binsstandardize on-1for out-of-environment samples in trajectory contexts.detect_place_fieldsreturns aPlaceFieldsResultwithexcluded_reasonset instead of silently returning[].batch_grid_scores/batch_border_scoresuse NaN as the explicit failure marker and warn once per batch.- Fitted-state checks at entry of
compute_spatial_rate(s),compute_egocentric_rate(s),compute_view_rate(s),decode_positionraise immediately instead of failing deep in the call stack. spike_timesvalidation rejects unsorted / negative / non-finite values with diagnostic messages.decode_position(validate=True)is the default; rejects negative spike counts and posteriors that don't sum to 1.- Canonical exception types throughout. Manual "not fitted" checks
migrated to
EnvironmentNotFittedError; warning-and-overwrite paths inRegions.__setitem__now raise. - Errors carry units and stack context. Length-mismatch errors
from
_binninginclude acontextarg so messages say "in compute_spatial_rate: ..."; magnitude errors include the offending unit. - Warning hygiene.
UserWarningfor data-quality,RuntimeWarningfor numerical fallbacks,stacklevel=2everywhere. - Production
print()calls replaced with module-levellogger.info/logger.debug. Environment.bin_attributes,edge_attributes,differential_operatorconverted from@cached_propertyto methods (get_bin_attributes(), etc.) so the cost is visible.Environment.unitsvalidated against a small registry ({"cm", "m", "mm", "px", None}) with aUserWarningfor unknown values. Documented as advisory.- Heading convention documented explicitly in every function that
takes a
headingsargument (allocentric world-frame: 0 = East, +π/2 = North; egocentric for OVC tuning: 0 = ahead, +π/2 = left). events.__init__is now eager (was lazy).- Bandit-task notebook prints the download URL and exits cleanly
when
data/is missing; CI no longer fails on the example.
Fixed¶
repr(env)name=Nonebug for empty-string names. Now usesrepr(self.name)so empty strings are visible as''.Environment._state_versioncache invalidation prevents stale-cache reads after mutating operations.- Polar environment misuse is now an error instead of producing silently-wrong distances or transforms.
Removed¶
Environment.save/Environment.load(pickle). Replaced byto_file/from_file.Environment.mask_for_region. Useregion_mask.from_image/from_maskfactory aliases. Replaced byfrom_pixel_mask/from_grid_mask.path_efficiencyfloat-returning function. Usecompute_path_efficiency.- All cross-domain re-exports from top-level
neurospatial.
Major feature additions (v0.3.x development cycle)¶
The following features were developed during the v0.3.x development line and ship as part of v0.4.0. The names below reflect the final v0.4 surface — many of these symbols were introduced under earlier names that were renamed during the M2 consolidation pass (see Breaking changes above for the v0.3 → v0.4 mapping).
Added (features)¶
- Spatial View Cells: Firing-rate fields indexed by gaze location
compute_view_rate()/compute_view_rates()— single / batchViewRateResultfrozen dataclass (firing_rate,occupancy,env, plusview_spatial_information(),is_spatial_view_cell(),sparsity(),selectivity()methods)is_spatial_view_cell()free-function classifier-
SpatialViewCellModelsimulation model with three gaze models (fixed_distance,ray_cast,boundary) -
Visibility and Gaze: Ray-casting visibility for view cells
FieldOfViewfrozen dataclass with species presets (FieldOfView.rat(),FieldOfView.primate())ViewshedResultfrozen dataclass (visible bins + visibility fraction)compute_viewed_location()— gaze-directed location projectioncompute_viewshed()/compute_viewshed_trajectory()/compute_view_field()— observer-position visibility analysisvisible_cues()— line-of-sight check to cue / landmark positions-
visibility_occupancy()— time each bin was visible -
Object-Vector Cells: Firing fields in egocentric polar coordinates
compute_egocentric_rate()/compute_egocentric_rates()EgocentricRateResult/EgocentricRatesResultfrozen dataclasses (firing_rate,occupancy,env, pluspreferred_distance(),preferred_direction(),egocentric_spatial_information(),is_object_vector_cell()methods)object_vector_score()— distance × direction selectivity metricis_object_vector_cell()free-function classifierplot_object_vector_tuning()— polar heatmap visualizationObjectVectorCellModelsimulation model with von Mises directional tuning-
ObjectVectorOverlayfor animation with object–animal vectors -
Egocentric Reference Frames: Foundation for object-vector and view cells
EgocentricFramedataclass withto_egocentric()/to_allocentric()allocentric_to_egocentric()/egocentric_to_allocentric()— batch frame transformscompute_egocentric_bearing()— angle to targets relative to heading (egocentric convention: 0 = ahead, +π/2 = left)compute_egocentric_distance()— Euclidean and geodesic distanceheading_from_velocity()/heading_from_body_orientation()— derive heading from tracking data-
Environment.from_polar_egocentric()— egocentric polar coordinate environment with optional circular connectivity -
3D Transform Support: Full N-dimensional affine transformation capabilities
- New
AffineNDclass for N-dimensional affine transforms using (N+1)×(N+1) homogeneous matrices Affine3Dtype alias for convenience (equivalent toAffineNDwith n_dims=3)- 3D factory functions:
translate_3d(),scale_3d(),from_rotation_matrix() - Integration with
scipy.spatial.transform.Rotationfor 3D rotations estimate_transform()now auto-detects dimensionality (2D or 3D) from input pointsapply_transform_to_environment()supports N-dimensional environments with validation- 45 new tests for 3D transforms including scipy integration
-
Backward compatible:
Affine2Dunchanged for existing 2D workflows -
N-D Probability Mapping:
alignment.pynow accepts N×N rotation matrices map_probabilities_to_nearest_target_bin()supports 3D environments- Updated
_transform_source_bin_centers()validates rotation matrix dimensionality - For 2D: Use
get_2d_rotation_matrix(angle_degrees) - For 3D: Use
scipy.spatial.transform.Rotation.as_matrix()
Changed (internal)¶
- Internal: Refactored
environment.py(5,335 lines) into modular package structure for improved maintainability - Split into 11 focused modules:
core.py(1,023 lines),factories.py(630 lines),queries.py(897 lines),trajectory.py(1,222 lines),transforms.py(634 lines),fields.py(564 lines),metrics.py(469 lines),regions.py(398 lines),serialization.py(315 lines),visualization.py(211 lines),decorators.py(77 lines) - Implemented mixin pattern: Environment inherits from all functionality mixins
- No breaking changes -
from neurospatial import Environmentcontinues to work - All 1,076 tests passing (100% success rate)
- Improved code organization for easier contribution and maintenance
- Largest module is trajectory.py at 1,222 lines (down from original analysis.py at 2,104 lines)
Documentation¶
- 3D Support: Updated dimensionality support documentation to reflect 3D transforms availability
- Updated
docs/dimensionality_support.mdwith 3D transform examples and feature matrix - Updated
docs/user-guide/alignment.mdwith comprehensive 3D transformation examples - Added complete 3D alignment workflow example
- Updated compatibility matrix: ~75% of neurospatial now works in 3D (up from 70%)
0.2.0 - 2025-11-04¶
Added¶
Environment Operations (Complete Feature Set)¶
Core Analysis Operations (P0)
Environment.occupancy()- Compute time-in-bin from trajectory data with speed filtering, gap handling, and optional kernel smoothingEnvironment.bin_sequence()- Convert trajectories to bin sequences with run-length encodingEnvironment.transitions()- Compute empirical transition matrices with adjacency filtering and normalizationEnvironment.components()- Find connected components in environment graphEnvironment.reachable_from()- Compute reachable bins via BFS or geodesic distance
Smoothing & Resampling (P1)
Environment.smooth()- Apply diffusion kernel smoothing to arbitrary fieldsEnvironment.rebin()- Conservative grid coarsening with mass/mean aggregation (grid-only)Environment.subset()- Extract subregions by bins, regions, or polygons
Interpolation & Field Utilities (P2)
Environment.interpolate()- Evaluate bin-valued fields at continuous points (nearest/linear modes)Environment.occupancy()linear mode - Ray-grid intersection for accurate boundary handling (grid-only)field_ops.pymodule:normalize_field()- Normalize to probability distributionclamp()- Bound field valuescombine_fields()- Weighted combination (mean/max/min)divergence()- KL/JS divergence and cosine distance
Utilities & Polish (P3)
Environment.region_membership()- Vectorized bin-to-region containment checksEnvironment.distance_to()- Compute distances to target bins or regions (Euclidean/geodesic)Environment.rings()- K-hop neighborhoods via BFS layersEnvironment.copy()- Deep/shallow copying with cache invalidationspatial.map_points_to_bins()- Enhanced withmax_distanceandmax_distance_factorthresholds for deterministic boundary decisions
Diffusion Kernel Infrastructure
kernels.pymodule:compute_diffusion_kernels()- Matrix-exponential heat kernel on graphs with volume correctionEnvironment.compute_kernel()- Convenience wrapper with caching- Support for both transition and density normalization modes
Documentation
docs/user-guide/spatial-analysis.md- Comprehensive 1,400+ line guide covering all operations with scientific contextdocs/examples/08_complete_workflow.ipynb- Enhanced workflow notebook with movement/navigation analysis- All methods have NumPy-style docstrings with working examples
- "Why This Matters" sections explaining scientific motivation for key operations
Changed¶
- GraphLayout: Now supports 1D layouts correctly (conditional
angle_2d, dynamicdimension_ranges) - KDTree operations: Now deterministic by default using
tie_break="lowest_index" - All environment operations: Use
@check_fitteddecorator for consistent state enforcement - Input validation: Comprehensive validation with diagnostic error messages across all operations
- Caching: Object identity-based caching for kernels and spatial queries
Fixed¶
- GraphLayout
angle_2dcomputation for 1D graphs (was unconditionally assuming 2D) - GraphLayout
dimension_rangesnow correctly handles 1D case - Disconnected graph handling in connectivity tests
- Hexagonal layout interpolation edge cases
Testing¶
- 1067 tests passing (up from 614 in v0.1.0)
- 0 skipped tests (eliminated all 12 previous skips)
- Performance benchmarks: occupancy on 100k samples, large transition matrices, kernel computation
- Integration tests: end-to-end workflows, multi-layout compatibility
- Edge case coverage: empty environments, single bins, disconnected graphs
Internal¶
- Systematic debugging skill used to eliminate all test skips
- Test-driven development for all features
- Code review and UX review completed
- Pre-commit hooks for code quality
0.1.0 - 2025-11-03¶
Added¶
- CompositeEnvironment API parity: Added
bins_in_region(),mask_for_region(),shortest_path(),info(),save(), andload()methods to CompositeEnvironment for full API compatibility with Environment class - KDTree-optimized spatial queries: CompositeEnvironment.bin_at() now uses KDTree for O(M log N) performance instead of O(N×M) sequential queries (enabled by default via
use_kdtree_query=True) - Structured logging infrastructure: New
_logging.pymodule with NullHandler by default, enabling optional logging for debugging and workflow tracing - Centralized numerical constants: New
_constants.pymodule consolidating all magic numbers (tolerances, KDTree parameters, epsilon values) for consistent behavior - Comprehensive type validation: CompositeEnvironment constructor now validates input types with actionable error messages
- Graph metadata validation: Added
validate_connectivity_graph()to enforce required node/edge attributes from layout engines - Dimensionality support documentation: New
docs/dimensionality_support.mdclarifying 1D/2D/3D feature support with compatibility matrix
Changed¶
- Updated alignment module: Now uses centralized constants (
IDW_MIN_DISTANCE,KDTREE_LEAF_SIZE) - Updated regions module: Uses
POINT_TOLERANCEconstant for consistent geometric comparisons - Enhanced error messages: CompositeEnvironment now provides detailed diagnostics for dimension mismatches and type errors
- Clarified 2D-only transforms: Updated
transforms.pydocstring to explicitly state 2D-only status and suggest scipy for 3D
Fixed¶
- Removed unused
type: ignorecomment inregular_grid.py - Fixed potential
KeyErrorin logging by renamingnameparameter toenv_name(avoids conflict with LogRecord reserved field)
Documentation¶
- Added comprehensive dimensionality support guide (1D/2D/3D feature matrix)
- Updated CLAUDE.md with latest patterns and requirements
- Added 18 new tests for CompositeEnvironment type validation
- Added 23 new tests for Environment error path coverage
- Added 28 new tests for graph validation
Internal¶
- Consolidated duplicate dimension inference code
- All 614 tests passing
- Ruff and mypy checks passing
- Test coverage: 78%
Additional release notes¶
-
Initial release of neurospatial
-
Core
Environmentclass with factory methods -
Multiple layout engines (regular grid, hexagonal, triangular, graph-based)
-
Region support for defining ROIs
-
Composite environment functionality
-
Alignment and transformation tools
-
Comprehensive test suite
-
NumPy-style docstrings throughout
-
Automatic active bin detection from data samples
-
NetworkX-based connectivity graphs
-
1D linearization for track-based experiments
-
Spatial queries (bin_at, neighbors, shortest_path, distance_between)
-
Visualization with matplotlib
-
Morphological operations (dilation, closing, hole filling)