Skip to content

Commit

Permalink
Merge branch 'iblrigv8dev' into valve_calibration_pt2
Browse files Browse the repository at this point in the history
  • Loading branch information
bimac committed May 21, 2024
2 parents 460979d + 83727e8 commit e44b2a1
Show file tree
Hide file tree
Showing 18 changed files with 402 additions and 313 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Changelog
* automated validation of rig components
* adaptive reward parameter for trainingPhaseChoiceWorld
* add validate_video entry-point
* switch from flake8 to ruff for linting & code-checks
* automatically set correct trigger-mode when setting up the cameras
* support rotary encoder on arbitrary module port
* add ambient sensor reading back to trial log
* allow negative stimulus gain (reverse wheel contingency)

8.18.0
------
Expand Down
27 changes: 17 additions & 10 deletions iblrig/base_choice_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,6 @@ def _run(self):
This is the method that runs the task with the actual state machine
:return:
"""
# make the bpod send spacer signals to the main sync clock for protocol discovery
self.send_spacers()
time_last_trial_end = time.time()
for i in range(self.task_params.NTRIALS): # Main loop
# t_overhead = time.time()
Expand All @@ -186,6 +184,7 @@ def _run(self):
self.bpod.run_state_machine(sma) # Locks until state machine 'exit' is reached
time_last_trial_end = time.time()
self.trial_completed(self.bpod.session.current_trial.export())
self.ambient_sensor_table.loc[i] = self.bpod.get_ambient_sensor_reading()
self.show_trial_log()

# handle pause and stop events
Expand Down Expand Up @@ -427,22 +426,24 @@ def next_trial(self):
pass

@property
def reward_amount(self):
def default_reward_amount(self):
return self.task_params.REWARD_AMOUNT_UL

def draw_next_trial_info(self, pleft=0.5, contrast=None, position=None):
"""Draw next trial variables.
This is called by the `next_trial` method before updating the Bpod state machine. This also
calls :meth:`send_trial_info_to_bonsai`.
This is called by the `next_trial` method before updating the Bpod state machine. This also
"""

def draw_next_trial_info(self, pleft=0.5, contrast=None, position=None, reward_amount=None):
if contrast is None:
contrast = misc.draw_contrast(self.task_params.CONTRAST_SET, self.task_params.CONTRAST_SET_PROBABILITY_TYPE)
assert len(self.task_params.STIM_POSITIONS) == 2, 'Only two positions are supported'
position = position or int(np.random.choice(self.task_params.STIM_POSITIONS, p=[pleft, 1 - pleft]))
quiescent_period = self.task_params.QUIESCENT_PERIOD + misc.truncated_exponential(
scale=0.35, min_value=0.2, max_value=0.5
)
reward_amount = self.default_reward_amount if reward_amount is None else reward_amount
self.trials_table.at[self.trial_num, 'quiescent_period'] = quiescent_period
self.trials_table.at[self.trial_num, 'contrast'] = contrast
self.trials_table.at[self.trial_num, 'stim_phase'] = random.uniform(0, 2 * math.pi)
Expand All @@ -452,7 +453,7 @@ def draw_next_trial_info(self, pleft=0.5, contrast=None, position=None):
self.trials_table.at[self.trial_num, 'stim_freq'] = self.task_params.STIM_FREQ
self.trials_table.at[self.trial_num, 'trial_num'] = self.trial_num
self.trials_table.at[self.trial_num, 'position'] = position
self.trials_table.at[self.trial_num, 'reward_amount'] = self.reward_amount
self.trials_table.at[self.trial_num, 'reward_amount'] = reward_amount
self.trials_table.at[self.trial_num, 'stim_probability_left'] = pleft
self.send_trial_info_to_bonsai()

Expand Down Expand Up @@ -527,13 +528,17 @@ def quiescent_period(self):
def position(self):
return self.trials_table.at[self.trial_num, 'position']

@property
def reverse_wheel(self):
return self.task_params.STIM_GAIN < 0

@property
def event_error(self):
return self.device_rotary_encoder.THRESHOLD_EVENTS[self.position]
return self.device_rotary_encoder.THRESHOLD_EVENTS[-self.position if self.reverse_wheel else self.position]

@property
def event_reward(self):
return self.device_rotary_encoder.THRESHOLD_EVENTS[-self.position]
return self.device_rotary_encoder.THRESHOLD_EVENTS[self.position if self.reverse_wheel else -self.position]


class HabituationChoiceWorldSession(ChoiceWorldSession):
Expand Down Expand Up @@ -628,7 +633,9 @@ def _run(self):
# starts online plotting
if self.interactive:
subprocess.Popen(
['viewsession', str(self.paths['DATA_FILE_PATH'])], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
['view_session', str(self.paths['DATA_FILE_PATH']), str(self.paths['SETTINGS_FILE_PATH'])],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
super()._run()

Expand Down Expand Up @@ -783,7 +790,7 @@ def __init__(self, training_phase=-1, adaptive_reward=-1.0, adaptive_gain=None,
self.trials_table['debias_trial'] = np.zeros(NTRIALS_INIT, dtype=bool)

@property
def reward_amount(self):
def default_reward_amount(self):
return self.session_info.get('ADAPTIVE_REWARD_AMOUNT_UL', self.task_params.REWARD_AMOUNT_UL)

def get_subject_training_info(self):
Expand Down
44 changes: 26 additions & 18 deletions iblrig/base_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import pybpodapi
from ibllib.oneibl.registration import IBLRegistrationClient
from iblrig import sound
from iblrig.constants import BASE_PATH, BONSAI_EXE
from iblrig.constants import BASE_PATH, BONSAI_EXE, PYSPIN_AVAILABLE
from iblrig.frame2ttl import Frame2TTL
from iblrig.hardware import SOFTCODE, Bpod, MyRotaryEncoder, sound_device_factory
from iblrig.hifi import HiFi
Expand Down Expand Up @@ -191,6 +191,8 @@ def _init_paths(self, append: bool = False):
>>> C:\iblrigv8_data\mainenlab\Subjects\SWC_043\2019-01-01\001\raw_task_data_00 # noqa
DATA_FILE_PATH: contains the bpod trials
>>> C:\iblrigv8_data\mainenlab\Subjects\SWC_043\2019-01-01\001\raw_task_data_00\_iblrig_taskData.raw.jsonable # noqa
SETTINGS_FILE_PATH: contains the task settings
>>>C:\iblrigv8_data\mainenlab\Subjects\SWC_043\2019-01-01\001\raw_task_data_00\_iblrig_taskSettings.raw.json # noqa
"""
rig_computer_paths = iblrig.path_helper.get_local_and_remote_paths(
local_path=self.iblrig_settings['iblrig_local_data_path'],
Expand Down Expand Up @@ -230,6 +232,7 @@ def _init_paths(self, append: bool = False):
self.session_info.SESSION_NUMBER = int(paths.SESSION_FOLDER.name)
paths.SESSION_RAW_DATA_FOLDER = paths.SESSION_FOLDER.joinpath(paths.TASK_COLLECTION)
paths.DATA_FILE_PATH = paths.SESSION_RAW_DATA_FOLDER.joinpath('_iblrig_taskData.raw.jsonable')
paths.SETTINGS_FILE_PATH = paths.SESSION_RAW_DATA_FOLDER.joinpath('_iblrig_taskSettings.raw.json')
return paths

def _setup_loggers(self, level='INFO', level_bpod='WARNING', file=None):
Expand Down Expand Up @@ -336,18 +339,19 @@ def _make_task_parameters_dict(self):
output_dict.update(patch_dict)
return output_dict

def save_task_parameters_to_json_file(self, destination_folder=None) -> Path:
def save_task_parameters_to_json_file(self, destination_folder: Path | None = None) -> Path:
"""
Given a session object, collects the various settings and parameters of the session and outputs them to a JSON file
Collects the various settings and parameters of the session and outputs them to a JSON file
Returns
-------
Path to the resultant JSON file
"""
output_dict = self._make_task_parameters_dict()
destination_folder = destination_folder or self.paths.SESSION_RAW_DATA_FOLDER
# Output dict to json file
json_file = destination_folder.joinpath('_iblrig_taskSettings.raw.json')
if destination_folder:
json_file = destination_folder.joinpath('_iblrig_taskSettings.raw.json')
else:
json_file = self.paths['SETTINGS_FILE_PATH']
json_file.parent.mkdir(parents=True, exist_ok=True)
with open(json_file, 'w') as outfile:
json.dump(output_dict, outfile, indent=4, sort_keys=True, default=str) # converts datetime objects to string
Expand All @@ -368,7 +372,10 @@ def one(self):
)
try:
self._one = ONE(
base_url=str(self.iblrig_settings['ALYX_URL']), username=self.iblrig_settings['ALYX_USER'], mode='remote'
base_url=str(self.iblrig_settings['ALYX_URL']),
username=self.iblrig_settings['ALYX_USER'],
mode='remote',
cache_rest=None,
)
log.info('instantiated ' + info_str)
except Exception:
Expand Down Expand Up @@ -453,7 +460,7 @@ def mock(self):
def create_session(self):
# create the session path and save json parameters in the task collection folder
# this will also create the protocol folder
self.save_task_parameters_to_json_file()
self.paths['TASK_PARAMETERS_FILE'] = self.save_task_parameters_to_json_file()
# enable file logging
logfile = self.paths.SESSION_RAW_DATA_FOLDER.joinpath('_ibl_log.info-acquisition.log')
self._setup_loggers(level=self._logger.level, file=logfile)
Expand Down Expand Up @@ -651,10 +658,13 @@ def start_mixin_bonsai_cameras(self):
configuration = self.hardware_settings.device_cameras[self.config]
if (workflow_file := self._camera_mixin_bonsai_get_workflow_file(configuration, 'setup')) is None:
return
# TODO: Disable Trigger in Bonsai workflow - PySpin won't help here
# if PYSPIN_AVAILABLE:
# from iblrig.video_pyspin import enable_camera_trigger
# enable_camera_trigger(True)

