Source code for dispel.providers.generic.tasks.pinch.steps
# pylint: disable=duplicate-code
# pylint: disable=too-many-lines
"""Pinch test related functionality.
This module contains functionality to extract measures for the *Pinch* test
(PINCH).
"""
from abc import ABCMeta, abstractmethod
from functools import partial
from typing import (
    Any,
    Callable,
    Dict,
    Generator,
    Iterable,
    List,
    Sequence,
    Tuple,
    Union,
    cast,
)
import numpy as np
import pandas as pd
from scipy.signal import find_peaks
from dispel.data.core import Reading
from dispel.data.flags import Flag
from dispel.data.levels import Level
from dispel.data.measures import MeasureValueDefinitionPrototype
from dispel.data.raw import DEFAULT_COLUMNS, USER_ACC_MAP, RawDataValueDefinition
from dispel.data.validators import GREATER_THAN_ZERO, RangeValidator
from dispel.data.values import AbbreviatedValue as AV
from dispel.data.values import ValueDefinition
from dispel.processing import ProcessingStep
from dispel.processing.data_set import transformation
from dispel.processing.extract import (
    BASIC_AGGREGATIONS,
    DEFAULT_AGGREGATIONS_CV,
    DEFAULT_AGGREGATIONS_Q95_CV,
    AggregateModalities,
    AggregateRawDataSetColumn,
    ExtractStep,
    agg_column,
)
from dispel.processing.level import LevelFilter, LevelIdFilter, ProcessingStepGroup
from dispel.processing.level_filters import AbsentDataSetFilter, NotEmptyDatasetFilter
from dispel.processing.modalities import (
    HandModality,
    HandModalityFilter,
    SensorModality,
)
from dispel.processing.transform import ConcatenateLevels, TransformStep
from dispel.processing.utils import parallel_explode
from dispel.providers.generic.activity.orientation import UpperLimbOrientationFlagger
from dispel.providers.generic.flags.ue_flags import OnlyOneHandPerformed
from dispel.providers.generic.sensor import (
    FREQ_20HZ,
    RenameColumns,
    Resample,
    SetTimestampIndex,
    TransformUserAcceleration,
)
from dispel.providers.generic.tasks.pinch.attempts import (
    PinchAttempt,
    PinchTarget,
    PinchTouch,
    double_touch_asynchrony,
    dwell_time,
    number_successful_pinches,
    pinching_duration,
    success_duration,
    total_duration,
    total_number_pinches,
)
from dispel.providers.generic.tasks.pinch.modalities import (
    AttemptOutcomeModality,
    AttemptSelectionModality,
    BubbleSizeModality,
    BubbleSizeModalityFilter,
    FingerModality,
)
from dispel.providers.generic.touch import Touch
from dispel.providers.generic.tremor import TremorMeasures
from dispel.signal.filter import butterworth_low_pass_filter
from dispel.stats.core import percentile_95, variation, variation_increase
TASK_NAME = AV("Pinch test", "PINCH")
#: A validator that ensures values are comprise between zero and forty seconds
ATTEMPT_DURATION_VALIDATOR = RangeValidator(lower_bound=0, upper_bound=40)
#: A validator that ensures values are comprise between zero and forty seconds
#: in milliseconds
ATTEMPT_DURATION_VALIDATOR_MS = RangeValidator(lower_bound=0, upper_bound=4e4)
_LEVELS = [
    f"{hand.abbr}-{size.variable}"
    for hand in HandModality
    for size in BubbleSizeModality
]
PINCH_BASIC_MODALITY_AGGREGATIONS: List[
    Tuple[Union[Callable[[Any], float], str], str]
] = [*BASIC_AGGREGATIONS, (variation, "coefficient of variation")]
PINCH_EXTENDED_MODALITY_AGGREGATIONS: List[
    Tuple[Union[Callable[[Any], float], str], str]
] = [*PINCH_BASIC_MODALITY_AGGREGATIONS, ("median", "median")]
[docs]
class BubbleSizeTransformMixin(metaclass=ABCMeta):
    """A raw data set transformation processing step for Bubble.
    Parameters
    ----------
    size
        Bubble size modality.
    """
[docs]
    def __init__(self, *args, **kwargs):
        self.size: BubbleSizeModality = kwargs.pop("size")
        super().__init__(*args, **kwargs)  # type: ignore
[docs]
    def get_definition(self, **kwargs) -> ValueDefinition:
        """Get the definition."""
        kwargs["size"] = self.size.av
        return super().get_definition(**kwargs)  # type: ignore
[docs]
    def get_level_filter(self) -> LevelFilter:
        """Get the level filter based on the bubble size."""
        return (
            LevelIdFilter(self.size.variable)
            & super().get_level_filter()  # type: ignore
        )
[docs]
class HandModalityFilterMixin(metaclass=ABCMeta):
    """A raw data set transformation processing step for Bubble.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    """
[docs]
    def __init__(self, *args, **kwargs):
        self.hand: HandModality = kwargs.pop("hand")
        super().__init__(*args, **kwargs)  # type: ignore
[docs]
    def get_definition(self, **kwargs) -> ValueDefinition:
        """Get the definition."""
        kwargs["hand"] = self.hand.av
        return super().get_definition(**kwargs)  # type: ignore
[docs]
    def get_level_filter(self) -> LevelFilter:
        """Get level filter."""
        return (
            HandModalityFilter(self.hand) & super().get_level_filter()  # type: ignore
        )
[docs]
class PinchConcatenateTargets(TransformStep):
    """A pinch target level concatenation step."""
    data_set_ids = "screen"
    new_data_set_id = "melted_targets"
    definitions = [
        RawDataValueDefinition(
            "targets",
            "Concatenated pinch level data",
            description="Array of PinchTarget objects for level {level_id}",
        )
    ]
    @staticmethod
    def _segment_pinches(levels: Iterable[Level]) -> Dict[str, List[PinchTarget]]:
        """Segment data from multiple pinch level data."""
        targets: Dict[str, List[PinchTarget]] = {}
        for level in filter(lambda x: x.has_raw_data_set("screen"), levels):
            target = PinchTarget.from_level(level)
            if target is not None:
                targets.setdefault(target.parent_id, []).append(target)
        return targets
    @transformation
    def _segment_levels(self, _, level: Level, reading: Reading) -> pd.DataFrame:
        pinches = self._segment_pinches(reading.levels)
        return pd.DataFrame({"targets": pinches[str(level.id)]})
[docs]
class PinchConcatenateHands(HandModalityFilterMixin, ConcatenateLevels):
    """A pinch hand level concatenation step.
    Parameters
    ----------
    hand
        The hand to which the levels are to be concatenated.
    data_set_id
        The data set id(s) that will be merged.
    """
[docs]
    def __init__(self, hand: HandModality, data_set_id: Union[str, List[str]]):
        super().__init__(hand=hand, new_level_id=hand.abbr, data_set_id=data_set_id)
[docs]
class PinchConcatenateBubbles(ConcatenateLevels):
    """A pinch bubble size level concatenation step for target data sets.
    Parameters
    ----------
    size
        The hand to which the levels are to be concatenated.
    """
[docs]
    def __init__(self, size: BubbleSizeModality):
        level_filter = AbsentDataSetFilter("melted_targets") & BubbleSizeModalityFilter(
            size
        )
        super().__init__(
            new_level_id=size.variable,
            data_set_id="melted_targets",
            level_filter=level_filter,
        )
[docs]
class PinchConcatenateTargetsLevels(ConcatenateLevels):
    """A level concatenation step."""
[docs]
    def __init__(self):
        super().__init__(
            new_level_id="melted_levels",
            data_set_id="melted_targets",
            level_filter=AbsentDataSetFilter("melted_targets"),
        )
[docs]
class TransformMeltedLevels(TransformStep):
    """Transformation step based on melted levels."""
    data_set_ids = "melted_targets"
[docs]
class TransformApplyMeltedLevels(TransformMeltedLevels, metaclass=ABCMeta):
    """A Transformation step that applies a function on targets."""
    apply: Callable[..., Any]
    target_dtype = "float64"
[docs]
    def get_apply_function(self) -> Any:
        """Get the function to be applied to the data set."""
        func = self.apply
        if func is not None and hasattr(func, "__func__"):
            return func.__func__  # type: ignore
        return func
[docs]
    def post_process_apply(self, data: Any) -> Any:
        """Post process the data returned from the applied function."""
        return data
    @transformation
    def _apply(self, data: pd.DataFrame) -> Any:
        return self.post_process_apply(
            data["targets"].apply(self.get_apply_function())
        ).astype(self.target_dtype)
