Intervals#

class pypillometry.Intervals(intervals, units=UNSET, label=None, event_labels=None, event_onsets=None, event_meta=None)[source]#

Bases: Stateful

Container for intervals with metadata.

This class wraps a list of (start, end) intervals and provides additional metadata such as units, labels, and associated event information. Intervals are end-exclusive: (start, end) covers the samples with start <= t < end.

Usually, Intervals objects are not created directly but returned by methods of the eye-data classes, e.g., get_intervals() (spans between two events; windows around single events are Epochs from get_epochs(), see to_intervals()), get_blinks() or mask_as_intervals(). Detection methods such as d.pupil.blinks_detect() store their results as Intervals. Several Intervals can be combined with + or concat_intervals() and subtracted with -.

Intervals in sample indices are only valid for the data object they came from. Converting between sample indices and time needs that data object (its time vector), e.g., iv.to_units("ms", d) or iv.to_mask(d); conversions between time units work without it.

Parameters:
  • intervals (list of tuples, np.ndarray or Quantity) – Intervals as list of (start, end) tuples or 2D array; can carry units, e.g. [(0, 100), (200, 300)] * u.ms or [("1 min", "2 min")]

  • units (str or Unit) – Units of the intervals (“ms”, “sec”, “min”, “h”, or “samples” for sample indices). Can be omitted if intervals carry units; otherwise, intervals with units are converted to units.

  • label (str, optional) – Descriptive label for the interval collection

  • event_labels (list, optional) – Event labels corresponding to each interval

  • event_onsets (list or np.ndarray, optional) – Original event onset times (in same units as intervals)

  • event_meta (dict, optional) – Metadata of the event corresponding to each interval (column name -> array with one value per interval; see Events)

Examples

>>> from pypillometry import u
>>> intervals = Intervals([(0, 100), (200, 300)], units="ms", label="stimulus")
>>> intervals = Intervals([(0, 100), (200, 300)] * u.ms, label="stimulus")
>>> len(intervals)
2
>>> for start, end in intervals:
...     print(f"{start}-{end}")
0.0-100.0
200.0-300.0

Methods

as_pandas

Represent intervals as a pandas.DataFrame.

complement

Return the periods of the recording not covered by the intervals.

filter

Filter intervals and return a new Intervals object.

from_mask

Create Intervals from a binary mask array.

merge

Merge overlapping or close intervals.

pad

Add padding to the left (start) and right (end) of each interval.

plot

Plot intervals as horizontal lines on a timeline.

stats

Get statistics about interval durations.

to_mask

Convert intervals to a binary mask array for the samples of data.

to_units

Convert intervals to different time units or indices.

Details

classmethod from_mask(mask, data=None, units='samples', label=None)[source]#

Create Intervals from a binary mask array.

Each contiguous run of non-zero values becomes one interval.

Parameters:
  • mask (ndarray) – Binary array where non-zero values indicate interval membership, one entry per sample.

  • data (EyeData, optional) – data object the mask belongs to (anything with tx and fs); needed for time units.

  • units (str, optional) – Units for the intervals. Default is “samples” (sample indices).

  • label (str) – Label for the intervals.

Return type:

Intervals

Returns:

New Intervals object created from the mask.

Examples

>>> mask = np.array([0, 0, 1, 1, 1, 0, 0, 1, 1, 0])
>>> intervals = Intervals.from_mask(mask)
>>> intervals.intervals
[(2, 5), (7, 9)]
as_pandas()[source]#

Represent intervals as a pandas.DataFrame.

Return type:

pandas.DataFrame

Returns:

DataFrame with columns start, end, duration and units. When metadata is available, the columns label and onset (from event_labels and event_onsets, named as in Epochs.as_pandas() and Events.to_table()) are included, and the metadata of the events (event_meta) are added as columns.

Raises:

ImportError – If pandas is not installed.

complement(data)[source]#

Return the periods of the recording not covered by the intervals.

