Skip to content

Commit

Permalink
add statistics at peak velocity
Browse files Browse the repository at this point in the history
  • Loading branch information
ZoeLi0525 committed Jan 31, 2024
1 parent 223d2cc commit 813de3c
Show file tree
Hide file tree
Showing 9 changed files with 433 additions and 19 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ repos:
--ignore-missing-imports,
--disallow-untyped-defs,
--warn-redundant-casts,
--no-namespace-packages,
]

- repo: https://github.com/asottile/reorder-python-imports
Expand Down
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@
html_theme_options = {
"navigation_depth": 4,
}

epsilon: float = 1e-12
8 changes: 8 additions & 0 deletions src/vstt/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def default_display_options() -> vstt.vtypes.DisplayOptions:
"peak_acceleration": False,
"to_target_spatial_error": False,
"to_center_spatial_error": False,
"movement_time_at_peak_velocity": False,
"total_time_at_peak_velocity": False,
"movement_distance_at_peak_velocity": False,
"rmse_movement_at_peak_velocity": False,
"averages": True,
}

Expand Down Expand Up @@ -57,6 +61,10 @@ def display_options_labels() -> Dict[str, str]:
"peak_acceleration": "Statistic: maximum acceleration during cursor movement",
"to_target_spatial_error": "Statistic: distance from the movement end point to the target",
"to_center_spatial_error": "Statistic: distance from the movement end point to the center",
"movement_time_at_peak_velocity": "Statistic: movement time to the point at the peak velocity",
"total_time_at_peak_velocity": "Statistic: total time to the point at the peak velocity",
"movement_distance_at_peak_velocity": "Statistic: movement distance to the point at the peak velocity",
"rmse_movement_at_peak_velocity": "Statistic: RMSE movement to the point at the peak velocity",
"averages": "Also show statistics averaged over all targets",
}

Expand Down
180 changes: 174 additions & 6 deletions src/vstt/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Tuple
from typing import Union

import numpy
import numpy as np
import pandas as pd
from numpy import linalg as LA
Expand All @@ -16,6 +17,8 @@
from shapely.ops import polygonize
from shapely.ops import unary_union

import docs.conf


def list_dest_stat_label_units() -> List[Tuple[str, List[Tuple[str, str, str]]]]:
list_dest_stats = []
Expand All @@ -36,6 +39,30 @@ def list_dest_stat_label_units() -> List[Tuple[str, List[Tuple[str, str, str]]]]
list_dest_stats.append(("", [("normalized_area", "Normalized Area", "")]))
list_dest_stats.append(("", [("peak_velocity", "Peak Velocity", "")]))
list_dest_stats.append(("", [("peak_acceleration", "Peak Acceleration", "")]))
list_dest_stats.append(
(
"",
[("movement_time_at_peak_velocity", "Movement Time at Peak Velocity", "s")],
)
)
list_dest_stats.append(
("", [("total_time_at_peak_velocity", "Total Time at Peak Velocity", "s")])
)
list_dest_stats.append(
(
"",
[
(
"movement_distance_at_peak_velocity",
"Movement Distance at Peak Velocity",
"",
)
],
)
)
list_dest_stats.append(
("", [("rmse_movement_at_peak_velocity", "RMSE Movement at Peak Velocity", "")])
)
return list_dest_stats


Expand Down Expand Up @@ -221,6 +248,67 @@ def stats_dataframe(trial_handler: TrialHandlerExt) -> pd.DataFrame:
),
axis=1,
)
df["movement_time_at_peak_velocity"] = df.apply(
lambda x: _movement_time_at_peak_velocity(
np.concatenate((x["to_target_timestamps"], x["to_center_timestamps"])),
np.concatenate(
(
x["to_target_mouse_positions"],
x["to_center_mouse_positions"].reshape(
x["to_center_mouse_positions"].shape[0], 2
),
)
),
x["to_target_num_timestamps_before_visible"],
),
axis=1,
)
df["total_time_at_peak_velocity"] = df.apply(
lambda x: _total_time_at_peak_velocity(
np.concatenate((x["to_target_timestamps"], x["to_center_timestamps"])),
np.concatenate(
(
x["to_target_mouse_positions"],
x["to_center_mouse_positions"].reshape(
x["to_center_mouse_positions"].shape[0], 2
),
)
),
x["to_target_num_timestamps_before_visible"],
),
axis=1,
)
df["movement_distance_at_peak_velocity"] = df.apply(
lambda x: _movement_distance_at_peak_velocity(
np.concatenate((x["to_target_timestamps"], x["to_center_timestamps"])),
np.concatenate(
(
x["to_target_mouse_positions"],
x["to_center_mouse_positions"].reshape(
x["to_center_mouse_positions"].shape[0], 2
),
)
),
x["to_target_num_timestamps_before_visible"],
),
axis=1,
)
df["rmse_movement_at_peak_velocity"] = df.apply(
lambda x: _rmse_movement_at_peak_velocity(
np.concatenate((x["to_target_timestamps"], x["to_center_timestamps"])),
np.concatenate(
(
x["to_target_mouse_positions"],
x["to_center_mouse_positions"].reshape(
x["to_center_mouse_positions"].shape[0], 2
),
)
),
x["target_pos"],
x["to_target_num_timestamps_before_visible"],
),
axis=1,
)
return df