[docs]
def reaction_time_extended(target: PinchTarget) -> Tuple:
    """Extract reaction time, hand, size and appearance of a target.
    The reaction time of the user between the bubble appearance and the first
    touch event.
    Parameters
    ----------
    target
        A pinch target object.
    Returns
    -------
    Tuple
        The user reaction time in ms, the hand, the size, and the appearance
        timestamp.
    """
    return (
        target.reaction_time.total_seconds() * 1e3,
        target.hand.name.lower(),
        target.size.name.lower(),
        target.appearance,
    )
[docs]
def compute_reaction_time(data: pd.Series, reading: Reading) -> pd.Series:
    """Compute the reaction time for each target extended with hand and size.
    Parameters
    ----------
    data
        The series of targets.
    reading
        The associated reading
    Returns
    -------
    pd.Series
        A Series of the reaction time, for each targets.
    """
    # find the timestamp when the first bubble appeared for left hand
    if "left" not in set(reading.level_ids):
        ts_min_left = None
    else:
        ts_min_left = (
            reading.get_level("left")
            .get_raw_data_set("melted_targets")
            .data["targets"]
            .apply(lambda x: x.appearance)
            .min()
        )
    # find the timestamp when the first bubble appeared for right hand
    if "right" not in set(reading.level_ids):
        ts_min_right = None
    else:
        ts_min_right = (
            reading.get_level("right")
            .get_raw_data_set("melted_targets")
            .data["targets"]
            .apply(lambda x: x.appearance)
            .min()
        )
    # Create a dataframe with targets, hand, size, appearance
    df = pd.DataFrame({"targets": data["targets"]})
    df["reaction_time"], df["hand"], df["size"], df["appearance"] = zip(
        *df.targets.map(reaction_time_extended)
    )
    # Create a mask to remove the first reaction_time of each hand
    l_mask = df.appearance == ts_min_left
    r_mask = df.appearance == ts_min_right
    mask = ~(l_mask | r_mask)
    return df.loc[mask, "reaction_time"]
[docs]
class TransformReactionTime(TransformMeltedLevels):
    """A raw data set transformation step to get user's reaction time."""
    new_data_set_id = "reaction-time"
    definitions = [
        RawDataValueDefinition(
            "reaction_time",
            "Reaction time data",
            "float64",
            description="The time spent between the appearance of the "
            "pinch target and actually touching the screen.",
        )
    ]
[docs]
    @transformation
    def compute_reaction_time(self, data: pd.Series, reading: Reading) -> pd.Series:
        """Overwrite transformation."""
        return compute_reaction_time(data, reading)
[docs]
class TransformTargetProperties(TransformApplyMeltedLevels):
    """A raw data set transformation processing step for target properties."""
    new_data_set_id = "target-properties"
    definitions = [
        RawDataValueDefinition(
            "total_pinches",
            "total pinches data",
            "int32",
            description="The total number of pinches, where a pinch "
            "attempt is any screen interaction with at least "
            "two fingers down.",
        ),
        RawDataValueDefinition(
            "successful_pinches",
            "successful pinches data",
            "int32",
            description="The number of successful pinches, where a "
            "successful pinch attempt is any screen "
            "interaction with at least two fingers down that"
            "eventually leads to the target bubble bursting.",
        ),
    ]
    target_dtype = "int32"
[docs]
    def get_apply_function(self) -> Any:
        """Get the number of total and successful pinch attempts."""
        return lambda t: pd.Series(
            {
                "total_pinches": total_number_pinches(t),
                "successful_pinches": number_successful_pinches(t),
            }
        )
[docs]
class TransformTotalDuration(TransformApplyMeltedLevels):
    """A raw data set transformation step to get pinch total duration."""
    new_data_set_id = "total-duration"
    definitions = [
        RawDataValueDefinition(
            "total_duration",
            "Total duration data",
            "float64",
            description="The total time spent during the pinching "
            "events of one pinch target.",
        )
    ]
    apply = total_duration
[docs]
class TransformApplyMeasureMixin(metaclass=ABCMeta):
    """A raw data set transformation processing step for measures."""
    description: str = ""
    measure: str = ""
    @property
    def data_id(self):
        """Get data set id from measure name."""
        return self.measure.replace(" ", "_")
    @property
    def measure_id(self):
        """Get data measure id from measure name."""
        return self.measure.replace(" ", "-")
[docs]
    def get_definitions(self) -> List[RawDataValueDefinition]:
        """Get definition."""
        return [
            RawDataValueDefinition(
                self.get_column_id(),
                self.get_column_name(),
                "float64",
                description=self.description.format(**self.__dict__),
            )
        ]
[docs]
class TransformReactionTimeByBubbleSize(
    BubbleSizeTransformMixin, TransformApplyMeasureMixin, TransformMeltedLevels
):
    """A transformation step to get user's reaction time by bubble size."""
    measure = "bubble reaction time"
    description = (
        "The time spent between the appearance of the "
        "pinch target and actually touching the screen"
        " for bubble size {size}."
    )
[docs]
    @transformation
    def compute_reaction_time(self, data: pd.Series, reading: Reading) -> pd.Series:
        """Overwrite transformation."""
        return compute_reaction_time(data, reading)
[docs]
class TransformParallelExplodeMeltedLevels(
    TransformApplyMeasureMixin, TransformApplyMeltedLevels, metaclass=ABCMeta
):
    """A Transformation step that applies function with a post process."""
[docs]
    def __init__(
        self,
        finger: FingerModality,
        outcome: AttemptOutcomeModality = AttemptOutcomeModality.ALL,
    ):
        self.finger = finger
        self.outcome = outcome
        super().__init__()
[docs]
    def get_new_data_set_id(self) -> str:
        """Get new data set id."""
        return f"{self.finger.abbr}_{self.outcome.abbr}_" f"{self.data_id}"
[docs]
    def get_column_id(self) -> str:
        """Get column id."""
        return f"{self.finger.abbr}-{self.outcome.abbr}-{self.measure_id}"
[docs]
    def get_column_name(self) -> str:
        """Get column name."""
        return f"{self.finger} {self.measure} data for {self.outcome} attempt"
[docs]
    @abstractmethod
    def get_property(self, target: PinchTarget) -> Any:
        """Get property from an attempt."""
        raise NotImplementedError
[docs]
    def get_apply_function(self) -> Any:
        """Get the pinching attempts' property."""
        return lambda target: pd.Series(
            {self.get_column_id(): self.get_property(target)}
        )
[docs]
    def post_process_apply(self, data: Any) -> Any:
        """Parallel explode passed data."""
        return (
            parallel_explode(data, self.target_dtype)
            if not parallel_explode(data, self.target_dtype).empty
            else pd.Series([np.nan])
        )
[docs]
class TransformContactDistance(TransformParallelExplodeMeltedLevels):
    """A raw data set transformation processing step for contact distance."""
    measure = "contact distance"
    description = (
        "The distance between the initial point of "
        "contact with the screen and the surface of "
        "the target bubble for the {finger} for "
        "{outcome} attempt."
    )
[docs]
    def get_property(self, target: PinchTarget) -> List[float]:
        """Get property from a target."""
        return target.contact_distances(self.finger, self.outcome)
[docs]
class TransformFirstPushes(TransformParallelExplodeMeltedLevels):
    """A raw data set transformation processing step first finger pushes."""
    measure = "first push"
    description = (
        "The value of the first push of "
        "pressure applied on the screen by the "
        "{finger} for {outcome} attempt."
    )
[docs]
    def get_property(self, target: PinchTarget) -> List[float]:
        """Get property from a target."""
        return target.first_pushes(self.finger, self.outcome)
[docs]
class TransformWrapExplodeMeltedLevels(TransformApplyMeltedLevels):
    """A Transformation step that applies a function on targets."""
[docs]
    def post_process_apply(self, data: Any) -> Any:
        """Apply post-processing."""
        return (
            data.explode().dropna()
            if not data.explode().dropna().empty
            else pd.Series([np.nan])
        )
[docs]
class TransformExplodeMeltedLevelsSuccess(
    TransformApplyMeasureMixin, TransformWrapExplodeMeltedLevels
):
    """A Transformation step that applies a function on targets."""
[docs]
    def __init__(self, outcome: AttemptOutcomeModality = AttemptOutcomeModality.ALL):
        self.outcome = outcome
        super().__init__()
[docs]
    def get_new_data_set_id(self) -> str:
        """Get new data set id."""
        return f"{self.outcome.abbr}_{self.data_id}"
[docs]
    def get_column_id(self) -> str:
        """Get column id."""
        return f"{self.outcome.abbr}-{self.measure_id}"
[docs]
    def get_column_name(self) -> str:
        """Get column name."""
        return f"{self.outcome} {self.measure}"
[docs]
class TransformSuccessDeformingDuration(TransformExplodeMeltedLevelsSuccess):
    """A raw data set transformation step to get user's success duration."""
    measure = "success duration"
    description = (
        "Time spent before succeeding at deforming the"
        " target bubble during {outcome} pinch attempt."
    )
