3 Commits 3e5a287e07 ... 6400ec29e7

Author SHA1 Message Date
  asobolev 6400ec29e7 merged changes 10 months ago
  asobolev f0dc4e95ba added RAW video writer 10 months ago
  asobolev 12e821d67c added passive stimulation script 11 months ago

+ 38 - 21
SIT.ipynb

@@ -59,12 +59,15 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "#cfg_filename = os.path.join('profiles', 'gokce_timeSIT_50_52.5.json')\n",
-    "#cfg_filename = os.path.join('profiles', 'gokce_timeSIT_90_60.json')\n",
+    "cfg_filename = os.path.join('profiles', 'gokce_timeSIT_50_100.json')\n",
+    "# cfg_filename = os.path.join('profiles', 'gokce_timeSIT_90_99.json')\n",
     "#'gokce_timeSIT_90_108.json')\n",
     "\n",
-    "# cfg_filename = os.path.join('profiles', 'social_test_actual.json')\n",
-    "cfg_filename = os.path.join('profiles', 'andrey_ppcSIT_SL_009266.json')"
+    "# cfg_filename = os.path.join('profiles', 'gokce_socialSIT.json')\n",
+    "#cfg_filename = os.path.join('profiles', 'miguel_socialSIT_fireface.json')\n",
+    "# cfg_filename = os.path.join('profiles', 'miguel_socialSIT_fireface_chirp.json')\n",
+    "\n",
+    "# cfg_filename = os.path.join('profiles', 'andrey_ppcSIT_SL_009266.json')"
    ]
   },
   {
@@ -114,7 +117,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -138,6 +141,8 @@
     "cfg['sound']['file_path'] = os.path.join(save_to, cfg['sound']['file_path'])\n",
     "cfg['position']['background_light'] = os.path.join('assets', cfg['position']['background_light'])\n",
     "cfg['position']['background_dark'] = os.path.join('assets', cfg['position']['background_dark'])\n",
+    "if 'wav_file' in cfg['sound']:\n",
+    "    cfg['sound']['wav_file'] = os.path.join('assets', cfg['sound']['wav_file'])\n",
     "if 'continuous' in cfg['sound']:\n",
     "    cfg['sound']['continuous']['wav_file'] = os.path.join('assets', cfg['sound']['continuous']['wav_file'])\n",
     "    \n",
@@ -155,7 +160,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -165,7 +170,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -176,7 +181,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 7,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -191,7 +196,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -218,7 +223,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 9,
    "metadata": {
     "scrolled": true
    },
@@ -230,6 +235,7 @@
       "Webcam stream 1024.0:768.0 at 30.00 FPS started\n",
       "Position tracker stopped\n",
       "Video writer stopped\n",
+      "Video writer stopped\n",
       "Camera released\n"
      ]
     }
@@ -266,10 +272,17 @@
     "vs = WebcamStream(cfg['camera'])\n",
     "vs.start()\n",
     "\n",
-    "# init video recorder\n",
-    "vw = VideoWriter(status, vs, cfg['video'])\n",
+    "# init video recorders\n",
+    "vw  = VideoWriter(status, vs, cfg['video'])  # recorder with infos\n",
     "vw.start()\n",
     "\n",
+    "cfg_video_raw = dict(cfg['video'])\n",
+    "cfg_video_raw['file_path']       = os.path.join(save_to, 'raw.avi')\n",
+    "cfg_video_raw['csv_path']        = os.path.join(save_to, 'raw.csv')\n",
+    "cfg_video_raw['frame_attr_name'] = 'frame_raw'\n",
+    "vwr = VideoWriter(status, vs, cfg_video_raw)  # recorder of raw video\n",
+    "vwr.start()\n",
+    "\n",
     "# init microphone controller, if required\n",
     "if cfg['microphones']['record_audio']:\n",
     "    mc = mp.Process(target=MicrophoneController.run, args=(status,cfg[\"microphones\"]))\n",
@@ -283,7 +296,7 @@
     "dc = SITDisplay(pt, cfg['video'])\n",
     "\n",
     "# playing sound in a separate process for performance\n",
-    "sc = mp.Process(target=SoundController.run, args=(sound, status, cfg['sound']))\n",
+    "sc = mp.Process(target=SoundController.run, args=(sound, status, cfg['sound'], SoundController.commutator))\n",
     "sc.start()\n",
     "\n",
     "cfg_pos = cfg['position']\n",
@@ -327,6 +340,7 @@
     "        \n",
     "        # ---------- rendering logic -----------\n",
     "        frame = vs.read()\n",
+    "                \n",
     "        if frame is None:\n",
     "            time.sleep(0.1)\n",
     "            continue # wait for the stream\n",
@@ -345,7 +359,6 @@
     "        if len(islands) > 0:  # target island X, Y\n",
     "            target = [i for i in islands if not i.is_distractor][0]\n",
     "            text_infos.append('Target: %.3f %.3f' % (target.x, target.y))\n",
-    "#         print(frame.shape)    \n",
     "        text_infos.append('Time: %.2f' % float(c_time - t_start) if t_start is not None else 'Time: Idle')  # stopwatch\n",
     "        text_infos.append('Trial: %.2f' % float(cfg_exp['trial_duration'] - (c_time - trial_start)) if phase == 1 else 'Trial: not started')\n",
     "        text_infos.append('Trial: %s' % trial)\n",
@@ -353,6 +366,7 @@
     "        text_infos.append('In target: %.2f' % float(c_time - target_since) if target_since is not None else 'In target: --')\n",
     "        text_infos.append('ITI distance: %.2f' % iti_distance if iti_distance > 0 else 'ITI distance: --')\n",
     "\n",
+    "        vs.frame_raw = frame  # save raw frame\n",
     "        frame, frame_to_save = dc.render(frame, status, islands=islands, angles=angles, text_infos=text_infos)\n",
     "            \n",
     "        # assign the frame back to the video stream for other controllers\n",
@@ -377,7 +391,8 @@
     "\n",
     "                    elif c_time - target_since > cfg_exp['target_duration']: # successful trial\n",
     "                        log_event(c_time, round(tgt.x, 4), round(tgt.y, 4), round(tgt.r, 4), trial, 1) # log trial success\n",
