Skip to content

One Environment for Cartesian and polar coordinates

Cartesian rooms and tracks and egocentric polar bins now use the same public Environment class. Choose the factory for the data you have. The factory sets what the coordinates mean; returning the same class does not make angle and length interchangeable.

import numpy as np
from neurospatial import Environment

room = Environment.from_grid_mask(
    np.ones((2, 2), dtype=bool),
    grid_edges=(np.arange(3.0),) * 2,
    units="cm",
)
polar = Environment.from_polar_egocentric(
    distance_range=(0.0, 3.0),
    angle_range=(-np.pi, np.pi),
    distance_bin_size=1.0,
    angle_bin_size=np.pi / 2,
    circular_angle=True,
    units="cm",
)
assert type(room) is type(polar) is Environment
assert room.coordinate_kind == "cartesian"
assert polar.coordinate_kind == "polar"
assert hasattr(polar, "bin_at")  # visible; calling it raises CartesianOnlyError
assert polar.smooth(
    np.ones(polar.n_bins), bandwidth=1.0, mode="average"
).shape == (polar.n_bins,)

Polar coordinates are (distance, angle): distance uses the declared length unit and angle is in radians. Zero angle is ahead of the animal; positive angles point to its left. Shared graph and field operations keep their existing polar math. Cartesian lookup, affine transforms, clipping and Cartesian trajectory operations still refuse polar inputs. Domain functions that read bin centers as Cartesian coordinates, such as resample_field and spatial_autocorrelation, refuse them too; functions with a graph-based mode, such as rate_map_centroid(method="geodesic"), keep that mode for polar bins. Decoded posterior means work on polar environments: mean_position is the centroid in the animal's egocentric plane, returned as (distance, angle). Type checkers infer that restriction from the factory return type; runtime checks also cover explicit unbound calls.

An unparameterized Environment annotation accepts either coordinate kind for shared operations. Loading from a file, dictionary or NWB record also returns an environment whose coordinate type is unknown until the record is read; the annotation does not promise Cartesian coordinates. Inspect coordinate_kind before choosing coordinate-specific operations. Factory-inferred Cartesian and polar types retain their stricter static checks.

Replace EgocentricPolarEnvironment.create(...) with Environment.from_polar_egocentric(...). The old public class and import are removed, with no alias. For metadata dispatch, use coordinate_kind instead of isinstance: both coordinate kinds are now instances of Environment.

env.layout and direct Environment(layout=...) construction are removed. Read supported environment properties such as bin_centers, bin_sizes, grid_edges, grid_shape, active_mask and edge_pairs. layout_type and layout_parameters remain descriptive construction history. They do not decide which operations are available. Construct from positions, polygons, masks or tracks using the existing factories; use from_bins for explicit owned geometry.

from neurospatial.binning import (
    BinConnections,
    Binning,
    MissingFaces,
    NearestBinLocator,
    SpatialBins,
)

bins = SpatialBins.from_arrays(
    centers=np.array([[0.0], [1.0]]), sizes=np.ones(2), measure_dim=1
)
connections = BinConnections.from_arrays(
    pairs=np.array([[0, 1]]), pair_distances=np.ones(1)
)
explicit = Environment.from_bins(
    Binning("measured bins", bins, connections, NearestBinLocator(bins.centers),
            MissingFaces("No measured cell interfaces")),
    units="cm",
)
assert explicit.n_bins == 2
assert not hasattr(explicit, "layout")
restored = Environment.from_dict(explicit.to_dict())
np.testing.assert_array_equal(restored.bin_centers, explicit.bin_centers)

Explicit bins need KnownFaces to enable numerical operations requiring cell interfaces, or MissingFaces with a reason when those interfaces are unknown. A custom provider must adapt to supported owned snapshots; live callbacks are not retained. Environments own their input buffers, metadata and regions. Use with_metadata, with_regions, subset, clip, rebin or apply_transform to obtain an updated value.

Supported dictionary, file and NWB records still load without running builders. No new scientific file schema is introduced by this class merge. Current copies, deep copies and pickles retain ownership. Historical pickles containing removed class paths or stored layouts are outside this migration contract: export those objects to a supported dictionary, file or NWB record using the older version, then load the export with this version.