[docs]
    def get_apply_function(self) -> Any:
        """Get the success duration from attempt."""
        return lambda t: success_duration(t, self.outcome)
[docs]
class TransformPinchingDuration(TransformExplodeMeltedLevelsSuccess):
    """
    A raw data set transformation step to get user's pinching duration.
    To get the pinching duration of the user.
    """
    measure = "pinching duration"
    description = (
        "Time spent actually deforming the target "
        "bubble during {outcome} pinch attempt."
    )
[docs]
    def get_apply_function(self) -> Any:
        """Get the pinching duration from attempt."""
        return lambda t: pinching_duration(t, self.outcome)
[docs]
class TransformExplodeMeltedLevelsAttempt(
    TransformApplyMeasureMixin, TransformWrapExplodeMeltedLevels
):
    """A Transformation step that applies a function on targets."""
[docs]
    def __init__(self, attempt: AttemptSelectionModality):
        self.attempt = attempt
        super().__init__()
[docs]
    def get_new_data_set_id(self) -> str:
        """Get new data set id."""
        return f"{self.attempt.abbr}_{self.data_id}"
[docs]
    def get_column_id(self) -> str:
        """Get column id."""
        return f"{self.attempt.abbr}-{self.measure_id}"
[docs]
    def get_column_name(self) -> str:
        """Get column name."""
        return f"{self.attempt} {self.measure}"
[docs]
class TransformDwellTime(TransformExplodeMeltedLevelsAttempt):
    """A raw data set transformation step to get user's dwell time."""
    measure = "dwell time"
    description = (
        "Time spent between the first screen touching "
        "and the initiation of the movement for "
        "{attempt} pinch attempts."
    )
[docs]
    def get_apply_function(self) -> Any:
        """Get the pinching attempts' dwell times."""
        return partial(dwell_time, attempt=self.attempt)
[docs]
class TransformDoubleTouchAsynchrony(TransformExplodeMeltedLevelsAttempt):
    """A transformation step to get user's double touch asynchrony."""
    measure = "double touch asynchrony"
    description = (
        "Time difference between the first and second"
        " finger touching the screen for {attempt} "
        "pinch attempts."
    )
[docs]
    def get_apply_function(self) -> Any:
        """Get the pinching attempts' double touch asynchrony."""
        return partial(double_touch_asynchrony, attempt=self.attempt)
[docs]
class TransformAttempt(
    TransformApplyMeasureMixin, TransformWrapExplodeMeltedLevels, metaclass=ABCMeta
):
    """A raw data set transformation processing step for finger.
    Parameters
    ----------
    finger
        The desired finger to compute ('top' or 'bottom').
    outcome
        Pinching attempt success modality.
    """
[docs]
    def __init__(
        self,
        finger: FingerModality,
        outcome: AttemptOutcomeModality = AttemptOutcomeModality.ALL,
    ):
        self.finger = finger
        self.outcome = outcome
        super().__init__()
[docs]
    def get_new_data_set_id(self) -> str:
        """Get new data set id."""
        return f"{self.finger.abbr}_{self.outcome.abbr}_" f"{self.data_id}"
[docs]
    def get_column_id(self) -> str:
        """Get column id."""
        return f"{self.finger.abbr}-{self.outcome.abbr}-{self.measure_id}"
[docs]
    def get_column_name(self) -> str:
        """Get column name."""
        return f"{self.finger} {self.measure} data for {self.outcome} attempt"
[docs]
    def get_finger(self, attempt: PinchAttempt) -> PinchTouch:
        """Get finger from an attempt."""
        if self.finger == FingerModality.TOP_FINGER:
            return attempt.top_finger
        return attempt.bottom_finger
[docs]
    @abstractmethod
    def get_property(self, attempt: Touch) -> Any:
        """Get property from an attempt."""
        raise NotImplementedError
    def _touch_function(self, target: PinchTarget) -> List[float]:
        """Get the measure for a specific finger for all attempt."""
        touch_method = [
            self.get_property(self.get_finger(attempt))
            for attempt in target.get_attempts_from(self.outcome)
            if self.get_finger(attempt) is not None
        ]
        if not touch_method or isinstance(touch_method[0], float):
            return touch_method
        return sum(touch_method, [])
[docs]
    def get_apply_function(self) -> Any:
        """Get the pinching attempts' for the specific measure."""
        return self._touch_function
[docs]
class TransformPressures(TransformAttempt):
    """A raw data set transformation processing step for finger pressures."""
    measure = "pressure"
    description = (
        "The pressure applied on the screen by the {finger} for {outcome} attempt."
    )
[docs]
    def get_property(self, attempt: Touch) -> List[float]:
        """Get property from an attempt."""
        return attempt.pressure.tolist()
[docs]
class TransformSpeed(TransformAttempt):
    """A raw data set transformation processing step to get finger speed."""
    measure = "speed"
    description = "The speed of the {finger} during {outcome} pinch attempts."
[docs]
    def get_property(self, attempt: Touch) -> List[float]:
        """Get property from an attempt."""
        return attempt.speed.tolist()
[docs]
class TransformJerk(TransformAttempt):
    """A raw data set transformation processing step to get jerk movements."""
    measure = "jerk"
    description = "The jerk of the {finger} during {outcome} pinch attempts."
[docs]
    def get_property(self, attempt: Touch) -> List[float]:
        """Get property from an attempt."""
        return attempt.jerk.tolist()
[docs]
class TransformMeanSquaredJerk(TransformAttempt):
    """A raw data set transformation processing step for mean squared jerk."""
    measure = "ms jerk"
    description = (
        "The mean squared jerk of the {finger} during {outcome} pinch attempts."
    )
[docs]
    def get_property(self, attempt: Touch) -> List[float]:
        """Get property from an attempt."""
        return [attempt.mean_squared_jerk]
[docs]
class TransformPressureJerk(TransformAttempt):
    """A raw data set transformation processing step to get jerk pressure."""
    measure = "pressure jerk"
    description = "The jerk pressure of the {finger} during {outcome} pinch attempts."
[docs]
    def get_property(self, attempt: Touch) -> List[float]:
        """Get property from an attempt."""
        return attempt.pressure_jerk.tolist()
[docs]
class TransformMeanSquaredPressureJerk(TransformAttempt):
    """A processing step to get mean squared jerk pressure."""
    measure = "ms pressure jerk"
    description = (
        "The mean squared jerk pressure of the {finger} "
        "during {outcome} pinch attempts."
    )
[docs]
    def get_property(self, attempt: Touch) -> List[float]:
        """Get property from an attempt."""
        return [attempt.mean_squared_pressure_jerk]
[docs]
class TargetPropertiesExtractStep(ExtractStep):
    """A base class for all extraction steps from target properties."""
    data_set_ids = "target-properties"
[docs]
class ExtractTotalPinchAttempts(TargetPropertiesExtractStep):
    """Extract the total pinch attempts."""
    transform_function = agg_column("total_pinches", "sum")
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("total pinch attempts", "att"),
        description="The total number of pinch attempts on {size} size for "
        "the {hand} hand.",
        data_type="int32",
        validator=GREATER_THAN_ZERO,
    )
[docs]
class ExtractSuccessfulPinchAttempts(TargetPropertiesExtractStep):
    """Extract successful pinch attempts."""
    transform_function = agg_column("successful_pinches", "sum")
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("successful pinch attempts", "succ"),
        description="The number successful pinch attempts on {size} size for "
        "the {hand} hand.",
        data_type="int32",
        validator=GREATER_THAN_ZERO,
    )
[docs]
class ExtractPinchAccuracy(TargetPropertiesExtractStep):
    """A pinch accuracy extraction step."""
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("pinching accuracy", "acc"),
        data_type="float64",
        validator=RangeValidator(0, 1),
        description="The ratio of successful pinches on {size} size "
        "amongst all pinch attempts for the {hand} hand.",
    )
[docs]
    def flag_data_sets(
        self,
        data_sets: Sequence[pd.DataFrame],
        level: Level,
        reading: Reading,
        **kwargs,
    ) -> Generator[Flag, None, None]:
        """Flag that there is at least one pinch attempt."""
        super().flag_data_sets(data_sets, level, reading, **kwargs)
        if data_sets[0]["total_pinches"].sum() <= 0:
            yield Flag(
                id_="pinch-technical-deviation-ma",
                reason=f"Missing pinch attempts for {level}.",
            )
[docs]
    @transformation
    def accuracy(self, data: pd.DataFrame) -> Union[float, np.float64]:
        """Extract pinch accuracy."""
        if (total_pinches := data["total_pinches"].sum()) == 0:
            return np.nan
        return np.float64(data["successful_pinches"].sum() / total_pinches)
