diff --git a/docs/reference/images/spatial_error.svg b/docs/reference/images/spatial_error.svg new file mode 100644 index 00000000..4fa3a3ba --- /dev/null +++ b/docs/reference/images/spatial_error.svg @@ -0,0 +1,12 @@ + + + Layer 1 + + + + + (Xn,Yn) + { + spatial error + + diff --git a/docs/reference/statistics.rst b/docs/reference/statistics.rst index 080682d4..5ae7b85b 100644 --- a/docs/reference/statistics.rst +++ b/docs/reference/statistics.rst @@ -116,3 +116,21 @@ Given pairs of :math:`(x, y)` cursor locations, and pairs of :math:`t` timestamp * Peak Acceleration * maximum Acceleration + + +Spatial Error +------------- + +.. figure:: images/spatial_error.svg + :alt: Spatial Error is the distance from movement end point and the center of the target minus target' radius + +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: + + * Spatial Error to target + * the distance between the end point of the movement to the center of the target - radius of target + + * 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 diff --git a/src/vstt/display.py b/src/vstt/display.py index 3bc7d572..91b5ce43 100644 --- a/src/vstt/display.py +++ b/src/vstt/display.py @@ -27,6 +27,8 @@ def default_display_options() -> vstt.vtypes.DisplayOptions: "normalized_area": False, "peak_velocity": False, "peak_acceleration": False, + "to_target_spatial_error": False, + "to_center_spatial_error": False, "averages": True, } @@ -53,6 +55,8 @@ def display_options_labels() -> Dict[str, str]: "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", + "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", "averages": "Also show statistics averaged over all targets", } diff --git a/src/vstt/stats.py b/src/vstt/stats.py index a5a0f1be..5142b062 100644 --- a/src/vstt/stats.py +++ b/src/vstt/stats.py @@ -28,6 +28,7 @@ def list_dest_stat_label_units() -> List[Tuple[str, List[Tuple[str, str, str]]]] ("distance", "Distance", ""), ("rmse", "RMSE", ""), ("success", "Success", ""), + ("spatial_error", "Spatial", ""), ]: stats.append((f"to_{destination}_{base_stat}", label, unit)) list_dest_stats.append((destination, stats)) @@ -46,11 +47,13 @@ def _get_trial_data_columns() -> List[str]: "condition_index", "target_index", "target_pos", + "target_radius", "to_target_timestamps", "to_target_mouse_positions", "to_target_success", "to_target_num_timestamps_before_visible", "center_pos", + "center_radius", "to_center_timestamps", "to_center_mouse_positions", "to_center_success", @@ -81,6 +84,9 @@ def _get_target_data( ) -> List: data = trial_handler.data condition_index = trial_handler.sequenceIndices[index] + conditions = trial_handler.trialList[condition_index] + target_radius = conditions["target_size"] + central_target_radius = conditions["central_target_size"] target_index = data["target_indices"][index][i_target] target_pos = np.array(data["target_pos"][index][i_target]) center_pos = np.array([0.0, 0.0]) @@ -114,11 +120,13 @@ def _get_target_data( condition_index, target_index, target_pos, + target_radius, to_target_timestamps, to_target_mouse_positions, to_target_success, to_target_num_timestamps_before_visible, center_pos, + central_target_radius, to_center_timestamps, to_center_mouse_positions, to_center_success, @@ -167,6 +175,14 @@ def stats_dataframe(trial_handler: TrialHandlerExt) -> pd.DataFrame: ), axis=1, ) + df[f"to_{destination}_spatial_error"] = df.apply( + lambda x: _spatial_error( + x[f"to_{destination}_mouse_positions"], + x[f"{destination}_pos"], + x[f"{destination}_radius"], + ), + axis=1, + ) df["area"] = df.apply( lambda x: _area(x["to_target_mouse_positions"], x["to_center_mouse_positions"]), axis=1, @@ -558,3 +574,12 @@ def get_acceleration( second_order_derivative = get_derivative(first_order_derivative, mouse_times[:-1]) acceleration = LA.norm(second_order_derivative, axis=0) return acceleration + + +def _spatial_error( + mouse_position: np.ndarray, target: np.ndarray, target_radius: float +) -> float: + if mouse_position.size < 1: + return 0 + spatial_error = xydist(mouse_position[-1], target) - target_radius + return max(spatial_error, 0) diff --git a/src/vstt/vtypes.py b/src/vstt/vtypes.py index 0319bcea..beac3ebe 100644 --- a/src/vstt/vtypes.py +++ b/src/vstt/vtypes.py @@ -70,6 +70,8 @@ class DisplayOptions(TypedDict): normalized_area: bool peak_velocity: bool peak_acceleration: bool + to_target_spatial_error: bool + to_center_spatial_error: bool class Metadata(TypedDict): diff --git a/tests/test_display.py b/tests/test_display.py index bf2162a8..15838e06 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -37,6 +37,8 @@ def test_import_display_options(caplog: pytest.LogCaptureFixture) -> None: "normalized_area": False, "peak_velocity": False, "peak_acceleration": False, + "to_target_spatial_error": False, + "to_center_spatial_error": 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 5344b85b..556aec7b 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -230,3 +230,19 @@ def test_peak_acceleration() -> None: vstt.stats._peak_acceleration(np.array([]), np.array([[0, 0]])), [0], ) + + +def test_spatial_error() -> None: + assert np.allclose( + vstt.stats._spatial_error(np.array([]), np.array([1, 1]), 0.01), [0] + ) + assert np.allclose( + vstt.stats._spatial_error(np.array([[1, 1]]), np.array([1, 1]), 0.01), [0] + ) + assert np.allclose( + vstt.stats._spatial_error(np.array([[0.99, 1]]), np.array([1, 1]), 0.02), [0] + ) + assert np.allclose( + vstt.stats._spatial_error(np.array([[-1, -1], [0, 0]]), np.array([1, 1]), 0.01), + [1.404213562], + ) diff --git a/tests/test_vis.py b/tests/test_vis.py index 7b924ef6..a4cdc61d 100644 --- a/tests/test_vis.py +++ b/tests/test_vis.py @@ -233,6 +233,8 @@ def test_display_results_nothing( "normalized_area": False, "peak_velocity": False, "peak_acceleration": False, + "to_target_spatial_error": False, + "to_center_spatial_error": False, } for all_trials_for_this_condition in [False, True]: # trial 0: 0,1,2 are trials without auto-move to center @@ -300,6 +302,8 @@ def test_display_results_everything( "normalized_area": True, "peak_velocity": True, "peak_acceleration": True, + "to_target_spatial_error": True, + "to_center_spatial_error": True, } for all_trials_for_this_condition in [False, True]: # trial 0: 0,1,2 are trials without auto-move to center