neurospatial.behavior.epochs¶
epochs
¶
Array-native epoch selection (restrict / in_epochs).
"Give me my running periods" / "give me trial N" -- restrict time-series and
spike data to a set of time intervals (epochs). Epochs are plain
(start, end) arrays, so this stays array-first and never imports pynapple;
a pynapple IntervalSet is nonetheless accepted transparently because it
is duck-typed (it exposes .start / .end arrays), not isinstance-d.
Functions:
| Name | Description |
|---|---|
restrict |
The headline one-liner: |
in_epochs |
The boolean mask primitive: |
restrict_spike_trains |
Ragged per-unit spike times are not aligned to a common |
labels_to_epochs |
Turn one label per sample (task phase, running direction, trial type) into
half-open |
Aligned vs ragged
restrict is for arrays that share one time axis (position samples, a single
spike train). restrict_spike_trains is for a ragged collection where each
unit has its own timestamps and there is no shared times -- so each train is
masked against itself.
Classes¶
LabelEpochsResult
dataclass
¶
LabelEpochsResult(labels: tuple[Hashable, ...], epoch_bounds: NDArray[float64], epoch_intervals: NDArray[intp], interval_labels: NDArray[intp], interval_supported: NDArray[bool_], unsupported_labels: tuple[Hashable, ...], max_gap: float | None, input_epochs: NDArray[float64] | None, missing_label: Hashable | None, n_declared_breaks: int)
Bases: ReadOnlyArraysMixin, ResultMixin, Mapping[Hashable, NDArray[float64]]
Half-open epochs per label, returned by :func:labels_to_epochs.
A read-only mapping from each label to its (n_epochs, 2) array of
[start, stop) windows in seconds. Pass result[label] as epochs=
to an analysis to restrict it to the time spent in that condition.
Attributes:
| Name | Type | Description |
|---|---|---|
labels |
tuple of hashable
|
The mapping's keys, in the order of their first retained interval. |
epoch_bounds |
(NDArray[float64], shape(n_epochs, 2))
|
|
epoch_labels |
(NDArray[intp], shape(n_epochs))
|
Index into |
epoch_intervals |
(NDArray[intp], shape(n_epochs, 2))
|
Inclusive first and last interval |
interval_labels |
(NDArray[intp], shape(max(n_samples - 1, 0)))
|
Index into |
interval_supported |
(NDArray[bool_], shape(max(n_samples - 1, 0)))
|
True where the gap between the valid samples around the interval
passes |
unsupported_labels |
tuple of hashable
|
Labels that start an interval but have no supported interval, in first-seen order. They are not keys. |
n_samples |
int
|
Number of input samples, derived as |
max_gap |
float or None
|
The gap limit that was applied, in seconds. |
input_epochs |
(NDArray[float64] or None, shape(n_windows, 2))
|
The normalized |
missing_label |
hashable or None
|
The label value treated as missing, besides |
n_declared_breaks |
int
|
Number of breaks declared by |
Notes
Each label is one condition; the epochs of different labels never overlap.
Examples:
>>> import numpy as np
>>> from neurospatial.behavior import labels_to_epochs
>>> times = np.arange(5) * 0.25
>>> conditions = labels_to_epochs(times, ["a", "a", "b", "b", "b"])
>>> list(conditions)
['a', 'b']
>>> conditions["b"]
array([[0.5, 1. ]])
Attributes¶
durations
property
¶
Functions¶
epochs_table
¶
One row per epoch, in time order.
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns |
Examples:
>>> import numpy as np
>>> from neurospatial.behavior import labels_to_epochs
>>> times = np.arange(4) * 0.25
>>> table = labels_to_epochs(times, ["a", "a", "b", "b"]).epochs_table()
>>> table[["label", "start", "stop", "n_intervals"]]
label start stop n_intervals
0 a 0.0 0.50 2
1 b 0.5 0.75 1
The epochs= windows that were applied are recorded as
input_epochs:
>>> result = labels_to_epochs(times, ["a", "a", "b", "b"], epochs=[(0.0, 0.5)])
>>> result.input_epochs
array([[0. , 0.5]])
>>> result.epochs_table().attrs["input_epochs"]
[[0.0, 0.5]]
Source code in src/neurospatial/behavior/epochs.py
summary
¶
Flat dict of scalar headline metrics.
n_unsupported_intervals counts intervals the support rules
excluded; n_missing_label_intervals counts supported intervals
whose label is missing. labeled_duration is in seconds.
Examples:
>>> import numpy as np
>>> from neurospatial.behavior import labels_to_epochs
>>> times = np.arange(4) * 0.25
>>> summary = labels_to_epochs(times, ["a", None, "b", "b"]).summary()
>>> summary["n_missing_label_intervals"], summary["labeled_duration"]
(1, 0.5)
Source code in src/neurospatial/behavior/epochs.py
Functions¶
in_epochs
¶
Boolean mask: True where each element of t falls in any epoch.
Vectorized over samples (broadcasting, no Python loop) and taking the union across intervals (overlapping intervals behave as their union).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
ndarray
|
Timestamps to test (any shape; the mask has the same shape). |
required |
epochs
|
(start, stop), array-like of shape (n, 2), IntervalSet, or None
|
Windows in seconds, on the same clock as the input timestamps. Rows
are sorted and overlapping or touching rows merge. |
required |
closed
|
('both', 'left', 'right', 'neither')
|
Which endpoints are inclusive. Default |
"both"
|
Returns:
| Type | Description |
|---|---|
ndarray of bool
|
Same shape as |
See Also
restrict : Slice times and aligned arrays to the epochs. neurospatial.events.filter_by_intervals : Keep the rows of an events DataFrame that fall in an intervals DataFrame.
Notes
NaN timestamps evaluate False at every >=/<=/>/<
comparison, so they are (deliberately) excluded from every epoch.
Examples:
>>> import numpy as np
>>> from neurospatial.behavior import in_epochs
>>> in_epochs(np.array([0.0, 2.5, 5.0, 6.0]), (0.0, 5.0))
array([ True, True, False, False])
>>> in_epochs(np.array([0.0, 2.5, 5.0, 6.0]), (0.0, 5.0), closed="both")
array([ True, True, True, False])
Source code in src/neurospatial/behavior/epochs.py
restrict
¶
restrict(times: NDArray[float64], *arrays: NDArray[Any], epochs: Any, closed: _Closed = 'left') -> NDArray[Any] | tuple[NDArray[Any], ...]
Restrict times and time-aligned arrays to a set of epochs.
The headline one-liner::
t, pos = restrict(times, positions, epochs=run_epochs)
times is the reference 1-D time axis. Each of *arrays must be
aligned to times (len(arr) == len(times); e.g. positions of
shape (n, n_dims)). All are sliced by the single mask
in_epochs(times, epochs, closed=closed), so alignment and order are
preserved.
With no extra arrays, restrict restricts an event-time array by its
own timestamps -- restrict(spike_train, epochs=...) returns the in-epoch
spikes (here times is the spike train). For ragged per-unit spikes
(each unit its own timestamps, no shared axis), use
:func:restrict_spike_trains instead.
For occupancy, firing rates, velocity or decoding, pass the original arrays
with epochs= to the analysis. Sliced arrays cannot encode the excluded
time between windows, so an analysis may reconnect nearby windows, and the
sample on a window's stop is left out, so the window's last sampling
interval is lost. This function warns when aligned arrays would lose
either.
Windows are half-open by default, so disjoint train/test windows split
point events without sharing any.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
times
|
(ndarray, shape(n))
|
Reference time array. |
required |
*arrays
|
ndarray
|
Zero or more arrays aligned to |
()
|
epochs
|
(start, stop), array-like of shape (n, 2), IntervalSet, or None
|
Windows in seconds, on the same clock as the input timestamps. Rows
are sorted and overlapping or touching rows merge. |
required |
closed
|
('both', 'left', 'right', 'neither')
|
Endpoint inclusivity, forwarded to :func: |
"both"
|
Returns:
| Type | Description |
|---|---|
ndarray or tuple of ndarray
|
If no extra arrays were passed, the masked |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any array's first-axis length differs from |
See Also
in_epochs : The boolean mask this function slices by. restrict_spike_trains : Restrict ragged per-unit spike trains. neurospatial.events.filter_by_intervals : Keep the rows of an events DataFrame that fall in an intervals DataFrame.
Examples:
>>> import numpy as np
>>> from neurospatial.behavior import restrict
>>> times = np.array([0.0, 1.0, 2.0, 3.0, 4.0])
>>> positions = np.column_stack([times, times])
>>> t, pos = restrict(times, positions, epochs=(1.0, 3.0))
>>> t
array([1., 2.])
Source code in src/neurospatial/behavior/epochs.py
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 | |
restrict_spike_trains
¶
restrict_spike_trains(spike_times: Sequence[NDArray[float64]], epochs: Any, *, closed: _Closed = 'left') -> list[NDArray[np.float64]]
Restrict each ragged per-unit spike train by its own timestamps.
Ragged per-unit spike times are not aligned to a common times axis, so
each train is masked against itself: [t[in_epochs(t, epochs)] for t in
spike_times].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spike_times
|
sequence of ndarray, or SpikeTrains
|
Per-unit 1-D spike-time arrays. A
:class: |
required |
epochs
|
(start, stop), array-like of shape (n, 2), IntervalSet, or None
|
Windows in seconds, on the same clock as the input timestamps. Rows
are sorted and overlapping or touching rows merge. |
required |
closed
|
('both', 'left', 'right', 'neither')
|
Endpoint inclusivity, forwarded to :func: |
"both"
|
Returns:
| Type | Description |
|---|---|
list of ndarray
|
One masked train per input train, in the same order. Always a plain
|
Examples:
>>> import numpy as np
>>> from neurospatial.behavior import restrict_spike_trains
>>> spike_times = [np.array([0.1, 1.5, 2.9]), np.array([0.5, 3.0, 6.0])]
>>> restrict_spike_trains(spike_times, (1.0, 3.5))
[array([1.5, 2.9]), array([3.])]
Source code in src/neurospatial/behavior/epochs.py
labels_to_epochs
¶
labels_to_epochs(times: Any, labels: Any, *, valid: Any = None, break_before: Any = None, max_gap: float | None = DEFAULT_MAX_GAP, epochs: Any = None, missing_label: Hashable | None = None) -> LabelEpochsResult
Convert one label per sample into half-open epochs per label.
Use this to split a session into behavioral conditions (outbound and
inbound runs, task phases, cue or trial types) and then estimate a rate
map for each condition by passing conditions[label] as epochs=
together with the original times, positions and spike_times.
Interval k is [times[k], times[k + 1]) and carries the label of
its start sample, labels[k]. An unobserved sample (valid False)
is skipped as if its row were deleted: an interval inside the gap
between two observed samples carries the label of the observed sample
that starts the gap. An interval is kept when its gap passes
max_gap, lies inside one epochs row and contains no declared
break, and its label is not missing. Consecutive kept intervals with the
same label form one epoch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
times
|
(array - like, shape(n_samples))
|
Sample times in seconds; finite and strictly increasing. |
required |
labels
|
(array - like, shape(n_samples))
|
One hashable label per sample. |
required |
valid
|
array-like of bool, shape (n_samples,)
|
False marks a sample that was not observed (for example lost tracking). Default: every sample is valid. |
None
|
break_before
|
array-like of bool, shape (n_samples,)
|
True at sample |
None
|
max_gap
|
float or None
|
Longest stretch of missing tracking, in seconds, to bridge: the time
between consecutive valid samples (a dropped frame and a NaN row are
the same missing sample). Longer gaps are excluded as tracking losses;
|
1.0
|
epochs
|
(start, stop), array-like of shape (n, 2), IntervalSet, or None
|
Half-open windows in seconds; only intervals entirely inside one window are kept. |
None
|
missing_label
|
hashable
|
A label value that means "no label" (for example |
None
|
Returns:
| Type | Description |
|---|---|
LabelEpochsResult
|
A mapping from each label to its |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
See Also
neurospatial.behavior.laps_to_direction_labels : Per-sample labels from laps. neurospatial.behavior.running_direction_labels : Per-sample running direction.
Notes
Time is allocated to the label at the start of each interval, so the
final sample adds no time and a label held by a single sample before a
change contributes one interval. The same label after a gap longer than
max_gap (counting unobserved samples as missing rows), a declared
break, an excluded window or a different label starts a new epoch; no
duration is inferred across any of them. A label that starts intervals but has none kept is listed in
unsupported_labels rather than returned with an empty window set.
Labels here are disjoint by construction: each interval has one label.
Conditions passed to an analysis as separate epochs= sets may overlap
elsewhere. Labels assigned after the fact, for example from the path the
animal later took, describe the trajectory; they are not causal
predictors of the animal's choice.
Examples:
>>> import numpy as np
>>> from neurospatial.behavior import labels_to_epochs
>>> times = np.arange(10) / 10
>>> labels = np.array(["out"] * 5 + ["in"] * 5)
>>> valid = np.ones(10, dtype=bool)
>>> valid[7] = False # tracking lost at 0.7 s for one frame: bridged
>>> conditions = labels_to_epochs(times, labels, valid=valid)
>>> list(conditions)
['out', 'in']
>>> conditions["in"]
array([[0.5, 0.9]])
>>> labels_to_epochs(times, labels, valid=valid, max_gap=0.15)["in"]
array([[0.5, 0.6],
[0.8, 0.9]])
Tuples are single labels, for combined conditions:
>>> labels = [("arm1", "out")] * 3 + [("arm1", "in")] * 2
>>> list(labels_to_epochs(np.arange(5.0) / 10, labels))
[('arm1', 'out'), ('arm1', 'in')]
Source code in src/neurospatial/behavior/epochs.py
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 | |