[docs]
class ExtractSuccessBase(TransformApplyMeasureMixin, AggregateRawDataSetColumn):
    """A measure extraction processing step for success modality.
    Parameters
    ----------
    outcome
        Pinching attempt success modality
    """
[docs]
    def __init__(self, outcome: AttemptOutcomeModality = AttemptOutcomeModality.ALL):
        self.outcome = outcome
        super().__init__(
            data_set_id=self.get_data_set_id(), column_id=self.get_column_id()
        )
[docs]
    def get_data_set_id(self) -> str:
        """Get data set id."""
        return f"{self.outcome.av.abbr}_{self.data_id}"
[docs]
    def get_column_id(self) -> str:
        """Get column id."""
        return f"{self.outcome.av.abbr}-{self.measure_id}"
[docs]
    def get_definition(self, **kwargs) -> ValueDefinition:
        """Get value definition."""
        kwargs["outcome"] = self.outcome.av
        return super().get_definition(**kwargs)
[docs]
class ExtractSuccessDeformingDuration(ExtractSuccessBase):
    """A measure extraction processing step for multiple success durations."""
    measure = "success duration"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("success duration", "sd"),
        data_type="float64",
        unit="s",
        validator=ATTEMPT_DURATION_VALIDATOR,
        description="Time {aggregation} spent before succeeding at "
        "deforming {size} size during "
        "{outcome} pinch attempt with the "
        "{hand} hand.",
    )
    aggregations = PINCH_BASIC_MODALITY_AGGREGATIONS
[docs]
class ExtractPinchingDuration(ExtractSuccessBase):
    """A measure extraction processing step for multiple pinching durations."""
    measure = "pinching duration"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("pinching duration", "pd"),
        data_type="float64",
        unit="s",
        validator=ATTEMPT_DURATION_VALIDATOR,
        description="Time {aggregation} spent actually deforming {size} size "
        "during {outcome} pinch attempt with the {hand} hand.",
    )
    aggregations = PINCH_BASIC_MODALITY_AGGREGATIONS
[docs]
class ExtractDwellTime(AggregateRawDataSetColumn):
    """A measure extraction processing step for multiple dwell times."""
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("dwell time", "dt"),
        data_type="float64",
        unit="ms",
        validator=GREATER_THAN_ZERO,
        description="The {aggregation} time spent between the first screen"
        " touching and the initiation of the movement "
        "for {attempt} attempts made of the bubble of size "
        "{size} with the {hand} hand.",
    )
    aggregations = DEFAULT_AGGREGATIONS_CV
[docs]
    def __init__(self, attempt: AttemptSelectionModality):
        self.attempt = attempt
        super().__init__(
            data_set_id=f"{self.attempt.abbr}_dwell_time",
            column_id=f"{self.attempt.abbr}-dwell-time",
        )
[docs]
class ExtractDoubleTouchAsynchrony(AggregateRawDataSetColumn):
    """An extraction step for multiple double touch asynchrony values."""
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("double touch asynchrony", "dta"),
        data_type="float64",
        unit="ms",
        validator=GREATER_THAN_ZERO,
        description="The {aggregation} time difference between the first "
        "and second finger touching the screen for "
        " {attempt} attempts made of the bubble of size "
        "{size} with the {hand} hand.",
    )
    aggregations = DEFAULT_AGGREGATIONS_CV
[docs]
    def __init__(self, attempt: AttemptSelectionModality):
        self.attempt = attempt
        super().__init__(
            data_set_id=f"{self.attempt.abbr}_double_touch_asynchrony",
            column_id=f"{self.attempt.abbr}-double-touch-asynchrony",
        )
[docs]
class ExtractFingerSuccessBase(TransformApplyMeasureMixin, AggregateRawDataSetColumn):
    """A measure extraction processing step for finger and success modalities.
    Parameters
    ----------
    finger
        The desired finger to compute ('top' or 'bottom').
    outcome
        Pinching attempt success modality
    """
[docs]
    def __init__(
        self,
        finger: FingerModality,
        outcome: AttemptOutcomeModality = AttemptOutcomeModality.ALL,
    ):
        self.outcome = outcome
        self.finger = finger
        super().__init__(
            data_set_id=self.get_data_set_id(), column_id=self.get_column_id()
        )
[docs]
    def get_data_set_id(self) -> str:
        """Get data set id."""
        return f"{self.finger.av.abbr}_{self.outcome.av.abbr}_{self.data_id}"
[docs]
    def get_column_id(self) -> str:
        """Get column id."""
        return f"{self.finger.av.abbr}-{self.outcome.av.abbr}" f"-{self.measure_id}"
[docs]
    def get_definition(self, **kwargs) -> ValueDefinition:
        """Get value definition."""
        kwargs["finger"] = self.finger.av
        kwargs["outcome"] = self.outcome.av
        return super().get_definition(**kwargs)
[docs]
class ExtractContactDistance(ExtractFingerSuccessBase):
    """A measure extraction processing step for multiple contact distances."""
    measure = "contact distance"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("contact distance", "cd"),
        data_type="float64",
        unit="point",
        description="The {aggregation} distance between the initial "
        "point of contact on the screen with the {finger} "
        "of the {hand} hand and the surface of the target "
        "bubble for {size} size for the {outcome} attempt.",
    )
    aggregations = PINCH_EXTENDED_MODALITY_AGGREGATIONS
[docs]
class ExtractFirstPushes(ExtractFingerSuccessBase):
    """A measure extraction processing step for first finger pushes."""
    measure = "first push"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("first push", "fp"),
        data_type="float64",
        validator=GREATER_THAN_ZERO,
        description="The {aggregation} first pressure value applied on"
        " the screen for {outcome} attempt by the {finger} of "
        "the {hand} hand for {size} size.",
    )
    aggregations = [
        ("mean", "mean"),
        ("median", "median"),
        (variation, "coefficient of variation"),
    ]
[docs]
class ExtractPressures(ExtractFingerSuccessBase):
    """A measure extraction processing step for multiple finger pressures."""
    measure = "pressure"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("pressure", "press"),
        data_type="float64",
        validator=GREATER_THAN_ZERO,
        description="The {aggregation} pressure applied on the screen "
        "by the {finger} of the {hand} hand for {size} size for "
        "{outcome} attempt.",
    )
    aggregations = [
        ("mean", "mean"),
        ("median", "median"),
        ("std", "standard deviation"),
        ("skew", "skewness"),
        ("kurtosis", "kurtosis"),
        (variation, "coefficient of variation"),
    ]
[docs]
class ExtractSpeed(ExtractFingerSuccessBase):
    """A measure extraction processing step for multiple finger speed."""
    measure = "speed"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("speed", "speed"),
        data_type="float64",
        validator=GREATER_THAN_ZERO,
        description="The {aggregation} speed "
        "by the {finger} of the {hand} hand for {size} size for"
        " {outcome} attempts.",
    )
    aggregations = [
        ("mean", "mean"),
        ("median", "median"),
        ("std", "standard deviation"),
        (percentile_95, "95th percentile"),
    ]
[docs]
class ExtractJerk(ExtractFingerSuccessBase):
    """A measure extraction processing step for multiple jerk finger."""
    measure = "jerk"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("movement jerk", "jerk"),
        data_type="float64",
        description="The {aggregation} jerk "
        "by the {finger} of the {hand} hand for {size} size for "
        "{outcome}  attempts.",
    )
    aggregations = [
        ("mean", "mean"),
        ("median", "median"),
        ("std", "standard deviation"),
    ]
[docs]
class ExtractMeanSquaredJerk(ExtractFingerSuccessBase):
    """A measure extraction for multiple mean squared jerk."""
    measure = "ms jerk"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("mean squared jerk", "msj"),
        data_type="float64",
        unit="px^2/ms^6",
        validator=GREATER_THAN_ZERO,
        description="The {aggregation} mean squared jerk"
        " applied on the screen for "
        "{outcome} attempts by the"
        " {finger} of the {hand} hand for {size} size.",
    )
    aggregations = PINCH_EXTENDED_MODALITY_AGGREGATIONS
[docs]
class ExtractPressureJerk(ExtractFingerSuccessBase):
    """A measure extraction processing step for multiple jerk pressures."""
    measure = "pressure jerk"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("pressure jerk", "press_jerk"),
        data_type="float64",
        description="The {aggregation} jerk pressure applied on "
        "the screen for the {outcome} "
        "attempts by the {finger} "
        "of the {hand} hand for {size} size.",
    )
    aggregations = PINCH_EXTENDED_MODALITY_AGGREGATIONS
