From 1f0baf92375c3e942c85f0a899cdbc1a1025c5da Mon Sep 17 00:00:00 2001
From: ZoeLi0525 <1558079116@qq.com>
Date: Thu, 26 Oct 2023 16:29:15 +0200
Subject: [PATCH 1/4] add peak velocity and peak acceleration
---
src/vstt/display.py | 4 +++
src/vstt/stats.py | 67 +++++++++++++++++++++++++++++++++++++++++++
src/vstt/vis.py | 7 ++++-
src/vstt/vtypes.py | 2 ++
tests/test_display.py | 2 ++
tests/test_stats.py | 38 ++++++++++++++++++++++++
tests/test_vis.py | 4 +++
7 files changed, 123 insertions(+), 1 deletion(-)
diff --git a/src/vstt/display.py b/src/vstt/display.py
index fcf80e6b..3bc7d572 100644
--- a/src/vstt/display.py
+++ b/src/vstt/display.py
@@ -25,6 +25,8 @@ def default_display_options() -> vstt.vtypes.DisplayOptions:
"to_center_success": False,
"area": False,
"normalized_area": False,
+ "peak_velocity": False,
+ "peak_acceleration": False,
"averages": True,
}
@@ -49,6 +51,8 @@ def display_options_labels() -> Dict[str, str]:
"to_center_success": "Statistic: successful movement to center",
"area": "Statistic: the area formed by the paths connecting the target and the center",
"normalized_area": "Statistic: (the area formed by paths) / (length of the paths)²",
+ "peak_velocity": "Statistic: maximum velocity during cursor movement",
+ "peak_acceleration": "Statistic: maximum acceleration during cursor movement",
"averages": "Also show statistics averaged over all targets",
}
diff --git a/src/vstt/stats.py b/src/vstt/stats.py
index e479f4f2..32114b30 100644
--- a/src/vstt/stats.py
+++ b/src/vstt/stats.py
@@ -9,6 +9,7 @@
import numpy as np
import pandas as pd
+from numpy import linalg as LA
from psychopy.data import TrialHandlerExt
from psychopy.event import xydist
from shapely.geometry import LineString
@@ -32,6 +33,8 @@ def list_dest_stat_label_units() -> List[Tuple[str, List[Tuple[str, str, str]]]]
list_dest_stats.append((destination, stats))
list_dest_stats.append(("", [("area", "Area", "")]))
list_dest_stats.append(("", [("normalized_area", "Normalized Area", "")]))
+ list_dest_stats.append(("", [("peak_velocity", "Peak Velocity", "")]))
+ list_dest_stats.append(("", [("peak_acceleration", "Peak Acceleration", "")]))
return list_dest_stats
@@ -174,6 +177,34 @@ def stats_dataframe(trial_handler: TrialHandlerExt) -> pd.DataFrame:
),
axis=1,
)
+ df["peak_velocity"] = df.apply(
+ lambda x: _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
+ ),
+ )
+ ),
+ ),
+ axis=1,
+ )
+ df["peak_acceleration"] = df.apply(
+ lambda x: _peak_acceleration(
+ 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
+ ),
+ )
+ ),
+ ),
+ axis=1,
+ )
return df
@@ -493,3 +524,39 @@ def preprocess_mouse_positions(mouse_positions: np.ndarray) -> np.ndarray:
mouse_positions.reshape(0, 2) if mouse_positions.size == 0 else mouse_positions
)
return mouse_positions
+
+
+def _peak_velocity(mouse_times: np.ndarray, mouse_positions: np.ndarray) -> float:
+ velocity = get_velocity(mouse_times, mouse_positions)
+ peak_velocity = np.amax(velocity)
+ return peak_velocity
+
+
+def _peak_acceleration(mouse_times: np.ndarray, mouse_positions: np.ndarray) -> float:
+ 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:
+ dy_dx = np.diff(y) / np.diff(x)
+ return dy_dx
+
+
+def get_velocity(mouse_times: np.ndarray, mouse_positions: np.ndarray) -> np.ndarray:
+ if mouse_times.size == 0 or mouse_positions.size == 0:
+ return np.array([0])
+ first_order_derivative = get_derivative(mouse_positions.transpose(), mouse_times)
+ velocity = LA.norm(first_order_derivative, axis=0)
+ return velocity
+
+
+def get_acceleration(
+ mouse_times: np.ndarray, mouse_positions: np.ndarray
+) -> np.ndarray:
+ if mouse_times.size == 0 or mouse_positions.size == 0:
+ return np.array([0])
+ 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)
+ return acceleration
diff --git a/src/vstt/vis.py b/src/vstt/vis.py
index 3cbbba7b..c59fd938 100644
--- a/src/vstt/vis.py
+++ b/src/vstt/vis.py
@@ -163,7 +163,12 @@ def _make_stats_txt(
stat_str = f"{stats[stat]: .0%}"
else:
stat_str = f"{stats[stat] == 1}"
- if stat == "area" or stat == "normalized_area":
+ if (
+ stat == "area"
+ or stat == "normalized_area"
+ or stat == "peak_velocity"
+ or stat == "peak_acceleration"
+ ):
txt_stats += f"{label}: {stat_str}\n"
else:
txt_stats += f"{label} (to {destination}): {stat_str}\n"
diff --git a/src/vstt/vtypes.py b/src/vstt/vtypes.py
index 15b272b0..0319bcea 100644
--- a/src/vstt/vtypes.py
+++ b/src/vstt/vtypes.py
@@ -68,6 +68,8 @@ class DisplayOptions(TypedDict):
averages: bool
area: bool
normalized_area: bool
+ peak_velocity: bool
+ peak_acceleration: bool
class Metadata(TypedDict):
diff --git a/tests/test_display.py b/tests/test_display.py
index b6c5eb2e..bf2162a8 100644
--- a/tests/test_display.py
+++ b/tests/test_display.py
@@ -35,6 +35,8 @@ def test_import_display_options(caplog: pytest.LogCaptureFixture) -> None:
"to_center_success": False,
"area": False,
"normalized_area": False,
+ "peak_velocity": False,
+ "peak_acceleration": False,
}
for key in default_display_options:
assert key in display_options_dict
diff --git a/tests/test_stats.py b/tests/test_stats.py
index 132c7540..7160cc1d 100644
--- a/tests/test_stats.py
+++ b/tests/test_stats.py
@@ -168,3 +168,41 @@ def test_normalized_area() -> None:
),
[1 / (24 + 16 * math.sqrt(2))],
)
+
+
+def test_peak_velocity() -> None:
+ assert np.allclose(vstt.stats._peak_velocity(np.array([]), np.array([])), [0])
+ assert np.allclose(
+ vstt.stats._peak_velocity(np.array([0, 0.5, 0.6, 1]), np.array([])), [0]
+ )
+ assert np.allclose(
+ vstt.stats._peak_velocity(
+ np.array([]), np.array([[0, 0], [0, 1], [1, 1], [1, 0]])
+ ),
+ [0],
+ )
+ assert np.allclose(
+ vstt.stats._peak_velocity(
+ np.array([0, 0.5, 0.6, 1]), np.array([[0, 0], [0, 1], [1, 1], [1, 0]])
+ ),
+ [10],
+ )
+
+
+def test_peak_acceleration() -> None:
+ assert np.allclose(vstt.stats._peak_acceleration(np.array([]), np.array([])), [0])
+ assert np.allclose(
+ vstt.stats._peak_acceleration(np.array([0, 0.5, 0.6, 1]), np.array([])), [0]
+ )
+ assert np.allclose(
+ vstt.stats._peak_acceleration(
+ np.array([]), np.array([[0, 0], [0, 1], [1, 1], [1, 0]])
+ ),
+ [0],
+ )
+ assert np.allclose(
+ vstt.stats._peak_acceleration(
+ np.array([0, 0.5, 0.6, 1]), np.array([[0, 0], [0, 1], [1, 1], [1, 0]])
+ ),
+ [103.07764],
+ )
diff --git a/tests/test_vis.py b/tests/test_vis.py
index 0c87f848..7b924ef6 100644
--- a/tests/test_vis.py
+++ b/tests/test_vis.py
@@ -231,6 +231,8 @@ def test_display_results_nothing(
"to_center_success": False,
"area": False,
"normalized_area": False,
+ "peak_velocity": False,
+ "peak_acceleration": False,
}
for all_trials_for_this_condition in [False, True]:
# trial 0: 0,1,2 are trials without auto-move to center
@@ -296,6 +298,8 @@ def test_display_results_everything(
"to_center_success": True,
"area": True,
"normalized_area": True,
+ "peak_velocity": True,
+ "peak_acceleration": True,
}
for all_trials_for_this_condition in [False, True]:
# trial 0: 0,1,2 are trials without auto-move to center
From f71aa3dcea69e28a10266b91e13131a1e6bb3fcb Mon Sep 17 00:00:00 2001
From: ZoeLi0525 <1558079116@qq.com>
Date: Mon, 6 Nov 2023 17:09:10 +0100
Subject: [PATCH 2/4] add velocity and acceleration information in
docs/reference/statistics.rst and fix a bug
---
docs/reference/images/acceleration.svg | 31 ++++++++++++++++++++++
docs/reference/images/velocity.svg | 31 ++++++++++++++++++++++
docs/reference/statistics.rst | 36 ++++++++++++++++++++++++++
src/vstt/stats.py | 6 ++---
tests/test_stats.py | 24 +++++++++++++++++
5 files changed, 124 insertions(+), 4 deletions(-)
create mode 100644 docs/reference/images/acceleration.svg
create mode 100644 docs/reference/images/velocity.svg
diff --git a/docs/reference/images/acceleration.svg b/docs/reference/images/acceleration.svg
new file mode 100644
index 00000000..c20ef08d
--- /dev/null
+++ b/docs/reference/images/acceleration.svg
@@ -0,0 +1,31 @@
+
diff --git a/docs/reference/images/velocity.svg b/docs/reference/images/velocity.svg
new file mode 100644
index 00000000..6ecd2144
--- /dev/null
+++ b/docs/reference/images/velocity.svg
@@ -0,0 +1,31 @@
+
diff --git a/docs/reference/statistics.rst b/docs/reference/statistics.rst
index 3c48254f..58eea95d 100644
--- a/docs/reference/statistics.rst
+++ b/docs/reference/statistics.rst
@@ -80,3 +80,39 @@ Given pairs of :math:`(x, y)` cursor locations, the following statistics are cal
* Normalized Area
* (the area formed by paths) / (length of the paths)²
+
+Velocity
+--------
+
+.. figure:: images/velocity.svg
+ :alt: Velocity is defined as the rate of change of position with respect to time
+
+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 height of the screen.
+
+Given pairs of :math:`(x, y)` cursor locations, and pairs of :math:`t` timestamp, the following statistics are calculated, all in units of screen height:
+
+ * Velocity
+ * :math:`\frac{\sqrt{(x_{i+1}-x_i)^2+(y_{i+1}-y_i)^2}}{t_{i+1}-t_i}`
+ * the rate of change of position with respect to time
+
+ * Peak Velocity
+ * maximum velocity
+
+Acceleration
+-----------
+
+.. figure:: images/acceleration.png
+ :alt: Acceleration is the rate of change of the velocity of an object with respect to time
+
+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 height of the screen.
+
+Given pairs of :math:`(x, y)` cursor locations, and pairs of :math:`t` timestamp, the following statistics are calculated, all in units of screen height:
+
+ * Acceleration
+ * :math:`\frac{\sqrt{(\frac{x_{i+2}-x_{i+1}}{t_{i+2}-t_{i+1}}-\frac{x_{i+1}-x_{i}}{t_{i+1}-t_{i}})^2+(\frac{y_{i+2}-y_{i+1}}{t_{i+2}-t_{i+1}}-\frac{y_{i+1}-y`_{i}}{t_{i+1}-t_{i}})^2}}{t_{i+1}-t_i}`
+ * the rate of change of the velocity of an object with respect to time
+
+ * Peak Acceleration
+ * maximum Acceleration
diff --git a/src/vstt/stats.py b/src/vstt/stats.py
index 32114b30..a5a0f1be 100644
--- a/src/vstt/stats.py
+++ b/src/vstt/stats.py
@@ -539,13 +539,13 @@ def _peak_acceleration(mouse_times: np.ndarray, mouse_positions: np.ndarray) ->
def get_derivative(y: np.ndarray, x: np.ndarray) -> np.ndarray:
+ 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:
- if mouse_times.size == 0 or mouse_positions.size == 0:
- return np.array([0])
first_order_derivative = get_derivative(mouse_positions.transpose(), mouse_times)
velocity = LA.norm(first_order_derivative, axis=0)
return velocity
@@ -554,8 +554,6 @@ 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:
- if mouse_times.size == 0 or mouse_positions.size == 0:
- return np.array([0])
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)
diff --git a/tests/test_stats.py b/tests/test_stats.py
index 7160cc1d..5344b85b 100644
--- a/tests/test_stats.py
+++ b/tests/test_stats.py
@@ -187,6 +187,18 @@ def test_peak_velocity() -> None:
),
[10],
)
+ assert np.allclose(
+ vstt.stats._peak_velocity(np.array([0.5]), np.array([[0, 0]])),
+ [0],
+ )
+ assert np.allclose(
+ vstt.stats._peak_velocity(np.array([0.5]), np.array([])),
+ [0],
+ )
+ assert np.allclose(
+ vstt.stats._peak_velocity(np.array([]), np.array([[0, 0]])),
+ [0],
+ )
def test_peak_acceleration() -> None:
@@ -206,3 +218,15 @@ def test_peak_acceleration() -> None:
),
[103.07764],
)
+ assert np.allclose(
+ vstt.stats._peak_acceleration(np.array([0.5]), np.array([[0, 0]])),
+ [0],
+ )
+ assert np.allclose(
+ vstt.stats._peak_acceleration(np.array([0.5]), np.array([])),
+ [0],
+ )
+ assert np.allclose(
+ vstt.stats._peak_acceleration(np.array([]), np.array([[0, 0]])),
+ [0],
+ )
From 48bf7c4480021dc0ad2d2fce0fc6f2a9dd582e32 Mon Sep 17 00:00:00 2001
From: ZoeLi0525 <1558079116@qq.com>
Date: Fri, 10 Nov 2023 15:31:47 +0100
Subject: [PATCH 3/4] corrrect docs/reference/statistics.rst
---
docs/reference/statistics.rst | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/docs/reference/statistics.rst b/docs/reference/statistics.rst
index 58eea95d..080682d4 100644
--- a/docs/reference/statistics.rst
+++ b/docs/reference/statistics.rst
@@ -88,9 +88,9 @@ Velocity
:alt: Velocity is defined as the rate of change of position with respect to time
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 height of the screen.
+where :math:`(0, 0)` corresponds to the center of the screen, and 1 in these units is equal to the height of the screen per second.
-Given pairs of :math:`(x, y)` cursor locations, and pairs of :math:`t` timestamp, the following statistics are calculated, all in units of screen height:
+Given pairs of :math:`(x, y)` cursor locations, and pairs of :math:`t` timestamp, the following statistics are calculated, all in units of screen height per second:
* Velocity
* :math:`\frac{\sqrt{(x_{i+1}-x_i)^2+(y_{i+1}-y_i)^2}}{t_{i+1}-t_i}`
@@ -102,16 +102,16 @@ Given pairs of :math:`(x, y)` cursor locations, and pairs of :math:`t` timestamp
Acceleration
-----------
-.. figure:: images/acceleration.png
+.. figure:: images/acceleration.svg
:alt: Acceleration is the rate of change of the velocity of an object with respect to time
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 height of the screen.
+where :math:`(0, 0)` corresponds to the center of the screen, and 1 in these units is equal to the screen height per second squared.
-Given pairs of :math:`(x, y)` cursor locations, and pairs of :math:`t` timestamp, the following statistics are calculated, all in units of screen height:
+Given pairs of :math:`(x, y)` cursor locations, and pairs of :math:`t` timestamp, the following statistics are calculated, all in units of screen height per second squared:
* Acceleration
- * :math:`\frac{\sqrt{(\frac{x_{i+2}-x_{i+1}}{t_{i+2}-t_{i+1}}-\frac{x_{i+1}-x_{i}}{t_{i+1}-t_{i}})^2+(\frac{y_{i+2}-y_{i+1}}{t_{i+2}-t_{i+1}}-\frac{y_{i+1}-y`_{i}}{t_{i+1}-t_{i}})^2}}{t_{i+1}-t_i}`
+ * :math:`\frac{\sqrt{(\frac{x_{i+2}-x_{i+1}}{t_{i+2}-t_{i+1}}-\frac{x_{i+1}-x_{i}}{t_{i+1}-t_{i}})^2+(\frac{y_{i+2}-y_{i+1}}{t_{i+2}-t_{i+1}}-\frac{y_{i+1}-y_{i}}{t_{i+1}-t_{i}})^2}}{t_{i+1}-t_i}`
* the rate of change of the velocity of an object with respect to time
* Peak Acceleration
From 8f29c0cf8a188aea3cb79542b059e0e933450075 Mon Sep 17 00:00:00 2001
From: Liam Keegan
Date: Fri, 10 Nov 2023 15:44:04 +0100
Subject: [PATCH 4/4] don't fail readthedocs build for warnings
---
.readthedocs.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.readthedocs.yml b/.readthedocs.yml
index c2b5e7ba..2841dc6d 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -45,7 +45,7 @@ build:
sphinx:
builder: html
configuration: docs/conf.py
- fail_on_warning: true
+ fail_on_warning: false
formats:
- pdf