# enable trigger of cameras (so Bonsai can disable it again ... sigh)
if PYSPIN_AVAILABLE:
from iblrig.video_pyspin import enable_camera_trigger

enable_camera_trigger(True)

call_bonsai(workflow_file, wait=True) # TODO Parameterize using configuration cameras
log.info('Bonsai cameras setup module loaded: OK')

Expand Down Expand Up @@ -793,7 +803,8 @@ def start_mixin_bpod(self):
self.bpod.set_status_led(False)
assert self.bpod.is_connected
log.info('Bpod hardware module loaded: OK')
# self.send_spacers()
# make the bpod send spacer signals to the main sync clock for protocol discovery
self.send_spacers()

def send_spacers(self):
log.info('Starting task by sending a spacer signal on BNC1')
Expand Down Expand Up @@ -971,16 +982,15 @@ def start_mixin_sound(self):
match self.hardware_settings.device_sound['OUTPUT']:
case 'harp':
assert self.bpod.sound_card is not None, 'No harp sound-card connected to Bpod'
module_port = f'Serial{self.bpod.sound_card.serial_port}'
sound.configure_sound_card(
sounds=[self.sound.GO_TONE, self.sound.WHITE_NOISE],
indexes=[self.task_params.GO_TONE_IDX, self.task_params.WHITE_NOISE_IDX],
sample_rate=self.sound['samplerate'],
)
self.bpod.define_harp_sounds_actions(
module=self.bpod.sound_card,
go_tone_index=self.task_params.GO_TONE_IDX,
noise_index=self.task_params.WHITE_NOISE_IDX,
sound_port=module_port,
)
case 'hifi':
module = self.bpod.get_module('^HiFi')
Expand All @@ -991,12 +1001,10 @@ def start_mixin_sound(self):
hifi.load(index=self.task_params.WHITE_NOISE_IDX, data=self.sound.WHITE_NOISE)
hifi.push()
hifi.close()
module_port = f'Serial{module.serial_port}'
self.bpod.define_harp_sounds_actions(
module=module,
go_tone_index=self.task_params.GO_TONE_IDX,
noise_index=self.task_params.WHITE_NOISE_IDX,
sound_port=module_port,
module=module,
)
case _:
self.bpod.define_xonar_sounds_actions()
Expand Down
10 changes: 6 additions & 4 deletions iblrig/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,17 +338,19 @@ def remove_local_sessions(weeks=2, local_path=None, remote_path=None, dry=False,
return removed


def viewsession():
def view_session():
"""
Entry point for command line: usage as below
>>> viewsession /full/path/to/jsonable/_iblrig_taskData.raw.jsonable
>>> view_session /full/path/to/jsonable/_iblrig_taskData.raw.jsonable
:return: None
"""
parser = argparse.ArgumentParser()
parser.add_argument('file_jsonable', help='full file path to jsonable file')
parser.add_argument('file_settings', help='full file path to settings file', nargs='?', default=None)
args = parser.parse_args()
self = OnlinePlots()
self.run(Path(args.file_jsonable))

online_plots = OnlinePlots(task_file=args.file_jsonable, settings_file=args.file_settings)
online_plots.run(task_file=args.file_jsonable)


def flush():
Expand Down
7 changes: 6 additions & 1 deletion iblrig/gui/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dataclasses import dataclass
from pathlib import Path

import numpy as np
import pyqtgraph as pg
from pydantic import ValidationError
from PyQt5 import QtCore, QtGui, QtWidgets
Expand Down Expand Up @@ -45,7 +46,7 @@
from iblrig.path_helper import load_pydantic_yaml
from iblrig.pydantic_definitions import HardwareSettings, RigSettings
from iblrig.tools import alyx_reachable, get_anydesk_id, internet_available
from iblrig.version_management import check_for_updates, get_changelog, is_dirty
from iblrig.version_management import check_for_updates, get_changelog
from iblutil.util import setup_logger
from one.webclient import AlyxClient
from pybpodapi.exceptions.bpod_error import BpodErrorException
Expand Down Expand Up @@ -825,6 +826,9 @@ def controls_for_extra_parameters(self):
)
widget.valueChanged.emit(widget.value())

case 'reward_set_ul':
label = 'Reward Set, μl'

case 'adaptive_gain':
label = 'Stimulus Gain'
minimum = 0
Expand All @@ -844,6 +848,7 @@ def controls_for_extra_parameters(self):

case 'stim_gain':
label = 'Stimulus Gain'
widget.setMinimum(-np.inf)

widget.wheelEvent = lambda event: None
layout.addRow(self.tr(label), widget)
Expand Down
Loading

0 comments on commit e44b2a1

Please sign in to comment.