[docs]
class ExtractMeanSquaredPressureJerk(ExtractFingerSuccessBase):
    """A measure extraction for multiple mean squared jerk of pressures."""
    measure = "ms pressure jerk"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("mean squared pressure jerk", "mspj"),
        data_type="float64",
        unit="pa^2/ms^6",
        validator=GREATER_THAN_ZERO,
        description="The {aggregation} mean squared jerk pressure"
        " applied on the screen for the "
        "{outcome} attempt by the "
        "{finger} of the {hand} hand for {size} size.",
    )
    aggregations = [
        ("mean", "mean"),
        ("median", "median"),
        ("std", "standard deviation"),
    ]
[docs]
class ExtractReactionTime(AggregateRawDataSetColumn):
    """A measure reaction time extraction processing step."""
    level_filter = LevelIdFilter("melted_levels")
    data_set_ids = "reaction-time"
    column_id = "reaction_time"
    definition = MeasureValueDefinitionPrototype(
        task_name=TASK_NAME,
        measure_name=AV("reaction time", "rt"),
        data_type="float64",
        unit="ms",
        validator=ATTEMPT_DURATION_VALIDATOR_MS,
        description="The {aggregation} time spent between the appearance "
        "of target bubbles and actually touching the screen.",
    )
    aggregations = [
        ("mean", "mean"),
        (variation_increase, "coefficient of variation increase"),
        ("median", "median"),
        ("std", "standard deviation"),
        ("min", "minimum"),
        ("max", "maximum"),
        (variation, "coefficient of variation"),
        (percentile_95, "95th percentile"),
    ]
[docs]
class ExtractReactionTimeByBubbleSize(
    BubbleSizeTransformMixin, AggregateRawDataSetColumn
):
    """A measure reaction time by bubble size extraction processing step."""
    data_set_ids = "bubble_reaction_time"
    column_id = "bubble-reaction-time"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("reaction time", "rt"),
        data_type="float64",
        unit="ms",
        validator=ATTEMPT_DURATION_VALIDATOR_MS,
        description="The {aggregation} time spent between the appearance "
        "of target bubble {size} size and actually touching the "
        "screen.",
    )
    aggregations = PINCH_BASIC_MODALITY_AGGREGATIONS
[docs]
class ExtractReactionTimeByHand(HandModalityFilterMixin, AggregateRawDataSetColumn):
    """A pinch reaction time by hand extraction step."""
    data_set_ids = "reaction-time"
    column_id = "reaction_time"
    definition = MeasureValueDefinitionPrototype(
        task_name=TASK_NAME,
        measure_name=AV("reaction time", "rt"),
        data_type="float64",
        unit="ms",
        validator=ATTEMPT_DURATION_VALIDATOR_MS,
        description="The {aggregation} time spent between the appearance "
        "of target bubbles and actually touching the screen.",
    )
    aggregations = [
        ("mean", "mean"),
        (variation_increase, "coefficient of variation increase"),
        ("median", "median"),
        ("std", "standard deviation"),
        ("min", "minimum"),
        ("max", "maximum"),
        (variation, "coefficient of variation"),
        (percentile_95, "95th percentile"),
    ]
[docs]
    def get_level_filter(self) -> LevelFilter:
        """Get level filter."""
        return LevelIdFilter(self.hand.variable)
[docs]
class ExtractFirstReactionTimeByHand(HandModalityFilterMixin, ExtractStep):
    """A first pinch reaction time by hand extraction step."""
    data_set_ids = "melted_targets"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("first pinch reaction time", "fprt"),
        data_type="float64",
        unit="ms",
        validator=ATTEMPT_DURATION_VALIDATOR_MS,
        description="The time spent between the appearance "
        "of the target and actually touching the "
        "screen for the {hand} hand.",
    )
[docs]
    def get_level_filter(self) -> LevelFilter:
        """Get level filter."""
        return LevelIdFilter(self.hand.variable)
    @transformation
    def _first_pinch_reaction_time(self, data: pd.DataFrame) -> float:
        first_target: PinchTarget = min(data["targets"], key=lambda t: t.appearance)
        return first_target.reaction_time.total_seconds() * 1e3
[docs]
class ExtractTotalDuration(AggregateRawDataSetColumn):
    """A measure total duration extraction processing step."""
    data_set_ids = "total-duration"
    column_id = "total_duration"
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("duration", "dur"),
        data_type="float64",
        unit="s",
        validator=ATTEMPT_DURATION_VALIDATOR,
        description="The {aggregation} time spent during pinching events "
        "of {size} size using the {hand} hand.",
    )
    aggregations = PINCH_BASIC_MODALITY_AGGREGATIONS
[docs]
class PinchAggregateModalitiesByHand(AggregateModalities):
    """Base step to aggregate measures by hand for PINCH task.
    Parameters
    ----------
    hand
        The hand modality for which to aggregate measures.
    """
[docs]
    def get_definition(self, **kwargs) -> ValueDefinition:
        """Get the definition."""
        new_kwargs = kwargs.copy()
        new_kwargs["modalities"] = [self.hand.av]
        new_kwargs["hand"] = self.hand.av
        return cast(MeasureValueDefinitionPrototype, self.definition).create_definition(
            **new_kwargs
        )
[docs]
    def get_modalities(self) -> List[List[Union[str, AV]]]:
        """Get the modalities."""
        ids = []
        for size in BubbleSizeModality:
            ids.append([self.hand.av, size.av])
        return ids
[docs]
class AggregateSuccessfulPinchesByHand(PinchAggregateModalitiesByHand):
    """Aggregate successful pinch attempts by hand."""
    definition = ExtractSuccessfulPinchAttempts.definition.derive(
        description="The number of successful pinch attempts for the {hand} hand."
    )
    @staticmethod
    def _agg_method(data):
        return None if len(data) == 0 else sum(data)
    aggregation_method = _agg_method
[docs]
class AggregateDoubleTouchAsynchronyByHand(AggregateModalities):
    """Aggregate double touch asynchrony by hand."""
[docs]
    def __init__(self, hand: HandModality, attempt: AttemptSelectionModality):
        super().__init__()
        self.hand = hand
        self.attempt = attempt
[docs]
    def get_definition(self, **kwargs) -> ValueDefinition:
        """Get the definition."""
        new_kwargs = kwargs.copy()
        new_kwargs["modalities"] = [self.hand.av, self.attempt.av]
        new_kwargs["hand"] = self.hand.av
        new_kwargs["attempt"] = self.attempt.av
        return cast(MeasureValueDefinitionPrototype, self.definition).create_definition(
            **new_kwargs
        )
[docs]
    def get_modalities(self) -> List[List[Union[str, AV]]]:
        """Get the modalities."""
        ids = []
        for size in BubbleSizeModality:
            ids.append([self.hand.av, size.av, self.attempt.av])
        return ids
    definition = ExtractDoubleTouchAsynchrony.definition.derive(
        aggregation="mean",
        description="The mean time difference between the first "
        "and second finger touching the screen for {attempt} "
        "pinch attempt made with the {hand} hand.",
    )
[docs]
class AggregatePinchAccuracyByHand(PinchAggregateModalitiesByHand):
    """Aggregate pinch accuracy by hand."""
    definition = ExtractPinchAccuracy.definition.derive(
        description="The ratio of successful pinches amongst all pinch "
        "attempts for the {hand} hand."
    )
[docs]
class AggregateTotalPinchAttemptsByHand(PinchAggregateModalitiesByHand):
    """Extract the total pinch attempts."""
    definition = ExtractTotalPinchAttempts.definition.derive(
        description="The total number of pinch attempts for the {hand} hand."
    )
    @staticmethod
    def _agg_method(data):
        return None if len(data) == 0 else sum(data)
    aggregation_method = _agg_method
[docs]
class PinchProcessingLevel(ProcessingStepGroup):
    """A group of pinch processing steps for measures by level id.
    Parameters
    ----------
    level_id
        Level id.
    """
[docs]
    def __init__(self, level_id: str):
        steps = [
            PinchConcatenateTargets(level_filter=LevelIdFilter(level_id)),
        ]
        super().__init__(steps)
[docs]
class PinchProcessingLevelTarget(ProcessingStepGroup):
    """A group of pinch processing steps for measures by targets."""
[docs]
    def __init__(self):
        steps = [PinchConcatenateTargetsLevels(), TransformReactionTime()]
        super().__init__(steps, task_name=TASK_NAME)
[docs]
class PinchProcessingSize(ProcessingStepGroup):
    """A group of pinch processing steps for measures by bubble size."""
[docs]
    def __init__(self, size: BubbleSizeModality):
        steps = [
            PinchConcatenateBubbles(size=size),
            TransformReactionTimeByBubbleSize(size=size),
            ExtractReactionTimeByBubbleSize(size=size),
        ]
        super().__init__(
            steps, task_name=TASK_NAME, modalities=[size.av], size=size.variable
        )
[docs]
class PinchConcatenateHandsGroup(ProcessingStepGroup):
    """A group of pinch processing steps for measures by hands.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    """