These are the gaps between the intervals and before the first and after the last one, within the whole recording. Overlapping intervals are merged first. Event information (labels, onsets) is not kept.

Parameters:

data (EyeData) – data object the intervals belong to; the complement is taken within the whole recording, from the first sample to the end of the last one (one sample past it, as intervals are end-exclusive)

Return type:

Intervals

Returns:

the periods not covered, in the units of this object

Examples

>>> import pypillometry as pp
>>> d = pp.EyeData(left_pupil=np.ones(100), sampling_rate=100)
>>> iv = Intervals([(10, 20), (50, 60)], units="samples")
>>> iv.complement(d).intervals
[(0, 10), (20, 50), (60, 100)]
filter(selector=None, min_duration=None, max_duration=None, time_range=None)[source]#

Filter intervals and return a new Intervals object.

This method supports multiple filtering criteria that can be combined: - Custom function: applied to each interval - Duration constraints: minimum and/or maximum duration - Time range: intervals must overlap with the specified range

All specified criteria are combined with AND logic.

Parameters:
  • selector (callable) – Function called as selector(start, end) for each interval that returns bool: True to keep the interval, False to discard it.

  • min_duration (float) – Minimum duration of intervals to keep (in current units, unless it carries units, e.g. "50 ms")

  • max_duration (float) – Maximum duration of intervals to keep (as min_duration)

  • time_range (tuple) – (start, end) time range (in current units, unless it carries units, e.g. (1, 2) * u.min). Only keeps intervals that overlap with this range. An interval overlaps if its end > range_start AND its start < range_end.

Return type:

Intervals

Returns:

New Intervals object containing only the filtered intervals

Examples

>>> intervals = Intervals([(0, 100), (200, 400), (500, 550)], units="ms")
>>> # Filter by duration
>>> short = intervals.filter(max_duration=100)
>>> len(short)
2
>>> # Filter by minimum duration
>>> long = intervals.filter(min_duration=150)
>>> len(long)
1
>>> # Filter by time range (keeps intervals overlapping with range)
>>> middle = intervals.filter(time_range=(150, 450))
>>> # Filter by custom function
>>> starts_at_multiple_of_100 = intervals.filter(
...     selector=lambda start, end: start % 100 == 0
... )
>>> # Combine multiple criteria
>>> filtered = intervals.filter(
...     min_duration=50,
...     max_duration=200,
...     time_range=(0, 500)
... )
merge(merge_sep='_', distance=0)[source]#

Merge overlapping or close intervals.

When intervals are merged, metadata is handled as follows: - event_labels: Labels of merged intervals are joined with merge_sep - event_onsets: First onset of merged group is kept - event_meta: Metadata of the first interval of merged group is kept

Parameters:
  • merge_sep (str, optional) – Separator to use when joining event labels of merged intervals. Default is ‘_’.

  • distance (float) –

    Maximum gap between intervals to still merge them, in the units of the Intervals object unless it carries units (e.g., 50 * u.ms; for intervals in sample indices, convert them to time units first or give the distance in samples). If two intervals are separated by less than or equal to this distance, they will be merged. Default is 0 (only merge overlapping intervals).

    Example: If distance=2 and intervals are (10, 15) and (17, 22), they will be merged into (10, 22) because the gap (17-15=2) is <= distance.

Return type:

Intervals

Returns:

New Intervals object with merged intervals and updated metadata.

Raises:

ValueError – If distance has time units and the intervals are in sample indices (or the other way round).

pad(left=0, right=0)[source]#

Add padding to the left (start) and right (end) of each interval.

The padding is in the current units of the Intervals object, unless it carries units (e.g., left=50 * u.ms). Padding is applied independently to each interval without merging. Use .merge() after padding if you want to merge overlapping intervals. The intervals are not clipped to the data (e.g., a padded interval in sample indices may start before 0); the methods that read the data handle the parts outside of it.

Parameters:
  • left (float) – Amount to subtract from the start of each interval (expand left). Default is 0.

  • right (float) – Amount to add to the end of each interval (expand right). Default is 0.