Expand Down Expand Up @@ -343,7 +431,7 @@ def _reaction_time(
mouse_times: np.ndarray,
mouse_positions: np.ndarray,
to_target_num_timestamps_before_visible: int,
epsilon: float = 1e-12,
# epsilon: float = 1e-12,
) -> float:
"""
The reaction time is defined as the timestamp where the cursor first moves,
Expand All @@ -364,9 +452,9 @@ def _reaction_time(
):
return np.nan
i = 0
while xydist(mouse_positions[0], mouse_positions[i]) < epsilon and i + 1 < len(
mouse_times
):
while xydist(
mouse_positions[0], mouse_positions[i]
) < docs.conf.epsilon and i + 1 < len(mouse_times):
i += 1
return mouse_times[i] - mouse_times[to_target_num_timestamps_before_visible]

Expand Down Expand Up @@ -542,10 +630,13 @@ def preprocess_mouse_positions(mouse_positions: np.ndarray) -> np.ndarray:
return mouse_positions


def _peak_velocity(mouse_times: np.ndarray, mouse_positions: np.ndarray) -> float:
def _peak_velocity(
mouse_times: np.ndarray, mouse_positions: np.ndarray
) -> Tuple[numpy.floating, numpy.integer]:
velocity = get_velocity(mouse_times, mouse_positions)
peak_velocity = np.amax(velocity)
return peak_velocity
peak_index = np.argmax(velocity)
return peak_velocity, peak_index


def _peak_acceleration(mouse_times: np.ndarray, mouse_positions: np.ndarray) -> float:
Expand Down Expand Up @@ -583,3 +674,80 @@ def _spatial_error(
return 0
spatial_error = xydist(mouse_position[-1], target) - target_radius
return max(spatial_error, 0)


def get_first_movement_index(
mouse_times: np.ndarray,
mouse_positions: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> int | None:
if (
mouse_times.shape[0] != mouse_positions.shape[0]
or mouse_times.shape[0] == 0
or mouse_times.shape[0] < to_target_num_timestamps_before_visible
):
return None
i = 0
while xydist(
mouse_positions[0], mouse_positions[i]
) < docs.conf.epsilon and i + 1 < len(mouse_times):
i += 1
return i


def _movement_time_at_peak_velocity(
mouse_times: np.ndarray,
mouse_positions: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> float | None:
i = get_first_movement_index(
mouse_times, mouse_positions, to_target_num_timestamps_before_visible
)
_, peak_index = _peak_velocity(mouse_times, mouse_positions)
# print(f"type of movement_time:{type(mouse_times[peak_index] - mouse_times[i])}\n")
return mouse_times[peak_index] - mouse_times[i] if i is not None else None


def _total_time_at_peak_velocity(
mouse_times: np.ndarray,
mouse_positions: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> float | None:
_, peak_index = _peak_velocity(mouse_times, mouse_positions)
return (
mouse_times[peak_index] - mouse_times[to_target_num_timestamps_before_visible]
if to_target_num_timestamps_before_visible < peak_index
else None
)


def _movement_distance_at_peak_velocity(
mouse_times: np.ndarray,
mouse_positions: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> float | None:
i = get_first_movement_index(
mouse_times, mouse_positions, to_target_num_timestamps_before_visible
)
_, peak_index = _peak_velocity(mouse_times, mouse_positions)
# print(f"type of movement_distance:{type(_distance(mouse_positions[i: peak_index + 1]))}\n")
return _distance(mouse_positions[i : peak_index + 1]) if i is not None else None


def _rmse_movement_at_peak_velocity(
mouse_times: np.ndarray,
mouse_positions: np.ndarray,
target_position: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> numpy.floating | None:
i = get_first_movement_index(
mouse_times, mouse_positions, to_target_num_timestamps_before_visible
)
if i is not None:
p1 = mouse_positions[i]
else:
return None
p2 = target_position
_, peak_index = _peak_velocity(mouse_times, mouse_positions)
p3 = mouse_positions[peak_index]
return LA.norm(np.cross(p2 - p1, p1 - p3)) / LA.norm(p2 - p1)
16 changes: 10 additions & 6 deletions src/vstt/vis.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,16 @@ def _make_stats_txt(
stat_str = f"{stats[stat]: .0%}"
else:
stat_str = f"{stats[stat] == 1}"
if (
stat == "area"
or stat == "normalized_area"
or stat == "peak_velocity"
or stat == "peak_acceleration"
):
if stat in [
"area",
"normalized_area",
"peak_velocity",
"peak_acceleration",
"movement_time_at_peak_velocity",
"total_time_at_peak_velocity",
"movement_distance_at_peak_velocity",
"rmse_movement_at_peak_velocity",
]:
txt_stats += f"{label}: {stat_str}\n"
else:
txt_stats += f"{label} (to {destination}): {stat_str}\n"
Expand Down
4 changes: 4 additions & 0 deletions src/vstt/vtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ class DisplayOptions(TypedDict):
peak_acceleration: bool
to_target_spatial_error: bool
to_center_spatial_error: bool
movement_time_at_peak_velocity: bool
total_time_at_peak_velocity: bool
movement_distance_at_peak_velocity: bool
rmse_movement_at_peak_velocity: bool


class Metadata(TypedDict):
Expand Down
4 changes: 4 additions & 0 deletions tests/test_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def test_import_display_options(caplog: pytest.LogCaptureFixture) -> None:
"peak_acceleration": False,
"to_target_spatial_error": False,
"to_center_spatial_error": False,
"movement_time_at_peak_velocity": False,
"total_time_at_peak_velocity": False,
"movement_distance_at_peak_velocity": False,
"rmse_movement_at_peak_velocity": False,
}
for key in default_display_options:
assert key in display_options_dict
Expand Down
Loading

0 comments on commit 813de3c

Please sign in to comment.