[docs]
    def __init__(self, hand: HandModality):
        steps: List[ProcessingStep] = [
            PinchConcatenateHands(
                hand,
                list(
                    map(
                        str,
                        [
                            SensorModality.ACCELEROMETER,
                            SensorModality.GYROSCOPE,
                            "melted_targets",
                        ],
                    )
                ),
            )
        ]
        super().__init__(steps, modalities=[hand.av], hand=hand, task_name=TASK_NAME)
[docs]
class PinchReactionTimeByHand(ProcessingStepGroup):
    """A group of pinch processing steps for reaction time measures by hands.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    """
[docs]
    def __init__(self, hand: HandModality):
        steps: List[ProcessingStep] = [
            ExtractFirstReactionTimeByHand(hand=hand),
            TransformReactionTime(level_filter=LevelIdFilter(hand.variable)),
            ExtractReactionTimeByHand(hand=hand),
        ]
        super().__init__(steps, modalities=[hand.av], hand=hand, task_name=TASK_NAME)
[docs]
class PinchProcessingHandSensor(ProcessingStepGroup):
    """A group of pinch processing steps for tremor measures.
    Parameters
    ----------
    hand
        The hand on which the tremor measures are to be computed.
    sensor
        The sensor on which the tremor measures are to be computed.
    """
[docs]
    def __init__(self, hand: HandModality, sensor: SensorModality):
        data_set_id = str(sensor)
        steps = [
            RenameColumns(data_set_id, hand.abbr, **USER_ACC_MAP),
            SetTimestampIndex(
                f"{data_set_id}_renamed", DEFAULT_COLUMNS, "ts", duplicates="last"
            ),
            Resample(
                f"{data_set_id}_renamed_ts",
                aggregations=["mean", "ffill"],
                columns=DEFAULT_COLUMNS,
                freq=FREQ_20HZ,
            ),
            TremorMeasures(
                sensor=sensor, data_set_id=f"{data_set_id}_renamed_ts_resampled"
            ),
        ]
        super().__init__(
            steps,
            task_name=TASK_NAME,
            modalities=[hand.av],
            hand=hand,
            level_filter=LevelIdFilter(hand.abbr) & NotEmptyDatasetFilter(data_set_id),
        )
[docs]
class PinchProcessingHandSize(ProcessingStepGroup):
    """A group of pinch processing steps for measures by bubbles and hands.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    size
        Bubble size modality.
    """
[docs]
    def __init__(self, hand: HandModality, size: BubbleSizeModality):
        steps = [
            TransformTargetProperties(),
            TransformTotalDuration(),
            TransformSuccessDeformingDuration(),
            TransformPinchingDuration(),
            ExtractSuccessDeformingDuration(),
            ExtractPinchingDuration(),
            ExtractTotalPinchAttempts(),
            ExtractSuccessfulPinchAttempts(),
            ExtractPinchAccuracy(),
            ExtractTotalDuration(),
        ]
        super().__init__(
            steps,
            task_name=TASK_NAME,
            modalities=[hand.av, size.av],
            hand=hand,
            size=size,
            level_filter=HandModalityFilter(hand) & BubbleSizeModalityFilter(size),
        )
[docs]
class PinchProcessingHandSizeAttempt(ProcessingStepGroup):
    """A group of pinch processing steps by bubbles, hands and attempts.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    size
        Bubble size modality.
    attempt
        Pinching attempt selection modality.
    """
[docs]
    def __init__(
        self,
        hand: HandModality,
        size: BubbleSizeModality,
        attempt: AttemptSelectionModality,
    ):
        steps = [
            TransformDwellTime(attempt),
            TransformDoubleTouchAsynchrony(attempt),
            ExtractDwellTime(attempt),
            ExtractDoubleTouchAsynchrony(attempt),
        ]
        super().__init__(
            steps,
            task_name=TASK_NAME,
            modalities=[hand.av, size.av, attempt.av],
            hand=hand,
            size=size,
            attempt=attempt,
            level_filter=HandModalityFilter(hand) & BubbleSizeModalityFilter(size),
        )
[docs]
class PinchProcessingHandSizeFinger(ProcessingStepGroup):
    """A group of pinch processing steps by bubbles, hands and finger.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    size
        Bubble size modality.
    finger
        Pinching fingers modality.
    """
[docs]
    def __init__(
        self, hand: HandModality, size: BubbleSizeModality, finger: FingerModality
    ):
        steps = [
            TransformFirstPushes(finger),
            TransformContactDistance(finger),
            TransformPressures(finger),
            TransformSpeed(finger),
            TransformJerk(finger),
            TransformPressureJerk(finger),
            TransformMeanSquaredJerk(finger),
            TransformMeanSquaredPressureJerk(finger),
            ExtractContactDistance(finger),
            ExtractFirstPushes(finger),
            ExtractPressures(finger),
            ExtractSpeed(finger),
            ExtractJerk(finger),
            ExtractPressureJerk(finger),
            ExtractMeanSquaredJerk(finger),
            ExtractMeanSquaredPressureJerk(finger),
        ]
        super().__init__(
            steps,
            task_name=TASK_NAME,
            modalities=[hand.av, size.av, finger.av],
            hand=hand,
            size=size,
            finger=finger,
            level_filter=HandModalityFilter(hand) & BubbleSizeModalityFilter(size),
        )
[docs]
def duration_extended(target: PinchTarget) -> Tuple:
    """Extract several parameters from a pinch target.
    The parameters extracted are: duration, hand, size, appearance,  number of
    attempts and whether the first attempt was successful.
    Parameters
    ----------
    target
        A pinch target object.
    Returns
    -------
    Tuple
        The user duration of the first attempt time in seconds, the hand,
        the size, the appearance timestamp, the number of attempts for the
        target and if the first attempt was successful.
    """
    duration = None
    is_success = False
    if len(target.attempts) != 0:
        duration = target.attempts[0].duration.total_seconds()
        is_success = target.attempts[0].is_successful
    return (
        duration,
        target.hand.name.lower(),
        target.size.name.lower(),
        target.appearance,
        len(target.attempts),
        is_success,
    )
[docs]
class TransformSingleShotDuration(TransformMeltedLevels):
    """A raw data set transformation step to get single shot duration."""
    new_data_set_id = "single-shot-duration"
    definitions = [
        RawDataValueDefinition(
            "targets", "targets", description="The targets objects."
        ),
        RawDataValueDefinition(
            "duration",
            "duration",
            data_type="float64",
            unit="ms",
            description="The {aggregation} time spent during pinching a bubble"
            "successfully at first attempt using the {hand} hand.",
        ),
        RawDataValueDefinition(
            "hand",
            "hand",
            data_type="str",
            description="The hand being used to pinch the bubble.",
        ),
        RawDataValueDefinition(
            "size",
            "size",
            data_type="str",
            description="The size of the bubble being pinched.",
        ),
        RawDataValueDefinition(
            "appearance", "appearance", description="The timestamp the bubble appeared."
        ),
        RawDataValueDefinition(
            "n_attempts",
            "number of attempts",
            description="The number of attempt to pinch the bubble.",
        ),
        RawDataValueDefinition(
            "is_successful",
            "is successful",
            description="A boolean indicating if the target is successful.",
        ),
    ]
[docs]
    @transformation
    def single_shot(self, data: pd.DataFrame) -> pd.DataFrame:
        """Return a data set of single shot duration."""
        df = pd.DataFrame({"targets": data["targets"]})
        (
            df["duration"],
            df["hand"],
            df["size"],
            df["appearance"],
            df["n_attempts"],
            df["is_successful"],
        ) = zip(*df.targets.map(duration_extended))
        return df.loc[(df["n_attempts"] == 1) & df.is_successful]
[docs]
class ExtractSingleShotDuration(HandModalityFilterMixin, AggregateRawDataSetColumn):
    """Aggregate single shot duration processing step."""
    data_set_ids = "single-shot-duration"
    column_id = "duration"
    aggregations = PINCH_BASIC_MODALITY_AGGREGATIONS
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("single_shot_dur", "dur1shot"),
        data_type="float64",
        unit="s",
        validator=ATTEMPT_DURATION_VALIDATOR,
        description="The {aggregation} time spent during pinching a bubble"
        "successfully at first attempt using the {hand} hand.",
    )
[docs]
    def get_level_filter(self) -> LevelFilter:
        """Get level filter."""
        return LevelIdFilter(self.hand.variable)
[docs]
class Pinch1ShotDurationByHand(ProcessingStepGroup):
    """A group of pinch processing steps for single shot duration by hands.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    """
[docs]
    def __init__(self, hand: HandModality):
        steps = [
            TransformSingleShotDuration(level_filter=LevelIdFilter(hand.variable)),
            ExtractSingleShotDuration(hand=hand),
        ]
        super().__init__(steps, modalities=[hand.av], task_name=TASK_NAME, hand=hand)
