Source code for dispel.data.epochs

"""A data model to describe epochs in time.

When processing signals, one of the fundamental concepts is to describe specific
aspects of a signal in a defined period. :class:`Epoch` provides the basic mechanics.
"""
from datetime import datetime
from typing import Any, Callable, Iterable, Optional, Union

import pandas as pd
from numpy import datetime64

from dispel.data.flags import FlagMixIn
from dispel.data.values import DefinitionId


[docs] class EpochDefinition: """The definition of an epoch. Parameters ---------- id_ The identifier of the epoch. This identifier does not have to be unique across multiple epochs and can serve as a type of epoch. name An optional plain-text name of the epoch definition. description A detailed description of the epoch providing additional resolution beyond the ``name`` property. Attributes ---------- name An optional plain-text name of the epoch definition. description A detailed description of the epoch providing additional resolution beyond the ``name`` property. """
[docs] def __init__( self, id_: Union[str, DefinitionId], name: Optional[str] = None, description: Optional[str] = None, ): self.id = id_ # type: ignore self.name = name self.description = description
@property def id(self) -> DefinitionId: """Get the ID of the definition. Returns ------- DefinitionId The ID of the epoch definition. """ return self._id @id.setter def id(self, value: Union[str, DefinitionId]): """Set the ID of the definition. Parameters ---------- value The ID of the definition. The ID has to be unique with respect to the time points of the :class:`Epoch`, i.e., if an epoch has the same ID, start, and end, it is considered equal. """ if not isinstance(value, DefinitionId): value = DefinitionId(value) self._id = value
[docs] class Epoch(FlagMixIn): """An epoch marking a specific time point or period. Parameters ---------- start The beginning of the epoch. end An optional end of the epoch. If no end is provided, the epoch end will be considered in the future and the :data:`Epoch.is_incomplete` property will be `True`. definition An optional definition of the epoch. """
[docs] def __init__( self, start: Any, end: Any, definition: Optional[EpochDefinition] = None, ): super().__init__() self.start = start self.end = end self.definition = definition
@property def start(self) -> pd.Timestamp: """Get the beginning of the epoch. Returns ------- pandas.Timestamp The beginning of the epoch. """ return self._start @start.setter def start(self, value: Union[int, float, str, datetime, datetime64]): """Set the beginning of the epoch. Parameters ---------- value The start of the epoch. Raises ------ ValueError Risen if the provided value is null. """ self._start = pd.Timestamp(value) if pd.isnull(self.start): raise ValueError("Start date cannot be null") @property def end(self) -> Optional[pd.Timestamp]: """Get the end of the epoch. Returns ------- pandas.Timestamp The end of the epoch. `None`, if the epoch end has not been observed (i.e., was not set). """ return self._end @end.setter def end(self, value: Optional[Union[int, float, str, datetime, datetime64]]): """Set the end of the epoch. Parameters ---------- value The end of the epoch. If `None` is provided, the epoch end is considered to be in the future and :data:`Epoch.is_incomplete` is ``True``. Raises ------ ValueError If the `start` is after the `end`. """ self._end = pd.Timestamp(value) if self.start > self.end: raise ValueError(f"Start cannot be after end: {self.start} > {self.end}") @property def id(self) -> DefinitionId: """Get the ID from the definition of the epoch. Returns ------- DefinitionId The id of the :data:`Epoch.definition`. Raises ------ AttributeError Will be risen if no definition was set for the epoch. """ if self.definition is None: raise AttributeError("No definition was provided for epoch") return self.definition.id def __hash__(self): return hash((self.id, self.start, self.end)) def __repr__(self): return f"<{self.__class__.__name__}: {self.start} - {self.end}>" def _test_overlap_contain( self, other: Union["Epoch", datetime, pd.Timestamp], method: Callable[[Iterable[bool]], bool], ) -> bool: if isinstance(other, Epoch): return method((self.overlaps(other.start), self.overlaps(other.end))) assert self.end is not None, "Can only test with closed epochs" if isinstance(other, (datetime, pd.Timestamp)): return self.start <= other <= self.end raise ValueError("Can only test for datetime or Epoch values") @property def duration(self) -> pd.Timedelta: """Get the duration of the epoch. Returns ------- pandas.Timedelta The duration of the epoch. Raises ------ ValueError If the epoch has no end. """ if self.is_incomplete: raise ValueError("Cannot retrieve duration for incomplete epochs") return self.end - self.start @property def is_incomplete(self) -> bool: """Check if the epoch has an end date. An epoch is considered incomplete if it does not have an end date time. Returns ------- bool `True` if the end date time is unknown. Otherwise, `False`. """ return pd.isnull(self.end)
[docs] def overlaps(self, other: Union["Epoch", datetime, pd.Timestamp]) -> bool: """Test if `other` overlaps with this epoch. Parameters ---------- other The other epoch or datetime-like object to be tested. Returns ------- bool If an epoch is provided ``overlap`` will be ``True`` if either the ``start`` or ``end`` of the ``other`` epoch is within the ``start`` or ``end`` of this epoch. If only a datetime object is provided, the result is ``True`` if the time is between ``start`` and ``end`` including the boundaries. """ return self._test_overlap_contain(other, any)
[docs] def contains(self, other: Union["Epoch", datetime, pd.Timestamp]) -> bool: """Test if ``other`` is contained within this epoch. Parameters ---------- other The other epoch or datetime-like object to be tested. Returns ------- bool If an epoch is provided ``contains`` will be ``True`` if both the ``start`` and ``end`` of the ``other`` epoch is within the ``start`` and ``end`` of this epoch. If only a datetime object is provided, the result is ``True`` if the time is between ``start`` and ``end`` including the boundaries. """ return self._test_overlap_contain(other, all)