Return type:

Intervals

Returns:

New Intervals object with padded intervals.

Notes

  • Negative padding values will shrink intervals (left adds to start, right subtracts from end); a ValueError is raised if an interval would end before it starts

Examples

>>> intervals = Intervals([(100, 200), (300, 400)], units="ms")
>>> padded = intervals.pad(left=10, right=20)
>>> padded.intervals
[(90, 220), (290, 420)]
>>> # Pad and then merge overlapping intervals
>>> intervals.pad(left=50, right=50).merge().intervals
[(50, 450)]
plot(show_labels=True, title=None, ax=None, **kwargs)[source]#

Plot intervals as horizontal lines on a timeline.

Each interval is shown as a horizontal line at a different y-level.

Parameters:
  • show_labels (bool) – Whether to display event labels for each interval (default True)

  • title (str) – title of the plot; default: the label of the intervals

  • ax (matplotlib.axes.Axes, optional) – axes to plot on; default: the current axes

  • **kwargs (dict) – Additional keyword arguments passed to matplotlib.axes.Axes.plot()

Return type:

matplotlib.axes.Axes

Returns:

the axes of the plot

Examples

>>> import pypillometry as pp
>>> from pypillometry import u
>>> import matplotlib.pyplot as plt
>>> d = pp.get_example_data("rlmw_002_short")
>>> intervals = d.get_epochs("S_", (-500, 2500) * u.ms).to_intervals("sec")
>>> fig, ax = plt.subplots(figsize=(8, 3))
>>> ax = intervals.plot(ax=ax)
../../_images/pypillometry-Intervals-1.png
stats()[source]#

Get statistics about interval durations.

Return type:

dict

Returns:

Dictionary with summary statistics (n, mean, sd, min, max, total_duration, units); for empty Intervals, mean/sd/min/max are NaN and total_duration is 0

to_mask(data)[source]#

Convert intervals to a binary mask array for the samples of data.

Parameters:

data (EyeData) – data object the intervals belong to (anything with a time vector tx in ms and a sampling rate fs); the mask has one entry per sample of data

Return type:

ndarray

Returns:

Binary array with 1s where intervals exist, 0s elsewhere.

Examples

>>> import pypillometry as pp
>>> d = pp.get_example_data("rlmw_002_short").pupil.blinks_detect()
>>> blinks = d.get_blinks("left", "pupil")["left_pupil"]
>>> mask = blinks.to_mask(d)
>>> len(mask) == len(d)
True
to_units(target_units, data=None)[source]#

Convert intervals to different time units or indices.

Conversions between time units (“ms”, “sec”, “min”, “h”) only rescale the values. Conversions between sample indices and time use the time vector of data, the data object the intervals belong to: a sample index stands for the time of that sample, and a time is converted to the first sample at or after it (so (start, end) covers the samples with start <= t < end).

Parameters:
  • target_units (str) – Target units: “ms”, “sec”, “min”, “h”, or “samples” for sample indices (aliases such as “seconds” or “indices” work, too).

  • data (EyeData, optional) – data object the intervals belong to (anything with a time vector tx in ms and a sampling rate fs); needed to convert between sample indices and time

Return type:

Intervals

Returns:

New Intervals object with converted units

Raises:

ValueError – If the conversion involves sample indices and data is not given

Examples

>>> import pypillometry as pp
>>> d = pp.get_example_data("rlmw_002_short")
>>> intervals = d.get_intervals(start="F_", end="R_", units="ms")
>>> intervals_sec = intervals.to_units("sec")
>>> intervals_seconds = intervals.to_units("seconds")  # alias works too

Convert between indices and time units (needs the data object):

>>> d = d.pupil.blinks_detect()
>>> blinks = d.get_blinks("left", "pupil")["left_pupil"]
>>> blinks_idx = blinks.to_units("samples", d)
>>> blinks_ms = blinks_idx.to_units("ms", d)