Migrating to v0.6¶
Status: v0.6 is released. This guide describes the current v0.6 API.
v0.6 makes the result-object surface consistent across every encoding and decoding domain (the "naming contract"). Most of this is purely additive, but two changes are clean breaks — there is no transition shim, so you must update call sites. This page gives copy-paste BEFORE/AFTER for both, then lists the deprecations (which still work, with a warning, until removal in 0.7).
Breaking change 1 — to_xarray() returns a Dataset, not a DataArray¶
to_xarray() previously returned an xarray.DataArray indexed by integer
neuron. It now returns a labeled xarray.Dataset whose unit_id
coordinate holds your real per-unit identity labels (result.unit_ids), so
units are selected by label, not position.
# BEFORE — DataArray with integer "neuron" coords
da = result.to_xarray()
firing = da.sel(neuron=0) # select the first neuron by integer position
values = da.values # raw rate matrix
# AFTER — Dataset with real unit_id labels
ds = result.to_xarray()
firing = ds.sel(unit_id=result.unit_ids[0]) # select by label
values = ds["firing_rate"].values # rate matrix lives in a data var
Key points:
- Population rate results (
SpatialRatesResult,DirectionalRatesResult,ViewRatesResult,EgocentricRatesResult) → dims("unit_id", "bin"). The rate matrix is thefiring_ratedata var;occupancyis a("bin",)data var. Thebindimension carries non-indexbin_center_x/bin_center_y(/bin_center_z) coordinates (orbin_center_distance/bin_center_anglefor the polar egocentric result). - Decode results (
DecodingResult) → dims("time", "bin")— a posterior over space per time bin, with nounit_idaxis. The posterior lives in theposteriordata var. - Duplicate
unit_idsnow raiseValueError— label-based selection requires unique labels. to_xarray()requires the optionalxarrayextra:uv pip install "neurospatial[xarray]"(oruv add xarray).
Breaking change 2 — batch to_dataframe() is dense; the summary moved to summary_table()¶
On the batch (plural) encoding results — SpatialRatesResult,
DirectionalRatesResult, ViewRatesResult, EgocentricRatesResult —
to_dataframe() used to return a per-unit summary (one row per neuron, with
peak_x, peak_rate, spatial_info, cell_type, …). That summary has moved
to the new summary_table(). to_dataframe() is now dense tidy: one
row per (unit, bin), always carrying a unit_id column.
# BEFORE — to_dataframe() was the per-unit summary
df = result.to_dataframe() # one row per neuron: 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
The old neuron_ids= keyword on the per-unit to_dataframe() is replaced by
unit_ids= on summary_table() (defaulting to the result's own unit_ids).
Deprecations (still work; removed in 0.7)¶
These emit a DeprecationWarning and forward to their replacement with
unchanged behavior. Update at your leisure before 0.7.
| Old (deprecated) | New |
|---|---|
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(...) |
ViewRateResult.peak_view_location() |
ViewRateResult.peak_location() |
ViewRatesResult.peak_view_location() |
ViewRatesResult.peak_locations() |
detect_region_crossings(position_bins, times, region_name, env, ...) |
detect_region_crossings(position_bins, times, env, *, region_name, ...) |
Notes:
classifyvslabel_cell_typesare deliberately separate.classify()is a single-type boolean predicate (NDArray[bool]);label_cell_types()is the multi-class string labeler ("place"/"grid"/"border"/"unclassified"). They are not merged because collapsing a bool predicate and a str labeler under one name would silently change a return type and breakdf[col == "place"]filters.detect_region_crossingsargument order movedenvto slot 3 to match the behavioral-segmentation convention. The transitional dispatch detects the old order (3rd positional is astr) and warns; passregion_name=as a keyword to be future-proof.
Runnable migration snippet¶
The new way for both breaks, end to end:
import numpy as np
from neurospatial import Environment
from neurospatial.encoding import compute_spatial_rates
# --- tiny simulated session: 3 units on a small open arena ---
rng = np.random.default_rng(0)
t = np.linspace(0.0, 30.0, 600)
positions = np.column_stack(
[
50.0 + 30.0 * np.cos(2 * np.pi * t / 30.0),
50.0 + 30.0 * np.sin(2 * np.pi * t / 30.0),
]
)
times = t
env = Environment.from_samples(positions, bin_size=5.0)
# Three units, each with a different (sparse) spike train.
spike_times = [times[::5], times[::7], times[::9]]
unit_ids = np.array(["u1", "u2", "u3"])
result = compute_spatial_rates(
env, spike_times, times, positions, unit_ids=unit_ids
)
# --- Break 2a: summary_table() is the per-unit summary (one row per unit) ---
summary = result.summary_table()
assert len(summary) == 3 # one row per unit
assert "u2" in summary.index # unit_id-indexed
# --- Break 2b: to_dataframe() is now dense (one row per (unit, bin)) ---
dense = result.to_dataframe()
assert "unit_id" in dense.columns
assert len(dense) == 3 * env.n_bins # one row per (unit, bin)
# --- Break 1: to_xarray() is a labeled Dataset selected by unit_id label ---
# Requires the optional `xarray` extra; demonstrated skip-safe so this snippet
# runs on the default (no-xarray) install too.
try:
ds = result.to_xarray()
except ImportError:
print("xarray not installed; skipping to_xarray() demo")
else:
assert set(ds.dims) >= {"unit_id", "bin"}
firing = ds.sel(unit_id="u2")["firing_rate"] # select by label, not position
assert firing.shape == (env.n_bins,)
print("to_xarray() Dataset OK:", dict(ds.dims))
print("migration snippet OK")