From 0b1a10f415140861e56a7f63d6502feca1255e6c Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Thu, 20 Feb 2025 14:30:26 +0000 Subject: [PATCH 1/7] add warning for legacy tasks --- iblrig/base_choice_world.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/iblrig/base_choice_world.py b/iblrig/base_choice_world.py index e7ed50ee9..866058933 100644 --- a/iblrig/base_choice_world.py +++ b/iblrig/base_choice_world.py @@ -190,6 +190,19 @@ def _run(self): # Start state machine definition # ============================================================================= sma = self.get_state_machine_trial(i) + + # Check if state machine uses deprecated way of waiting for the camera / initial delay + if i == 0: + if (5, SOFTCODE.TRIGGER_CAMERA) in sma.output_matrix[0] and sma.state_names[1] == 'delay_initiation': + log.warning('********************************************************') + log.warning('ATTENTION! YOUR TASK NEEDS UPDATING!') + log.warning('Camera and initial delay should not be handled by task.') + log.warning("Please refer to IBLRIG's documentation for details.") + log.warning('********************************************************') + time.sleep(5) + else: + pass # todo: add delay until camera is started + log.debug('Sending state machine to bpod') # Send state machine description to Bpod device self.bpod.send_state_machine(sma) From d0ebf50b34e5a7b6b830e6e290f6580c6cebc9ae Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Thu, 20 Feb 2025 14:36:00 +0000 Subject: [PATCH 2/7] clean up state machine loop --- iblrig/base_choice_world.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/iblrig/base_choice_world.py b/iblrig/base_choice_world.py index 866058933..927ba8716 100644 --- a/iblrig/base_choice_world.py +++ b/iblrig/base_choice_world.py @@ -183,15 +183,11 @@ def _run(self): """Run the task with the actual state machine.""" time_last_trial_end = time.time() for i in range(self.task_params.NTRIALS): # Main loop - # t_overhead = time.time() + # obtain state machine definition self.next_trial() - log.info(f'Starting trial: {i}') - # ============================================================================= - # Start state machine definition - # ============================================================================= sma = self.get_state_machine_trial(i) - # Check if state machine uses deprecated way of waiting for the camera / initial delay + # check if state machine uses deprecated way of waiting for camera / initial delay if i == 0: if (5, SOFTCODE.TRIGGER_CAMERA) in sma.output_matrix[0] and sma.state_names[1] == 'delay_initiation': log.warning('********************************************************') @@ -203,21 +199,25 @@ def _run(self): else: pass # todo: add delay until camera is started - log.debug('Sending state machine to bpod') # Send state machine description to Bpod device + log.debug('Sending state machine to bpod') self.bpod.send_state_machine(sma) - # t_overhead = time.time() - t_overhead + # The ITI_DELAY_SECS defines the grey screen period within the state machine, where the # Bpod TTL is HIGH. The DEAD_TIME param defines the time between last trial and the next dead_time = self.task_params.get('DEAD_TIME', 0.5) dt = self.task_params.ITI_DELAY_SECS - dead_time - (time.time() - time_last_trial_end) + # wait to achieve the desired ITI duration if dt > 0: time.sleep(dt) - # Run state machine + + # run state machine + log.info(f'Starting trial: {i}') log.debug('running state machine') self.bpod.run_state_machine(sma) # Locks until state machine 'exit' is reached time_last_trial_end = time.time() + # handle pause event flag_pause = self.paths.SESSION_FOLDER.joinpath('.pause') flag_stop = self.paths.SESSION_FOLDER.joinpath('.stop') From 8356581c0604263d93bd644e38884763a6f74283 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Thu, 20 Feb 2025 18:01:07 +0000 Subject: [PATCH 3/7] move waiting for camera trigger / initial delay out of the task's state machine --- iblrig/base_choice_world.py | 158 ++++++++++++------ .../task.py | 30 +--- 2 files changed, 115 insertions(+), 73 deletions(-) diff --git a/iblrig/base_choice_world.py b/iblrig/base_choice_world.py index 927ba8716..fb77add70 100644 --- a/iblrig/base_choice_world.py +++ b/iblrig/base_choice_world.py @@ -1,6 +1,7 @@ """Extends the base_tasks modules by providing task logic around the Choice World protocol.""" import abc +import enum import logging import math import random @@ -8,7 +9,7 @@ import time from pathlib import Path from string import ascii_letters -from typing import Annotated, Any +from typing import Annotated, Any, final import numpy as np import pandas as pd @@ -29,6 +30,7 @@ NTRIALS_INIT = 2000 NBLOCKS_INIT = 100 + # TODO: task parameters should be verified through a pydantic model # # Probability = Annotated[float, Field(ge=0.0, le=1.0)] @@ -179,41 +181,116 @@ def start_hardware(self): self.start_mixin_bonsai_visual_stimulus() self.bpod.register_softcodes(self.softcode_dictionary()) + @final + def _wait_for_camera_and_initial_delay(self): + initial_delay = self.task_params.get('SESSION_DELAY_START', 0) + + # temporary IntEnum for storing softcodes + # SOFTCODE.TRIGGER_CAMERA is being reused, we add three more unique values + class TemporarySoftcodes(enum.IntEnum): + START_CAMERA_RECORDING = SOFTCODE.TRIGGER_CAMERA.value + WAIT_FOR_CAMERA_TRIGGER = enum.auto() + CAMERA_TRIGGER_RECEIVED = enum.auto() + STARTING_INITIAL_DELAY = enum.auto() + + # store the original softcode handler + original_softcode_handler = self.bpod.softcode_handler_function + + # define temporary softcode handler + def temporary_softcode_handler(softcode: int): + match softcode: + case TemporarySoftcodes.START_CAMERA_RECORDING: + original_softcode_handler(softcode) # pass to original handler + case TemporarySoftcodes.WAIT_FOR_CAMERA_TRIGGER: + log.info('Waiting to receive first camera trigger ...') + case TemporarySoftcodes.CAMERA_TRIGGER_RECEIVED: + log.info('Camera trigger received') + case TemporarySoftcodes.STARTING_INITIAL_DELAY: + if initial_delay > 0: + log.info(f'Waiting for {initial_delay} s') + else: + log.info('No initial delay defined') + + # overwrite softcode handler + self.bpod.softcode_handler_function = temporary_softcode_handler + + # define and run state machine + sma = StateMachine(self.bpod) + sma.add_state( + state_name='start_camera_workflow', + output_actions=[('SoftCode', TemporarySoftcodes.START_CAMERA_RECORDING)], + state_change_conditions={'Tup': 'wait_for_camera_trigger'}, + ) + sma.add_state( + state_name='wait_for_camera_trigger', + output_actions=[('SoftCode', TemporarySoftcodes.WAIT_FOR_CAMERA_TRIGGER)], + state_change_conditions={'Port1In': 'camera_trigger_received'}, + ) + sma.add_state( + state_name='camera_trigger_received', + output_actions=[('SoftCode', TemporarySoftcodes.CAMERA_TRIGGER_RECEIVED), ('BNC1', 255)], + state_change_conditions={'Tup': 'delay_initiation'}, + ) + sma.add_state( + state_name='delay_initiation', + state_timer=self.task_params.get('SESSION_DELAY_START', 0), + output_actions=[('SoftCode', TemporarySoftcodes.STARTING_INITIAL_DELAY)], + state_change_conditions={'Tup': 'exit'}, + ) + self.bpod.send_state_machine(sma) + self.bpod.run_state_machine(sma) # blocking until state-machine is finished + if initial_delay > 0: + log.info('Initial delay has passed') + + # restore original softcode handler + self.bpod.softcode_handler_function = original_softcode_handler + def _run(self): """Run the task with the actual state machine.""" time_last_trial_end = time.time() - for i in range(self.task_params.NTRIALS): # Main loop + for trial_number in range(self.task_params.NTRIALS): # Main loop # obtain state machine definition self.next_trial() - sma = self.get_state_machine_trial(i) + sma = self.get_state_machine_trial(trial_number) - # check if state machine uses deprecated way of waiting for camera / initial delay - if i == 0: + # Waiting for camera / initial delay will be handled just prior to the first trial + # This is done here to allow for backward compatibility with unadapted tasks + if trial_number == 0: + # warn if state machine uses deprecated way of waiting for camera / initial delay if (5, SOFTCODE.TRIGGER_CAMERA) in sma.output_matrix[0] and sma.state_names[1] == 'delay_initiation': - log.warning('********************************************************') - log.warning('ATTENTION! YOUR TASK NEEDS UPDATING!') - log.warning('Camera and initial delay should not be handled by task.') - log.warning("Please refer to IBLRIG's documentation for details.") - log.warning('********************************************************') - time.sleep(5) + log.warning('') + log.warning('**********************************************') + log.warning('ATTENTION: YOUR TASK DEFINITION NEEDS UPDATING') + log.warning('**********************************************') + log.warning('Camera and initial delay should not be handled') + log.warning('within the `get_state_machine_trial()` method.') + log.warning("Please see IBLRIG's documentation for details.") + log.warning('**********************************************') + log.warning('') + log.info('Waiting for 10s so you actually read this message ;-)') + time.sleep(10) else: - pass # todo: add delay until camera is started + self._wait_for_camera_and_initial_delay() - # Send state machine description to Bpod device + # send state machine description to Bpod device log.debug('Sending state machine to bpod') self.bpod.send_state_machine(sma) - # The ITI_DELAY_SECS defines the grey screen period within the state machine, where the - # Bpod TTL is HIGH. The DEAD_TIME param defines the time between last trial and the next - dead_time = self.task_params.get('DEAD_TIME', 0.5) - dt = self.task_params.ITI_DELAY_SECS - dead_time - (time.time() - time_last_trial_end) + # handle ITI durations + if trial_number > 0: + # The ITI_DELAY_SECS defines the grey screen period within the state machine, where the + # Bpod TTL is HIGH. The DEAD_TIME param defines the time between last trial and the next + dead_time = self.task_params.get('DEAD_TIME', 0.5) + dt = self.task_params.ITI_DELAY_SECS - dead_time - (time.time() - time_last_trial_end) - # wait to achieve the desired ITI duration - if dt > 0: - time.sleep(dt) + # wait to achieve the desired ITI duration + if dt > 0: + log.debug(f'Waiting {dt} s to achieve an ITI duration of {self.task_params.ITI_DELAY_SECS} s') + time.sleep(dt) # run state machine - log.info(f'Starting trial: {i}') + log.info('-----------------------') + log.info(f'Starting trial: {trial_number}') log.debug('running state machine') self.bpod.run_state_machine(sma) # Locks until state machine 'exit' is reached time_last_trial_end = time.time() @@ -221,8 +298,8 @@ def _run(self): # handle pause event flag_pause = self.paths.SESSION_FOLDER.joinpath('.pause') flag_stop = self.paths.SESSION_FOLDER.joinpath('.stop') - if flag_pause.exists() and i < (self.task_params.NTRIALS - 1): - log.info(f'Pausing session inbetween trials {i} and {i + 1}') + if flag_pause.exists() and trial_number < (self.task_params.NTRIALS - 1): + log.info(f'Pausing session inbetween trials {trial_number} and {trial_number + 1}') while flag_pause.exists() and not flag_stop.exists(): time.sleep(1) self.trials_table.at[self.trial_num, 'pause_duration'] = time.time() - time_last_trial_end @@ -231,12 +308,12 @@ def _run(self): # save trial and update log self.trial_completed(self.bpod.session.current_trial.export()) - self.ambient_sensor_table.loc[i] = self.bpod.get_ambient_sensor_reading() + self.ambient_sensor_table.loc[trial_number] = self.bpod.get_ambient_sensor_reading() self.show_trial_log() # handle stop event if flag_stop.exists(): - log.info('Stopping session after trial %d', i) + log.info('Stopping session after trial %d', trial_number) flag_stop.unlink() break @@ -328,30 +405,13 @@ def get_state_machine_trial(self, i): # we define the trial number here for subclasses that may need it sma = self._instantiate_state_machine(trial_number=i) - if i == 0: # First trial exception start camera - session_delay_start = self.task_params.get('SESSION_DELAY_START', 0) - log.info('First trial initializing, will move to next trial only if:') - log.info('1. camera is detected') - log.info(f'2. {session_delay_start} sec have elapsed') - sma.add_state( - state_name='trial_start', - state_timer=0, - state_change_conditions={'Port1In': 'delay_initiation'}, - output_actions=[('SoftCode', SOFTCODE.TRIGGER_CAMERA), ('BNC1', 255)], - ) # start camera - sma.add_state( - state_name='delay_initiation', - state_timer=session_delay_start, - output_actions=[], - state_change_conditions={'Tup': 'reset_rotary_encoder'}, - ) - else: - sma.add_state( - 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)], - ) # stop all sounds + # Signal trial start and stop all sounds + sma.add_state( + 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)], + ) # Reset the rotary encoder by sending the following opcodes via the modules serial interface # - 'Z' (ASCII 90): Set current rotary encoder position to zero diff --git a/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py index 5e240b58d..8c5e8b96f 100644 --- a/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py @@ -54,30 +54,12 @@ def choice_to_feedback_delay(self): 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) - log.info('First trial initializing, will move to next trial only if:') - log.info('1. camera is detected') - log.info(f'2. {session_delay_start} sec have elapsed') - sma.add_state( - state_name='trial_start', - state_timer=0, - state_change_conditions={'Port1In': 'delay_initiation'}, - output_actions=[('SoftCode', SOFTCODE.TRIGGER_CAMERA), ('BNC1', 255)], - ) # start camera - sma.add_state( - state_name='delay_initiation', - state_timer=session_delay_start, - output_actions=[], - state_change_conditions={'Tup': 'reset_rotary_encoder'}, - ) - else: - sma.add_state( - 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)], - ) # stop all sounds + sma.add_state( + 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)], + ) sma.add_state( state_name='reset_rotary_encoder', From 5b36351b0c55b3f1fffe16e41a09d62242f74446 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Thu, 20 Feb 2025 18:05:18 +0000 Subject: [PATCH 4/7] ruff --- iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py | 1 - 1 file changed, 1 deletion(-) diff --git a/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py b/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py index 8c5e8b96f..94efa07aa 100644 --- a/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py +++ b/iblrig_tasks/_iblrig_tasks_neuroModulatorChoiceWorld/task.py @@ -5,7 +5,6 @@ import iblrig.misc from iblrig.base_choice_world import BiasedChoiceWorldSession, BiasedChoiceWorldTrialData -from iblrig.hardware import SOFTCODE from pybpodapi.protocol import StateMachine REWARD_AMOUNTS_UL = (1, 3) From 1297d82781e5088c536c060149b7c6b052495d94 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Thu, 20 Feb 2025 18:58:24 +0000 Subject: [PATCH 5/7] documentation / version increment / changelog --- CHANGELOG.md | 4 ++ docs/source/deprecation_notes.rst | 64 +++++++++++++++++++++++++++++++ docs/source/index.rst | 1 + iblrig/__init__.py | 2 +- iblrig/base_choice_world.py | 3 +- 5 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 docs/source/deprecation_notes.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index fa6f415da..e6a6ffe79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +8.28.0 +------ +* changed: Handling of camera and initial delay moved out of ChoiceWorld's state machine definition + 8.27.3 ------ * changed: reset camera(s) prior to starting task when inconsistencies have been detected diff --git a/docs/source/deprecation_notes.rst b/docs/source/deprecation_notes.rst new file mode 100644 index 000000000..24dd453ce --- /dev/null +++ b/docs/source/deprecation_notes.rst @@ -0,0 +1,64 @@ +***************** +Deprecation Notes +***************** + + +8.28.0: Handling of Camera and Initial Delay +============================================ + +With release 8.28.0 of IBLRIG, handling of the camera initialization and the session's initial delay has been moved out of :py:class:`~iblrig.base_choice_world.ChoiceWorldSession`'s state machine definition. + +Previously, :py:meth:`~iblrig.base_choice_world.ChoiceWorldSession.get_state_machine_trial` would define the first trials as such: + +.. code-block:: python + + def get_state_machine_trial(self, i): + # we define the trial number here for subclasses that may need it + sma = self._instantiate_state_machine(trial_number=i) + + if i == 0: # First trial exception start camera + session_delay_start = self.task_params.get('SESSION_DELAY_START', 0) + log.info('First trial initializing, will move to next trial only if:') + log.info('1. camera is detected') + log.info(f'2. {session_delay_start} sec have elapsed') + sma.add_state( + state_name='trial_start', + state_timer=0, + state_change_conditions={'Port1In': 'delay_initiation'}, + output_actions=[('SoftCode', SOFTCODE.TRIGGER_CAMERA), ('BNC1', 255)], + ) # start camera + sma.add_state( + state_name='delay_initiation', + state_timer=session_delay_start, + output_actions=[], + state_change_conditions={'Tup': 'reset_rotary_encoder'}, + ) + else: + sma.add_state( + 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)], + ) # stop all sounds + + # [...] + +This has been reduced to the following: + +.. code-block:: python + + def get_state_machine_trial(self, i): + # we define the trial number here for subclasses that may need it + sma = self._instantiate_state_machine(trial_number=i) + + # Signal trial start and stop all sounds + sma.add_state( + 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)], + ) + + # [...] + +If your custom task inherits from :py:class:`~iblrig.base_choice_world.ChoiceWorldSession` and overrides :py:meth:`~iblrig.base_choice_world.ChoiceWorldSession.get_state_machine_trial` adapt it accordingly. diff --git a/docs/source/index.rst b/docs/source/index.rst index 39b97ed6c..f6101ccb1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -13,6 +13,7 @@ hardware developer_guide faq + deprecation_notes .. if-builder:: html diff --git a/iblrig/__init__.py b/iblrig/__init__.py index 525379b5e..f26ab8950 100644 --- a/iblrig/__init__.py +++ b/iblrig/__init__.py @@ -6,4 +6,4 @@ # 5) git tag the release in accordance to the version number below (after merge!) # >>> git tag 8.15.6 # >>> git push origin --tags -__version__ = '8.27.3' +__version__ = '8.28.0' diff --git a/iblrig/base_choice_world.py b/iblrig/base_choice_world.py index fb77add70..a7166a3eb 100644 --- a/iblrig/base_choice_world.py +++ b/iblrig/base_choice_world.py @@ -264,7 +264,8 @@ def _run(self): log.warning('**********************************************') log.warning('Camera and initial delay should not be handled') log.warning('within the `get_state_machine_trial()` method.') - log.warning("Please see IBLRIG's documentation for details.") + log.warning('For further details, please refer to section ') + log.warning("'Deprecation Notes' in IBLRIG's documentation.") log.warning('**********************************************') log.warning('') log.info('Waiting for 10s so you actually read this message ;-)') From 268262a873ba4cd2dcc6f35a5842cdef424fa15f Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Fri, 21 Feb 2025 15:10:46 +0000 Subject: [PATCH 6/7] remove BNC high in `_wait_for_camera_and_initial_delay()` --- iblrig/base_choice_world.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iblrig/base_choice_world.py b/iblrig/base_choice_world.py index a7166a3eb..dfd4698ae 100644 --- a/iblrig/base_choice_world.py +++ b/iblrig/base_choice_world.py @@ -228,7 +228,7 @@ def temporary_softcode_handler(softcode: int): ) sma.add_state( state_name='camera_trigger_received', - output_actions=[('SoftCode', TemporarySoftcodes.CAMERA_TRIGGER_RECEIVED), ('BNC1', 255)], + output_actions=[('SoftCode', TemporarySoftcodes.CAMERA_TRIGGER_RECEIVED)], state_change_conditions={'Tup': 'delay_initiation'}, ) sma.add_state( From 7a8067aabde92b327f094a867e8c468fa2066124 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Mon, 24 Feb 2025 12:05:38 +0000 Subject: [PATCH 7/7] add a few more details to the doc strings / typing --- iblrig/base_choice_world.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/iblrig/base_choice_world.py b/iblrig/base_choice_world.py index dfd4698ae..9979bfdc2 100644 --- a/iblrig/base_choice_world.py +++ b/iblrig/base_choice_world.py @@ -182,11 +182,17 @@ def start_hardware(self): self.bpod.register_softcodes(self.softcode_dictionary()) @final - def _wait_for_camera_and_initial_delay(self): + def _wait_for_camera_and_initial_delay(self) -> None: + """Wait for the camera to start recording and manage the initial delay. + + This method implements a temporary state machine to coordinate the process of waiting for the camera recording + to commence and to handle any specified initial delay. It should be called just prior to the start of the task. + The states defined here were previously part of the task's main state machine (see `get_state_machine_trial()`). + """ initial_delay = self.task_params.get('SESSION_DELAY_START', 0) # temporary IntEnum for storing softcodes - # SOFTCODE.TRIGGER_CAMERA is being reused, we add three more unique values + # SOFTCODE.TRIGGER_CAMERA is being reused; we add three more unique values class TemporarySoftcodes(enum.IntEnum): START_CAMERA_RECORDING = SOFTCODE.TRIGGER_CAMERA.value WAIT_FOR_CAMERA_TRIGGER = enum.auto() @@ -245,8 +251,11 @@ def temporary_softcode_handler(softcode: int): # restore original softcode handler self.bpod.softcode_handler_function = original_softcode_handler - def _run(self): - """Run the task with the actual state machine.""" + def _run(self) -> None: + """Execute the task using the defined state machine. + + This method orchestrates the execution of the task by running a state machine for a specified number of trials. + """ time_last_trial_end = time.time() for trial_number in range(self.task_params.NTRIALS): # Main loop # obtain state machine definition