-    "                        board.feed(n_pellets = cfg_exp['n_pellets'])\n",
+    "                        tf = threading.Timer(0, board.feed, args=[])\n",
+    "                        tf.start()\n",
     "                        \n",
     "                        # init inter-trial interval and new trial\n",
     "                        trial_start = c_time + cfg_exp['iti_duration'] + 5 * np.random.rand()  # random b/w 20-25 sec\n",
@@ -593,7 +608,7 @@
     "    \n",
     "    cv2.destroyAllWindows()\n",
     "    sc.join()\n",
-    "    for ctrl in [pt, vw, vs]:\n",
+    "    for ctrl in [pt, vw, vwr, vs]:\n",
     "        ctrl.stop()\n",
     "\n",
     "    # stop microphones recording process\n",
@@ -615,7 +630,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 10,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -627,7 +642,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 11,
    "metadata": {},
    "outputs": [
     {
@@ -637,7 +652,7 @@
      "traceback": [
       "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
       "\u001b[1;31mIndexError\u001b[0m                                Traceback (most recent call last)",
-      "\u001b[1;32m<ipython-input-12-df6d003c4298>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m      3\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[1;31m# do pack data to HDF5\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[0mh5name\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mpack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msession_path\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      6\u001b[0m \u001b[0mtrial\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
+      "\u001b[1;32m<ipython-input-11-df6d003c4298>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m      3\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[1;31m# do pack data to HDF5\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[0mh5name\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mpack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msession_path\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      6\u001b[0m \u001b[0mtrial\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;32mD:\\runSIT\\..\\pipeline\\postprocessing\\pack.py\u001b[0m in \u001b[0;36mpack\u001b[1;34m(session_path)\u001b[0m\n\u001b[0;32m    165\u001b[0m         \u001b[1;31m# head direction\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    166\u001b[0m         \u001b[0mtemp_tl\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcolumn_stack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mpos_at_freq\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mx_smooth\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my_smooth\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspeed\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 167\u001b[1;33m         \u001b[0mhd\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mhead_direction\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtemp_tl\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m    168\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    169\u001b[0m         \u001b[1;31m# trial numbers\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;32mD:\\runSIT\\..\\pipeline\\postprocessing\\head_direction.py\u001b[0m in \u001b[0;36mhead_direction\u001b[1;34m(tl, hd_update_speed)\u001b[0m\n\u001b[0;32m     22\u001b[0m     \u001b[0mcrit\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwhere\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdiff\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0midle_idxs\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     23\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 24\u001b[1;33m     \u001b[0midle_periods\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[1;33m(\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m)\u001b[0m  \u001b[1;31m# first idle period\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     25\u001b[0m     \u001b[1;32mfor\u001b[0m \u001b[0mi\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mpoint\u001b[0m \u001b[1;32min\u001b[0m \u001b[0menumerate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m-\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     26\u001b[0m         \u001b[0midx_start\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mi\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;31mIndexError\u001b[0m: index 0 is out of bounds for axis 0 with size 0"
@@ -840,7 +855,9 @@
    "execution_count": null,
    "metadata": {},
    "outputs": [],
-   "source": []
+   "source": [
+    "\n"
+   ]
   }
  ],
  "metadata": {
@@ -859,7 +876,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.10.8"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,

+ 4 - 4
controllers/camera.ipynb

@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 1,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -17,7 +17,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 2,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -95,7 +95,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 3,
    "metadata": {},
    "outputs": [
     {
@@ -113,7 +113,7 @@
      "traceback": [
       "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
       "\u001b[1;31mKeyboardInterrupt\u001b[0m                         Traceback (most recent call last)",
-      "\u001b[1;32m<ipython-input-9-0e84daf67e4d>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m     10\u001b[0m             \u001b[0mcv2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mimshow\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'Webcam'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mframe\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     11\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 12\u001b[1;33m         \u001b[0mk\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcv2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwaitKey\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m33\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     13\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[0mk\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0mord\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'q'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     14\u001b[0m             \u001b[1;32mbreak\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
+      "\u001b[1;32m<ipython-input-3-0e84daf67e4d>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m     10\u001b[0m             \u001b[0mcv2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mimshow\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'Webcam'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mframe\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     11\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 12\u001b[1;33m         \u001b[0mk\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcv2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwaitKey\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m33\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     13\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[0mk\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0mord\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'q'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     14\u001b[0m             \u001b[1;32mbreak\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;31mKeyboardInterrupt\u001b[0m: "
      ]
     }

+ 1 - 1
controllers/display.ipynb

@@ -40,7 +40,7 @@
     "        \n",
     "        # mask unused areas\n",
     "        frame = cv2.bitwise_and(src1=input_frame, src2=self.pt.mask)\n",
-    "        self.masked_frame = frame.copy()\n",
+    "        #self.masked_frame = frame.copy()\n",
     "\n",
     "        # size of the arena and status indicator\n",
     "        cv2.circle(frame, (self.pt.cfg['arena_x'], self.pt.cfg['arena_y']), self.pt.cfg['arena_radius'], COLORS['red'], 2)\n",

+ 5 - 5
controllers/position.ipynb

@@ -129,7 +129,7 @@
     "\n",
     "            if self.status.value == 2 and self.positions_in_px is not None:\n",
     "                self.save_position()\n",
-    "                self.save_contours()                  \n",
+    "                #self.save_contours()                  \n",
     "\n",
     "    # generic interface\n",
     "\n",
@@ -187,8 +187,8 @@
     "        \n",
     "        with open(cfg['file_path'], 'w') as f:\n",
     "            f.write(\"time,x,y\\n\")\n",
-    "        with open(cfg['contour_path'], 'w') as f:\n",
-    "            f.write(\"x:y,...\\n\")\n",
+    "        #with open(cfg['contour_path'], 'w') as f:\n",
+    "        #    f.write(\"x:y,...\\n\")\n",
     "            \n",
     "    def detect_position(self, frame):\n",
     "        masked_frame = cv2.bitwise_and(src1=frame, src2=self.mask)\n",
@@ -325,8 +325,8 @@
     "       \n",
     "        with open(cfg['file_path'], 'w') as f:\n",
     "            f.write(\"time,x1,y1,x2,y2\\n\")\n",
-    "        with open(cfg['contour_path'], 'w') as f:\n",
-    "            f.write(\"x:y,...\\n\")\n",
+    "        #with open(cfg['contour_path'], 'w') as f:\n",
+    "        #    f.write(\"x:y,...\\n\")\n",
     "\n",
     "    def detect_position(self, frame):\n",
     "        masked_frame = cv2.bitwise_and(src1=frame, src2=self.mask)\n",

+ 16 - 16
controllers/sound.ipynb

@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [
     {
@@ -46,6 +46,18 @@
     "        \"file_path\": \"sounds.csv\"\n",
     "    }\n",
     "    \n",
+    "    commutator = {\n",
+    "        -1: 'noise',\n",
+    "        0:  'silence',\n",
+    "        1:  'background',\n",
+    "        2:  'target',\n",
+    "        3:  'distractor1',\n",
+    "        4:  'distractor2',\n",
+    "        5:  'distractor3',\n",
+    "        6:  'distractor4',\n",
+    "        7:  'distractor5'\n",
+    "    }\n",
+    "        \n",
     "    @classmethod\n",
     "    def get_pure_tone(cls, freq, duration, sample_rate=44100):\n",
     "        x = np.linspace(0, duration * freq * 2*np.pi, int(duration*sample_rate), dtype=np.float32)\n",
@@ -107,7 +119,7 @@
     "        return sounds\n",
     "        \n",
     "    @classmethod\n",
-    "    def run(cls, selector, status, cfg):\n",
+    "    def run(cls, selector, status, cfg, commutator):\n",
     "        \"\"\"\n",
     "        selector        mp.Value object to set the sound to be played\n",
     "        status          mp.Value object to stop the loop\n",
@@ -116,18 +128,6 @@
     "        import numpy as np\n",
     "        import time\n",
     "        \n",
-    "        commutator = {\n",
-    "            -1: 'noise',\n",
-    "            0:  'silence',\n",
-    "            1:  'background',\n",
-    "            2:  'target',\n",
-    "            3:  'distractor1',\n",
-    "            4:  'distractor2',\n",
-    "            5:  'distractor3',\n",
-    "            6:  'distractor4',\n",
-    "            7:  'distractor5'\n",
-    "        }\n",
-    "        \n",
     "        sounds = cls.get_tone_stack(cfg)\n",
     "\n",
     "        sd.default.device = cfg['device']\n",
@@ -999,7 +999,7 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3 (ipykernel)",
+   "display_name": "Python 3",
    "language": "python",
    "name": "python3"
   },
@@ -1013,7 +1013,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.13"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,

+ 13 - 13
controllers/sound.py

@@ -29,6 +29,18 @@ class SoundController:
         "file_path": "sounds.csv"
     }
     
+    commutator = {
+        -1: 'noise',
+        0:  'silence',
+        1:  'background',
+        2:  'target',
+        3:  'distractor1',
+        4:  'distractor2',
+        5:  'distractor3',
+        6:  'distractor4',
+        7:  'distractor5'
+    }
+        
     @classmethod
     def get_pure_tone(cls, freq, duration, sample_rate=44100):
         x = np.linspace(0, duration * freq * 2*np.pi, int(duration*sample_rate), dtype=np.float32)
@@ -90,7 +102,7 @@ class SoundController:
         return sounds
         
     @classmethod
-    def run(cls, selector, status, cfg):
+    def run(cls, selector, status, cfg, commutator):
         """
         selector        mp.Value object to set the sound to be played
         status          mp.Value object to stop the loop
@@ -99,18 +111,6 @@ class SoundController:
         import numpy as np
         import time
         
-        commutator = {
-            -1: 'noise',
-            0:  'silence',
-            1:  'background',
-            2:  'target',
-            3:  'distractor1',
-            4:  'distractor2',
-            5:  'distractor3',
-            6:  'distractor4',
-            7:  'distractor5'
-        }
-        
         sounds = cls.get_tone_stack(cfg)
 
         sd.default.device = cfg['device']

+ 4 - 4
controllers/video.ipynb

@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 2,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -18,7 +18,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -26,7 +26,7 @@
     "    # precise timing https://stackoverflow.com/questions/42565297/precise-loop-timing-in-python\n",
     "    \n",
     "    default_cfg = {\n",
-    "        'fps': 20,\n",
+    "        'fps': 30,\n",
     "        'file_path': 'test_video.avi',\n",
     "    }\n",
     "    \n",
@@ -65,7 +65,7 @@
     "                    continue  # TODO sleep here?\n",
     "\n",
     "                #frame = self.video_stream.read()\n",
-    "                frame = self.video_stream.frame_with_infos\n",
+    "                frame = getattr(self.video_stream, self.cfg['frame_attr_name'])\n",
     "                if frame is not None:\n",
     "                    self.count()  # count FPS\n",
     "                    t0 = time.time()\n",

+ 736 - 0
passive.ipynb

@@ -0,0 +1,736 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# include controllers to the path\n",
+    "import sys, os\n",
+    "sys.path.append(os.getcwd())\n",
+    "sys.path.append(os.path.join(os.getcwd(), 'controllers'))\n",
+    "\n",
+    "# include pipeline repo to compute performance\n",
+    "sys.path.append(os.path.join(os.getcwd(), '..'))\n",
+    "sys.path.append(os.path.join(os.getcwd(), '..', 'pipeline'))\n",
+    "sys.path.append(os.path.join(os.getcwd(), '..', 'pipeline', 'postprocessing'))\n",
+    "\n",
+    "import cv2\n",
+    "import threading\n",
+    "import math\n",
+    "import time\n",
+    "import random\n",
+    "import json\n",
+    "import datetime\n",
+    "import os, shutil\n",
+    "import numpy as np\n",
+    "import multiprocess as mp\n",
+    "\n",
+    "# controllers\n",
+    "import nbimporter\n",
+    "from controllers.situtils import FPSTimes\n",
+    "from controllers.camera import WebcamStream\n",
+    "from controllers.video import VideoWriter\n",
+    "from controllers.microphones import MicrophoneController\n",
+    "from controllers.position import PositionTrackerSingle, PositionTrackerDouble\n",
+    "from controllers.sound import SoundController, ContinuousSoundStream\n",
+    "from controllers.serial import MCSArduino, FakeArduino, SpeakerMotor, CableMotor\n",
+    "from controllers.display import SITDisplay\n",
+    "from controllers.island import IslandFactory\n",
+    "\n",
+    "from pipeline.postprocessing.pack import pack\n",
+    "from pipeline.postprocessing.performance import calculate_performance, dump_performance_to_H5"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Load experiment settings\n",
+    "\n",
+    "For every experimental cofiguration you can copy the original 'settings.json' file, build your own specific experimental preset, save it in this folder as e.g. 'settings_elena.json' and load it here instead of 'settings.json'."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "cfg_filename = os.path.join('profiles', 'passive.json')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 25,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "with open(os.path.join('profiles', 'default.json')) as json_file:\n",
+    "    cfg = json.load(json_file)\n",
+    "with open(cfg_filename) as json_file:\n",
+    "    cfg_local = json.load(json_file)\n",
+    "\n",
+    "for key in cfg.keys():\n",
+    "    if key in cfg_local: # only update if the key exists in the local config, otherwise keep default (important for backward compatibility with cfg files before microphones)\n",
+    "        cfg[key].update(cfg_local[key])\n",
+    "cfg['experiment']['experiment_date'] = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')\n",
+    "\n",
+    "# print loaded settings\n",
+    "#print(json.dumps(cfg, indent=4))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Initialize session folder\n",
+    "\n",
+    "Run the upcoming cell, to create a session folder and to save the chosen experimetal parameters to a JSON-file (\"experiment_id_parameters.json\"). The session folder will be created here where this notebook is located."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 26,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# This session's protocols will be saved to this folder\n",
+    "cfg_exp = cfg['experiment']\n",
+    "experiment_id = \"%s_%s_%s\" % (cfg_exp['subject'], cfg_exp['experiment_type'], cfg_exp['experiment_date'])\n",
+    "save_to = os.path.join('sessions', experiment_id)\n",
+    "             \n",
+    "if not os.path.exists(save_to):\n",
+    "    os.makedirs(save_to)\n",
+    "\n",
+    "# update paths (assuming this paths are relative to this notebook)\n",
+    "cfg['video']['file_path'] = os.path.join(save_to, cfg['video']['file_path'])\n",
+    "cfg['video']['csv_path'] = os.path.join(save_to, cfg['video']['csv_path'])\n",
+    "cfg['microphones']['file_path'] = os.path.join(save_to, cfg['microphones']['file_path'])\n",
+    "cfg['microphones']['csv_path'] = os.path.join(save_to, cfg['microphones']['csv_path'])\n",
+    "cfg['position']['file_path'] = os.path.join(save_to, cfg['position']['file_path'])\n",
+    "cfg['position']['contour_path'] = os.path.join(save_to, cfg['position']['contour_path'])\n",
+    "cfg['experiment']['file_path'] = os.path.join(save_to, cfg['experiment']['file_path'])\n",
+    "cfg['experiment']['islands_path'] = os.path.join(save_to, 'islands.csv')\n",
+    "cfg['sound']['file_path'] = os.path.join(save_to, cfg['sound']['file_path'])\n",
+    "cfg['position']['background_light'] = os.path.join('assets', cfg['position']['background_light'])\n",
+    "cfg['position']['background_dark'] = os.path.join('assets', cfg['position']['background_dark'])\n",
+    "if 'wav_file' in cfg['sound']:\n",
+    "    cfg['sound']['wav_file'] = os.path.join('assets', cfg['sound']['wav_file'])\n",
+    "if 'continuous' in cfg['sound']:\n",
+    "    cfg['sound']['continuous']['wav_file'] = os.path.join('assets', cfg['sound']['continuous']['wav_file'])\n",
+    "    \n",
+    "# Saves all parameters to a JSON file with the user-defined \"Experiment ID\" as filename\n",
+    "with open(os.path.join(save_to, experiment_id + '.json'), 'w') as f:\n",
+    "    json.dump(cfg, f, indent=4)\n",
+    "    \n",
+    "with open(cfg['experiment']['file_path'], 'w') as f:\n",
+    "    # state: 0 - trial start, 1 - trial success, 2 - trial fail\n",
+    "    f.write('time, target_x, target_y, target_r, trial, state\\n')\n",
+    "\n",
+    "with open(cfg['experiment']['islands_path'], 'w') as f:\n",
+    "    f.write('tgt_x, tgt_y, tgt_r, d1_x, d1_y, d1_r, d2_x, d2_y, d2_r, d3_x, d3_y, d3_r\\n')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 27,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def timeout(t_start):\n",
+    "    return time.time() - t_start > cfg_exp['session_duration'] if t_start is not None else False"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 28,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def log_event(*args):  # log start / end of a trial\n",
+    "    with open(cfg_exp['file_path'], 'a') as f:\n",
+    "        f.write(\",\".join([str(x) for x in args]) + \"\\n\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 29,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def log_islands(islands):  # log position of the islands\n",
+    "    sorted_islands = sorted(islands, key=lambda x: x.sound_id, reverse=False)\n",
+    "    args = [(i.x, i.y, i.r) for i in sorted_islands]\n",
+    "    to_dump = [x for xs in args for x in xs]\n",
+    "        \n",
+    "    with open(cfg_exp['islands_path'], 'a') as f:    \n",
+    "        f.write(\",\".join([str(round(x, 4)) for x in to_dump]) + \"\\n\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 30,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def switch_light(pt, board):\n",
+    "    pt.switch_background()\n",
+    "    board.switch_light()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Start the experiment\n",
+    "\n",
+    "This cell contains code for animal tracking. We hope that the comments provided in the code suffice to understand the individual steps and to adjust them to your own setup and needs, if necessary.\n",
+    "\n",
+    "- press 's' to start recording\n",
+    "- press 's' again to stop recording\n",
+    "- press 'q' to quit\n",
+    "\n",
+    "The experiment will stop automatically if the pre-defined session duration is reached."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 31,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Webcam stream 1024.0:768.0 at 30.00 FPS started\n",
+      "Position tracker stopped\n",
+      "Video writer stopped\n",
+      "Camera released\n"
+     ]
+    }
+   ],
+   "source": [
+    "# actual sound selector: -1 - noise, 0 - silence, 1 - foraging, 2 - target, 3 - distractor\n",
+    "sound = mp.Value('i', 1)\n",
+    "if 'noise_when_idle' in cfg_exp and cfg_exp['noise_when_idle']:\n",
+    "    sound.value = -1\n",
+    "\n",
+    "# experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\n",
+    "status = mp.Value('i', 1)\n",
+    "\n",
+    "# init the sync with the acquisition system and feeder via Arduino\n",
+    "if cfg['experiment']['MCSArduinoPort'] == 'fake':\n",
+    "    board = FakeArduino()\n",
+    "else:\n",
+    "    board = MCSArduino(cfg['experiment']['MCSArduinoPort'])\n",
+    "\n",
+    "# init speaker motor control\n",
+    "if 'motors_port' in cfg['experiment'] and cfg['experiment']['enable_motors']:\n",
+    "    motor_board = SpeakerMotor(cfg['experiment']['motors_port'])\n",
+    "    \n",
+    "# init ephys cable rotation motor\n",
+    "if 'cable_motor_port' in cfg['experiment']:\n",
+    "    cable_board = CableMotor(cfg['experiment']['cable_motor_port'])\n",
+    "\n",
+    "# init continuous sound, if required\n",
+    "if 'continuous' in cfg['sound']:\n",
+    "    cst = ContinuousSoundStream(cfg['sound']['continuous'])\n",
+    "    cst.start()\n",
+    "    \n",
+    "# start the camera stream\n",
+    "vs = WebcamStream(cfg['camera'])\n",
+    "vs.start()\n",
+    "\n",
+    "# init video recorder\n",
+    "vw = VideoWriter(status, vs, cfg['video'])\n",
+    "vw.start()\n",
+    "\n",
+    "# init microphone controller, if required\n",
+    "if cfg['microphones']['record_audio']:\n",
+    "    mc = mp.Process(target=MicrophoneController.run, args=(status,cfg[\"microphones\"]))\n",
+    "    mc.start()\n",
+    "\n",
+    "# start position tracking\n",
+    "pt = PositionTrackerSingle(status, vs, cfg['position']) if cfg['position']['single_agent'] else PositionTrackerDouble(status, vs, cfg['position'])\n",
+    "pt.start()\n",
+    "# print(pt.mask.shape)\n",
+    "# init frame renderer\n",
+    "dc = SITDisplay(pt, cfg['video'])\n",
+    "\n",
+    "# playing sound in a separate process for performance\n",
+    "sounds = cfg['sound']['sounds']\n",
+    "commutator = {\n",
+    "    -1: 'noise',\n",
+    "    0:  'silence'\n",
+    "}\n",
+    "\n",
+    "sound_order = 1\n",
+    "for s_type in ('F', 'D', 'I', 'L'):\n",
+    "    keys = sorted([key for key in sounds.keys() if key.startswith(s_type)])\n",
+    "    for key in keys:\n",
+    "        commutator[sound_order] = key\n",
+    "        sound_order += 1\n",
+    "sc = mp.Process(target=SoundController.run, args=(sound, status, cfg['sound'], commutator))\n",
+    "sc.start()\n",
+    "\n",
+    "cfg_pos = cfg['position']\n",
+    "isl_factory = IslandFactory(cfg_pos['floor_r_in_meters'], cfg_pos['angle_compensation'], cfg['experiment'])\n",
+    "\n",
+    "timers = []\n",
+    "fps = FPSTimes()\n",
+    "names = ['camera', 'video', 'position', 'main']\n",
+    "distr_count = cfg['experiment']['distractor_islands']\n",
+    "trial = 0\n",
+    "rewards = 0\n",
+    "t_start = None\n",
+    "target_since = None\n",
+    "distractor_since = None\n",
+    "punishment_since = None\n",
+    "trial_start = time.time()\n",
+    "phase = 0  # 0 - idle, 1 - foraging, 2 - inter-trial interval\n",
+    "cfg_exp = cfg['experiment']\n",
+    "cfg_pos = cfg['position']\n",
+    "islands = []\n",
+    "iti_distance = 0.0\n",
+    "last_x, last_y = None, None\n",
+    "cfg_exp['trial_number'] = max([int(key) for key in commutator.keys()])  # equals number of diff sounds\n",
+    "\n",
+    "try:\n",
+    "    while trial <= cfg_exp['trial_number'] and not timeout(t_start):\n",
+    "        \n",
+    "        # ---------- rendering logic -----------\n",
+    "        frame = vs.read()\n",
+    "        if frame is None:\n",
+    "            time.sleep(0.1)\n",
+    "            continue # wait for the stream\n",
+    "            \n",
+    "        c_time = time.time()\n",
+    "        fps.count()\n",
+    "        angles, text_infos = [], []\n",
+    "        \n",
+    "        if not cfg_exp['target_angle'] == 'random':\n",
+    "            phi = isl_factory.phi_initial\n",
+    "            angles = [pt.correct_angle(phi), pt.correct_angle(phi) + np.deg2rad(cfg_exp['phi_max'])]\n",
+    "        \n",
+    "        for i, ctrl in enumerate([vs, vw, pt, fps]):  # FPS indicators\n",
+    "            text_infos.append('%s: %.2f FPS' % (names[i], ctrl.get_avg_fps()))\n",
+    "        \n",
+    "        if len(islands) > 0:  # target island X, Y\n",
+    "            target = [i for i in islands if not i.is_distractor][0]\n",
+    "            text_infos.append('Target: %.3f %.3f' % (target.x, target.y))\n",
+    "\n",
+    "        text_infos.append('Time: %.2f' % float(c_time - t_start) if t_start is not None else 'Time: Idle')  # stopwatch\n",
+    "        text_infos.append('Trial: %.2f' % float(cfg_exp['trial_duration'] - (c_time - trial_start)) if phase == 1 else 'Trial: not started')\n",
+    "        text_infos.append('Trial: %s' % trial)\n",
+    "        text_infos.append('Rewards: %s' % rewards)  # rewards\n",
+    "        text_infos.append('In target: %.2f' % float(c_time - target_since) if target_since is not None else 'In target: --')\n",
+    "        text_infos.append('ITI distance: %.2f' % iti_distance if iti_distance > 0 else 'ITI distance: --')\n",
+    "\n",
+    "        frame, frame_to_save = dc.render(frame, status, islands=islands, angles=angles, text_infos=text_infos)\n",
+    "            \n",
+    "        # assign the frame back to the video stream for other controllers\n",
+    "        vs.frame_with_infos = frame_to_save\n",
+    "        \n",
+    "        cv2.imshow('Press (s)-to start/stop, (q)-to end', frame)\n",
+    "\n",
+    "        # -------- experiment logic ---------------\n",
+    "        \n",
+    "        if status.value == 2 and c_time - trial_start > cfg_exp['trial_duration']:\n",
+    "            log_event(c_time, 0, 0, 0, trial, 1) # log trial end\n",
+    "            trial += 1\n",
+    "            if int(sound.value) < cfg_exp['trial_number']:\n",
+    "                sound.value += 1\n",
+    "            trial_start = c_time\n",
+    "            log_event(c_time, 0, 0, 0, trial, 0) # log trial start\n",
+    "        \n",
+    "        # -------- key press events ---------------\n",
+    "        \n",
+    "        k = cv2.waitKey(33)\n",
+    "        \n",
+    "        # light on/off\n",
+    "        if k == ord('l'):\n",
+    "            switch_light(pt, board)\n",
+    "        \n",
+    "        # motors\n",
+    "        if k == ord('d'):\n",
+    "            if cfg['experiment']['enable_motors']:\n",
+    "                motor_board.switch_diodes()\n",
+    "         \n",
+    "        # ephys cable on top\n",
+    "        if k == ord('i') and 'cable_motor_port' in cfg['experiment']:\n",
+    "            tf = threading.Timer(0, cable_board.turn_CCW, args=[])\n",
+    "            tf.start()\n",
+    "        if k == ord('o') and 'cable_motor_port' in cfg['experiment']:\n",
+    "            tf = threading.Timer(0, cable_board.turn_CW, args=[])\n",
+    "            tf.start()\n",
+    "            \n",
+    "        # feeding\n",
+    "        if k == ord('f'):\n",
+    "            tf = threading.Timer(0, board.feed, args=[])\n",
+    "            tf.start()\n",
+    "\n",
+    "        if k == ord('c'):\n",
+    "            f_name = cfg_pos['background_light'] if pt.is_light else cfg_pos['background_dark']\n",
+    "            cv2.imwrite(f_name, dc.masked_frame)\n",
+    "            tf = threading.Timer(0.2, pt.reload_background, args=[])\n",
+    "            tf.start()            \n",
+    "\n",
+    "        # quit the session\n",
+    "        if k == ord('q'):\n",
+    "            break\n",
+    "\n",
+    "        # start the experiment\n",
+    "        if k == ord('s'):\n",
+    "            board.start_or_stop()  # start/stop data acquisition\n",
+    "            c_time = time.time()   # more accurate time\n",
+    "            \n",
+    "            if cfg['experiment']['enable_motors']:\n",
+    "                if not motor_board.diodes_on:\n",
+    "                    motor_board.switch_diodes()\n",
+    "\n",
+    "            if status.value == 1: # start the session\n",
+    "                if t_start is None:\n",
+    "                    t_start = c_time\n",
+    "                trial_start = c_time\n",
+    "                status.value = 2\n",
+    "                \n",
+    "                # init_new_trial\n",
+    "                sound.value = 1\n",
+    "                phase = 1\n",
+    "                trial += 1\n",
+    "\n",
+    "                # log trial start\n",
+    "                log_event(c_time, 0, 0, 0, trial, 0) # log trial start\n",
+    "                \n",
+    "            elif status.value == 2:  # pause the session\n",
+    "                if len(islands) > 0:\n",
+    "                    tgt = [i for i in islands if not i.is_distractor][0]\n",
+    "                    x, y, r = round(tgt.x, 4), round(tgt.y, 4), round(tgt.r, 4)\n",
+    "                else:\n",
+    "                    x, y, r = 0, 0, 0\n",
+    "                log_event(c_time, x, y, r, trial, -1)  # log experiment break\n",
+    "                \n",
+    "                if 'noise_when_idle' in cfg_exp and cfg_exp['noise_when_idle']:\n",
+    "                    sound.value = -1\n",
+    "                status.value = 1\n",
+    "                phase = 0\n",
+    "                islands = []\n",
+    "                for t in timers:\n",
+    "                    t.cancel()\n",
+    "                    \n",
+    "        if k == ord('a'):\n",
+    "            sound.value = -1 if sound.value >= 0 else 0\n",
+    "            switch_light(pt, board)  # turn lights off \n",
+    "\n",
+    "finally:\n",
+    "    if status.value == 2:  # stop data acquisition, in case exited via timeout\n",
+    "        board.start_or_stop()\n",
+    "        if len(islands) > 0:\n",
+    "            tgt = [i for i in islands if not i.is_distractor][0]\n",
+    "            x, y, r = round(tgt.x, 4), round(tgt.y, 4), round(tgt.r, 4)\n",
+    "        else:  \n",
+    "            x, y, r = 0, 0, 0\n",
+    "        log_event(c_time, x, y, r, trial, -1)  # log experiment end\n",
+    "        \n",
+    "    status.value = 0\n",
+    "    time.sleep(0.01)\n",
+    "    for t in timers:\n",
+    "        t.cancel()\n",
+    "    \n",
+    "    if board.is_light_off:\n",
+    "        board.switch_light()  # turn light back on\n",
+    "        time.sleep(0.1)\n",
+    "    board.exit()\n",
+    "    \n",
+    "    if cfg['experiment']['enable_motors']:\n",
+    "        if motor_board.diodes_on:\n",
+    "            motor_board.switch_diodes()\n",
+    "        motor_board.exit()\n",
+    "    if 'cable_motor_port' in cfg['experiment']:\n",
+    "        cable_board.exit()\n",
+    "    \n",
+    "    cv2.destroyAllWindows()\n",
+    "    sc.join()\n",
+    "    for ctrl in [pt, vw, vs]:\n",
+    "        ctrl.stop()\n",
+    "\n",
+    "    # stop microphones recording process\n",
+    "    if cfg['microphones']['record_audio']:\n",
+    "        mc.join()\n",
+    "\n",
+    "    if 'continuous' in cfg['sound']:\n",
+    "        time.sleep(cfg['sound']['continuous']['end_sleep'])\n",
+    "        cst.stop()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Merge data in HDF5 file"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 32,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "session_path = save_to\n",
+    "#trial = 5\n",
+    "#session_path = os.path.join('sessions', '2021-07-30_09-24-14')  # some particular session\n",
+    "#session_path = 'Y:\\\\Michael\\\\FreeBehaving\\\\SIT_sessions\\\\51_aSIT_2021-12-03_13-31-51'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 33,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "D:\\runSIT\\..\\pipeline\\postprocessing\\pack.py:59: UserWarning: loadtxt: Empty input file: \"sessions\\000000_hippoSIT_2023-07-11_09-32-45\\islands.csv\"\n",
+      "  data = np.loadtxt(filename, delimiter=',', skiprows=1)\n"
+     ]
+    },
+    {
+     "ename": "IndexError",
+     "evalue": "index 0 is out of bounds for axis 0 with size 0",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[1;31mIndexError\u001b[0m                                Traceback (most recent call last)",
+      "\u001b[1;32m<ipython-input-33-df6d003c4298>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m      3\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[1;31m# do pack data to HDF5\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[0mh5name\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mpack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msession_path\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      6\u001b[0m \u001b[0mtrial\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
+      "\u001b[1;32mD:\\runSIT\\..\\pipeline\\postprocessing\\pack.py\u001b[0m in \u001b[0;36mpack\u001b[1;34m(session_path)\u001b[0m\n\u001b[0;32m    165\u001b[0m         \u001b[1;31m# head direction\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    166\u001b[0m         \u001b[0mtemp_tl\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcolumn_stack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mpos_at_freq\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mx_smooth\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my_smooth\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspeed\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 167\u001b[1;33m         \u001b[0mhd\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mhead_direction\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtemp_tl\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m    168\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    169\u001b[0m         \u001b[1;31m# trial numbers\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
+      "\u001b[1;32mD:\\runSIT\\..\\pipeline\\postprocessing\\head_direction.py\u001b[0m in \u001b[0;36mhead_direction\u001b[1;34m(tl, hd_update_speed)\u001b[0m\n\u001b[0;32m     22\u001b[0m     \u001b[0mcrit\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwhere\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdiff\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0midle_idxs\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     23\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 24\u001b[1;33m     \u001b[0midle_periods\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[1;33m(\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m)\u001b[0m  \u001b[1;31m# first idle period\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     25\u001b[0m     \u001b[1;32mfor\u001b[0m \u001b[0mi\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mpoint\u001b[0m \u001b[1;32min\u001b[0m \u001b[0menumerate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m-\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     26\u001b[0m         \u001b[0midx_start\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mi\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
+      "\u001b[1;31mIndexError\u001b[0m: index 0 is out of bounds for axis 0 with size 0"
+     ]
+    }
+   ],
+   "source": [
+    "if not trial > 0:\n",
+    "    raise SystemExit('Nothing recorded. No sense to continue.')\n",
+    "\n",
+    "# do pack data to HDF5\n",
+    "h5name = pack(session_path)\n",
+    "trial = 0"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Plot sessions stats"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import h5py\n",
+    "import numpy as np\n",
+    "from scipy import signal"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "arena_r = 0.4  # in meters\n",
+    "\n",
+    "with h5py.File(h5name, 'r') as f:\n",
+    "    tl = np.array(f['processed']['timeline'])\n",
+    "    trial_idxs = np.array(f['processed']['trial_idxs'])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "fig = plt.figure(figsize=(12, 12))\n",
+    "\n",
+    "# trajectory and islands\n",
+    "ax = fig.add_subplot(221)\n",
+    "ax.scatter(tl[:, 1], tl[:, 2], s=1, alpha=0.1)  # positions\n",
+    "scat = ax.scatter(trial_idxs[:, 2], trial_idxs[:, 3], s=1000, facecolors='none', edgecolors='r')  # islands, radius approx.\n",
+    "ax.add_patch(plt.Circle((0, 0), arena_r, color='r', fill=False))\n",
+    "ax.set_aspect('equal')\n",
+    "ax.set_xlabel('X, m', fontsize=14)\n",
+    "ax.set_ylabel('Y, m', fontsize=14)\n",
+    "ax.set_title('Running', fontsize=14)\n",
+    "ax.grid()\n",
+    "\n",
+    "# occupancy\n",
+    "sigma = 0.1\n",
+    "lin_profile = np.linspace(-15, 15, 20)\n",
+    "bump = np.exp(-sigma * lin_profile**2)\n",
+    "bump /= np.trapz(bump)  # normalize to 1\n",
+    "kernel = bump[:, np.newaxis] * bump[np.newaxis, :]\n",
+    "occupancy_map, _, _ = np.histogram2d(tl[:, 1], tl[:, 2], bins=[40, 40], range=np.array([[-0.5, 0.5], [-0.5, 0.5]]))\n",
+    "occupancy_map = signal.convolve2d(occupancy_map, kernel, mode='same')\n",
+    "\n",
+    "ax = fig.add_subplot(222)\n",
+    "ax.imshow(occupancy_map.T, origin='lower', extent=(-0.5, 0.5, -0.5, 0.5), cmap='Blues')\n",
+    "ax.add_patch(plt.Circle((0, 0), arena_r, color='r', fill=False))\n",
+    "ax.set_xlabel('X, m', fontsize=14)\n",
+    "ax.set_title('Occupancy', fontsize=14)\n",
+    "ax.grid()\n",
+    "\n",
+    "# trials\n",
+    "durations = tl[trial_idxs[:, 1].astype(int)][:, 0] - tl[trial_idxs[:, 0].astype(int)][:, 0]\n",
+    "colors = ['red' if x == 1 else 'grey' for x in trial_idxs[:, 5]]\n",
+    "\n",
+    "ax = fig.add_subplot(223)\n",
+    "ax.barh(np.arange(len(trial_idxs)), durations, color=colors, align='center')\n",
+    "ax.set_xlabel('Time, s', fontsize=14)\n",
+    "ax.set_ylabel('Trial, #', fontsize=14)\n",
+    "ax.set_title('Trials', fontsize=14)\n",
+    "\n",
+    "# speed\n",
+    "ax = fig.add_subplot(224)\n",
+    "\n",
+    "s_rate = 100  # Hz\n",
+    "window = 60   # secs\n",
+    "step = 10     # secs\n",
+    "duration = tl[-1][0]\n",
+    "x_vals = np.arange(int(duration/step))\n",
+    "\n",
+    "inst_speed = [tl[x*step*s_rate:(x*step + window)*s_rate][:, 3].mean() for x in x_vals]\n",
+    "ax.plot(x_vals*step, inst_speed)\n",
+    "ax.set_ylabel('Speed, m/s', fontsize=14)\n",
+    "ax.set_xlabel('Time, s', fontsize=14)\n",
+    "ax.set_title('Speed', fontsize=14)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Performance"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [],
+   "source": [
+    "h5name = os.path.join(session_path, experiment_id + '.h5')\n",
+    "jsname = os.path.join(session_path, experiment_id + '.json')\n",
+    "\n",
+    "# loading position and trial data\n",
+    "with h5py.File(h5name, 'r') as f:\n",
+    "    tl = np.array(f['processed']['timeline'])  # time, X, Y, speed\n",
+    "    trial_idxs = np.array(f['processed']['trial_idxs'])  # idx start, idx end, X, Y, R, trial result (idx to tl)\n",
+    "\n",
+    "# loading session configuration\n",
+    "with open(jsname, 'r') as f:\n",
+    "    cfg = json.load(f)\n",
+    "\n",
+    "timepoints = cfg['experiment']['timepoints']\n",
+    "s_duration = cfg['experiment']['session_duration']\n",
+    "if len(timepoints) > 0:\n",
+    "    periods = [[0, s_duration], [0, timepoints[0]], [timepoints[1], timepoints[2]], [timepoints[3], s_duration]]\n",
+    "else:\n",
+    "    periods = [[0, s_duration]]\n",
+    "\n",
+    "# separate ALL, L, D, L'\n",
+    "ds_names = ['performance_ALL', 'performance_L', 'performance_D', 'performance_Lp']\n",
+    "ds_names = ['performance_ALL']  # only light\n",
+    "\n",
+    "for i, ds_name in enumerate(ds_names):\n",
+    "    t_start, t_end = periods[i]\n",
+    "    trial_starts = tl[trial_idxs[:, 0].astype(np.int32)][:, 0]\n",
+    "    trial_ends   = tl[trial_idxs[:, 1].astype(np.int32)][:, 0]\n",
+    "    tr_idxs = trial_idxs[(trial_starts >= t_start) & (trial_ends <= t_end)]\n",
+    "\n",
+    "    dataset = calculate_performance(tl, tr_idxs, cfg)\n",
+    "    dump_performance_to_H5(h5name, ds_name, dataset)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fig = plt.figure(figsize=(4, 4))\n",
+    "\n",
+    "with h5py.File(h5name, 'r') as f:\n",
+    "    perf = np.array(f['analysis']['performance_ALL'])\n",
+    "    x = perf[:, 6]\n",
+    "\n",
+    "ax = fig.add_subplot(111)\n",
+    "\n",
+    "ax.plot(x, perf[:, 0])  # performance\n",
+    "ax.plot(x, perf[:, 3])  # chance\n",
+    "ax.fill_between(x, perf[:, 0] + perf[:, 1], perf[:, 0] + perf[:, 2], alpha=0.4)\n",
+    "ax.fill_between(x, perf[:, 3] + perf[:, 4], perf[:, 3] + perf[:, 5], alpha=0.4)\n",
+    "ax.set_ylim(0, 110)\n",
+    "ax.set_xlim(0, 65)\n",
+    "ax.grid()\n",
+    "ax.set_title(experiment_id[-19:], fontsize=14)\n",
+    "ax.set_xlabel('Time, s', fontsize=14)\n",
+    "\n",
+    "if i == 0:\n",
+    "    ax.set_ylabel('Successful trials, %', fontsize=14)\n",
+    "        \n",
+    "fig.tight_layout()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "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.8.8"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}

+ 66 - 13
profiles/default.json

@@ -10,15 +10,21 @@
     "video": {
         "fps": 30,
         "file_path": "video.avi",
+        "frame_attr_name": "frame_with_infos",
         "save_contours": false,
         "csv_path": "video.csv"
     },
     "microphones": {
         "record_audio": false,
         "sample_rate": 192000,
-        "device": 58,
+        "device": "ASIO Fireface USB",
         "number_channels": 4,
-        "channel_selectors": [8,9,10,11],
+        "channel_selectors": [
+            8,
+            9,
+            10,
+            11
+        ],
         "file_path": "audio.mat5",
         "csv_path": "microphones.csv"
     },
@@ -45,17 +51,56 @@
         "flip_y": false
     },
     "sound": {
-        "device": [1, 26],
-        "n_channels": 10,
+        "device": "ASIO Fireface USB",
+        "n_channels": 16,
         "sounds": {
-            "noise": {"amp": 0.2, "channels": [6, 8]},
-            "background": {"freq": 660, "amp": 0.05, "duration": 0.05, "harmonics": true, "channels": [1, 8]},
-            "target": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": true, "channels": [2, 8]}, 
-            "distractor1": {"freq": 660, "amp": 0.05, "duration": 0.05, "harmonics": true, "channels": [3, 8], "enabled": true},
-            "distractor2": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": true, "channels": [2, 8], "enabled": false}
+            "noise": {
+                "amp": 0.2,
+                "channels": [
+                    6
+                ]
+            },
+            "background": {
+                "freq": 660,
+                "amp": 0.05,
+                "duration": 0.05,
+                "harmonics": true,
+                "channels": [
+                    5
+                ]
+            },
+            "target": {
+                "freq": 660,
+                "amp": 0.1,
+                "duration": 0.05,
+                "harmonics": true,
+                "channels": [
+                    4
+                ]
+            },
+            "distractor1": {
+                "freq": 660,
+                "amp": 0.05,
+                "duration": 0.05,
+                "harmonics": true,
+                "channels": [
+                    5
+                ],
+                "enabled": true
+            },
+            "distractor2": {
+                "freq": 660,
+                "amp": 0.1,
+                "duration": 0.05,
+                "harmonics": true,
+                "channels": [
+                    4
+                ],
+                "enabled": false
+            }
         },
         "pulse_duration": 0.05,
-        "sample_rate": 44100,
+        "sample_rate": 192000,
         "latency": 0.25,
         "volume": 0.7,
         "roving": 5.0,
@@ -73,9 +118,17 @@
         "experiment_type": "hippoSIT",
         "MCSArduinoPort": "COM10",
         "file_path": "events.csv",
-        "light_events": [9600, 92400],
+        "light_events": [
+            9600,
+            92400
+        ],
         "phi_max": 45,
-        "timepoints": [9600, 91200, 91800, 92400],
+        "timepoints": [
+            9600,
+            91200,
+            91800,
+            92400
+        ],
         "iti_distance": 2.0,
         "iti_duration": 20,
         "punishment_duration": 10,
@@ -84,4 +137,4 @@
         "enable_motors": false,
         "n_pellets": 1
     }
-}
+}