diff --git a/docs/index.rst b/docs/index.rst index 86bc1b7a..3beb6661 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,7 @@ running and analyzing motor skill acquisition experiments. notebooks/example notebooks/raw_data notebooks/area_calculation + notebooks/path_and_velocity .. toctree:: :maxdepth: 2 diff --git a/docs/notebooks/path_and_velocity.ipynb b/docs/notebooks/path_and_velocity.ipynb new file mode 100644 index 00000000..ae1318ce --- /dev/null +++ b/docs/notebooks/path_and_velocity.ipynb @@ -0,0 +1,510 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Show some statistics of the enclosed geometric object with psydat file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import vstt\n", + "from ipywidgets import interact\n", + "from scipy.signal import savgol_filter\n", + "from vstt.stats import get_velocity" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Import\n", + "A psydat file can be imported using the psychopy `fromFile` function: \n", + "If you want to know the detailed content of the data in psydat file, please check the notebook 'raw_data.ipynb'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "experiment = vstt.Experiment(\"example.psydat\")\n", + "stats = experiment.stats" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot of results for each trial\n", + "For example, a scatter plot of the mouse positions for each trial, labelled by the condition, trial number and repetition number:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_to_target(ax, group, colors):\n", + " for target_pos, target_radius, positions, color in zip(\n", + " group.target_pos, group.target_radius, group.to_target_mouse_positions, colors\n", + " ):\n", + " ax.plot(positions[:, 0], positions[:, 1], color=color)\n", + " ax.add_patch(\n", + " plt.Circle(\n", + " target_pos,\n", + " target_radius,\n", + " edgecolor=\"none\",\n", + " facecolor=color,\n", + " alpha=0.1,\n", + " )\n", + " )\n", + "\n", + "\n", + "def plot_to_center(ax, group, colors):\n", + " for central_target_radius, positions, color in zip(\n", + " group.center_radius,\n", + " group.to_center_mouse_positions,\n", + " colors,\n", + " ):\n", + " ax.plot(positions[:, 0], positions[:, 1], color=color)\n", + " ax.add_patch(\n", + " plt.Circle(\n", + " [0, 0],\n", + " central_target_radius,\n", + " edgecolor=\"none\",\n", + " facecolor=\"black\",\n", + " alpha=0.1,\n", + " )\n", + " )\n", + "\n", + "\n", + "def concatenate(array1, array2):\n", + " return np.concatenate(\n", + " (\n", + " array1,\n", + " array2,\n", + " ),\n", + " axis=0,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "colors = [\"blue\", \"green\", \"red\", \"cyan\", \"magenta\", \"yellow\", \"black\", \"orange\"]\n", + "\n", + "nTrials = len(stats[\"i_trial\"].unique())\n", + "nReps = len(stats[\"i_rep\"].unique())\n", + "fig, axs = plt.subplots(nTrials, nReps, figsize=(6, 6 * nTrials * nReps))\n", + "axs = np.reshape(\n", + " axs, (nTrials, nReps)\n", + ") # ensure axs is a 2d-array even if nTrials or nReps is 1\n", + "for (trial, rep, condition_index), group in stats.groupby(\n", + " [\"i_trial\", \"i_rep\", \"condition_index\"]\n", + "):\n", + " ax = axs[trial, rep]\n", + " ax.set_title(f\"[Condition {condition_index}] Trial {trial}, Rep {rep}\")\n", + " plot_to_target(ax, group, colors)\n", + " if not experiment.trial_list[condition_index][\"automove_cursor_to_center\"]:\n", + " plot_to_center(ax, group, colors)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot of results for each target\n", + "For example, a scatter plot of the mouse positions for each target, labelled by trial number, repetition number, target number and condition:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = plt.subplots(nTrials, nReps * 8, figsize=(6 * 8, 6 * nTrials * nReps))\n", + "axs = np.reshape(\n", + " axs, (nTrials, nReps * 8)\n", + ") # ensure axs is a 2d-array even if nTrials or nReps is 1\n", + "for (trial, rep, condition_index), group in stats.groupby(\n", + " [\"i_trial\", \"i_rep\", \"condition_index\"]\n", + "):\n", + " for positions, color, i in zip(\n", + " group.to_target_mouse_positions, colors, range(len(group))\n", + " ):\n", + " ax = axs[(trial, rep + i)]\n", + " ax.set_title(\n", + " f\"[Condition {condition_index}] Trial {trial}, Rep {rep}, Target {i}\"\n", + " )\n", + " ax.set_xlim(-0.5, 0.5)\n", + " ax.set_ylim(-0.5, 0.5)\n", + " if not experiment.trial_list[condition_index][\"automove_cursor_to_center\"]:\n", + " positions = concatenate(positions, group.to_center_mouse_positions.iloc[i])\n", + " ax.plot(positions[:, 0], positions[:, 1], color=color)\n", + "\n", + "fig.delaxes(axs[2][6])\n", + "fig.delaxes(axs[2][7])\n", + "fig.delaxes(axs[3][6])\n", + "fig.delaxes(axs[3][7])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot velocity for each trial\n", + "For example, a scatter plot of the velocity for each target,displayed in a single plot with velocities shown in the time sequence, labelled by trial number, repetition number and condition:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = plt.subplots(nTrials, nReps, figsize=(6, 6 * nTrials * nReps))\n", + "axs = np.reshape(\n", + " axs, (nTrials, nReps)\n", + ") # ensure axs is a 2d-array even if nTrials or nReps is 1\n", + "for (trial, rep, condition_index), group in stats.groupby(\n", + " [\"i_trial\", \"i_rep\", \"condition_index\"]\n", + "):\n", + " ax = axs[trial, rep]\n", + " ax.set_title(f\"[Condition {condition_index}] Trial {trial}, Rep {rep} \")\n", + " for positions, timestamps, color, i in zip(\n", + " group.to_target_mouse_positions,\n", + " group.to_target_timestamps,\n", + " colors,\n", + " range(len(group)),\n", + " ):\n", + " if not experiment.trial_list[condition_index][\"automove_cursor_to_center\"]:\n", + " positions = concatenate(positions, group.to_center_mouse_positions.iloc[i])\n", + " timestamps = concatenate(timestamps, group.to_center_timestamps.iloc[i])\n", + " ax.plot(timestamps[:-1], get_velocity(timestamps, positions), color=color)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For example, a scatter plot of the velocity for each target in separate plot, labelled by trial number, repetition number and condition:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = plt.subplots(nTrials, nReps * 8, figsize=(6 * 8, 6 * nTrials * nReps))\n", + "axs = np.reshape(\n", + " axs, (nTrials, nReps * 8)\n", + ") # ensure axs is a 2d-array even if nTrials or nReps is 1\n", + "for (trial, rep, condition_index), group in stats.groupby(\n", + " [\"i_trial\", \"i_rep\", \"condition_index\"]\n", + "):\n", + " for positions, timestamps, color, i in zip(\n", + " group.to_target_mouse_positions,\n", + " group.to_target_timestamps,\n", + " colors,\n", + " range(len(group)),\n", + " ):\n", + " ax = axs[(trial, rep + i)]\n", + " ax.set_title(\n", + " f\"[Condition {condition_index}] Trial {trial}, Rep {rep}, Target {i}\"\n", + " )\n", + " if not experiment.trial_list[condition_index][\"automove_cursor_to_center\"]:\n", + " positions = concatenate(positions, group.to_center_mouse_positions.iloc[i])\n", + " timestamps = concatenate(timestamps, group.to_center_timestamps.iloc[i])\n", + " ax.plot(timestamps[:-1], get_velocity(timestamps, positions), color=color)\n", + "\n", + "\n", + "fig.delaxes(axs[2][6])\n", + "fig.delaxes(axs[2][7])\n", + "fig.delaxes(axs[3][6])\n", + "fig.delaxes(axs[3][7])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For example, a scatter plot of the velocity for each target,displayed in a single plot with velocities starting from the same time point 0,labelled by trial number, repetition number and condition:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = plt.subplots(nTrials, nReps, figsize=(6, 6 * nTrials * nReps))\n", + "axs = np.reshape(\n", + " axs, (nTrials, nReps)\n", + ") # ensure axs is a 2d-array even if nTrials or nReps is 1\n", + "for (trial, rep, condition_index), group in stats.groupby(\n", + " [\"i_trial\", \"i_rep\", \"condition_index\"]\n", + "):\n", + " ax = axs[trial, rep]\n", + " ax.set_title(f\"[Condition {condition_index}] Trial {trial}, Rep {rep}\")\n", + " for positions, timestamps, color, i in zip(\n", + " group.to_target_mouse_positions,\n", + " group.to_target_timestamps,\n", + " colors,\n", + " range(len(group)),\n", + " ):\n", + " if not experiment.trial_list[condition_index][\"automove_cursor_to_center\"]:\n", + " positions = concatenate(positions, group.to_center_mouse_positions.iloc[i])\n", + " timestamps = concatenate(timestamps, group.to_center_timestamps.iloc[i])\n", + " ax.plot(\n", + " timestamps[:-1] - timestamps[0],\n", + " get_velocity(timestamps, positions),\n", + " color=color,\n", + " )\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### apply a Savitzky-Golay filter example\n", + "Here is an example for illustrating how to apply a [Savitzky-Golay filter](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.savgol_filter.html) to the velocity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_filter(window_length, polyorder):\n", + " \"\"\"\n", + " plot the original function and filtered function\n", + " :param window_length: The length of the filter window (i.e., the number of coefficients). If mode is ‘interp’, window_length must be less than or equal to the size of x.\n", + " :param polyorder: The order of the polynomial used to fit the samples. polyorder must be less than window_length.\n", + " \"\"\"\n", + " for _, group in stats.groupby([\"i_trial\", \"i_rep\", \"condition_index\"]):\n", + " for positions, timestamps in zip(\n", + " group.to_target_mouse_positions,\n", + " group.to_target_timestamps,\n", + " ):\n", + " velocity = get_velocity(timestamps, positions)\n", + " plt.plot(timestamps[:-1], velocity, linestyle=\"dashed\")\n", + " filtered_velocity = savgol_filter(velocity, window_length, polyorder)\n", + " plt.plot(timestamps[:-1], filtered_velocity)\n", + " break\n", + " break\n", + " plt.legend([\"original velocity\", \"filtered velocity\"], loc=\"upper left\")\n", + " plt.show()\n", + "\n", + "\n", + "interact(plot_filter, window_length=30, polyorder=8);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### apply filter to the mouse positions\n", + "For example, a scatter plot of the movement for each target in separate plots, the filtered movement is displayed in black dashed line:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = plt.subplots(nTrials, nReps * 8, figsize=(6 * 8, 6 * nTrials * nReps))\n", + "axs = np.reshape(\n", + " axs, (nTrials, nReps * 8)\n", + ") # ensure axs is a 2d-array even if nTrials or nReps is 1\n", + "for (trial, rep, condition_index), group in stats.groupby(\n", + " [\"i_trial\", \"i_rep\", \"condition_index\"]\n", + "):\n", + " for positions, color, i in zip(\n", + " group.to_target_mouse_positions, colors, range(len(group))\n", + " ):\n", + " ax = axs[(trial, rep + i)]\n", + " ax.set_title(\n", + " f\"[Condition {condition_index}] Trial {trial}, Rep {rep}, Target {i}\"\n", + " )\n", + " ax.set_xlim(-0.5, 0.5)\n", + " ax.set_ylim(-0.5, 0.5)\n", + " if not experiment.trial_list[condition_index][\"automove_cursor_to_center\"]:\n", + " positions = concatenate(positions, group.to_center_mouse_positions.iloc[i])\n", + " ax.plot(positions[:, 0], positions[:, 1], color=color, linestyle=\"dashed\")\n", + " window_length = len(positions[:, 0])\n", + " polyorder = 8\n", + " filtered_y = savgol_filter(positions[:, 1], window_length, polyorder)\n", + " ax.plot(positions[:, 0], filtered_y, color=\"black\")\n", + "\n", + "\n", + "fig.delaxes(axs[2][6])\n", + "fig.delaxes(axs[2][7])\n", + "fig.delaxes(axs[3][6])\n", + "fig.delaxes(axs[3][7])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### apply filter to the mouse position first, then plot the velocity\n", + "For example, a scatter plot of the velocity for each target in separate plots, the filtered velocity is displayed in black dashed line:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = plt.subplots(nTrials, nReps * 8, figsize=(6 * 8, 6 * nTrials * nReps))\n", + "axs = np.reshape(\n", + " axs, (nTrials, nReps * 8)\n", + ") # ensure axs is a 2d-array even if nTrials or nReps is 1\n", + "for (trial, rep, condition_index), group in stats.groupby(\n", + " [\"i_trial\", \"i_rep\", \"condition_index\"]\n", + "):\n", + " for positions, timestamps, color, i in zip(\n", + " group.to_target_mouse_positions,\n", + " group.to_target_timestamps,\n", + " colors,\n", + " range(len(group)),\n", + " ):\n", + " ax = axs[(trial, rep + i)]\n", + " ax.set_title(\n", + " f\"[Condition {condition_index}] Trial {trial}, Rep {rep}, Target {i}\"\n", + " )\n", + " if not experiment.trial_list[condition_index][\"automove_cursor_to_center\"]:\n", + " positions = concatenate(positions, group.to_center_mouse_positions.iloc[i])\n", + " timestamps = concatenate(timestamps, group.to_center_timestamps.iloc[i])\n", + " velocity = get_velocity(timestamps, positions)\n", + " ax.plot(timestamps[:-1], velocity, color=color, linestyle=\"dashed\")\n", + " filtered_positions = positions.copy()\n", + " window_length = len(filtered_positions[:, 1])\n", + " polyorder = 8\n", + " filtered_positions[:, 1] = savgol_filter(\n", + " filtered_positions[:, 1], window_length, polyorder\n", + " )\n", + " velocity_with_filtered_positions = get_velocity(timestamps, filtered_positions)\n", + " ax.plot(\n", + " timestamps[:-1],\n", + " velocity_with_filtered_positions,\n", + " color=\"black\",\n", + " )\n", + "\n", + "fig.delaxes(axs[2][6])\n", + "fig.delaxes(axs[2][7])\n", + "fig.delaxes(axs[3][6])\n", + "fig.delaxes(axs[3][7])\n", + "warnings.filterwarnings(\"ignore\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### apply filter to the velocity plot to make it smoother\n", + "For example, a scatter plot of the velocity for each target in separate plots, the filtered velocity is displayed in black dashed line:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = plt.subplots(nTrials, nReps * 8, figsize=(6 * 8, 6 * nTrials * nReps))\n", + "axs = np.reshape(\n", + " axs, (nTrials, nReps * 8)\n", + ") # ensure axs is a 2d-array even if nTrials or nReps is 1\n", + "for (trial, rep, condition_index), group in stats.groupby(\n", + " [\"i_trial\", \"i_rep\", \"condition_index\"]\n", + "):\n", + " for positions, timestamps, color, i in zip(\n", + " group.to_target_mouse_positions,\n", + " group.to_target_timestamps,\n", + " colors,\n", + " range(len(group)),\n", + " ):\n", + " ax = axs[(trial, rep + i)]\n", + " ax.set_title(\n", + " f\"[Condition {condition_index}] Trial {trial}, Rep {rep}, Target {i}\"\n", + " )\n", + " if not experiment.trial_list[condition_index][\"automove_cursor_to_center\"]:\n", + " positions = concatenate(positions, group.to_center_mouse_positions.iloc[i])\n", + " timestamps = concatenate(timestamps, group.to_center_timestamps.iloc[i])\n", + " velocity = get_velocity(timestamps, positions)\n", + " ax.plot(timestamps[:-1], velocity, color=color, linestyle=\"dashed\")\n", + " window_length = len(velocity)\n", + " polyorder = len(velocity) - 1\n", + " filtered_velocity = savgol_filter(velocity, window_length, polyorder)\n", + " ax.plot(timestamps[:-1], filtered_velocity, color=\"black\")\n", + "\n", + "\n", + "fig.delaxes(axs[2][6])\n", + "fig.delaxes(axs[2][7])\n", + "fig.delaxes(axs[3][6])\n", + "fig.delaxes(axs[3][7])\n", + "warnings.filterwarnings(\"ignore\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}