{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Calculate the area of the enclosed geometric object with psydat file" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from psychopy.misc import fromFile" ] }, { "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": [ "psydata = fromFile(\"example.psydat\")" ] }, { "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": [ "colors = [\"blue\", \"green\", \"red\", \"cyan\", \"magenta\", \"yellow\", \"black\", \"orange\"]\n", "\n", "nTrials, nReps = psydata.sequenceIndices.shape\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 in range(nTrials):\n", " for rep in range(nReps):\n", " loc = (trial, rep)\n", " condition = psydata.sequenceIndices[loc]\n", " target_radius = psydata.trialList[condition][\"target_size\"]\n", " central_target_radius = psydata.trialList[condition][\"central_target_size\"]\n", " ax = axs[loc]\n", " ax.set_title(f\"Trial {trial}, Rep {rep} [Condition {condition}]\")\n", " for positions, target_pos, color in zip(\n", " psydata.data[\"to_target_mouse_positions\"][loc],\n", " psydata.data[\"target_pos\"][loc],\n", " 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", " if not psydata.trialList[condition][\"automove_cursor_to_center\"]:\n", " for positions, color in zip(\n", " psydata.data[\"to_center_mouse_positions\"][loc],\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", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Plot of area calculation results for each trial\n", "\n", "How to calculate the area of irregular geometric object with given coordinates from psydat file?\n", "\n", "The build-in operation `area` in the library `shapely` can calculate the area of geometry object. However, only for the valid one->not self intersected.\n", "\n", "To tackle the self-intersection problem, the strategy is to split one self intersected object into the union of `LineString`(a geometry type composed of one or more line segments), then construct a bunch of valid polygons from these lines, then calculate the area of each valid polygon, sum them up." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from typing import List\n", "from typing import Tuple\n", "\n", "from shapely.geometry import LineString\n", "from shapely.ops import polygonize\n", "from shapely.ops import unary_union" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_area_and_polygons(\n", " to_target_mouse_positions: np.ndarray, to_center_mouse_positions: np.ndarray\n", ") -> Tuple[float, List[np.ndarray]]:\n", " \"\"\"\n", " Calculates the total area enclosed by the mouse positions and the corresponding list of closed polygons\n", "\n", " Uses the built-in operation `area` in the library `shapely` to calculate the area of geometry object.\n", " However, this is only available for valid (not self intersected) geometries.\n", " To tackle the self-intersection problem,\n", " the strategy is to split one self intersected object into the union of LineString (a geometry type composed of one or more line segments),\n", " then construct a bunch of valid polygons from these lines,\n", " then calculate the area of each valid polygon and sum them up.\n", "\n", " :param to_target_mouse_positions: x,y mouse positions moving towards the target\n", " :param to_center_mouse_positions: x,y mouse positions moving towards the center\n", " :return: area, list of x and y arrays of corresponding closed polygons\n", "\n", " \"\"\"\n", " coords = np.concatenate(\n", " [\n", " to_target_mouse_positions,\n", " to_center_mouse_positions,\n", " to_target_mouse_positions[0:1],\n", " ]\n", " )\n", " polygons = polygonize(unary_union(LineString(coords)))\n", " area = sum(polygon.area for polygon in polygons)\n", " xy_arrays = [np.array(xy) for polygon in polygons for xy in polygon.exterior.xy]\n", " return area, xy_arrays" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_and_calculate_area(data):\n", " colors = [\"blue\", \"green\", \"red\", \"cyan\", \"magenta\", \"yellow\", \"black\", \"orange\"]\n", " nTrials, nReps = data.sequenceIndices.shape\n", " for trial in range(nTrials):\n", " for rep in range(nReps):\n", " loc = (trial, rep)\n", " condition = data.sequenceIndices[loc]\n", " target_radius = data.trialList[condition][\"target_size\"]\n", " central_target_radius = data.trialList[condition][\"central_target_size\"]\n", "\n", " # if condition \"automove_cursor_to_center\" is deselected, plot the line to center, fill the enclosed area and output the area\n", " if not data.trialList[condition][\"automove_cursor_to_center\"]:\n", " fig, ax = plt.subplots(1, 1, figsize=(6, 6))\n", " ax.set_xbound(-0.5, 0.5)\n", " ax.set_ybound(-0.5, 0.5)\n", " ax.set_title(\n", " f\"area plot for Trial {trial}, Rep {rep} [Condition {condition}]\"\n", " )\n", " print(\"---------------------------------------------\")\n", " print(f\"area of Trial {trial}, Rep {rep} [Condition {condition}]\")\n", " for (\n", " to_target_mouse_positions,\n", " to_center_mouse_positions,\n", " target_pos,\n", " color,\n", " ) in zip(\n", " data.data[\"to_target_mouse_positions\"][loc],\n", " data.data[\"to_center_mouse_positions\"][loc],\n", " data.data[\"target_pos\"][loc],\n", " colors,\n", " ):\n", " area, polygons = get_area_and_polygons(\n", " to_target_mouse_positions, to_center_mouse_positions\n", " )\n", " ax.fill(*polygons, facecolor=color)\n", "\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", " print(f\"{color}, area: {area:f}\")\n", "\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", " plt.show()\n", "\n", "\n", "plot_and_calculate_area(psydata)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Special cases\n", "This strategy can also be applied to the following special cases:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### no movement\n", "There is no lines in the plot" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "psydata_no_movement = fromFile(\"example_no_movement.psydat\")\n", "plot_and_calculate_area(psydata_no_movement)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### too much movement\n", "The whole plot is full of lines" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "psydata_over_movement = fromFile(\"example_over_movement.psydat\")\n", "plot_and_calculate_area(psydata_over_movement)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### some targets got reached, some not\n", "For some targets, no line is approaching them, the corresponding area is 0" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "psydata_target_not_reached = fromFile(\"example_target_not_reached.psydat\")\n", "plot_and_calculate_area(psydata_target_not_reached)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### select \"Automatically move cursor to center\" condition\n", "The trials with condition \"Automatically move cursor to center\" selected will not be drawn, only the trials with condition \"Automatically move cursor to center\" deselected will be shown in the plot." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "psydata_select_back_to_center = fromFile(\"example_select_back_to_center.psydat\")\n", "plot_and_calculate_area(psydata_select_back_to_center)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### not closed line\n", "For the path from the center to target and back to center is not closed, will be closed by the first and last coordinates automatically." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "psydata_not_closed_line = fromFile(\"example_not_closed_line.psydat\")\n", "plot_and_calculate_area(psydata_not_closed_line)" ] }, { "cell_type": "markdown", "metadata": {}, "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.9.15" } }, "nbformat": 4, "nbformat_minor": 4 }