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 Feb 20, 2024
1 parent 813de3c commit 8a5450d
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 89 deletions.
Binary file added docs/reference/images/RMSE_movement.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/reference/images/movement_distance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/reference/images/peak.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions docs/reference/statistics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,31 @@ Given pairs of :math:`(x, y)` cursor locations,the following statistics are calc

* Spatial Error to central target
* the distance between the end point of the movement to the center of the central target - radius of central target


Statistics at peak velocity
---------------------------

.. figure:: images/peak.png
:alt: the statistics include movement time, total time, movement distance, RMSE movement at peak velocity

The cursor location at a timestamp is given by a pair of :math:`(x, y)` coordinates,
where :math:`(0, 0)` corresponds to the center of the screen, and 1 in these units is equal to the screen height.

Given pairs of :math:`(x, y)` cursor locations,the following statistics are calculated, all in units of screen height:

* Movement time
* :math:`t_{peak} - t_{move}`
* Time from first cursor movement to the movement at peak velocity

* Total time
* :math:`t_{peak} - t_{display}`
* Time from target being displayed to the movement at peak velocity

* Movement distance
* .. figure:: images/movement_distance.png
* Euclidean point-to-point distance travelled from first cursor movement to the peak velocity

* RMSE movement
* .. figure:: images/RMSE_movement.png
* Root Mean Square Error (RMSE) of the perpendicular distance from the peak velocity mouse point to the straight line that intersects the first mouse location and the target.
91 changes: 88 additions & 3 deletions src/vstt/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ def _rmse(mouse_positions: np.ndarray, target_position: np.ndarray) -> float:
:param mouse_positions: The array of mouse positions
:param target: The x,y coordinates of the target
:return: The RMSE of the distance from the ideal trajectoty
:return: The RMSE of the distance from the ideal trajectory
"""
if mouse_positions.shape[0] <= 1:
return np.nan
Expand Down Expand Up @@ -633,26 +633,54 @@ def preprocess_mouse_positions(mouse_positions: np.ndarray) -> np.ndarray:
def _peak_velocity(
mouse_times: np.ndarray, mouse_positions: np.ndarray
) -> Tuple[numpy.floating, numpy.integer]:
"""
get peak velocity and the corresponding index
:param mouse_times: The array of timestamps
:param mouse_positions: The array of mouse positions
:return: peak velocity and the corresponding index
"""
velocity = get_velocity(mouse_times, mouse_positions)
peak_velocity = np.amax(velocity)
peak_index = np.argmax(velocity)
return peak_velocity, peak_index


def _peak_acceleration(mouse_times: np.ndarray, mouse_positions: np.ndarray) -> float:
"""
get peak acceleration
:param mouse_times: The array of timestamps
:param mouse_positions: The array of mouse positions
:return: peak acceleration
"""
acceleration = get_acceleration(mouse_times, mouse_positions)
peak_acceleration = np.amax(acceleration)
return peak_acceleration


def get_derivative(y: np.ndarray, x: np.ndarray) -> np.ndarray:
"""
get derivative dy/dx
:param y: the array of y
:param x: the array of x
:return: the array of dy/dx
"""
if x.size <= 1 or y.size <= 1:
return np.array([0])
dy_dx = np.diff(y) / np.diff(x)
return dy_dx


def get_velocity(mouse_times: np.ndarray, mouse_positions: np.ndarray) -> np.ndarray:
"""
get velocity
:param mouse_times: The array of timestamps
:param mouse_positions: The array of mouse positions
:return: the array of velocity
"""
first_order_derivative = get_derivative(mouse_positions.transpose(), mouse_times)
velocity = LA.norm(first_order_derivative, axis=0)
return velocity
Expand All @@ -661,6 +689,13 @@ def get_velocity(mouse_times: np.ndarray, mouse_positions: np.ndarray) -> np.nda
def get_acceleration(
mouse_times: np.ndarray, mouse_positions: np.ndarray
) -> np.ndarray:
"""
get acceleration
:param mouse_times: The array of timestamps
:param mouse_positions: The array of mouse positions
:return: the array of acceleration
"""
first_order_derivative = get_derivative(mouse_positions.transpose(), mouse_times)
second_order_derivative = get_derivative(first_order_derivative, mouse_times[:-1])
acceleration = LA.norm(second_order_derivative, axis=0)
Expand All @@ -670,6 +705,14 @@ def get_acceleration(
def _spatial_error(
mouse_position: np.ndarray, target: np.ndarray, target_radius: float
) -> float:
"""
at timeout linear distance from cursor to target - target radius
:param mouse_position: The array of mouse positions
:param target: The position of the target
:param target_radius: radius of the target
:return: the distance between the end point of the movement to the center of the target - target radius
"""
if mouse_position.size < 1:
return 0
spatial_error = xydist(mouse_position[-1], target) - target_radius
Expand All @@ -681,6 +724,14 @@ def get_first_movement_index(
mouse_positions: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> int | None:
"""
get index of the first movement in mouse_times
:param mouse_times: The array of timestamps
:param mouse_positions: The array of mouse positions
:param to_target_num_timestamps_before_visible: The index of the first timestamp where the target is visible
:return: the first movement index in mouse_times
"""
if (
mouse_times.shape[0] != mouse_positions.shape[0]
or mouse_times.shape[0] == 0
Expand All @@ -700,11 +751,18 @@ def _movement_time_at_peak_velocity(
mouse_positions: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> float | None:
"""
get the time from first movement to the peak velocity
:param mouse_times: The array of timestamps
:param mouse_positions: The array of mouse positions
:param to_target_num_timestamps_before_visible: The index of the first timestamp where the target is visible
:return: the time from first movement to the peak velocity
"""
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


Expand All @@ -713,6 +771,14 @@ def _total_time_at_peak_velocity(
mouse_positions: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> float | None:
"""
get the time from the target becomes visible to the peak velocity
:param mouse_times: The array of timestamps
:param mouse_positions: The array of mouse positions
:param to_target_num_timestamps_before_visible: The index of the first timestamp where the target is visible
:return: the time from the target becomes visible to the peak velocity
"""
_, peak_index = _peak_velocity(mouse_times, mouse_positions)
return (
mouse_times[peak_index] - mouse_times[to_target_num_timestamps_before_visible]
Expand All @@ -726,11 +792,18 @@ def _movement_distance_at_peak_velocity(
mouse_positions: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> float | None:
"""
get the euclidean point-to-point distance travelled from first movement to the peak velocity
:param mouse_times: The array of timestamps
:param mouse_positions: The array of mouse positions
:param to_target_num_timestamps_before_visible: The index of the first timestamp where the target is visible
:return: the distance travelled from first movement -> the peak velocity
"""
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


Expand All @@ -740,6 +813,18 @@ def _rmse_movement_at_peak_velocity(
target_position: np.ndarray,
to_target_num_timestamps_before_visible: int,
) -> numpy.floating | None:
"""
The Root Mean Square Error (RMSE) of the perpendicular distance from the peak velocity mouse point
to the straight line that intersects the first mouse location and the target.
See: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_two_points.
:param mouse_times: The array of timestamps
:param mouse_positions: The array of mouse positions
:param target_position: The position of the target
:param to_target_num_timestamps_before_visible: The index of the first timestamp where the target is visible
:return: The RMSE of the distance from the ideal trajectory
"""
i = get_first_movement_index(
mouse_times, mouse_positions, to_target_num_timestamps_before_visible
)
Expand Down
87 changes: 1 addition & 86 deletions tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,19 +259,13 @@ def test_movement_time_at_peak_velocity() -> None:
),
None,
)
# assert np.allclose(
# vstt.stats._movement_time_at_peak_velocity(np.array([]), np.array([[0, 0], [0.1, 0.4], [0.4, 0.6], [0.6, 0.75], [1, 1.2], [1.1, 1.5]]), 0), [None]
# )

np.testing.assert_equal(
vstt.stats._movement_time_at_peak_velocity(
np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]), np.array([]), 0
),
None,
)
# assert np.allclose(
# vstt.stats._movement_time_at_peak_velocity(np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]), np.array([]), 0), [None]
# )

np.testing.assert_equal(
vstt.stats._movement_time_at_peak_velocity(
Expand All @@ -283,16 +277,6 @@ def test_movement_time_at_peak_velocity() -> None:
),
0.020000000000000004,
)
# assert np.allclose(
# vstt.stats._movement_time_at_peak_velocity(
# np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]),
# np.array(
# [[0, 0], [0.1, 0.4], [0.4, 0.6], [0.6, 0.75], [1, 1.2], [1.1, 1.5]]
# ),
# 0,
# ),
# [0.02],
# )


def test_total_time_at_peak_velocity() -> None:
Expand All @@ -306,9 +290,6 @@ def test_total_time_at_peak_velocity() -> None:
),
None,
)
# assert np.allclose(
# vstt.stats._total_time_at_peak_velocity(np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]), np.array([[0, 0], [0.1, 0.4], [0.4, 0.6], [0.6, 0.75], [1, 1.2], [1.1, 1.5]]), 5), [None]
# )

np.testing.assert_equal(
vstt.stats._total_time_at_peak_velocity(
Expand All @@ -320,16 +301,6 @@ def test_total_time_at_peak_velocity() -> None:
),
0.03,
)
# assert np.allclose(
# vstt.stats._total_time_at_peak_velocity(
# np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]),
# np.array(
# [[0, 0], [0.1, 0.4], [0.4, 0.6], [0.6, 0.75], [1, 1.2], [1.1, 1.5]]
# ),
# 0,
# ),
# [0.03],
# )


def test_movement_distance_at_peak_velocity() -> None:
Expand All @@ -343,21 +314,13 @@ def test_movement_distance_at_peak_velocity() -> None:
),
None,
)
# assert np.allclose(
# vstt.stats._movement_distance_at_peak_velocity(np.array([]), np.array(
# [[0, 0], [0.1, 0.4], [0.4, 0.6], [0.6, 0.75], [1, 1.2], [1.1, 1.5]]), 0), [None]
# )

np.testing.assert_equal(
vstt.stats._movement_distance_at_peak_velocity(
np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]), np.array([]), 0
),
None,
)
# assert np.allclose(
# vstt.stats._movement_distance_at_peak_velocity(np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]), np.array([]), 0),
# [None]
# )

np.testing.assert_equal(
vstt.stats._movement_distance_at_peak_velocity(
Expand All @@ -377,24 +340,6 @@ def test_movement_distance_at_peak_velocity() -> None:
),
0.610555127546399,
)
# assert np.allclose(
# vstt.stats._movement_distance_at_peak_velocity(
# np.array([0.09, 0.1, 0.11, 0.12, 0.13, 0.14, 0.15]),
# np.array(
# [
# [0, 0],
# [0, 0],
# [0.1, 0.4],
# [0.4, 0.6],
# [0.6, 0.75],
# [1, 1.2],
# [1.1, 1.5],
# ]
# ),
# 0,
# ),
# [0.610555],
# )


def test_rmse_movement_at_peak_velocity() -> None:
Expand All @@ -409,17 +354,7 @@ def test_rmse_movement_at_peak_velocity() -> None:
),
None,
)
# assert np.allclose(
# vstt.stats._rmse_movement_at_peak_velocity(
# np.array([]),
# np.array(
# [[0, 0], [0.1, 0.4], [0.4, 0.6], [0.6, 0.75], [1, 1.2], [1.1, 1.5]]
# ),
# np.array([1.1, 1.5]),
# 0,
# ),
# [None],
# )

np.testing.assert_equal(
vstt.stats._rmse_movement_at_peak_velocity(
np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]),
Expand All @@ -429,15 +364,6 @@ def test_rmse_movement_at_peak_velocity() -> None:
),
None,
)
# assert np.allclose(
# vstt.stats._rmse_movement_at_peak_velocity(
# np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]),
# np.array([]),
# np.array([1.1, 1.5]),
# 0,
# ),
# [None],
# )

np.testing.assert_equal(
vstt.stats._rmse_movement_at_peak_velocity(
Expand All @@ -450,14 +376,3 @@ def test_rmse_movement_at_peak_velocity() -> None:
),
0.13453455879926254,
)
# assert np.allclose(
# vstt.stats._rmse_movement_at_peak_velocity(
# np.array([0.1, 0.11, 0.12, 0.13, 0.14, 0.15]),
# np.array(
# [[0, 0], [0.1, 0.4], [0.4, 0.6], [0.6, 0.75], [1, 1.2], [1.1, 1.5]]
# ),
# np.array([1.1, 1.5]),
# 0,
# ),
# [0.13453455879926254],
# )

0 comments on commit 8a5450d

Please sign in to comment.