[docs]
def attempts_from_targets(target: PinchTarget) -> Tuple:
    """Extract several parameters from a pinch target.
    The parameters extracted are: hand, size, appearance and attempts.
    Parameters
    ----------
    target
        A pinch target object.
    Returns
    -------
    Tuple
        The hand, the size, the appearance timestamp, the attempts for the
        target.
    """
    return (
        target.hand.name.lower(),
        target.size.name.lower(),
        target.appearance,
        target.attempts,
    )
[docs]
def peak_properties(attempt: PinchAttempt) -> Tuple:
    """Extract several peak properties from a pinch target."""
    # Top finger computation
    tf_data = attempt.top_finger.speed
    tf_data = tf_data.loc[tf_data.index.drop_duplicates(keep="last")]
    tf_data = tf_data.resample(pd.Timedelta(1 / 200, unit="s")).agg("ffill")
    # return NaN if we don't have enough non NaN values
    if len(tf_data.dropna()) <= 12:
        return np.nan, np.nan, np.nan, np.nan, np.nan, np.nan
    tf_data_f = butterworth_low_pass_filter(
        tf_data.dropna(), order=3, cutoff=10, zero_phase=True
    )
    # identify the peaks on the top finger trace
    tf_peaks_index, _ = find_peaks(tf_data_f)
    tf_peaks = tf_data_f.iloc[tf_peaks_index]
    # Bottom Finger Computation
    bf_data = attempt.bottom_finger.speed
    bf_data = bf_data.loc[bf_data.index.drop_duplicates(keep="last")]
    bf_data = bf_data.resample(pd.Timedelta(1 / 200, unit="s")).agg("ffill")
    # return NaN if we don't have enough non NaN values
    if len(bf_data.dropna()) <= 12:
        return np.nan, np.nan, np.nan, np.nan, np.nan, np.nan
    bf_data_f = butterworth_low_pass_filter(
        bf_data.dropna(), order=3, cutoff=10, zero_phase=True
    )
    # identify the peaks on the bottom finger trace
    bf_peaks_index, _ = find_peaks(bf_data_f)
    bf_peaks = bf_data_f.iloc[bf_peaks_index]
    return (
        tf_data.index[0],
        tf_peaks.index,
        tf_peaks.values,
        bf_data.index[0],
        bf_peaks.index,
        bf_peaks.values,
    )
[docs]
class TransformAttemptPeakSpeed(TransformMeltedLevels):
    """Compute attempts peak speed properties for successful pinches."""
    new_data_set_id = "peak-speed-properties"
    definitions = [
        RawDataValueDefinition(
            "targets",
            "targets",
        ),
        RawDataValueDefinition(
            "attempts",
            "attempts",
        ),
        RawDataValueDefinition(
            "hand",
            "hand",
            data_type="str",
            description="The hand used to pinch the bubble.",
        ),
        RawDataValueDefinition(
            "size",
            "size",
            data_type="str",
            description="The size of the bubble pinched.",
        ),
        RawDataValueDefinition(
            "appearance",
            "appearance",
            description="The timestamp when the bubble appeared.",
        ),
        RawDataValueDefinition(
            "attempt_start",
            "attempt_start",
            description="The timestamp when the attempt starts.",
        ),
        RawDataValueDefinition(
            "tf_start",
            "tf_start",
            description="The timestamp when the top finger touch starts.",
        ),
        RawDataValueDefinition(
            "tf_peaks_index",
            "tf_peaks_index",
            description="The timestamps at which peaks in the top finger "
            "speed are detected.",
        ),
        RawDataValueDefinition(
            "tf_peaks_amp",
            "tf_peaks_amp",
            description="The speed amplitude of the top finger speed peaks.",
        ),
        RawDataValueDefinition(
            "bf_start",
            "bf_start",
            description="The timestamp when the bottom finger touch starts.",
        ),
        RawDataValueDefinition(
            "bf_peaks_index",
            "bf_peaks_index",
            description="The timestamps at which peaks in the bottom finger "
            "speed are detected.",
        ),
        RawDataValueDefinition(
            "bf_peaks_amp",
            "bf_peaks_amp",
            description="The speed amplitude of the bottom finger speed peaks.",
        ),
    ]
[docs]
    @transformation
    def get_peak_speed_properties(self, data: pd.DataFrame) -> pd.DataFrame:
        """Return a data set of single shot duration."""
        df = pd.DataFrame({"targets": data["targets"]})
        df["hand"], df["size"], df["appearance"], df["attempts"] = zip(
            *df.targets.map(attempts_from_targets)
        )
        # Explode all the attempts
        df = df.explode("attempts").dropna()
        # Only keep successful attempts
        mask = df.attempts.apply(lambda x: x.is_successful)
        df = df.loc[mask]
        # add attempt start
        df["attempt_start"] = df.attempts.apply(lambda x: x.first_touch.begin)
        if len(df) == 0:
            df["tf_start"] = None
            df["tf_peaks_index"] = None
            df["tf_peaks_amp"] = None
            df["bf_start"] = None
            df["bf_peaks_index"] = None
            df["bf_peaks_amp"] = None
            return df
        # Compute peak properties
        (
            df["tf_start"],
            df["tf_peaks_index"],
            df["tf_peaks_amp"],
            df["bf_start"],
            df["bf_peaks_index"],
            df["bf_peaks_amp"],
        ) = zip(*df.attempts.map(peak_properties))
        return df
[docs]
class TransformTimeToPeak(TransformStep):
    """Compute attempts time to peak speed properties."""
    data_set_ids = "peak-speed-properties"
    new_data_set_id = "time-to-peak-speed"
    definitions = [
        RawDataValueDefinition(
            "tf_peak_time",
            "tf_peak_time",
            description="The time to the first peak of speed amplitude of the "
            "top finger.",
        ),
        RawDataValueDefinition(
            "bf_peak_time",
            "bf_peak_time",
            description="The time to the first peak of speed amplitude of the "
            "bottom finger.",
        ),
        RawDataValueDefinition(
            "tf_n_peaks",
            "tf_n_peaks",
            description="The number of peak speed for the top finger.",
        ),
        RawDataValueDefinition(
            "bf_n_peaks",
            "bf_n_peaks",
            description="The number of peak speed for the bottom finger.",
        ),
        RawDataValueDefinition(
            "peak_asynchro",
            "peak_asynchro",
            description="The difference between timestamp the top finger "
            "reach its first peak speed and the timestamp the "
            "bottom finger reach its first peak speed.",
            unit="ms",
        ),
    ]
[docs]
    @transformation
    def get_time_to_peak(self, data: pd.DataFrame) -> pd.DataFrame:
        """Return a data set of time to peak."""
        # Add number of peaks
        res = data.copy().dropna()
        if len(res) == 0:
            return pd.DataFrame(
                columns=[
                    "bf_n_peaks",
                    "bf_peak_time",
                    "tf_n_peaks",
                    "tf_peak_time",
                    "peak_asynchro",
                ]
            )
        res["bf_n_peaks"] = res.bf_peaks_index.map(len)
        res["tf_n_peaks"] = res.tf_peaks_index.map(len)
        mask = (res.bf_n_peaks > 0) & (res.tf_n_peaks > 0)
        # Select timestamp of the first peak speed for the bottom finger
        res["bf_peak_time"] = None
        res["bf_1_peak_ts"] = None
        res.loc[mask, "bf_1_peak_ts"] = res.loc[mask, "bf_peaks_index"].apply(
            lambda x: x[0]
        )
        # Compute time to first peak for the bottom finger
        res.loc[mask, "bf_peak_time"] = (
            pd.to_datetime(res.loc[mask, "bf_1_peak_ts"]) - res.loc[mask, "bf_start"]
        ).dt.total_seconds()
        # Select timestamp of the first peak speed for the top finger
        res["tf_peak_time"] = None
        res["tf_1_peak_ts"] = None
        res.loc[mask, "tf_1_peak_ts"] = res.loc[mask, "tf_peaks_index"].apply(
            lambda x: x[0]
        )
        # Compute time to first peak speed for the top finger
        res.loc[mask, "tf_peak_time"] = (
            pd.to_datetime(res.loc[mask, "tf_1_peak_ts"]) - res.loc[mask, "tf_start"]
        ).dt.total_seconds()
        # Compute asynchronicity
        res["peak_asynchro"] = (
            res["tf_1_peak_ts"] - res["bf_1_peak_ts"]
        ).dt.total_seconds() * 1000
        return res[
            [
                "bf_n_peaks",
                "bf_peak_time",
                "tf_n_peaks",
                "tf_peak_time",
                "peak_asynchro",
            ]
        ]
[docs]
class ExtractTimeToPeakSpeedFinger(HandModalityFilterMixin, AggregateRawDataSetColumn):
    """Aggregate single time to peak processing step."""
