From 6313314f313ec26f88a1b31d506d50d6ee9bf606 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Wed, 22 Nov 2023 17:46:27 +0000 Subject: [PATCH 01/15] Update scale.py --- iblrig/scale.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/iblrig/scale.py b/iblrig/scale.py index f6f631a8e..a1dd321be 100644 --- a/iblrig/scale.py +++ b/iblrig/scale.py @@ -12,16 +12,16 @@ def __init__(self, *args, **kwargs): self.assert_setting('1M') self.assert_setting('0FMT') - def assert_setting(self, query: str, response: str = 'OK!') -> None: - assert self.query_line(query) == response + def assert_setting(self, query: str, expected_response: str = 'OK!') -> None: + assert self.query_line(query) == expected_response def query_line(self, query: str) -> str: self.reset_input_buffer() self.write(query + '\r\n') return self.readline().strip().decode() - def zero(self) -> bool: - return self.assert_setting('Z') + def zero(self) -> None: + self.assert_setting('Z') @property def version(self) -> str: From d15bb60065a684ff0c97144a5164be6325190bc0 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Thu, 23 Nov 2023 12:51:49 +0000 Subject: [PATCH 02/15] revert visibility of Bonsai editor during session --- CHANGELOG.md | 4 ++++ iblrig/base_tasks.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 444122fe4..884c11852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog --------- +8.12.10 +------- +* revert visibility of Bonsai editor during session (c.f. 8.12.7) + 8.12.9 ------ * usability improvements for "Show Training Level" tool diff --git a/iblrig/base_tasks.py b/iblrig/base_tasks.py index 3cea25bc5..4c0df43b4 100644 --- a/iblrig/base_tasks.py +++ b/iblrig/base_tasks.py @@ -586,7 +586,7 @@ def trigger_bonsai_cameras(self): cmd = [ str(self.paths.BONSAI), str(workflow_file), - '--no-editor', + '--start', # '--no-editor', f"-p:FileNameLeft={self.paths.SESSION_FOLDER / 'raw_video_data' / '_iblrig_leftCamera.raw.avi'}", f"-p:FileNameLeftData={self.paths.SESSION_FOLDER / 'raw_video_data' / '_iblrig_leftCamera.frameData.bin'}", f"-p:FileNameMic={self.paths.SESSION_RAW_DATA_FOLDER / '_iblrig_micData.raw.wav'}", From 4c8cae79195e88e9ede9849d3ce866de7a031e6e Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Fri, 24 Nov 2023 11:39:42 +0000 Subject: [PATCH 03/15] use setup_logger exclusively --- iblrig/alyx.py | 5 ++--- iblrig/base_choice_world.py | 2 +- iblrig/frame2TTL.py | 5 +++-- iblrig/hardware.py | 5 ++--- iblrig/misc.py | 5 +++-- iblrig/path_helper.py | 5 ++--- iblrig/raw_data_loaders.py | 5 +++-- iblrig/sound.py | 5 ++--- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/iblrig/alyx.py b/iblrig/alyx.py index 4dd64c5bf..93a68f4c1 100644 --- a/iblrig/alyx.py +++ b/iblrig/alyx.py @@ -1,9 +1,8 @@ -import logging - import iblrig +from iblutil.util import setup_logger from one.registration import RegistrationClient -log = logging.getLogger('iblrig') +log = setup_logger('iblrig') def register_session(session_path, settings_dict, one=None): diff --git a/iblrig/base_choice_world.py b/iblrig/base_choice_world.py index cff78ee0c..c24f8737d 100644 --- a/iblrig/base_choice_world.py +++ b/iblrig/base_choice_world.py @@ -25,7 +25,7 @@ from pybpodapi.com.messaging.trial import Trial from pybpodapi.protocol import StateMachine -log = setup_logger(__name__) +log = setup_logger('iblrig') NTRIALS_INIT = 2000 NBLOCKS_INIT = 100 diff --git a/iblrig/frame2TTL.py b/iblrig/frame2TTL.py index e69bc7276..283786b5d 100644 --- a/iblrig/frame2TTL.py +++ b/iblrig/frame2TTL.py @@ -3,14 +3,15 @@ # @Author: Niccolo' Bonacchi (@nbonacchi) # @Date: Friday, November 5th 2021, 12:47:34 pm # @Creation_Date: 2018-06-08 11:04:05 -import logging import struct import time import numpy as np import serial -log = logging.getLogger('iblrig') +from iblutil.util import setup_logger + +log = setup_logger('iblrig') def frame2ttl_factory(serial_port: str, version: int = 2): diff --git a/iblrig/hardware.py b/iblrig/hardware.py index 1ef5522cc..9989ff13e 100644 --- a/iblrig/hardware.py +++ b/iblrig/hardware.py @@ -1,7 +1,6 @@ """ This modules contains hardware classes used to interact with modules. """ -import logging import struct import threading import time @@ -12,14 +11,14 @@ import sounddevice as sd from iblrig.tools import static_vars -from iblutil.util import Bunch +from iblutil.util import Bunch, setup_logger from pybpod_rotaryencoder_module.module import RotaryEncoder from pybpod_rotaryencoder_module.module_api import RotaryEncoderModule from pybpodapi.bpod.bpod_io import BpodIO SOFTCODE = IntEnum('SOFTCODE', ['STOP_SOUND', 'PLAY_TONE', 'PLAY_NOISE', 'TRIGGER_CAMERA']) -log = logging.getLogger(__name__) +log = setup_logger('iblrig') class Bpod(BpodIO): diff --git a/iblrig/misc.py b/iblrig/misc.py index 1f3cbaffc..be9299b7a 100644 --- a/iblrig/misc.py +++ b/iblrig/misc.py @@ -7,12 +7,13 @@ """ import argparse import datetime -import logging from pathlib import Path from typing import Literal import numpy as np +from iblutil.util import setup_logger + FLAG_FILE_NAMES = [ 'transfer_me.flag', 'create_me.flag', @@ -20,7 +21,7 @@ 'passive_data_for_ephys.flag', ] -log = logging.getLogger('iblrig') +log = setup_logger('iblrig') def _get_task_argument_parser(parents=None): diff --git a/iblrig/path_helper.py b/iblrig/path_helper.py index 79314eedf..382e7ad36 100644 --- a/iblrig/path_helper.py +++ b/iblrig/path_helper.py @@ -1,7 +1,6 @@ """ Various get functions to return paths of folders and network drives """ -import logging import os import re import subprocess @@ -14,9 +13,9 @@ import iblrig from ibllib.io import session_params from ibllib.io.raw_data_loaders import load_settings -from iblutil.util import Bunch +from iblutil.util import Bunch, setup_logger -log = logging.getLogger('iblrig') +log = setup_logger('iblrig') def iterate_previous_sessions(subject_name, task_name, n=1, **kwargs): diff --git a/iblrig/raw_data_loaders.py b/iblrig/raw_data_loaders.py index af0790b05..0e4c097ea 100644 --- a/iblrig/raw_data_loaders.py +++ b/iblrig/raw_data_loaders.py @@ -1,10 +1,11 @@ import json -import logging from typing import Any import pandas as pd -log = logging.getLogger('iblrig') +from iblutil.util import setup_logger + +log = setup_logger('iblrig') def load_task_jsonable(jsonable_file: str, offset: int | None = None) -> tuple[pd.DataFrame, list[Any]]: diff --git a/iblrig/sound.py b/iblrig/sound.py index cf07ad168..b6748ae23 100644 --- a/iblrig/sound.py +++ b/iblrig/sound.py @@ -1,11 +1,10 @@ -import logging - import numpy as np from scipy.signal import chirp +from iblutil.util import setup_logger from pybpod_soundcard_module.module_api import DataType, SampleRate, SoundCardModule -log = logging.getLogger('iblrig') +log = setup_logger('iblrig') def make_sound(rate=44100, frequency=5000, duration=0.1, amplitude=1, fade=0.01, chans='L+TTL'): From 6cefdc20d55b2fe590610cf5ef8bdef9f8d3825e Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Fri, 24 Nov 2023 17:27:35 +0000 Subject: [PATCH 04/15] Create hardware_tests.py --- iblrig/hardware_tests.py | 136 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 iblrig/hardware_tests.py diff --git a/iblrig/hardware_tests.py b/iblrig/hardware_tests.py new file mode 100644 index 000000000..51dfaff2f --- /dev/null +++ b/iblrig/hardware_tests.py @@ -0,0 +1,136 @@ +import logging +import re +from abc import ABC, abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Literal + +import yaml +from serial import Serial, SerialException +from serial.tools import list_ports +from serial_singleton import SerialSingleton, filter_ports + +from iblrig.constants import BASE_DIR +from iblutil.util import setup_logger + +_file_settings = Path(BASE_DIR).joinpath('settings', 'hardware_settings.yaml') +_hardware_settings = yaml.safe_load(_file_settings.read_text()) + +log = setup_logger('iblrig', level='DEBUG') + + +@dataclass +class TestResult: + status: Literal['PASS', 'INFO', 'FAIL'] = 'FAIL' + message: str = '' + ext_message: str = '' + solution: str = '' + url: str = '' + exception: Exception | None = None + + +class TestHardwareException(Exception): + def __init__(self, results: TestResult): + super().__init__(results.message) + self.results = results + + +class TestHardware(ABC): + log_results: bool = True + raise_fail_as_exception: bool = False + + def __init__(self): + self.last_result = self.run() + + @abstractmethod + def run(self): + ... + + def process(self, results: TestResult) -> None: + if self.log_results: + match results.status: + case 'PASS': + log_level = logging.INFO + log_symbol = '✔' + case 'INFO': + log_level = logging.INFO + log_symbol = 'i' + case 'WARN': + log_level = logging.WARNING + log_symbol = '!' + case 'FAIL': + log_level = logging.CRITICAL + log_symbol = '✘' + case _: + log_level = 'critical' + log_symbol = '?' + log.log(log_level, f' {log_symbol} {results.message}.') + + if self.raise_fail_as_exception and results.status == 'FAIL': + if results.exception is not None: + raise TestHardwareException(results) from results.exception + else: + raise TestHardwareException(results) + + +class TestHardwareDevice(TestHardware): + device_name: str + + @abstractmethod + def run(self): + ... + + def __init__(self): + if self.log_results: + log.info(f'Running hardware tests for {self.device_name}:') + super().__init__() + + +class TestSerialDevice(TestHardwareDevice): + port: str + port_properties: None | dict[str, Any] + serial_queries: None | dict[tuple[object, int], bytes] + + def run(self) -> TestResult: + if self.port is None: + result = TestResult('FAIL', f'No serial port defined for {self.device_name}') + elif next((p for p in list_ports.comports() if p.device == self.port), None) is None: + result = TestResult('FAIL', f'`{self.port}` is not a valid serial port') + else: + try: + Serial(self.port, timeout=1).close() + except SerialException as e: + result = TestResult('FAIL', f'`{self.port}` cannot be connected to', exception=e) + else: + result = TestResult('PASS', f'`{self.port}` is a valid serial port that can be connected to') + self.process(result) + + # first, test for properties of the serial port without opening the latter (VID, PID, etc) + passed = self.port in filter_ports(**self.port_properties) if self.port_properties is not None else False + + # query the devices for characteristic responses + if passed and self.serial_queries is not None: + with SerialSingleton(self.port, timeout=1) as ser: + for query, regex_pattern in self.serial_queries.items(): + return_string = ser.query(*query) + ser.flush() + if not (passed := bool(re.search(regex_pattern, return_string))): + break + + if passed: + result = TestResult('PASS', f'Device on `{self.port}` does in fact seem to be a {self.device_name}') + else: + result = TestResult('FAIL', f'Device on `{self.port}` does NOT seem to be a {self.device_name}') + self.process(result) + + return result + + +class TestRotaryEncoder(TestSerialDevice): + device_name = 'Rotary Encoder Module' + port = _hardware_settings['device_rotary_encoder']['COM_ROTARY_ENCODER'] + port_properties = {'vid': 0x16C0} + serial_queries = {(b'Q', 2): b'^..$', (b'P00', 1): b'\x01'} + + def run(self): + super().run() From 6cb8bce0cf046b7dcaf463ecfaeabb7f220013df Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Fri, 24 Nov 2023 17:28:09 +0000 Subject: [PATCH 05/15] Update pyproject.toml --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3d71430df..faf0f8329 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ dependencies = [ [project.optional-dependencies] DEV = [ "coverage[toml]", - "coveralls", "flake8", "mypy", "myst-parser", @@ -45,6 +44,8 @@ DEV = [ "sphinx-autobuild", "sphinx_lesson", "sphinx_rtd_theme", + "types-PyYAML", + "types-requests", ] [project.scripts] From f7bb8015088118a46600beb86a487f6311d65106 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Mon, 27 Nov 2023 17:19:39 +0000 Subject: [PATCH 06/15] add documentation for positioning/resizing bonsai visualizers --- docs/source/faq.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/source/faq.rst b/docs/source/faq.rst index c0710aa53..378c774b3 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -47,11 +47,27 @@ Sound Issues Screen Issues ============= +General +^^^^^^^ + * The ribbon cable attaching the screen to the driver board is notoriously finicky. If you are having brightness issues or do not have a signal, try gently repositioning this cable and ensure it is tightly seated in its connection. * Screen and ribbon cable can be easily damaged. It is useful to have backup at hand. +* Screen flashing can occur if the power supply does not match the screen specifications. Use a 12V adapter with at least 1A. * If the Bonsai display is appearing on the PC screen when a task starts, try unplugging the rig screen, rebooting and plugging the screen back in. Other variations of screen unplugging and rebooting may also work. Also make sure, that the ``DISPLAY_IDX`` value in ``hardware_settings.yaml`` is set correctly. -* Screen flashing can occur if the power supply does not match the screen specifications. Use a 12V adapter with at least 1A. + +Define Default Position & Size of Bonsai Visualizers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Is the preview window of the video recording showing on the iPad screen instead of the computer's main display? To +redefine the default position and size of the Bonsai visualizer used for displaying the video recording during a session: + +1. Execute `C:\iblrigv8\Bonsai\Bonsai.exe` +2. Open the respective Bonsai workflow (``C:\iblrigv8\devices\camera_recordings\TrainingRig_SaveVideo_TrainingTasks.bonsai``) +3. Start the workflow +4. Position / resize the windows as to your preference +5. Stop the workflow +6. Save the workflow Frame2TTL From 258df1a6dd903b61f7b9ed7ef751ce4d24b7ce2d Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Mon, 27 Nov 2023 17:28:38 +0000 Subject: [PATCH 07/15] Update faq.rst --- docs/source/faq.rst | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 378c774b3..7572276ad 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -59,15 +59,20 @@ General Define Default Position & Size of Bonsai Visualizers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Is the preview window of the video recording showing on the iPad screen instead of the computer's main display? To -redefine the default position and size of the Bonsai visualizer used for displaying the video recording during a session: - -1. Execute `C:\iblrigv8\Bonsai\Bonsai.exe` -2. Open the respective Bonsai workflow (``C:\iblrigv8\devices\camera_recordings\TrainingRig_SaveVideo_TrainingTasks.bonsai``) -3. Start the workflow -4. Position / resize the windows as to your preference -5. Stop the workflow -6. Save the workflow +Is the preview window of the video recording showing on the iPad screen instead of the computer's main display during a +session? To redefine the default position and size of the Bonsai visualizer: + +#. Open the Bonsai executable distributed with IBLRIG: ``C:\iblrigv8\Bonsai\Bonsai.exe``. +#. Open the respective Bonsai workflow: + + .. code:: + + C:\iblrigv8\devices\camera_recordings\TrainingRig_SaveVideo_TrainingTasks.bonsai + +#. Start the workflow by clicking on the play-button. +#. Adjust the position and size of the windows as per your preference. +#. Stop the workflow. +#. Save the workflow. Frame2TTL From 94625a8de65cbe04d6b5065f52cc5bdd11cd86b9 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Tue, 28 Nov 2023 23:34:05 +0000 Subject: [PATCH 08/15] add restart_com_port() --- iblrig/hardware.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/iblrig/hardware.py b/iblrig/hardware.py index 9989ff13e..55be008a8 100644 --- a/iblrig/hardware.py +++ b/iblrig/hardware.py @@ -1,14 +1,20 @@ """ This modules contains hardware classes used to interact with modules. """ +import os +import re +import shutil import struct +import subprocess import threading import time from enum import IntEnum +from pathlib import Path import numpy as np import serial import sounddevice as sd +from serial.tools import list_ports from iblrig.tools import static_vars from iblutil.util import Bunch, setup_logger @@ -269,3 +275,52 @@ def sound_device_factory(output='sysdefault', samplerate=None): else: raise ValueError(f'{output} soundcard is neither xonar, harp or sysdefault. Fix your hardware_settings.yam') return sd, samplerate, channels + + +def restart_com_port(regexp: str) -> bool: + """ + Restart the communication port(s) matching the specified regular expression. + + Parameters + ---------- + regexp : str + A regular expression used to match the communication port(s). + + Returns + ------- + bool + Returns True if all matched ports are successfully restarted, False otherwise. + + Raises + ------ + NotImplementedError + If the operating system is not Windows. + + FileNotFoundError + If the required 'pnputil.exe' executable is not found. + + Examples + -------- + >>> restart_com_port("COM3") # Restart the communication port with serial number 'COM3' + True + + >>> restart_com_port("COM[1-3]") # Restart communication ports with serial numbers 'COM1', 'COM2', 'COM3' + True + """ + if not os.name == 'nt': + raise NotImplementedError('Only implemented for Windows OS.') + if not (file_pnputil := Path(shutil.which('pnputil'))).exists(): + raise FileNotFoundError('Could not find pnputil.exe') + result = [] + for port in list_ports.grep(regexp): + pnputil_output = subprocess.check_output([file_pnputil, '/enum-devices', '/connected', '/class', 'ports']) + instance_id = re.search(rf'(\S*{port.serial_number}\S*)', pnputil_output.decode()) + if instance_id is None: + continue + result.append( + subprocess.check_call( + [file_pnputil, '/restart-device', f'"{instance_id.group}"'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + == 0 + ) + return all(result) From 7062fdf50fd8762fbec93072280db3d8ceb1fbff Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Wed, 29 Nov 2023 09:53:27 +0000 Subject: [PATCH 09/15] bonsai layout from template --- .gitignore | 3 +++ CHANGELOG.md | 2 +- ...g_SaveVideo_TrainingTasks.bonsai.layout_template} | 0 ...sai.layout => setup_video.bonsai.layout_template} | 0 iblrig/base_tasks.py | 12 ++++++++++++ 5 files changed, 16 insertions(+), 1 deletion(-) rename devices/camera_recordings/{TrainingRig_SaveVideo_TrainingTasks.bonsai.layout => TrainingRig_SaveVideo_TrainingTasks.bonsai.layout_template} (100%) rename devices/camera_setup/{setup_video.bonsai.layout => setup_video.bonsai.layout_template} (100%) diff --git a/.gitignore b/.gitignore index 7bac68d02..a0ccef16b 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ iblrig/_version.py .coverage* coverage.xml + +devices/camera_setup/*.layout +devices/camera_recordings/*.layout diff --git a/CHANGELOG.md b/CHANGELOG.md index 884c11852..b44323f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Changelog 8.12.10 ------- -* revert visibility of Bonsai editor during session (c.f. 8.12.7) +* ignore user-side changes to bonsai layouts (for camera workflows only) 8.12.9 ------ diff --git a/devices/camera_recordings/TrainingRig_SaveVideo_TrainingTasks.bonsai.layout b/devices/camera_recordings/TrainingRig_SaveVideo_TrainingTasks.bonsai.layout_template similarity index 100% rename from devices/camera_recordings/TrainingRig_SaveVideo_TrainingTasks.bonsai.layout rename to devices/camera_recordings/TrainingRig_SaveVideo_TrainingTasks.bonsai.layout_template diff --git a/devices/camera_setup/setup_video.bonsai.layout b/devices/camera_setup/setup_video.bonsai.layout_template similarity index 100% rename from devices/camera_setup/setup_video.bonsai.layout rename to devices/camera_setup/setup_video.bonsai.layout_template diff --git a/iblrig/base_tasks.py b/iblrig/base_tasks.py index 4c0df43b4..510608273 100644 --- a/iblrig/base_tasks.py +++ b/iblrig/base_tasks.py @@ -9,6 +9,7 @@ import inspect import json import os +import shutil import signal import subprocess import time @@ -570,6 +571,7 @@ def start_mixin_bonsai_cameras(self): # cam.TriggerMode.SetValue(True) bonsai_camera_file = self.paths.IBLRIG_FOLDER.joinpath('devices', 'camera_setup', 'setup_video.bonsai') + self.create_bonsai_layout_from_template(bonsai_camera_file) # this locks until Bonsai closes cmd = [str(self.paths.BONSAI), str(bonsai_camera_file), '--start-no-debug', '--no-boot'] self.logger.info('starting Bonsai microphone recording') @@ -577,12 +579,22 @@ def start_mixin_bonsai_cameras(self): subprocess.call(cmd, cwd=bonsai_camera_file.parent) self.logger.info('Bonsai cameras setup module loaded: OK') + def create_bonsai_layout_from_template(self, workflow_file: Path) -> Path: + if not (layout_file := workflow_file.with_suffix('.bonsai.layout')).exists(): + self.logger.info(f'creating default {layout_file.name}') + template_file = workflow_file.with_suffix('.bonsai.layout_template') + if not template_file.exists(): + FileNotFoundError(template_file) + shutil.copy(template_file, layout_file) + return template_file + def trigger_bonsai_cameras(self): workflow_file = self._camera_mixin_bonsai_get_workflow_file(self.hardware_settings.get('device_cameras', None)) if workflow_file is None: return self.logger.info('attempt to launch Bonsai camera recording') workflow_file = self.paths.IBLRIG_FOLDER.joinpath(*workflow_file.split('/')) + self.create_bonsai_layout_from_template(workflow_file) cmd = [ str(self.paths.BONSAI), str(workflow_file), From e6795e92511925c20db56b51ec7bec0a37e08af5 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Wed, 29 Nov 2023 12:10:29 +0000 Subject: [PATCH 10/15] Create disable_bpod_led.ps1 --- scripts/disable_bpod_led.ps1 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 scripts/disable_bpod_led.ps1 diff --git a/scripts/disable_bpod_led.ps1 b/scripts/disable_bpod_led.ps1 new file mode 100644 index 000000000..7c4d42963 --- /dev/null +++ b/scripts/disable_bpod_led.ps1 @@ -0,0 +1,20 @@ +$portstring = $args[0] + +# open connection +$port = new-Object System.IO.Ports.SerialPort($portstring, 9600, 'None', 8, 'one') +$port.Open() + +# handshake +$port.Write('6') +if ($port.ReadByte() -ne 53) { + exit 1 +} + +# disable led +Start-Sleep -m 200 +$port.Write(':') +$port.Write([byte[]] (0), 0, 1) +Start-Sleep -m 200 + +# close connection +$port.Close() From 03c5d9dc80bcaa6085afc42b43e9c92438f6be1a Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Wed, 29 Nov 2023 12:12:07 +0000 Subject: [PATCH 11/15] Update faq.rst --- docs/source/faq.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 7572276ad..4ecf6b84f 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -56,8 +56,8 @@ General * If the Bonsai display is appearing on the PC screen when a task starts, try unplugging the rig screen, rebooting and plugging the screen back in. Other variations of screen unplugging and rebooting may also work. Also make sure, that the ``DISPLAY_IDX`` value in ``hardware_settings.yaml`` is set correctly. -Define Default Position & Size of Bonsai Visualizers -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Defining Default Position & Size of Bonsai Visualizers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Is the preview window of the video recording showing on the iPad screen instead of the computer's main display during a session? To redefine the default position and size of the Bonsai visualizer: From f47b56a4e4b543b930a107359b9e2a45c46c2fac Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Wed, 29 Nov 2023 12:36:54 +0000 Subject: [PATCH 12/15] linting / formatting --- devices/F2TTL/ArCOM_F2TTL.py | 67 ++++---- devices/F2TTL/Frame2TTLv2.py | 60 +++---- devices/F2TTL/Frame2TTLv2_Demo.py | 2 +- .../wrkflws/calibration_plots.py | 94 +++++------ .../wrkflws/photodiode_server.py | 27 ++-- iblrig/gui/wizard.py | 8 +- iblrig/online_plots.py | 11 +- .../_iblrig_tasks_ImagingChoiceWorld/task.py | 4 +- .../_iblrig_tasks_advancedChoiceWorld/task.py | 98 ++++++++---- .../_iblrig_tasks_biasedChoiceWorld/task.py | 4 +- .../_iblrig_tasks_ephysChoiceWorld/task.py | 27 ++-- .../task.py | 4 +- .../task.py | 149 +++++++++--------- .../task_validation.py | 24 +-- .../_iblrig_tasks_passiveChoiceWorld/task.py | 26 +-- .../_iblrig_tasks_spontaneous/task.py | 6 +- .../_iblrig_tasks_trainingChoiceWorld/task.py | 39 +++-- .../task.py | 38 +++-- pyproject.toml | 5 +- scripts/hardware_validation/frame2ttlv2.py | 9 +- scripts/hardware_validation/harp.py | 24 +-- .../hardware_validation/identify_hardware.py | 32 ++-- .../verify_camera_trigger.py | 29 ++-- .../hardware_validation/verify_hardware.py | 49 +++--- scripts/ibllib/purge_rig_data.py | 8 +- scripts/ibllib/register_session.py | 10 +- scripts/ibllib/screen_stimulus_from_wheel.py | 29 ++-- 27 files changed, 481 insertions(+), 402 deletions(-) diff --git a/devices/F2TTL/ArCOM_F2TTL.py b/devices/F2TTL/ArCOM_F2TTL.py index 9a96e249a..7f61b7ae1 100644 --- a/devices/F2TTL/ArCOM_F2TTL.py +++ b/devices/F2TTL/ArCOM_F2TTL.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding:utf-8 -*- # @File: F2TTL\ARCOM.py # @Author: Niccolo' Bonacchi (@nbonacchi) # @Date: Tuesday, December 7th 2021, 12:01:15 pm @@ -26,21 +25,21 @@ import serial -class ArCOM(object): +class ArCOM: def __init__(self, serialPortName, baudRate): self.serialObject = 0 self.typeNames = ( - "uint8", - "int8", - "char", - "uint16", - "int16", - "uint32", - "int32", - "single", + 'uint8', + 'int8', + 'char', + 'uint16', + 'int16', + 'uint32', + 'int32', + 'single', ) self.typeBytes = (1, 1, 1, 2, 2, 4, 4) - self.typeSymbols = ("B", "b", "c", "H", "h", "L", "l") + self.typeSymbols = ('B', 'b', 'c', 'H', 'h', 'L', 'l') self.serialObject = serial.Serial(serialPortName, timeout=10, rtscts=True) def open(self, serialPortName, baudRate): @@ -60,14 +59,14 @@ def write(self, *arg): """ nTypes = int(len(arg) / 2) argPos = 0 - messageBytes = b"" + messageBytes = b'' for i in range(0, nTypes): data = arg[argPos] argPos += 1 datatype = arg[argPos] argPos += 1 # not needed if datatype not in self.typeNames: - raise ArCOMError("Error: " + datatype + " is not a data type supported by ArCOM.") + raise ArCOMError('Error: ' + datatype + ' is not a data type supported by ArCOM.') # datatypePos = self.typeNames.index(datatype) # Not used? if type(data).__module__ == np.__name__: @@ -86,7 +85,7 @@ def read(self, *arg): # Read an array of values argPos += 1 datatype = arg[argPos] if (datatype in self.typeNames) is False: - raise ArCOMError("Error: " + datatype + " is not a data type supported by ArCOM.") + raise ArCOMError('Error: ' + datatype + ' is not a data type supported by ArCOM.') argPos += 1 typeIndex = self.typeNames.index(datatype) byteWidth = self.typeBytes[typeIndex] @@ -95,11 +94,7 @@ def read(self, *arg): # Read an array of values nBytesRead = len(messageBytes) if nBytesRead < nBytes2Read: raise ArCOMError( - "Error: serial port timed out. " - + str(nBytesRead) - + " bytes read. Expected " - + str(nBytes2Read) - + " byte(s)." + 'Error: serial port timed out. ' + str(nBytesRead) + ' bytes read. Expected ' + str(nBytes2Read) + ' byte(s).' ) thisOutput = np.frombuffer(messageBytes, datatype) outputs.append(thisOutput) @@ -116,45 +111,45 @@ class ArCOMError(Exception): pass -if __name__ == "__main__": +if __name__ == '__main__': import struct - port = "/dev/ttyACM3" + port = '/dev/ttyACM3' nsamples = 6 # Hello ser ser = serial.Serial(port, 115200, timeout=1) - ser.write(b"C") - print(int.from_bytes(ser.read(1), byteorder="little", signed=False)) - ser.write(struct.pack("c", b"#")) - print(int.from_bytes(ser.read(1), byteorder="little", signed=False)) + ser.write(b'C') + print(int.from_bytes(ser.read(1), byteorder='little', signed=False)) + ser.write(struct.pack('c', b'#')) + print(int.from_bytes(ser.read(1), byteorder='little', signed=False)) s = 0 samples = [] while s < nsamples: - ser.write(b"V") + ser.write(b'V') response = ser.read(4) - samples.append(int.from_bytes(response, byteorder="little", signed=False)) + samples.append(int.from_bytes(response, byteorder='little', signed=False)) s += 1 print(samples) # ser.write(struct.pack('cI', b"V", nsamples)) - ser.write(b"V" + int.to_bytes(nsamples, 4, byteorder="little", signed=False)) + ser.write(b'V' + int.to_bytes(nsamples, 4, byteorder='little', signed=False)) serout = ser.read(nsamples * 2) print(serout) - print(np.frombuffer(serout, "uint16")) + print(np.frombuffer(serout, 'uint16')) ser.close() # Hello arc arc = ArCOM(port, 115200) - arc.write(ord("C"), "uint8") - print(arc.read(1, "uint8")) - arc.write(ord("#"), "uint8") - print(arc.read(1, "uint8")) + arc.write(ord('C'), 'uint8') + print(arc.read(1, 'uint8')) + arc.write(ord('#'), 'uint8') + print(arc.read(1, 'uint8')) - arc.read(1, "uint8") + arc.read(1, 'uint8') # arc.write(ord("V"), "uint8", nsamples, "uint32") - arc.write(ord("V"), "uint8") - arcout = arc.read(1, "uint16") + arc.write(ord('V'), 'uint8') + arcout = arc.read(1, 'uint16') print(arcout) del arc diff --git a/devices/F2TTL/Frame2TTLv2.py b/devices/F2TTL/Frame2TTLv2.py index a3619af87..7bfedc646 100644 --- a/devices/F2TTL/Frame2TTLv2.py +++ b/devices/F2TTL/Frame2TTLv2.py @@ -1,24 +1,24 @@ #!/usr/bin/env python -# -*- coding:utf-8 -*- # @File: F2TTL\Frame2TTLv2.py # @Author: Niccolo' Bonacchi (@nbonacchi) # @Date: Tuesday, December 7th 2021, 12:01:50 pm -from ArCOM_F2TTL import ArCOM -import numpy as np import time +import numpy as np +from ArCOM_F2TTL import ArCOM + -class Frame2TTLv2(object): +class Frame2TTLv2: def __init__(self, PortName): self.Port = ArCOM(PortName, 115200) - self.Port.write(ord("C"), "uint8") - handshakeByte = self.Port.read(1, "uint8") + self.Port.write(ord('C'), 'uint8') + handshakeByte = self.Port.read(1, 'uint8') if handshakeByte != 218: - raise F2TTLError("Error: Frame2TTL not detected on port " + PortName + ".") - self.Port.write(ord("#"), "uint8") - hardwareVersion = self.Port.read(1, "uint8") + raise F2TTLError('Error: Frame2TTL not detected on port ' + PortName + '.') + self.Port.write(ord('#'), 'uint8') + hardwareVersion = self.Port.read(1, 'uint8') if hardwareVersion != 2: - raise F2TTLError("Error: Frame2TTLv2 requires hardware version 2.") + raise F2TTLError('Error: Frame2TTLv2 requires hardware version 2.') self._lightThreshold = 150 # This is not a threshold of raw sensor data. self._darkThreshold = -150 @@ -31,7 +31,7 @@ def lightThreshold(self): @lightThreshold.setter def lightThreshold(self, value): - self.Port.write(ord("T"), "uint8", (value, self.darkThreshold), "int16") + self.Port.write(ord('T'), 'uint8', (value, self.darkThreshold), 'int16') self._lightThreshold = value @property @@ -40,24 +40,24 @@ def darkThreshold(self): @darkThreshold.setter def darkThreshold(self, value): - self.Port.write(ord("T"), "uint8", (self.lightThreshold, value), "int16") + self.Port.write(ord('T'), 'uint8', (self.lightThreshold, value), 'int16') self._darkThreshold = value def setLightThreshold_Auto(self): # Run with the sync patch set to black - self.Port.write(ord("L"), "uint8") + self.Port.write(ord('L'), 'uint8') time.sleep(3) - newThreshold = self.Port.read(1, "int16") + newThreshold = self.Port.read(1, 'int16') self.lightThreshold = newThreshold[0] def setDarkThreshold_Auto(self): # Run with the sync patch set to white - self.Port.write(ord("D"), "uint8") + self.Port.write(ord('D'), 'uint8') time.sleep(3) - newThreshold = self.Port.read(1, "int16") + newThreshold = self.Port.read(1, 'int16') self.darkThreshold = newThreshold[0] def read_sensor(self, nSamples): # Return contiguous samples (raw sensor data) - self.Port.write(ord("V"), "uint8", nSamples, "uint32") - value = self.Port.read(nSamples, "uint16") + self.Port.write(ord('V'), 'uint8', nSamples, 'uint32') + value = self.Port.read(nSamples, 'uint16') return value def measure_photons(self, num_samples: int = 250) -> dict: @@ -66,12 +66,12 @@ def measure_photons(self, num_samples: int = 250) -> dict: """ sensorData = self.read_sensor(num_samples) out = { - "mean_value": float(sensorData.mean()), - "max_value": float(sensorData.max()), - "min_value": float(sensorData.min()), - "std_value": float(sensorData.std()), - "sem_value": float(sensorData.std() / np.sqrt(num_samples)), - "nsamples": float(num_samples), + 'mean_value': float(sensorData.mean()), + 'max_value': float(sensorData.max()), + 'min_value': float(sensorData.min()), + 'std_value': float(sensorData.std()), + 'sem_value': float(sensorData.std() / np.sqrt(num_samples)), + 'nsamples': float(num_samples), } return out @@ -80,10 +80,10 @@ def __repr__(self): with no properties or methods specified """ return ( - "\nBpodHiFi with user properties:" + "\n\n" - "Port: ArCOMObject(" + self.Port.serialObject.port + ")" + "\n" - "lightThreshold: " + str(self.lightThreshold) + "\n" - "darkThreshold: " + str(self.darkThreshold) + "\n" + '\nBpodHiFi with user properties:' + '\n\n' + 'Port: ArCOMObject(' + self.Port.serialObject.port + ')' + '\n' + 'lightThreshold: ' + str(self.lightThreshold) + '\n' + 'darkThreshold: ' + str(self.darkThreshold) + '\n' ) def __del__(self): @@ -94,11 +94,11 @@ class F2TTLError(Exception): pass -if __name__ == "__main__": +if __name__ == '__main__': # Example usage: # port = 'COM4' # port = '/dev/ttyACM0' - port = "/dev/serial/by-id/usb-Teensyduino_USB_Serial_10295450-if00" + port = '/dev/serial/by-id/usb-Teensyduino_USB_Serial_10295450-if00' f = Frame2TTLv2(port) print(f) diff --git a/devices/F2TTL/Frame2TTLv2_Demo.py b/devices/F2TTL/Frame2TTLv2_Demo.py index 3aff70dc2..c8504d3b7 100644 --- a/devices/F2TTL/Frame2TTLv2_Demo.py +++ b/devices/F2TTL/Frame2TTLv2_Demo.py @@ -1,6 +1,6 @@ from Frame2TTLv2 import Frame2TTLv2 -F = Frame2TTLv2("/dev/ttyACM3") +F = Frame2TTLv2('/dev/ttyACM3') F.lightThreshold = 150 # See note about threshold units in Frame2TTLv2.py F.darkThreshold = -150 myRawData = F.read_sensor(6) # Read 20k samples of raw, contiguous sensor data diff --git a/devices/screen_calibration/wrkflws/calibration_plots.py b/devices/screen_calibration/wrkflws/calibration_plots.py index 21334ef37..dd5a92e0a 100644 --- a/devices/screen_calibration/wrkflws/calibration_plots.py +++ b/devices/screen_calibration/wrkflws/calibration_plots.py @@ -9,23 +9,23 @@ def find_calibration_files(folder_path: str) -> list: folder_path = Path(folder_path) - files = [str(x) for x in folder_path.glob("*_iblrig_calibration_screen_*.raw.ssv")] + files = [str(x) for x in folder_path.glob('*_iblrig_calibration_screen_*.raw.ssv')] return files def raw_to_df(file_path): file_path = Path(file_path) - df = pd.read_csv(file_path, sep=" ", header=None) + df = pd.read_csv(file_path, sep=' ', header=None) df = df.drop(columns=4) - df.columns = ["val", "r", "g", "b"] + df.columns = ['val', 'r', 'g', 'b'] # # select which channel to use - if "screen_red" in file_path.name or "screen_bright" in file_path.name: - df = df.drop(columns=["b", "g"]) - elif "screen_green" in file_path.name: - df = df.drop(columns=["r", "b"]) - elif "screen_blue" in file_path.name: - df = df.drop(columns=["r", "g"]) - df.columns = ["val", "int"] # df.r + if 'screen_red' in file_path.name or 'screen_bright' in file_path.name: + df = df.drop(columns=['b', 'g']) + elif 'screen_green' in file_path.name: + df = df.drop(columns=['r', 'b']) + elif 'screen_blue' in file_path.name: + df = df.drop(columns=['r', 'g']) + df.columns = ['val', 'int'] # df.r # # Find first peak at start to remove the gray screen inputs # peaks, _ = find_peaks(df.val, height=df.val.mean()-df.val.sem()) @@ -44,28 +44,28 @@ def fit_n_plot(df, fname=None): # ffit_vals = poly.polyval(x_new, coefs) # ffit = poly.Polynomial(coefs) - if "screen_red" in fname: - c = "red" - label = "Red channel" - elif "screen_green" in fname: - c = "green" - label = "Green channel" - elif "screen_blue" in fname: - c = "blue" - label = "Blue channel" - elif "screen_bright" in fname: - c = "gray" - label = "All channels" - - plt.plot(df.int, df.val, ".", c=c, label=label) + if 'screen_red' in fname: + c = 'red' + label = 'Red channel' + elif 'screen_green' in fname: + c = 'green' + label = 'Green channel' + elif 'screen_blue' in fname: + c = 'blue' + label = 'Blue channel' + elif 'screen_bright' in fname: + c = 'gray' + label = 'All channels' + + plt.plot(df.int, df.val, '.', c=c, label=label) # plt.plot(x_new, ffit(x_new), c=c) - plt.plot(x_new, x_new, c="k") + plt.plot(x_new, x_new, c='k') plt.show() # if __name__ == "__main__": -folder_path = r"C:\iblrig\devices\screen_calibration" -folder_path = "/home/nico/Projects/IBL/github/iblrig/devices/screen_calibration" +folder_path = r'C:\iblrig\devices\screen_calibration' +folder_path = '/home/nico/Projects/IBL/github/iblrig/devices/screen_calibration' files = find_calibration_files(folder_path) # file_path = files[-1] @@ -88,29 +88,29 @@ def fit_n_plot(df, fname=None): plt.figure() x_new = np.round(np.linspace(0.01, 1, 100), 2) for file_path in files: - if "screen_red" in file_path: - c = "red" - label = "Red channel" - elif "screen_green" in file_path: - c = "green" - label = "Green channel" - elif "screen_blue" in file_path: - c = "blue" - label = "Blue channel" - elif "screen_bright" in file_path: - c = "gray" - label = "All channels" + if 'screen_red' in file_path: + c = 'red' + label = 'Red channel' + elif 'screen_green' in file_path: + c = 'green' + label = 'Green channel' + elif 'screen_blue' in file_path: + c = 'blue' + label = 'Blue channel' + elif 'screen_bright' in file_path: + c = 'gray' + label = 'All channels' df = raw_to_df(file_path) - plt.plot(df.int, df.val, ".", c=c, label=label) - plt.yscale("log") - plt.xscale("log") - plt.plot(x_new, x_new, c="k") -plt.title("Screen calibration") + plt.plot(df.int, df.val, '.', c=c, label=label) + plt.yscale('log') + plt.xscale('log') + plt.plot(x_new, x_new, c='k') +plt.title('Screen calibration') plt.legend() -plt.xlabel("Intensity requested") -plt.ylabel("Photodiode raw output (lower is brighter)") +plt.xlabel('Intensity requested') +plt.ylabel('Photodiode raw output (lower is brighter)') plt.show() -print(".") +print('.') # plt.axhline(40, ls='--', alpha=0.5) # plt.axhline(80, ls='--', alpha=0.5) diff --git a/devices/screen_calibration/wrkflws/photodiode_server.py b/devices/screen_calibration/wrkflws/photodiode_server.py index 8e67dba07..d2ffcc73f 100644 --- a/devices/screen_calibration/wrkflws/photodiode_server.py +++ b/devices/screen_calibration/wrkflws/photodiode_server.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding:utf-8 -*- # @Author: Niccolò Bonacchi # @Date: Tuesday, October 16th 2018, 12:13:00 pm import argparse @@ -8,30 +7,30 @@ from pythonosc import udp_client -class Frame2TTLServer(object): - def __init__(self, comport="COM6"): - self.osc_client = udp_client.SimpleUDPClient("127.0.0.1", 6667) +class Frame2TTLServer: + def __init__(self, comport='COM6'): + self.osc_client = udp_client.SimpleUDPClient('127.0.0.1', 6667) self.frame2ttl = comport # /dev/ttyACM1' self.ser = serial.Serial(port=self.frame2ttl, baudrate=115200, timeout=1) - self.ser.write(b"S") # Start the stream, stream rate 100Hz - self.ser.write(b"S") # Start the stream, stream rate 100Hz + self.ser.write(b'S') # Start the stream, stream rate 100Hz + self.ser.write(b'S') # Start the stream, stream rate 100Hz self.read = True def read_and_send_data(self): i = 0 while self.read: d = self.ser.read(4) - d = int.from_bytes(d, byteorder="little") - self.osc_client.send_message("/d", d) + d = int.from_bytes(d, byteorder='little') + self.osc_client.send_message('/d', d) i += 1 if i == 100: - self.osc_client.send_message("/i", i) + self.osc_client.send_message('/i', i) i = 0 print(i, d) def stop(self): self.read = False - print("Done!") + print('Done!') def main(comport): @@ -40,10 +39,10 @@ def main(comport): return obj -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Delete files from rig") - parser.add_argument("port", help="COM port fro frame2TTL device") +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Delete files from rig') + parser.add_argument('port', help='COM port fro frame2TTL device') args = parser.parse_args() main(args.port) - print(".") + print('.') diff --git a/iblrig/gui/wizard.py b/iblrig/gui/wizard.py index 18a9c1e83..1613c3634 100644 --- a/iblrig/gui/wizard.py +++ b/iblrig/gui/wizard.py @@ -276,10 +276,10 @@ def _on_menu_training_level_v7(self) -> None: box.setModal(False) box.setWindowTitle('Training Level') box.setText( - f"{session_path}\n\n" - f"training phase:\t{training_phase}\n" - f"reward:\t{reward_amount} uL\n" - f"stimulus gain:\t{stim_gain}" + f'{session_path}\n\n' + f'training phase:\t{training_phase}\n' + f'reward:\t{reward_amount} uL\n' + f'stimulus gain:\t{stim_gain}' ) if self.uiComboTask.currentText() == '_iblrig_tasks_trainingChoiceWorld': box.setStandardButtons(QtWidgets.QMessageBox.Apply | QtWidgets.QMessageBox.Close) diff --git a/iblrig/online_plots.py b/iblrig/online_plots.py index afea6e34d..b2acca5b3 100644 --- a/iblrig/online_plots.py +++ b/iblrig/online_plots.py @@ -243,12 +243,15 @@ def __init__(self, task_file=None): h.curve_reaction = {} for p in PROBABILITY_SET: h.curve_psych[p] = h.ax_psych.plot( - self.data.psychometrics.loc[p].index, self.data.psychometrics.loc[p]['choice'], '.-', - zorder=10, clip_on=False, label=f'p = {p}' + self.data.psychometrics.loc[p].index, + self.data.psychometrics.loc[p]['choice'], + '.-', + zorder=10, + clip_on=False, + label=f'p = {p}', ) h.curve_reaction[p] = h.ax_reaction.plot( - self.data.psychometrics.loc[p].index, self.data.psychometrics.loc[p]['response_time'], '.-', - label=f'p = {p}' + self.data.psychometrics.loc[p].index, self.data.psychometrics.loc[p]['response_time'], '.-', label=f'p = {p}' ) h.ax_psych.legend() h.ax_reaction.legend() diff --git a/iblrig_tasks/_iblrig_tasks_ImagingChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_ImagingChoiceWorld/task.py index b88a1cff4..2547d46d4 100644 --- a/iblrig_tasks/_iblrig_tasks_ImagingChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_ImagingChoiceWorld/task.py @@ -3,7 +3,7 @@ class Session(BiasedChoiceWorldSession): - protocol_name = "_iblrig_tasks_imagingChoiceWorld" + protocol_name = '_iblrig_tasks_imagingChoiceWorld' extractor_tasks = ['TrialRegisterRaw', 'ChoiceWorldTrials'] def draw_quiescent_period(self): @@ -14,7 +14,7 @@ def draw_quiescent_period(self): return iblrig.misc.truncated_exponential(factor=0.35 * 2, min_value=0.2 * 2, max_value=0.5 * 2) -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) sess.run() diff --git a/iblrig_tasks/_iblrig_tasks_advancedChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_advancedChoiceWorld/task.py index 8d752f964..3233ad1dc 100644 --- a/iblrig_tasks/_iblrig_tasks_advancedChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_advancedChoiceWorld/task.py @@ -1,8 +1,9 @@ from pathlib import Path + import yaml -from iblrig.base_choice_world import ActiveChoiceWorldSession import iblrig.misc +from iblrig.base_choice_world import ActiveChoiceWorldSession # read defaults from task_parameters.yaml with open(Path(__file__).parent.joinpath('task_parameters.yaml')) as f: @@ -16,46 +17,73 @@ class Session(ActiveChoiceWorldSession): It differs from TraininChoiceWorld in that it does not implement adaptive contrasts or debiasing, and it differs from BiasedChoiceWorld in that it does not implement biased blocks. """ - protocol_name = "_iblrig_tasks_advancedChoiceWorld" - extractor_tasks = ['TrialRegisterRaw', 'ChoiceWorldTrials', 'TrainingStatus'] - def __init__(self, - *args, - contrast_set: list[float] = DEFAULTS["CONTRAST_SET"], - contrast_set_probability_type: str = DEFAULTS["CONTRAST_SET_PROBABILITY_TYPE"], - probability_left: float = DEFAULTS["PROBABILITY_LEFT"], - reward_amount_ul: float = DEFAULTS["REWARD_AMOUNT_UL"], - stim_gain: float = DEFAULTS["STIM_GAIN"], - **kwargs): + protocol_name = '_iblrig_tasks_advancedChoiceWorld' + extractor_tasks = ['TrialRegisterRaw', 'ChoiceWorldTrials', 'TrainingStatus'] + def __init__( + self, + *args, + contrast_set: list[float] = DEFAULTS['CONTRAST_SET'], + contrast_set_probability_type: str = DEFAULTS['CONTRAST_SET_PROBABILITY_TYPE'], + probability_left: float = DEFAULTS['PROBABILITY_LEFT'], + reward_amount_ul: float = DEFAULTS['REWARD_AMOUNT_UL'], + stim_gain: float = DEFAULTS['STIM_GAIN'], + **kwargs, + ): super(Session, self).__init__(*args, **kwargs) - self.task_params["CONTRAST_SET"] = contrast_set - self.task_params["CONTRAST_SET_PROBABILITY_TYPE"] = contrast_set_probability_type - self.task_params["PROBABILITY_LEFT"] = probability_left - self.task_params["REWARD_AMOUNT_UL"] = reward_amount_ul - self.task_params["STIM_GAIN"] = stim_gain + self.task_params['CONTRAST_SET'] = contrast_set + self.task_params['CONTRAST_SET_PROBABILITY_TYPE'] = contrast_set_probability_type + self.task_params['PROBABILITY_LEFT'] = probability_left + self.task_params['REWARD_AMOUNT_UL'] = reward_amount_ul + self.task_params['STIM_GAIN'] = stim_gain @staticmethod def extra_parser(): - """ :return: argparse.parser() """ + """:return: argparse.parser()""" parser = super(Session, Session).extra_parser() - parser.add_argument('--contrast_set', option_strings=['--contrast_set'], - dest='contrast_set', default=DEFAULTS["CONTRAST_SET"], nargs='+', - type=float, help='set of contrasts to present') - parser.add_argument('--contrast_set_probability_type', option_strings=['--contrast_set_probability_type'], - dest='contrast_set_probability_type', default=DEFAULTS["CONTRAST_SET_PROBABILITY_TYPE"], - type=str, choices=['skew_zero', 'uniform'], help=f'probability type for contrast set ' - f'(default: {DEFAULTS["CONTRAST_SET_PROBABILITY_TYPE"]})') - parser.add_argument('--probability_left', option_strings=['--probability_left'], - dest='probability_left', default=DEFAULTS["PROBABILITY_LEFT"], type=float, - help=f'probability for stimulus to appear on the left ' - f'(default: {DEFAULTS["PROBABILITY_LEFT"]:.1f})') - parser.add_argument('--reward_amount_ul', option_strings=['--reward_amount_ul'], - dest='reward_amount_ul', default=DEFAULTS["REWARD_AMOUNT_UL"], - type=float, help=f'reward amount (default: {DEFAULTS["REWARD_AMOUNT_UL"]}μl)') - parser.add_argument('--stim_gain', option_strings=['--stim_gain'], dest='stim_gain', - default=DEFAULTS["STIM_GAIN"], type=float, help=f'visual angle/wheel displacement ' - f'(deg/mm, default: {DEFAULTS["STIM_GAIN"]})') + parser.add_argument( + '--contrast_set', + option_strings=['--contrast_set'], + dest='contrast_set', + default=DEFAULTS['CONTRAST_SET'], + nargs='+', + type=float, + help='set of contrasts to present', + ) + parser.add_argument( + '--contrast_set_probability_type', + option_strings=['--contrast_set_probability_type'], + dest='contrast_set_probability_type', + default=DEFAULTS['CONTRAST_SET_PROBABILITY_TYPE'], + type=str, + choices=['skew_zero', 'uniform'], + help=f'probability type for contrast set ' f'(default: {DEFAULTS["CONTRAST_SET_PROBABILITY_TYPE"]})', + ) + parser.add_argument( + '--probability_left', + option_strings=['--probability_left'], + dest='probability_left', + default=DEFAULTS['PROBABILITY_LEFT'], + type=float, + help=f'probability for stimulus to appear on the left ' f'(default: {DEFAULTS["PROBABILITY_LEFT"]:.1f})', + ) + parser.add_argument( + '--reward_amount_ul', + option_strings=['--reward_amount_ul'], + dest='reward_amount_ul', + default=DEFAULTS['REWARD_AMOUNT_UL'], + type=float, + help=f'reward amount (default: {DEFAULTS["REWARD_AMOUNT_UL"]}μl)', + ) + parser.add_argument( + '--stim_gain', + option_strings=['--stim_gain'], + dest='stim_gain', + default=DEFAULTS['STIM_GAIN'], + type=float, + help=f'visual angle/wheel displacement ' f'(deg/mm, default: {DEFAULTS["STIM_GAIN"]})', + ) return parser def next_trial(self): @@ -65,7 +93,7 @@ def next_trial(self): self.draw_next_trial_info(pleft=self.task_params.PROBABILITY_LEFT) -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) sess.run() diff --git a/iblrig_tasks/_iblrig_tasks_biasedChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_biasedChoiceWorld/task.py index 20bd2ad3e..a9a684e03 100644 --- a/iblrig_tasks/_iblrig_tasks_biasedChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_biasedChoiceWorld/task.py @@ -1,12 +1,12 @@ -from iblrig.base_choice_world import BiasedChoiceWorldSession import iblrig.misc +from iblrig.base_choice_world import BiasedChoiceWorldSession class Session(BiasedChoiceWorldSession): extractor_tasks = ['TrialRegisterRaw', 'ChoiceWorldTrials', 'TrainingStatus'] -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) sess.run() diff --git a/iblrig_tasks/_iblrig_tasks_ephysChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_ephysChoiceWorld/task.py index 937cfb5c9..a515499f7 100644 --- a/iblrig_tasks/_iblrig_tasks_ephysChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_ephysChoiceWorld/task.py @@ -6,37 +6,44 @@ import pandas as pd -from iblrig.base_choice_world import BiasedChoiceWorldSession import iblrig.misc +from iblrig.base_choice_world import BiasedChoiceWorldSession class Session(BiasedChoiceWorldSession): - protocol_name = "_iblrig_tasks_ephysChoiceWorld" + protocol_name = '_iblrig_tasks_ephysChoiceWorld' extractor_tasks = ['TrialRegisterRaw', 'ChoiceWorldTrials', 'TrainingStatus'] def __init__(self, *args, session_template_id=0, **kwargs): super(Session, self).__init__(*args, **kwargs) self.task_params.SESSION_TEMPLATE_ID = session_template_id trials_table = pd.read_parquet(Path(__file__).parent.joinpath('trials_fixtures.pqt')) - self.trials_table = trials_table.loc[ - trials_table['session_id'] == session_template_id].reindex().drop(columns=['session_id']) + self.trials_table = ( + trials_table.loc[trials_table['session_id'] == session_template_id].reindex().drop(columns=['session_id']) + ) self.trials_table = self.trials_table.reset_index() # reconstruct the block dataframe from the trials table self.blocks_table = self.trials_table.groupby('block_num').agg( - probability_left=pd.NamedAgg(column="stim_probability_left", aggfunc="first"), - block_length=pd.NamedAgg(column="stim_probability_left", aggfunc="count"), + probability_left=pd.NamedAgg(column='stim_probability_left', aggfunc='first'), + block_length=pd.NamedAgg(column='stim_probability_left', aggfunc='count'), ) @staticmethod def extra_parser(): - """ :return: argparse.parser() """ + """:return: argparse.parser()""" parser = super(Session, Session).extra_parser() - parser.add_argument('--session_template_id', option_strings=['--session_template_id'], - dest='session_template_id', default=0, type=int, help='pre-generated session index (zero-based)') + parser.add_argument( + '--session_template_id', + option_strings=['--session_template_id'], + dest='session_template_id', + default=0, + type=int, + help='pre-generated session index (zero-based)', + ) return parser -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) sess.run() diff --git a/iblrig_tasks/_iblrig_tasks_habituationChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_habituationChoiceWorld/task.py index b2ff8eef4..17046b14e 100644 --- a/iblrig_tasks/_iblrig_tasks_habituationChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_habituationChoiceWorld/task.py @@ -1,12 +1,12 @@ -from iblrig.base_choice_world import HabituationChoiceWorldSession import iblrig.misc +from iblrig.base_choice_world import HabituationChoiceWorldSession class Session(HabituationChoiceWorldSession): extractor_tasks = ['TrialRegisterRaw', 'HabituationTrials'] -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) sess.run() diff --git a/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py index cceeb3dcc..a2187b7f7 100644 --- a/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py @@ -1,16 +1,15 @@ import numpy as np -from pybpodapi.protocol import StateMachine import iblrig.misc from iblrig.base_choice_world import BiasedChoiceWorldSession from iblrig.hardware import SOFTCODE - +from pybpodapi.protocol import StateMachine REWARD_AMOUNTS_UL = (1, 3) class Session(BiasedChoiceWorldSession): - protocol_name = "_iblrig_tasks_neuromodulatorChoiceWorld" + protocol_name = '_iblrig_tasks_neuromodulatorChoiceWorld' extractor_tasks = ['TrialRegisterRaw', 'ChoiceWorldNeuromodulators', 'TrainingStatus'] def __init__(self, *args, **kwargs): @@ -34,7 +33,7 @@ def next_trial(self): if self.task_params.VARIABLE_REWARDS: # the reward is a draw within an uniform distribution between 3 and 1 - reward_amount = 1.5 if self.block_num == 0 else np.random.choice(REWARD_AMOUNTS_UL, p=[.8, .2]) + reward_amount = 1.5 if self.block_num == 0 else np.random.choice(REWARD_AMOUNTS_UL, p=[0.8, 0.2]) self.trials_table.at[self.trial_num, 'reward_amount'] = reward_amount @property @@ -49,103 +48,103 @@ def get_state_machine_trial(self, i): sma = StateMachine(self.bpod) if i == 0: # First trial exception start camera - session_delay_start = self.task_params.get("SESSION_DELAY_START", 0) - self.logger.info("First trial initializing, will move to next trial only if:") - self.logger.info("1. camera is detected") - self.logger.info(f"2. {session_delay_start} sec have elapsed") + session_delay_start = self.task_params.get('SESSION_DELAY_START', 0) + self.logger.info('First trial initializing, will move to next trial only if:') + self.logger.info('1. camera is detected') + self.logger.info(f'2. {session_delay_start} sec have elapsed') sma.add_state( - state_name="trial_start", + state_name='trial_start', state_timer=0, - state_change_conditions={"Port1In": "delay_initiation"}, - output_actions=[("SoftCode", SOFTCODE.TRIGGER_CAMERA), ("BNC1", 255)], + state_change_conditions={'Port1In': 'delay_initiation'}, + output_actions=[('SoftCode', SOFTCODE.TRIGGER_CAMERA), ('BNC1', 255)], ) # start camera sma.add_state( - state_name="delay_initiation", + state_name='delay_initiation', state_timer=session_delay_start, output_actions=[], - state_change_conditions={"Tup": "reset_rotary_encoder"}, + state_change_conditions={'Tup': 'reset_rotary_encoder'}, ) else: sma.add_state( - state_name="trial_start", + state_name='trial_start', state_timer=0, # ~100µs hardware irreducible delay - state_change_conditions={"Tup": "reset_rotary_encoder"}, - output_actions=[self.bpod.actions.stop_sound, ("BNC1", 255)], + state_change_conditions={'Tup': 'reset_rotary_encoder'}, + output_actions=[self.bpod.actions.stop_sound, ('BNC1', 255)], ) # stop all sounds sma.add_state( - state_name="reset_rotary_encoder", + state_name='reset_rotary_encoder', state_timer=0, output_actions=[self.bpod.actions.rotary_encoder_reset], - state_change_conditions={"Tup": "quiescent_period"}, + state_change_conditions={'Tup': 'quiescent_period'}, ) sma.add_state( # '>back' | '>reset_timer' - state_name="quiescent_period", + state_name='quiescent_period', state_timer=self.quiescent_period, output_actions=[], state_change_conditions={ - "Tup": "stim_on", - self.movement_left: "reset_rotary_encoder", - self.movement_right: "reset_rotary_encoder", + 'Tup': 'stim_on', + self.movement_left: 'reset_rotary_encoder', + self.movement_right: 'reset_rotary_encoder', }, ) sma.add_state( - state_name="stim_on", + state_name='stim_on', state_timer=0.1, output_actions=[self.bpod.actions.bonsai_show_stim], state_change_conditions={ - "Tup": "interactive_delay", - "BNC1High": "interactive_delay", - "BNC1Low": "interactive_delay", + 'Tup': 'interactive_delay', + 'BNC1High': 'interactive_delay', + 'BNC1Low': 'interactive_delay', }, ) sma.add_state( - state_name="interactive_delay", + state_name='interactive_delay', state_timer=self.task_params.INTERACTIVE_DELAY, output_actions=[], - state_change_conditions={"Tup": "play_tone"}, + state_change_conditions={'Tup': 'play_tone'}, ) sma.add_state( - state_name="play_tone", + state_name='play_tone', state_timer=0.1, - output_actions=[self.bpod.actions.play_tone, ("BNC1", 255)], + output_actions=[self.bpod.actions.play_tone, ('BNC1', 255)], state_change_conditions={ - "Tup": "reset2_rotary_encoder", - "BNC2High": "reset2_rotary_encoder", + 'Tup': 'reset2_rotary_encoder', + 'BNC2High': 'reset2_rotary_encoder', }, ) sma.add_state( - state_name="reset2_rotary_encoder", + state_name='reset2_rotary_encoder', state_timer=0.05, output_actions=[self.bpod.actions.rotary_encoder_reset], - state_change_conditions={"Tup": "closed_loop"}, + state_change_conditions={'Tup': 'closed_loop'}, ) if self.omit_feedback: sma.add_state( - state_name="closed_loop", + state_name='closed_loop', state_timer=self.task_params.RESPONSE_WINDOW, output_actions=[self.bpod.actions.bonsai_closed_loop], state_change_conditions={ - "Tup": "omit_no_go", - self.event_error: "omit_error", - self.event_reward: "omit_correct", + 'Tup': 'omit_no_go', + self.event_error: 'omit_error', + self.event_reward: 'omit_correct', }, ) else: sma.add_state( - state_name="closed_loop", + state_name='closed_loop', state_timer=self.task_params.RESPONSE_WINDOW, output_actions=[self.bpod.actions.bonsai_closed_loop], state_change_conditions={ - "Tup": "delay_no_go", - self.event_error: "delay_error", - self.event_reward: "delay_reward", + 'Tup': 'delay_no_go', + self.event_error: 'delay_error', + self.event_reward: 'delay_reward', }, ) @@ -154,92 +153,95 @@ def get_state_machine_trial(self, i): for state_name in ['omit_error', 'omit_correct', 'omit_no_go']: sma.add_state( state_name=state_name, - state_timer=(self.task_params.FEEDBACK_NOGO_DELAY_SECS - + self.task_params.FEEDBACK_ERROR_DELAY_SECS - + self.task_params.FEEDBACK_CORRECT_DELAY_SECS) / 3, + state_timer=( + self.task_params.FEEDBACK_NOGO_DELAY_SECS + + self.task_params.FEEDBACK_ERROR_DELAY_SECS + + self.task_params.FEEDBACK_CORRECT_DELAY_SECS + ) + / 3, output_actions=[], - state_change_conditions={"Tup": "hide_stim"}, + state_change_conditions={'Tup': 'hide_stim'}, ) sma.add_state( - state_name="delay_no_go", + state_name='delay_no_go', state_timer=self.choice_to_feedback_delay, - state_change_conditions={"Tup": "no_go"}, + state_change_conditions={'Tup': 'no_go'}, output_actions=[], ) sma.add_state( - state_name="no_go", + state_name='no_go', state_timer=self.task_params.FEEDBACK_NOGO_DELAY_SECS, output_actions=[self.bpod.actions.bonsai_hide_stim, self.bpod.actions.play_noise], - state_change_conditions={"Tup": "exit_state"}, + state_change_conditions={'Tup': 'exit_state'}, ) sma.add_state( - state_name="delay_error", + state_name='delay_error', state_timer=self.choice_to_feedback_delay, - state_change_conditions={"Tup": "freeze_error"}, + state_change_conditions={'Tup': 'freeze_error'}, output_actions=[], ) sma.add_state( - state_name="freeze_error", + state_name='freeze_error', state_timer=0, output_actions=[self.bpod.actions.bonsai_freeze_stim], - state_change_conditions={"Tup": "error"}, + state_change_conditions={'Tup': 'error'}, ) sma.add_state( - state_name="error", + state_name='error', state_timer=self.task_params.FEEDBACK_ERROR_DELAY_SECS, output_actions=[self.bpod.actions.play_noise], - state_change_conditions={"Tup": "hide_stim"}, + state_change_conditions={'Tup': 'hide_stim'}, ) sma.add_state( - state_name="delay_reward", + state_name='delay_reward', state_timer=self.choice_to_feedback_delay, - state_change_conditions={"Tup": "freeze_reward"}, + state_change_conditions={'Tup': 'freeze_reward'}, output_actions=[], ) sma.add_state( - state_name="freeze_reward", + state_name='freeze_reward', state_timer=0, output_actions=[self.bpod.actions.bonsai_freeze_stim], - state_change_conditions={"Tup": "reward"}, + state_change_conditions={'Tup': 'reward'}, ) sma.add_state( - state_name="reward", + state_name='reward', state_timer=self.reward_time, - output_actions=[("Valve1", 255)], - state_change_conditions={"Tup": "correct"}, + output_actions=[('Valve1', 255)], + state_change_conditions={'Tup': 'correct'}, ) sma.add_state( - state_name="correct", + state_name='correct', state_timer=self.task_params.FEEDBACK_CORRECT_DELAY_SECS, output_actions=[], - state_change_conditions={"Tup": "hide_stim"}, + state_change_conditions={'Tup': 'hide_stim'}, ) sma.add_state( - state_name="hide_stim", + state_name='hide_stim', state_timer=0.1, output_actions=[self.bpod.actions.bonsai_hide_stim], state_change_conditions={ - "Tup": "exit_state", - "BNC1High": "exit_state", - "BNC1Low": "exit_state", + 'Tup': 'exit_state', + 'BNC1High': 'exit_state', + 'BNC1Low': 'exit_state', }, ) sma.add_state( - state_name="exit_state", + state_name='exit_state', state_timer=0.5, - output_actions=[("BNC1", 255)], - state_change_conditions={"Tup": "exit"}, + output_actions=[('BNC1', 255)], + state_change_conditions={'Tup': 'exit'}, ) return sma @@ -251,6 +253,7 @@ class SessionRelatedBlocks(Session): P0 P1 P2 P1 P2 P1 P2 P1 P2 R0 R1 R1 R2 R2 R1 R1 R2 R2 """ + # from iblrig_tasks._iblrig_tasks_neuroModulatorChoiceWorld.task import SessionRelatedBlocks # sess = SessionRelatedBlocks() def __init__(self, *args, **kwargs): @@ -289,7 +292,7 @@ def draw_reward_amount(self): return np.random.choice(REWARD_AMOUNTS, p=probas) -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) sess.run() diff --git a/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task_validation.py b/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task_validation.py index 550dd9e6a..ba82236f6 100644 --- a/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task_validation.py +++ b/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task_validation.py @@ -1,8 +1,9 @@ # first read jsonable file containing the trials in record oriented way -from iblutil.io import jsonable +import matplotlib.pyplot as plt import numpy as np import pandas as pd -import matplotlib.pyplot as plt + +from iblutil.io import jsonable task_data = jsonable.read('/Users/olivier/Downloads/D6/_iblrig_taskData.raw.jsonable') @@ -21,17 +22,20 @@ for i, bp in enumerate(bpod_data): sts = bp['States timestamps'] task_data.at[i, 'bpod_valve_time'] = np.diff(sts['reward'] if 'reward' in sts else np.NaN) - task_data.at[i, 'bpod_delay'] = np.nansum(np.r_[ - np.diff(sts['delay_reward'])[0] if 'delay_reward' in sts else 0, - np.diff(sts['delay_error'])[0] if 'delay_error' in sts else 0, - np.diff(sts['delay_nogo'])[0] if 'delay_nogo' in sts else 0, - ]) + task_data.at[i, 'bpod_delay'] = np.nansum( + np.r_[ + np.diff(sts['delay_reward'])[0] if 'delay_reward' in sts else 0, + np.diff(sts['delay_error'])[0] if 'delay_error' in sts else 0, + np.diff(sts['delay_nogo'])[0] if 'delay_nogo' in sts else 0, + ] + ) ## %% # if all checks out the valve time should almost proportional to the reward amound -r = np.corrcoef(task_data['reward_amount'][~np.isnan(task_data['valve_time'])], - task_data['valve_time'][~np.isnan(task_data['valve_time'])]) -assert r[0, 1] >= .9999 +r = np.corrcoef( + task_data['reward_amount'][~np.isnan(task_data['valve_time'])], task_data['valve_time'][~np.isnan(task_data['valve_time'])] +) +assert r[0, 1] >= 0.9999 # the other test is to check that we have delays - this fails on data collected 27/01/2023 # here the rewarded trials have no delay. Oups... diff --git a/iblrig_tasks/_iblrig_tasks_passiveChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_passiveChoiceWorld/task.py index 2a8fe5b7b..4970a8a16 100644 --- a/iblrig_tasks/_iblrig_tasks_passiveChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_passiveChoiceWorld/task.py @@ -4,12 +4,12 @@ import pandas as pd -from iblrig.base_choice_world import ChoiceWorldSession import iblrig.misc +from iblrig.base_choice_world import ChoiceWorldSession class Session(ChoiceWorldSession): - protocol_name = "_iblrig_tasks_passiveChoiceWorld" + protocol_name = '_iblrig_tasks_passiveChoiceWorld' extractor_tasks = ['PassiveRegisterRaw', 'PassiveTask'] def __init__(self, **kwargs): @@ -40,33 +40,33 @@ def _run(self): :return: """ self.trigger_bonsai_cameras() - self.logger.info("Starting spontaneous activity followed by receptive field mapping") + self.logger.info('Starting spontaneous activity followed by receptive field mapping') # Run the passive part i.e. spontaneous activity and RFMapping stim - self.run_passive_visual_stim(sa_time="00:10:00") + self.run_passive_visual_stim(sa_time='00:10:00') # Then run the replay of task events: V for valve, T for tone, N for noise, G for gratings - self.logger.info("Starting replay of task stims") + self.logger.info('Starting replay of task stims') for self.trial_num, trial in self.trials_table.iterrows(): - self.logger.info(f"Delay: {trial.stim_delay}; ID: {trial.stim_type}; Count: {self.trial_num}/300") + self.logger.info(f'Delay: {trial.stim_delay}; ID: {trial.stim_type}; Count: {self.trial_num}/300') sys.stdout.flush() time.sleep(trial.stim_delay) - if trial.stim_type == "V": + if trial.stim_type == 'V': self.valve_open(self.reward_time) - elif trial.stim_type == "T": + elif trial.stim_type == 'T': self.sound_play_tone(state_timer=0.102) - elif trial.stim_type == "N": + elif trial.stim_type == 'N': self.sound_play_noise(state_timer=0.510) - elif trial.stim_type == "G": + elif trial.stim_type == 'G': # this will send the current trial info to the visual stim self.send_trial_info_to_bonsai() - self.bonsai_visual_udp_client.send_message("/re", 2) # show_stim 2 + self.bonsai_visual_udp_client.send_message('/re', 2) # show_stim 2 time.sleep(0.3) - self.bonsai_visual_udp_client.send_message("/re", 1) # stop_stim 1 + self.bonsai_visual_udp_client.send_message('/re', 1) # stop_stim 1 if self.paths.SESSION_FOLDER.joinpath('.stop').exists(): self.paths.SESSION_FOLDER.joinpath('.stop').unlink() break -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover # python .\iblrig_tasks\_iblrig_tasks_spontaneous\task.py --subject mysubject kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) diff --git a/iblrig_tasks/_iblrig_tasks_spontaneous/task.py b/iblrig_tasks/_iblrig_tasks_spontaneous/task.py index 42ab7b847..d9c46aed6 100644 --- a/iblrig_tasks/_iblrig_tasks_spontaneous/task.py +++ b/iblrig_tasks/_iblrig_tasks_spontaneous/task.py @@ -2,15 +2,15 @@ The spontaneous protocol is used to record spontaneous activity in the mouse brain. The task does nothing, only creates the architecture for the data streams to be recorded. """ -from iblrig.base_tasks import SpontaneousSession import iblrig.misc +from iblrig.base_tasks import SpontaneousSession class Session(SpontaneousSession): - protocol_name = "_iblrig_tasks_spontaneous" + protocol_name = '_iblrig_tasks_spontaneous' -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover # python .\iblrig_tasks\_iblrig_tasks_spontaneous\task.py --subject mysubject kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) diff --git a/iblrig_tasks/_iblrig_tasks_trainingChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_trainingChoiceWorld/task.py index c722799c6..9d4b768f7 100644 --- a/iblrig_tasks/_iblrig_tasks_trainingChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_trainingChoiceWorld/task.py @@ -1,5 +1,5 @@ -from iblrig.base_choice_world import TrainingChoiceWorldSession import iblrig.misc +from iblrig.base_choice_world import TrainingChoiceWorldSession TRAINING_PHASE = -1 ADAPTIVE_REWARD = -1.0 @@ -10,21 +10,36 @@ class Session(TrainingChoiceWorldSession): @staticmethod def extra_parser(): - """ :return: argparse.parser() """ + """:return: argparse.parser()""" parser = super(Session, Session).extra_parser() - parser.add_argument('--training_phase', option_strings=['--training_phase'], - dest='training_phase', default=TRAINING_PHASE, type=int, - help='defines the set of contrasts presented to the subject') - parser.add_argument('--adaptive_reward', option_strings=['--adaptive_reward'], - dest='adaptive_reward', default=ADAPTIVE_REWARD, type=float, - help='reward volume in microliters') - parser.add_argument('--adaptive_gain', option_strings=['--adaptive_gain'], - dest='adaptive_gain', default=None, type=float, - help='Gain of the wheel in degrees/mm') + parser.add_argument( + '--training_phase', + option_strings=['--training_phase'], + dest='training_phase', + default=TRAINING_PHASE, + type=int, + help='defines the set of contrasts presented to the subject', + ) + parser.add_argument( + '--adaptive_reward', + option_strings=['--adaptive_reward'], + dest='adaptive_reward', + default=ADAPTIVE_REWARD, + type=float, + help='reward volume in microliters', + ) + parser.add_argument( + '--adaptive_gain', + option_strings=['--adaptive_gain'], + dest='adaptive_gain', + default=None, + type=float, + help='Gain of the wheel in degrees/mm', + ) return parser -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) sess.run() diff --git a/iblrig_tasks/_iblrig_tasks_trainingPhaseChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_trainingPhaseChoiceWorld/task.py index 7875ac679..e1dc101b0 100644 --- a/iblrig_tasks/_iblrig_tasks_trainingPhaseChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_trainingPhaseChoiceWorld/task.py @@ -1,7 +1,9 @@ from pathlib import Path + import yaml -from iblrig.base_choice_world import TrainingChoiceWorldSession + import iblrig.misc +from iblrig.base_choice_world import TrainingChoiceWorldSession # read defaults from task_parameters.yaml with open(Path(__file__).parent.joinpath('task_parameters.yaml')) as f: @@ -9,31 +11,41 @@ class Session(TrainingChoiceWorldSession): - protocol_name = "_iblrig_tasks_trainingPhaseChoiceWorld" + protocol_name = '_iblrig_tasks_trainingPhaseChoiceWorld' extractor_tasks = ['TrialRegisterRaw', 'ChoiceWorldTrials', 'TrainingStatus'] - def __init__(self, *args, training_level=DEFAULTS["TRAINING_PHASE"], debias=DEFAULTS['DEBIAS'], **kwargs): + def __init__(self, *args, training_level=DEFAULTS['TRAINING_PHASE'], debias=DEFAULTS['DEBIAS'], **kwargs): super(Session, self).__init__(*args, training_phase=training_level, **kwargs) - self.task_params["TRAINING_PHASE"] = training_level - self.task_params["DEBIAS"] = debias + self.task_params['TRAINING_PHASE'] = training_level + self.task_params['DEBIAS'] = debias def check_training_phase(self): pass @staticmethod def extra_parser(): - """ :return: argparse.parser() """ + """:return: argparse.parser()""" parser = super(Session, Session).extra_parser() - parser.add_argument('--training_level', option_strings=['--training_level'], - dest='training_level', default=DEFAULTS["TRAINING_PHASE"], type=int, - help='defines the set of contrasts presented to the subject') - parser.add_argument('--debias', option_strings=['--debias'], - dest='debias', default=DEFAULTS['DEBIAS'], type=bool, - help='uses the debiasing protocol (only applies to levels 0-4)') + parser.add_argument( + '--training_level', + option_strings=['--training_level'], + dest='training_level', + default=DEFAULTS['TRAINING_PHASE'], + type=int, + help='defines the set of contrasts presented to the subject', + ) + parser.add_argument( + '--debias', + option_strings=['--debias'], + dest='debias', + default=DEFAULTS['DEBIAS'], + type=bool, + help='uses the debiasing protocol (only applies to levels 0-4)', + ) return parser -if __name__ == "__main__": # pragma: no cover +if __name__ == '__main__': # pragma: no cover kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) sess = Session(**kwargs) sess.run() diff --git a/pyproject.toml b/pyproject.toml index faf0f8329..91a2aacf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,11 +13,10 @@ dependencies = [ "ONE-api", "PyYAML", "graphviz", - "ibllib@git+https://github.com/int-brain-lab/ibllib.git", "iblbpod@git+https://github.com/int-brain-lab/iblbpod.git", + "ibllib@git+https://github.com/int-brain-lab/ibllib.git", "iblpybpod@git+https://github.com/int-brain-lab/iblpybpod.git@no-gui", "iblscripts@git+https://github.com/int-brain-lab/iblscripts.git", - "serial_singleton@git+https://github.com/int-brain-lab/serial_singleton.git", "iblutil >= 1.7.1", "numpy", "packaging", @@ -27,6 +26,7 @@ dependencies = [ "python-osc", "pywin32; sys_platform == 'win32'", "scipy", + "serial_singleton@git+https://github.com/int-brain-lab/serial_singleton.git", "sounddevice", ] @@ -67,6 +67,7 @@ version = { attr = "iblrig.__version__" } find = {} [tool.mypy] +files = [ "iblrig/**/*.py", "iblrig_tasks/**/*.py" ] ignore_missing_imports = true [tool.pytest.ini_options] diff --git a/scripts/hardware_validation/frame2ttlv2.py b/scripts/hardware_validation/frame2ttlv2.py index 379df6dee..47f6588ce 100644 --- a/scripts/hardware_validation/frame2ttlv2.py +++ b/scripts/hardware_validation/frame2ttlv2.py @@ -1,11 +1,12 @@ -from iblrig.frame2TTL import Frame2TTLv2 import numpy as np -COM_PORT = "COM" -f2ttl = Frame2TTLv2("/dev/ttyACM0") +from iblrig.frame2TTL import Frame2TTLv2 + +COM_PORT = 'COM' +f2ttl = Frame2TTLv2('/dev/ttyACM0') light_thresh = 24.8 -dark_thresh = - 17.7 +dark_thresh = -17.7 f2ttl.set_thresholds(light_thresh, dark_thresh) diff --git a/scripts/hardware_validation/harp.py b/scripts/hardware_validation/harp.py index cab0d3c27..da42f2e7d 100644 --- a/scripts/hardware_validation/harp.py +++ b/scripts/hardware_validation/harp.py @@ -6,22 +6,26 @@ from iblrig import path_helper from iblrig.base_choice_world import BiasedChoiceWorldSession -log = logging.getLogger("iblrig") +log = logging.getLogger('iblrig') # get hardware settings from 'settings/hardware_settings.yaml' file -hardware_settings = path_helper.load_settings_yaml("hardware_settings.yaml") +hardware_settings = path_helper.load_settings_yaml('hardware_settings.yaml') # check if bpod has had a COM port defined -if hardware_settings["device_bpod"]["COM_BPOD"] is None: - log.info("No COM port assigned for bpod, edit the 'settings/hardware_settings.yaml' file to add a bpod COM port; skipping " - "harp validation.") +if hardware_settings['device_bpod']['COM_BPOD'] is None: + log.info( + "No COM port assigned for bpod, edit the 'settings/hardware_settings.yaml' file to add a bpod COM port; skipping " + 'harp validation.' + ) exit() # verify harp is set in the 'settings/hardware_settings.yaml' file -if hardware_settings["device_sound"]["OUTPUT"] != "harp": - log.info(f"The sound device specified in 'settings/hardware_settings.yaml' is not 'harp', edit the settings file to change " - f"this.\nCurrently assigned soundcard: {hardware_settings['device_sound']['OUTPUT']}") +if hardware_settings['device_sound']['OUTPUT'] != 'harp': + log.info( + f"The sound device specified in 'settings/hardware_settings.yaml' is not 'harp', edit the settings file to change " + f"this.\nCurrently assigned soundcard: {hardware_settings['device_sound']['OUTPUT']}" + ) exit() # TODO: check device manager for lib-usb32 entries if on Windows system @@ -29,8 +33,8 @@ # connect to bpod and attempt to produce audio on harp cw = BiasedChoiceWorldSession(interactive=False, subject='harp_validator_subject') cw.start_mixin_bpod() -log.info("Successfully initialized to bpod.") +log.info('Successfully initialized to bpod.') cw.start_mixin_sound() -log.info("Successfully initialized to harp audio device") +log.info('Successfully initialized to harp audio device') # TODO: produce audio without creating state machine? diff --git a/scripts/hardware_validation/identify_hardware.py b/scripts/hardware_validation/identify_hardware.py index f77dbe39a..9daeeafb2 100644 --- a/scripts/hardware_validation/identify_hardware.py +++ b/scripts/hardware_validation/identify_hardware.py @@ -1,7 +1,8 @@ -import sys +import array import glob +import sys + from serial import Serial, SerialException -import array if sys.platform.startswith('win'): ports = ['COM%s' % (i + 1) for i in range(256)] @@ -11,7 +12,7 @@ elif sys.platform.startswith('darwin'): ports = glob.glob('/dev/tty.*') else: - raise EnvironmentError('Unsupported platform') + raise OSError('Unsupported platform') def query(s_obj, req, n=1): @@ -29,28 +30,33 @@ def query(s_obj, req, n=1): s.flush() if trial == 0 and query(s, b'6') == b'5': v = array.array('H', query(s, b'F', 4)) - match(v[1]): - case 1: hw = '0.5' - case 2: hw = 'r0.7+' - case 3: hw = 'r2.0-2.5' - case 4: hw = '2+ r1.0' - case _: hw = '(unknown version)' - m = 'Bpod {}, firmware {}'.format(hw, v[0]) + match v[1]: + case 1: + hw = '0.5' + case 2: + hw = 'r0.7+' + case 3: + hw = 'r2.0-2.5' + case 4: + hw = '2+ r1.0' + case _: + hw = '(unknown version)' + m = f'Bpod {hw}, firmware {v[0]}' s.write(b'Z') break elif trial == 1 and query(s, b'C') == (218).to_bytes(1, 'little'): v = 2 if len(query(s, b'#')) > 0 else 1 - m = 'frame2ttl, v{}'.format(v) + m = f'frame2ttl, v{v}' break elif trial == 2 and len(query(s, b'Q', 2)) > 1 and query(s, b'P00', 1) == (1).to_bytes(1, 'little'): v = '2+' if query(s, b'I') == (0).to_bytes(1, 'little') else 1 - m = 'rotary encoder module, v{}'.format(v) + m = f'rotary encoder module, v{v}' break elif trial == 3: pass s.flush() s.close() - print('{}: {}'.format(s.portstr, m)) + print(f'{s.portstr}: {m}') except (OSError, SerialException): pass diff --git a/scripts/hardware_validation/verify_camera_trigger.py b/scripts/hardware_validation/verify_camera_trigger.py index 69e690d73..f111b901e 100644 --- a/scripts/hardware_validation/verify_camera_trigger.py +++ b/scripts/hardware_validation/verify_camera_trigger.py @@ -1,10 +1,9 @@ +import types from pathlib import Path - -from pybpodapi.protocol import Bpod, StateMachine from time import time -import types import iblrig.base_tasks +from pybpodapi.protocol import Bpod, StateMachine last_time = time() @@ -14,7 +13,7 @@ def softcode_handler(self, data): now = time() d = 1 / (now - last_time) last_time = now - print("{:.2f} Hz".format(d)) + print(f'{d:.2f} Hz') file_settings = Path(iblrig.__file__).parents[1].joinpath('settings', 'hardware_settings.yaml') @@ -25,26 +24,22 @@ def softcode_handler(self, data): sma = StateMachine(bpod) sma.set_global_timer(1, 5) sma.add_state( - state_name="start", + state_name='start', state_timer=0, - state_change_conditions={"Tup": "wait"}, - output_actions=[("GlobalTimerTrig", 1), - ("PWM1", 0)] + state_change_conditions={'Tup': 'wait'}, + output_actions=[('GlobalTimerTrig', 1), ('PWM1', 0)], ) sma.add_state( - state_name="wait", + state_name='wait', state_timer=0, - state_change_conditions={"Port1In": "flash", - "GlobalTimer1_End": "exit"}, - output_actions=[("PWM1", 0)] + state_change_conditions={'Port1In': 'flash', 'GlobalTimer1_End': 'exit'}, + output_actions=[('PWM1', 0)], ) sma.add_state( - state_name="flash", + state_name='flash', state_timer=0.001, - state_change_conditions={"Tup": "wait", - "GlobalTimer1_End": "exit"}, - output_actions=[("PWM1", 255), - ("SoftCode", 1)] + state_change_conditions={'Tup': 'wait', 'GlobalTimer1_End': 'exit'}, + output_actions=[('PWM1', 255), ('SoftCode', 1)], ) bpod.send_state_machine(sma) diff --git a/scripts/hardware_validation/verify_hardware.py b/scripts/hardware_validation/verify_hardware.py index 904d8cf08..0ac8eb73b 100644 --- a/scripts/hardware_validation/verify_hardware.py +++ b/scripts/hardware_validation/verify_hardware.py @@ -1,19 +1,18 @@ import platform +import time from glob import glob from pathlib import Path -import time from struct import unpack +import numpy as np +import serial.tools.list_ports import usb.core - from serial import Serial -import serial.tools.list_ports -import numpy as np -# import pandas as pd +import iblrig.base_tasks +# import pandas as pd from iblutil.util import setup_logger -import iblrig.base_tasks from pybpodapi.protocol import Bpod, StateMachine # set up logging @@ -98,7 +97,7 @@ def log_fun(msg_type: str = 'info', msg: str = '', last: bool = False): # check individual serial ports port_info = [i for i in serial.tools.list_ports.comports()] -for (description, port) in ports.items(): +for description, port in ports.items(): log_fun('head', f'Checking serial port {description} ({port}):') # check if serial port exists @@ -122,12 +121,12 @@ def log_fun(msg_type: str = 'info', msg: str = '', last: bool = False): # check correct assignments of serial ports ok = False match description: - case "COM_BPOD": + case 'COM_BPOD': device_name = 'Bpod Finite State Machine' ok = query(s, b'6') == b'5' if ok: s.write(b'Z') - case "COM_F2TTL": + case 'COM_F2TTL': device_name = 'Frame2TTL' try: s.write(b'C') @@ -140,7 +139,7 @@ def log_fun(msg_type: str = 'info', msg: str = '', last: bool = False): continue finally: ok = s.read() == (218).to_bytes(1, 'little') - case "COM_ROTARY_ENCODER": + case 'COM_ROTARY_ENCODER': device_name = 'Rotary Encoder Module' ok = len(query(s, b'Q', 2)) > 1 and query(s, b'P00', 1) == (1).to_bytes(1, 'little') case _: @@ -153,7 +152,7 @@ def log_fun(msg_type: str = 'info', msg: str = '', last: bool = False): log_fun('fail', f'device on port {port} does not appear to be a {device_name}', last=True) # To Do: Look into this required delay - time.sleep(.02) + time.sleep(0.02) # check bpod modules bpod = Bpod(hw_settings['device_bpod']['COM_BPOD']) @@ -171,16 +170,16 @@ def log_fun(msg_type: str = 'info', msg: str = '', last: bool = False): s = serial.Serial(ports['COM_ROTARY_ENCODER']) s.write(b'I') - time.sleep(.02) + time.sleep(0.02) if s.in_waiting == 0: s.write(b'x') - v = "1.x" if s.read(1) == (1).to_bytes(1, 'little') else "2+" + v = '1.x' if s.read(1) == (1).to_bytes(1, 'little') else '2+' log_fun('info', f'hardware version: {v}') log_fun('info', f'firmware version: {bpod.modules[0].firmware_version}') s.write(b'Z') p = np.frombuffer(query(s, b'Q', 2), dtype=np.int16)[0] - log_fun('warn', 'please move the wheel to the left (animal\'s POV) by a quarter turn') + log_fun('warn', "please move the wheel to the left (animal's POV) by a quarter turn") while np.abs(p) < 200: p = np.frombuffer(query(s, b'Q', 2), dtype=np.int16)[0] if p > 0: @@ -198,13 +197,13 @@ def log_fun(msg_type: str = 'info', msg: str = '', last: bool = False): if not dev: log_fun('fail', 'Cannot find Harp Sound Card') else: - log_fun('pass', 'found USB device {:04X}:{:04X} (Harp Sound Card)'.format(dev.idVendor, dev.idProduct)) + log_fun('pass', f'found USB device {dev.idVendor:04X}:{dev.idProduct:04X} (Harp Sound Card)') dev = next((p for p in serial.tools.list_ports.comports() if (p.vid == 1027 and p.pid == 24577)), None) if not dev: - log_fun('fail', 'cannot find Harp Sound Card\'s Serial port - did you plug in *both* USB ports of the device?') + log_fun('fail', "cannot find Harp Sound Card's Serial port - did you plug in *both* USB ports of the device?") else: - log_fun('pass', 'found USB device {:04X}:{:04X} (FT232 UART), serial port: {}'.format(dev.vid, dev.pid, dev.name)) + log_fun('pass', f'found USB device {dev.vid:04X}:{dev.pid:04X} (FT232 UART), serial port: {dev.name}') module = [m for m in modules if m.name.startswith('SoundCard')] if len(module) == 0: @@ -212,9 +211,11 @@ def log_fun(msg_type: str = 'info', msg: str = '', last: bool = False): elif len(module) > 1: log_fun('fail', 'more than one Harp Sound Card connected to the Bpod', last=True) else: - log_fun('pass', - f'module "{module[0].name}" is connected to the Bpod\'s module port #{module[0].serial_port}', - last=True) + log_fun( + 'pass', + f'module "{module[0].name}" is connected to the Bpod\'s module port #{module[0].serial_port}', + last=True, + ) case _: pass @@ -224,7 +225,7 @@ def log_fun(msg_type: str = 'info', msg: str = '', last: bool = False): log_fun('pass', f'module "{module.name}" is connected to the Bpod\'s module port #{module.serial_port}') log_fun('info', f'firmware version: {module.firmware_version}') module.start_module_relay() - bpod.bpod_modules.module_write(module, "R") + bpod.bpod_modules.module_write(module, 'R') (t, p, h) = unpack('3f', bytes(bpod.bpod_modules.module_read(module, 12))) module.stop_module_relay() log_fun('info', f'temperature: {t:.1f} °C') @@ -233,13 +234,13 @@ def log_fun(msg_type: str = 'info', msg: str = '', last: bool = False): else: log_fun('fail', 'Could not find Ambient Module', last=True) -if "device_cameras" in hw_settings and isinstance(hw_settings["device_cameras"], dict): +if 'device_cameras' in hw_settings and isinstance(hw_settings['device_cameras'], dict): log_fun('head', 'Checking Camera Trigger:') sma = StateMachine(bpod) sma.add_state( - state_name="collect", + state_name='collect', state_timer=1, - state_change_conditions={"Tup": "exit"}, + state_change_conditions={'Tup': 'exit'}, ) bpod.send_state_machine(sma) bpod.run_state_machine(sma) diff --git a/scripts/ibllib/purge_rig_data.py b/scripts/ibllib/purge_rig_data.py index 2d679bcfb..0c9a9ae6b 100644 --- a/scripts/ibllib/purge_rig_data.py +++ b/scripts/ibllib/purge_rig_data.py @@ -7,8 +7,9 @@ import logging from fnmatch import fnmatch from pathlib import Path -from one.alf.io import iter_sessions, iter_datasets + from one.alf.files import get_session_path +from one.alf.io import iter_datasets, iter_sessions from one.api import ONE log = logging.getLogger('iblrig') @@ -61,7 +62,10 @@ def purge_local_data(local_folder, filename='*', lab=None, dry=False, one=None): parser.add_argument('folder', help='Local iblrig_data folder') parser.add_argument('file', help='File name to search and destroy for every session') parser.add_argument( - '-lab', required=False, default=None, help='Lab name, in case sessions conflict between labs. default: None', + '-lab', + required=False, + default=None, + help='Lab name, in case sessions conflict between labs. default: None', ) parser.add_argument('--dry', required=False, default=False, action='store_true', help='Dry run? default: False') args = parser.parse_args() diff --git a/scripts/ibllib/register_session.py b/scripts/ibllib/register_session.py index 59ed4dbac..4fdd4f352 100644 --- a/scripts/ibllib/register_session.py +++ b/scripts/ibllib/register_session.py @@ -4,15 +4,15 @@ from ibllib.oneibl.registration import IBLRegistrationClient -log = logging.getLogger("iblrig") +log = logging.getLogger('iblrig') -if __name__ == "__main__": +if __name__ == '__main__': IBLRIG_DATA = sys.argv[1] try: - log.info("Trying to register session in Alyx...") + log.info('Trying to register session in Alyx...') IBLRegistrationClient(one=None).create_sessions(IBLRIG_DATA, dry=False) - log.info("Done") + log.info('Done') except Exception: log.error(traceback.format_exc()) - log.warning("Failed to register session on Alyx, will try again from local server after transfer") + log.warning('Failed to register session on Alyx, will try again from local server after transfer') diff --git a/scripts/ibllib/screen_stimulus_from_wheel.py b/scripts/ibllib/screen_stimulus_from_wheel.py index 531f74208..e60f7aafa 100644 --- a/scripts/ibllib/screen_stimulus_from_wheel.py +++ b/scripts/ibllib/screen_stimulus_from_wheel.py @@ -1,7 +1,8 @@ import math -from one.api import ONE + import numpy as np +from one.api import ONE WHEEL_RADIUS = 31 USER_DEFINED_GAIN = 4.0 @@ -42,27 +43,27 @@ def get_stim_from_wheel(eid, tr): one = ONE() dataset_types = [ - "trials.goCue_times", - "trials.feedback_times", - "trials.feedbackType", - "trials.contrastLeft", - "trials.contrastRight", - "trials.choice", + 'trials.goCue_times', + 'trials.feedback_times', + 'trials.feedbackType', + 'trials.contrastLeft', + 'trials.contrastRight', + 'trials.choice', ] one.load(eid, dataset_types=dataset_types, dclass_output=True) - alf_path = one.path_from_eid(eid) / "alf" - trials = one.load_object(alf_path, "trials") - wheel = one.load_object(eid, "wheel") + alf_path = one.path_from_eid(eid) / 'alf' + trials = one.load_object(alf_path, 'trials') + wheel = one.load_object(eid, 'wheel') # check where stimulus started for initial shift - if np.isnan(trials["contrastLeft"][tr]): + if np.isnan(trials['contrastLeft'][tr]): init_pos = -35 else: init_pos = 35 # the screen stim is only coupled to the wheel in this time - wheel_start_idx = find_nearest(wheel.timestamps, trials["goCue_times"][tr]) - wheel_end_idx = find_nearest(wheel.timestamps, trials["feedback_times"][tr]) + wheel_start_idx = find_nearest(wheel.timestamps, trials['goCue_times'][tr]) + wheel_end_idx = find_nearest(wheel.timestamps, trials['feedback_times'][tr]) wheel_pos = wheel.position[wheel_start_idx:wheel_end_idx] wheel_times = wheel.timestamps[wheel_start_idx:wheel_end_idx] @@ -79,4 +80,4 @@ def get_stim_from_wheel(eid, tr): # f = interp1d(wheel_times, absolute_screen_deg) # as you might want to get values as shown on screen, i.e. at 60 Hz - return wheel_pos, screen_deg, trials["feedbackType"][tr], wheel_times + return wheel_pos, screen_deg, trials['feedbackType'][tr], wheel_times From 2f78d7e072e8a41469b566b4599526bc8d78fdbe Mon Sep 17 00:00:00 2001 From: Olivier Winter Date: Thu, 30 Nov 2023 16:47:05 +0000 Subject: [PATCH 13/15] Alyx fonctionalities: users and check lab location (#563) * add error message and box if lab location not found in alyx * add unit tests to hardware validation classes * finish the implementation of Alyx lab location test using hardware validators * get the set of users that have delegate access to populate the wizard combobox --- iblrig/gui/wizard.py | 24 +++-- ...rdware_tests.py => hardware_validation.py} | 88 ++++++++++++------- iblrig/path_helper.py | 4 - iblrig/test/test_hardware_validation.py | 47 ++++++++++ iblrig/test/test_path_helper.py | 5 -- 5 files changed, 119 insertions(+), 49 deletions(-) rename iblrig/{hardware_tests.py => hardware_validation.py} (51%) create mode 100644 iblrig/test/test_hardware_validation.py diff --git a/iblrig/gui/wizard.py b/iblrig/gui/wizard.py index 1613c3634..81c3a5865 100644 --- a/iblrig/gui/wizard.py +++ b/iblrig/gui/wizard.py @@ -14,17 +14,14 @@ from pathlib import Path from typing import Any, Optional -import yaml from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import QThread, QThreadPool from PyQt5.QtWidgets import QStyle import iblrig_tasks from one.api import ONE - try: import iblrig_custom_tasks - CUSTOM_TASKS = True except ImportError: CUSTOM_TASKS = False @@ -39,7 +36,9 @@ from iblrig.misc import _get_task_argument_parser from iblrig.version_management import check_for_updates, get_changelog, is_dirty from iblutil.util import setup_logger +import iblrig.hardware_validation from pybpodapi import exceptions +from iblrig.path_helper import load_settings_yaml log = setup_logger('iblrig') @@ -96,7 +95,8 @@ class RigWizardModel: subject_details: tuple | None = None def __post_init__(self): - self.iblrig_settings = iblrig.path_helper.load_settings_yaml() + self.iblrig_settings = load_settings_yaml('iblrig_settings.yaml') + self.hardware_settings = load_settings_yaml('hardware_settings.yaml') self.all_users = [self.iblrig_settings['ALYX_USER']] if self.iblrig_settings['ALYX_USER'] else [] self.all_procedures = sorted(PROCEDURES) @@ -118,8 +118,6 @@ def __post_init__(self): self.all_subjects = [self.test_subject_name] + sorted( [f.name for f in folder_subjects.glob('*') if f.is_dir() and f.name != self.test_subject_name] ) - file_settings = Path(iblrig.__file__).parents[1].joinpath('settings', 'hardware_settings.yaml') - self.hardware_settings = yaml.safe_load(file_settings.read_text()) def get_task_extra_parser(self, task_name=None): """ @@ -140,14 +138,28 @@ def connect(self, username=None, one=None): self.one = ONE(base_url=self.iblrig_settings['ALYX_URL'], username=username, mode='local') else: self.one = one + self.hardware_settings['RIG_NAME'] + # get subjects from alyx: this is the set of subjects that are alive and not stock in the lab defined in settings rest_subjects = self.one.alyx.rest('subjects', 'list', alive=True, stock=False, lab=self.iblrig_settings['ALYX_LAB']) self.all_subjects.remove(self.test_subject_name) self.all_subjects = sorted(set(self.all_subjects + [s['nickname'] for s in rest_subjects])) self.all_subjects = [self.test_subject_name] + self.all_subjects + # for the users we get all the users responsible for the set of subjects self.all_users = sorted(set([s['responsible_user'] for s in rest_subjects] + self.all_users)) + # then from the list of users we find all others users that have delegate access to the subjects + rest_users_with_delegates = self.one.alyx.rest('users', 'list', no_cache=True, + django=f'username__in,{self.all_users},allowed_users__isnull,False') + for user_with_delegate in rest_users_with_delegates: + self.all_users.extend(user_with_delegate['allowed_users']) + self.all_users = list(set(self.all_users)) + # then get the projects that map to the set of users rest_projects = self.one.alyx.rest('projects', 'list') projects = [p['name'] for p in rest_projects if (username in p['users'] or len(p['users']) == 0)] self.all_projects = sorted(set(projects + self.all_projects)) + # since we are connecting to Alyx, validate some parameters to ensure a smooth extraction + result = iblrig.hardware_validation.ValidateAlyxLabLocation().run(self.one) + if result.status == 'FAIL': + QtWidgets.QMessageBox().critical(None, 'Error', f"{result.message}\n\n{result.solution}") def get_subject_details(self, subject): self.subject_details_worker = SubjectDetailsWorker(subject) diff --git a/iblrig/hardware_tests.py b/iblrig/hardware_validation.py similarity index 51% rename from iblrig/hardware_tests.py rename to iblrig/hardware_validation.py index 51dfaff2f..6440c74be 100644 --- a/iblrig/hardware_tests.py +++ b/iblrig/hardware_validation.py @@ -2,25 +2,21 @@ import re from abc import ABC, abstractmethod from dataclasses import dataclass -from pathlib import Path from typing import Any, Literal +import requests -import yaml from serial import Serial, SerialException from serial.tools import list_ports from serial_singleton import SerialSingleton, filter_ports -from iblrig.constants import BASE_DIR +from iblrig.path_helper import load_settings_yaml from iblutil.util import setup_logger -_file_settings = Path(BASE_DIR).joinpath('settings', 'hardware_settings.yaml') -_hardware_settings = yaml.safe_load(_file_settings.read_text()) - log = setup_logger('iblrig', level='DEBUG') @dataclass -class TestResult: +class ValidateResult: status: Literal['PASS', 'INFO', 'FAIL'] = 'FAIL' message: str = '' ext_message: str = '' @@ -29,24 +25,29 @@ class TestResult: exception: Exception | None = None -class TestHardwareException(Exception): - def __init__(self, results: TestResult): +class ValidateHardwareException(Exception): + def __init__(self, results: ValidateResult): super().__init__(results.message) self.results = results -class TestHardware(ABC): +class ValidateHardware(ABC): log_results: bool = True raise_fail_as_exception: bool = False - def __init__(self): - self.last_result = self.run() + def __init__(self, iblrig_settings=None, hardware_settings=None): + self.iblrig_settings = iblrig_settings or load_settings_yaml('iblrig_settings.yaml') + self.hardware_settings = hardware_settings or load_settings_yaml('hardware_settings.yaml') @abstractmethod - def run(self): + def _run(self): ... - def process(self, results: TestResult) -> None: + def run(self, *args, **kwargs): + self.process(result := self._run(*args, **kwargs)) + return result + + def process(self, results: ValidateResult) -> None: if self.log_results: match results.status: case 'PASS': @@ -68,43 +69,41 @@ def process(self, results: TestResult) -> None: if self.raise_fail_as_exception and results.status == 'FAIL': if results.exception is not None: - raise TestHardwareException(results) from results.exception + raise ValidateHardwareException(results) from results.exception else: - raise TestHardwareException(results) + raise ValidateHardwareException(results) -class TestHardwareDevice(TestHardware): +class ValidateHardwareDevice(ValidateHardware): device_name: str @abstractmethod - def run(self): + def _run(self): ... - def __init__(self): + def __init__(self, *args, **kwargs): if self.log_results: log.info(f'Running hardware tests for {self.device_name}:') - super().__init__() + super().__init__(*args, **kwargs) -class TestSerialDevice(TestHardwareDevice): +class ValidateSerialDevice(ValidateHardwareDevice): port: str port_properties: None | dict[str, Any] serial_queries: None | dict[tuple[object, int], bytes] - def run(self) -> TestResult: + def _run(self) -> ValidateResult: if self.port is None: - result = TestResult('FAIL', f'No serial port defined for {self.device_name}') + result = ValidateResult('FAIL', f'No serial port defined for {self.device_name}') elif next((p for p in list_ports.comports() if p.device == self.port), None) is None: - result = TestResult('FAIL', f'`{self.port}` is not a valid serial port') + result = ValidateResult('FAIL', f'`{self.port}` is not a valid serial port') else: try: Serial(self.port, timeout=1).close() except SerialException as e: - result = TestResult('FAIL', f'`{self.port}` cannot be connected to', exception=e) + result = ValidateResult('FAIL', f'`{self.port}` cannot be connected to', exception=e) else: - result = TestResult('PASS', f'`{self.port}` is a valid serial port that can be connected to') - self.process(result) - + result = ValidateResult('PASS', f'`{self.port}` is a valid serial port that can be connected to') # first, test for properties of the serial port without opening the latter (VID, PID, etc) passed = self.port in filter_ports(**self.port_properties) if self.port_properties is not None else False @@ -118,19 +117,40 @@ def run(self) -> TestResult: break if passed: - result = TestResult('PASS', f'Device on `{self.port}` does in fact seem to be a {self.device_name}') + result = ValidateResult('PASS', f'Device on `{self.port}` does in fact seem to be a {self.device_name}') else: - result = TestResult('FAIL', f'Device on `{self.port}` does NOT seem to be a {self.device_name}') - self.process(result) + result = ValidateResult('FAIL', f'Device on `{self.port}` does NOT seem to be a {self.device_name}') return result -class TestRotaryEncoder(TestSerialDevice): +class ValidateRotaryEncoder(ValidateSerialDevice): device_name = 'Rotary Encoder Module' - port = _hardware_settings['device_rotary_encoder']['COM_ROTARY_ENCODER'] port_properties = {'vid': 0x16C0} serial_queries = {(b'Q', 2): b'^..$', (b'P00', 1): b'\x01'} - def run(self): + @property + def port(self): + return self.hardware_settings['device_rotary_encoder']['COM_ROTARY_ENCODER'] + + def _run(self): super().run() + + +class ValidateAlyxLabLocation(ValidateHardware): + """ + This class validates that the rig name in the hardware_settings.yaml file is exists in Alyx. + """ + raise_fail_as_exception: bool = False + + def _run(self, one): + try: + one.alyx.rest('locations', 'read', id=self.hardware_settings['RIG_NAME']) + results_kwargs = dict(status='PASS', message='') + except requests.exceptions.HTTPError: + error_message = f'Could not find rig name {self.hardware_settings["RIG_NAME"]} in Alyx' + solution = f'Please check the RIG_NAME key in the settings/hardware_settings.yaml file ' \ + f'and make sure it is created in Alyx here: ' \ + f'{self.iblrig_settings["ALYX_URL"]}/admin/misc/lablocation/' + results_kwargs = dict(status='FAIL', message=error_message, solution=solution) + return ValidateResult(**results_kwargs) diff --git a/iblrig/path_helper.py b/iblrig/path_helper.py index 382e7ad36..dfa4c6918 100644 --- a/iblrig/path_helper.py +++ b/iblrig/path_helper.py @@ -158,10 +158,6 @@ def get_iblrig_path() -> Path or None: return Path(iblrig.__file__).parents[1] -def get_iblrig_params_path() -> Path or None: - return get_iblrig_path().joinpath('pybpod_fixtures') - - def get_commit_hash(folder: str): here = os.getcwd() os.chdir(folder) diff --git a/iblrig/test/test_hardware_validation.py b/iblrig/test/test_hardware_validation.py new file mode 100644 index 000000000..929c70f11 --- /dev/null +++ b/iblrig/test/test_hardware_validation.py @@ -0,0 +1,47 @@ +import unittest + +from one.api import ONE +import iblrig.hardware_validation +from iblrig.path_helper import load_settings_yaml +from ibllib.tests import TEST_DB # noqa + +VALIDATORS_INIT_KWARGS = dict( + iblrig_settings=load_settings_yaml('iblrig_settings_template.yaml'), + hardware_settings=load_settings_yaml('hardware_settings_template.yaml') +) + + +class DummyValidateHardware(iblrig.hardware_validation.ValidateHardware): + + def _run(self, passes=True): + if passes: + return iblrig.hardware_validation.ValidateResult(status='PASS', message='Dummy test passed') + else: + return iblrig.hardware_validation.ValidateResult(status='FAIL', message='Dummy test failed') + + +class TestHardwareValidationBase(unittest.TestCase): + def test_dummy(self): + td = DummyValidateHardware(**VALIDATORS_INIT_KWARGS) + self.assertIsInstance(td.iblrig_settings, dict) + self.assertIsInstance(td.hardware_settings, dict) + td.run(passes=True) + td.run(passes=False) + + +class TestInstantiateClasses(unittest.TestCase): + + def test_hardware_classes(self): + iblrig.hardware_validation.ValidateRotaryEncoder(**VALIDATORS_INIT_KWARGS) + + +class TestAlyxValidation(unittest.TestCase): + + def test_lab_location(self): + one = ONE(**TEST_DB, mode='remote') + import copy + kwargs = copy.deepcopy(VALIDATORS_INIT_KWARGS) + kwargs['hardware_settings']['RIG_NAME'] = '_iblrig_carandinilab_ephys_0' + v = iblrig.hardware_validation.ValidateAlyxLabLocation(**kwargs) + result = v.run(one) + assert result.status == 'PASS' diff --git a/iblrig/test/test_path_helper.py b/iblrig/test/test_path_helper.py index 2c1e00f6a..cd2f60488 100644 --- a/iblrig/test/test_path_helper.py +++ b/iblrig/test/test_path_helper.py @@ -16,11 +16,6 @@ def test_get_iblrig_path(self): self.assertIsNotNone(p) self.assertIsInstance(p, Path) - def test_get_iblrig_params_path(self): - p = iblrig.path_helper.get_iblrig_params_path() - self.assertIsNotNone(p) - self.assertIsInstance(p, Path) - def test_get_commit_hash(self): import subprocess From da34d83b5a8d74eb7f506f93a8ae156416984280 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Thu, 30 Nov 2023 17:26:11 +0000 Subject: [PATCH 14/15] ready for 8.12.10 --- CHANGELOG.md | 3 +++ iblrig/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b44323f7f..3c9cfda53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog 8.12.10 ------- * ignore user-side changes to bonsai layouts (for camera workflows only) +* error message if rig-name is not defined in Alyx +* populate delegate users +* the usual: minor fixes, clean-ups and unit-tests 8.12.9 ------ diff --git a/iblrig/__init__.py b/iblrig/__init__.py index f8ed3e9d3..794d840ac 100644 --- a/iblrig/__init__.py +++ b/iblrig/__init__.py @@ -4,7 +4,7 @@ # 3) Check CI and eventually wet lab test # 4) Pull request to iblrigv8 # 5) git tag the release in accordance to the version number below (after merge!) -__version__ = '8.12.9' +__version__ = '8.12.10' # The following method call will try to get post-release information (i.e. the number of commits since the last tagged # release corresponding to the one above), plus information about the state of the local repository (dirty/broken) From 7cce1692e3a2b96364122c1a78ff2beda6fe0374 Mon Sep 17 00:00:00 2001 From: olivier Date: Thu, 30 Nov 2023 17:38:07 +0000 Subject: [PATCH 15/15] rescue CI --- iblrig/gui/wizard.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/iblrig/gui/wizard.py b/iblrig/gui/wizard.py index 81c3a5865..4051db57b 100644 --- a/iblrig/gui/wizard.py +++ b/iblrig/gui/wizard.py @@ -132,7 +132,13 @@ def get_task_extra_parser(self, task_name=None): spec.loader.exec_module(task) return task.Session.extra_parser() - def connect(self, username=None, one=None): + def connect(self, username=None, one=None, gui=False): + """ + :param username: + :param one: + :param gui: bool: whether or not a Qt Application is running to throw an error message + :return: + """ if one is None: username = username or self.iblrig_settings['ALYX_USER'] self.one = ONE(base_url=self.iblrig_settings['ALYX_URL'], username=username, mode='local') @@ -158,7 +164,7 @@ def connect(self, username=None, one=None): self.all_projects = sorted(set(projects + self.all_projects)) # since we are connecting to Alyx, validate some parameters to ensure a smooth extraction result = iblrig.hardware_validation.ValidateAlyxLabLocation().run(self.one) - if result.status == 'FAIL': + if result.status == 'FAIL' and gui: QtWidgets.QMessageBox().critical(None, 'Error', f"{result.message}\n\n{result.solution}") def get_subject_details(self, subject): @@ -583,7 +589,7 @@ def _set_task_arg(self, key, value): self.task_arguments[key] = value def alyx_connect(self): - self.model.connect() + self.model.connect(gui=True) self.model2view() def _filter_subjects(self):