[docs]
    def __init__(self, hand: HandModality, finger: FingerModality):
        self.finger = finger
        self.column_id = f"{self.finger.abbr}_peak_time"
        super().__init__(hand=hand)
    data_set_ids = "time-to-peak-speed"
    aggregations = DEFAULT_AGGREGATIONS_Q95_CV
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("peak_time", "peaktime"),
        data_type="float64",
        unit="s",
        description="The {aggregation} time to the first peak of speed "
        "of the {hand} hand for the {finger} finger for "
        "successful attempts.",
    )
[docs]
    def get_level_filter(self) -> LevelFilter:
        """Get level filter."""
        return LevelIdFilter(self.hand.variable)
[docs]
class ExtractTimeToPeakAsynchronicity(
    HandModalityFilterMixin, AggregateRawDataSetColumn
):
    """Aggregate time to peak asynchronicity."""
    column_id = "peak_asynchro"
    data_set_ids = "time-to-peak-speed"
    aggregations = DEFAULT_AGGREGATIONS_Q95_CV
    definition = MeasureValueDefinitionPrototype(
        measure_name=AV("peak speed asynchronicity", "peak_speed_async"),
        data_type="float64",
        unit="ms",
        description="The {aggregation} duration between the bottom finger "
        "first peak speed and the top finger first peak speed for "
        "successful pinches attempts of the {hand} hand.",
    )
[docs]
    def get_level_filter(self) -> LevelFilter:
        """Get level filter."""
        return LevelIdFilter(self.hand.variable)
[docs]
class PinchTimeToPeakByHand(ProcessingStepGroup):
    """A group of pinch steps for time to peak speed computation.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    """
[docs]
    def __init__(self, hand: HandModality):
        steps = [
            TransformAttemptPeakSpeed(level_filter=LevelIdFilter(hand.variable)),
            TransformTimeToPeak(
                level_filter=LevelIdFilter(hand.variable),
            ),
            ExtractTimeToPeakAsynchronicity(hand=hand),
        ]
        # pylint: disable=no-member
        super().__init__(
            steps,
            modalities=[hand.av, AttemptOutcomeModality.SUCCESS.av],
            task_name=TASK_NAME,
            hand=hand,
        )
        # pylint: enable=no-member
[docs]
class ExtractPinchTimeToPeakByHandFinger(ProcessingStepGroup):
    """A group of pinch extract steps for time to peak speed computation.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    finger
        Pinching fingers modality.
    """
[docs]
    def __init__(self, hand: HandModality, finger: FingerModality):
        steps = [ExtractTimeToPeakSpeedFinger(hand, finger)]
        outcome = AttemptOutcomeModality.SUCCESS
        # pylint: disable=no-member
        super().__init__(
            steps,
            modalities=[hand.av, finger.av, outcome.av],
            task_name=TASK_NAME,
            hand=hand,
            finger=finger,
        )
        # pylint: enable=no-member
[docs]
class PinchProcessingHandSizeSuccess(ProcessingStepGroup):
    """A group of pinch processing steps by bubbles, hands and success.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    size
        Bubble size modality.
    outcome
        Pinching attempt success modality.
    """
[docs]
    def __init__(
        self,
        hand: HandModality,
        size: BubbleSizeModality,
        outcome: AttemptOutcomeModality,
    ):
        steps = [
            TransformSuccessDeformingDuration(outcome),
            TransformPinchingDuration(outcome),
            ExtractSuccessDeformingDuration(outcome),
            ExtractPinchingDuration(outcome),
        ]
        super().__init__(
            steps,
            task_name=TASK_NAME,
            modalities=[hand.av, size.av, outcome.av],
            hand=hand.av,
            size=size.av,
            outcome=outcome.av,
            level_filter=HandModalityFilter(hand) & BubbleSizeModalityFilter(size),
        )
[docs]
class PinchProcessingHandSizeFingerSuccess(ProcessingStepGroup):
    """A group of pinch processing steps by bubbles, hands, finger and success.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    size
        Bubble size modality.
    outcome
        Pinching attempt success modality.
    finger
        Pinching fingers modality.
    """
[docs]
    def __init__(
        self,
        hand: HandModality,
        size: BubbleSizeModality,
        outcome: AttemptOutcomeModality,
        finger: FingerModality,
    ):
        steps = [
            TransformFirstPushes(finger, outcome),
            TransformContactDistance(finger, outcome),
            TransformPressures(finger, outcome),
            TransformSpeed(finger, outcome),
            TransformJerk(finger, outcome),
            TransformPressureJerk(finger, outcome),
            TransformMeanSquaredJerk(finger, outcome),
            TransformMeanSquaredPressureJerk(finger, outcome),
            ExtractContactDistance(finger, outcome),
            ExtractFirstPushes(finger, outcome),
            ExtractPressures(finger, outcome),
            ExtractSpeed(finger, outcome),
            ExtractJerk(finger, outcome),
            ExtractPressureJerk(finger, outcome),
            ExtractMeanSquaredJerk(finger, outcome),
            ExtractMeanSquaredPressureJerk(finger, outcome),
        ]
        super().__init__(
            steps,
            task_name=TASK_NAME,
            modalities=[hand.av, size.av, finger.av, outcome.av],
            hand=hand,
            size=size,
            outcome=outcome,
            finger=finger,
            level_filter=HandModalityFilter(hand) & BubbleSizeModalityFilter(size),
        )
[docs]
class PinchProcessingAggregate(ProcessingStepGroup):
    """A group of pinch aggregating steps on hands.
    Parameters
    ----------
    hand
        Handedness for tasks that are applied to different hands.
    """
[docs]
    def __init__(self, hand: HandModality):
        steps = [
            AggregateSuccessfulPinchesByHand(hand),
            AggregatePinchAccuracyByHand(hand),
            AggregateTotalPinchAttemptsByHand(hand),
            *(
                AggregateDoubleTouchAsynchronyByHand(hand, attempt)
                for attempt in AttemptSelectionModality
            ),
        ]
        super().__init__(steps, task_name=TASK_NAME)
[docs]
class PinchFlag(ProcessingStepGroup):
    """Processing group for all drawing flag."""
[docs]
    def __init__(self):
        steps = [
            TransformUserAcceleration(),
            UpperLimbOrientationFlagger(),
            OnlyOneHandPerformed(task_name=TASK_NAME),
        ]
        super().__init__(steps, task_name=TASK_NAME)
[docs]
class PinchProcessingStepGroup(ProcessingStepGroup):
    """A group of all pinch processing steps for measures extraction."""
[docs]
    def __init__(self):
        steps = [
            PinchFlag(),
            *(PinchProcessingLevel(level_id) for level_id in _LEVELS),
            PinchConcatenateTargetsLevels(),
            *(PinchConcatenateHandsGroup(hand) for hand in HandModality),
            *(PinchReactionTimeByHand(hand) for hand in HandModality),
            *(Pinch1ShotDurationByHand(hand) for hand in HandModality),
            *(PinchTimeToPeakByHand(hand) for hand in HandModality),
            *(
                ExtractPinchTimeToPeakByHandFinger(hand, finger)
                for hand in HandModality
                for finger in FingerModality
            ),
            *(PinchProcessingSize(size) for size in BubbleSizeModality),
            TransformReactionTime(level_filter=LevelIdFilter("melted_levels")),
            ExtractReactionTime(),
            *(
                PinchProcessingHandSensor(hand, sensor)
                for hand in HandModality
                for sensor in [SensorModality.ACCELEROMETER, SensorModality.GYROSCOPE]
            ),
            *(
                PinchProcessingHandSize(hand, size)
                for hand in HandModality
                for size in BubbleSizeModality
            ),
            *(
                PinchProcessingHandSizeAttempt(hand, size, attempt)
                for hand in HandModality
                for size in BubbleSizeModality
                for attempt in AttemptSelectionModality
            ),
            *(
                PinchProcessingHandSizeFinger(hand, size, finger)
                for hand in HandModality
                for size in BubbleSizeModality
                for finger in FingerModality
            ),
            *(
                PinchProcessingHandSizeSuccess(hand, size, outcome)
                for hand in HandModality
                for size in BubbleSizeModality
                for outcome in [
                    AttemptOutcomeModality.SUCCESS,
                    AttemptOutcomeModality.FAILURE,
                ]
            ),
            *(
                PinchProcessingHandSizeFingerSuccess(hand, size, outcome, finger)
                for hand in HandModality
                for size in BubbleSizeModality
                for outcome in [
                    AttemptOutcomeModality.SUCCESS,
                    AttemptOutcomeModality.FAILURE,
                ]
                for finger in FingerModality
            ),
            *(PinchProcessingAggregate(hand) for hand in HandModality),
        ]
        super().__init__(steps, task_name=TASK_NAME)
STEPS = [PinchProcessingStepGroup()]