Skip to content

Commit

Permalink
Merge pull request #730 from int-brain-lab/iblrigv8dev
Browse files Browse the repository at this point in the history
8.24.4
  • Loading branch information
bimac authored Oct 18, 2024
2 parents 0f4a573 + 9801a3e commit 60403a3
Show file tree
Hide file tree
Showing 17 changed files with 364 additions and 479 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ XDG_CACHE_HOME/
app.log
/.idea/*
/venv/
/.venv/
*.autosave
iblrig/_version.py
*.code-workspace
Expand All @@ -46,4 +47,4 @@ devices/camera_recordings/*.layout
.pdm-python
/dist
*~
docs/source/api/*
docs/source/api/*
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
Changelog
=========

8.24.4
------
* fix: correct gain for rotary encoder thresholds in trainingChoiceWorld
* overhaul of rotary encoder object

8.24.3
------
* fix: create the `raw_ephys_data` folder even if there are no probes (when running behavior sessions on ephys rig)
* fix: replace `np.NaN` with `np.nan`
* move some Qt related code to `iblqt` repository

8.24.2
Expand Down
2 changes: 1 addition & 1 deletion docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -260,5 +260,5 @@ Then switch to the desired version, for example `8.15.5`:

.. code-block:: powershell
git checkout tag/8.15.5
git checkout tags/8.15.5
pip install --upgrade -e .
2 changes: 1 addition & 1 deletion iblrig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# 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.24.3'
__version__ = '8.24.4'


from iblrig.version_management import get_detailed_version_string
Expand Down
19 changes: 10 additions & 9 deletions iblrig/base_choice_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ def __init__(self, *args, delay_secs=0, **kwargs):
self.trials_table = self.TrialDataModel.preallocate_dataframe(NTRIALS_INIT)
self.ambient_sensor_table = pd.DataFrame(
{
'Temperature_C': np.zeros(NTRIALS_INIT) * np.NaN,
'AirPressure_mb': np.zeros(NTRIALS_INIT) * np.NaN,
'RelativeHumidity': np.zeros(NTRIALS_INIT) * np.NaN,
'Temperature_C': np.zeros(NTRIALS_INIT) * np.nan,
'AirPressure_mb': np.zeros(NTRIALS_INIT) * np.nan,
'RelativeHumidity': np.zeros(NTRIALS_INIT) * np.nan,
}
)

Expand Down Expand Up @@ -491,15 +491,12 @@ def draw_next_trial_info(self, pleft=0.5, **kwargs):
quiescent_period = self.task_params.QUIESCENT_PERIOD + misc.truncated_exponential(
scale=0.35, min_value=0.2, max_value=0.5
)
stim_gain = (
self.session_info.ADAPTIVE_GAIN_VALUE if self.task_params.get('ADAPTIVE_GAIN', False) else self.task_params.STIM_GAIN
)
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)
self.trials_table.at[self.trial_num, 'stim_sigma'] = self.task_params.STIM_SIGMA
self.trials_table.at[self.trial_num, 'stim_angle'] = self.task_params.STIM_ANGLE
self.trials_table.at[self.trial_num, 'stim_gain'] = stim_gain
self.trials_table.at[self.trial_num, 'stim_gain'] = self.stimulus_gain
self.trials_table.at[self.trial_num, 'stim_freq'] = self.task_params.STIM_FREQ
self.trials_table.at[self.trial_num, 'stim_reverse'] = self.task_params.STIM_REVERSE
self.trials_table.at[self.trial_num, 'trial_num'] = self.trial_num
Expand Down Expand Up @@ -771,7 +768,7 @@ def trial_completed(self, bpod_data):
self.trials_table.at[self.trial_num, 'response_time'] = response_time
# get the trial outcome
state_names = ['correct', 'error', 'no_go', 'omit_correct', 'omit_error', 'omit_no_go']
raw_outcome = {sn: ~np.isnan(bpod_data['States timestamps'].get(sn, [[np.NaN]])[0][0]) for sn in state_names}
raw_outcome = {sn: ~np.isnan(bpod_data['States timestamps'].get(sn, [[np.nan]])[0][0]) for sn in state_names}
try:
outcome = next(k for k in raw_outcome if raw_outcome[k])
# Update response buffer -1 for left, 0 for nogo, and 1 for rightward
Expand Down Expand Up @@ -820,7 +817,7 @@ class BiasedChoiceWorldSession(ActiveChoiceWorldSession):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.blocks_table = pd.DataFrame(
{'probability_left': np.zeros(NBLOCKS_INIT) * np.NaN, 'block_length': np.zeros(NBLOCKS_INIT, dtype=np.int16) * -1}
{'probability_left': np.zeros(NBLOCKS_INIT) * np.nan, 'block_length': np.zeros(NBLOCKS_INIT, dtype=np.int16) * -1}
)

def new_block(self):
Expand Down Expand Up @@ -929,6 +926,10 @@ def __init__(self, training_phase=-1, adaptive_reward=-1.0, adaptive_gain=None,
def default_reward_amount(self):
return self.session_info.get('ADAPTIVE_REWARD_AMOUNT_UL', self.task_params.REWARD_AMOUNT_UL)

@property
def stimulus_gain(self) -> float:
return self.session_info.get('ADAPTIVE_GAIN_VALUE')

def get_subject_training_info(self):
"""
Get the previous session's according to this session parameters and deduce the
Expand Down
54 changes: 24 additions & 30 deletions iblrig/base_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

import numpy as np
import pandas as pd
import serial
import yaml
from pythonosc import udp_client

Expand All @@ -36,7 +35,7 @@
from iblrig import net, path_helper, sound
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.hardware import SOFTCODE, Bpod, RotaryEncoderModule, sound_device_factory
from iblrig.hifi import HiFi
from iblrig.path_helper import load_pydantic_yaml
from iblrig.pydantic_definitions import HardwareSettings, RigSettings, TrialDataModel
Expand Down Expand Up @@ -119,7 +118,13 @@ def __init__(
self._logger = None
self._setup_loggers(level=log_level)
if not isinstance(self, EmptySession):
log.info(f'Running iblrig {iblrig.__version__}, pybpod version {pybpodapi.__version__}')
log.info(f'iblrig version {iblrig.__version__}')
log.info(f'pybpod version {pybpodapi.__version__}')
log.info(
f'Session protocol: {self.protocol_name} '
f'({f"version {self.version})" if self.version is not None else "undefined version"})'
)

log.info(f'Session call: {" ".join(sys.argv)}')
self.interactive = interactive
self._one = one
Expand Down Expand Up @@ -989,38 +994,27 @@ def start_mixin_frame2ttl(self):
log.info('Frame2TTL: Thresholds set.')


class RotaryEncoderMixin(BaseSession):
class RotaryEncoderMixin(BaseSession, HasBpod):
"""Rotary encoder interface for state machine."""

def init_mixin_rotary_encoder(self, *args, **kwargs):
self.device_rotary_encoder = MyRotaryEncoder(
all_thresholds=self.task_params.STIM_POSITIONS + self.task_params.QUIESCENCE_THRESHOLDS,
gain=self.task_params.STIM_GAIN,
com=self.hardware_settings.device_rotary_encoder['COM_ROTARY_ENCODER'],
connect=False,
device_rotary_encoder: RotaryEncoderModule

@property
def stimulus_gain(self) -> float:
return self.task_params.STIM_GAIN

def init_mixin_rotary_encoder(self):
thresholds_deg = self.task_params.STIM_POSITIONS + self.task_params.QUIESCENCE_THRESHOLDS
self.device_rotary_encoder = RotaryEncoderModule(
self.hardware_settings.device_rotary_encoder, thresholds_deg, self.stimulus_gain
)

def start_mixin_rotary_encoder(self):
if self.hardware_settings['device_rotary_encoder']['COM_ROTARY_ENCODER'] is None:
raise ValueError(
'The value for device_rotary_encoder:COM_ROTARY_ENCODER in '
'settings/hardware_settings.yaml is null. Please '
'provide a valid port name.'
)
try:
self.device_rotary_encoder.connect()
except serial.serialutil.SerialException as e:
raise serial.serialutil.SerialException(
f'The rotary encoder COM port {self.device_rotary_encoder.RE_PORT} is already in use. This is usually'
f' due to a Bonsai process currently running on the computer. Make sure all Bonsai windows are'
f' closed prior to running the task'
) from e
except Exception as e:
raise Exception(
"The rotary encoder couldn't connect. If the bpod is glowing in green,"
'disconnect and reconnect bpod from the computer'
) from e
log.info('Rotary encoder module loaded: OK')
self.device_rotary_encoder.gain = self.stimulus_gain
self.device_rotary_encoder.open()
self.device_rotary_encoder.write_parameters()
self.device_rotary_encoder.close()
log.info('Rotary Encoder Module loaded: OK')


class ValveMixin(BaseSession, HasBpod):
Expand Down
2 changes: 1 addition & 1 deletion iblrig/ephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def neuropixel24_micromanipulator_coordinates(ref_shank, pname, ba=None, shank_s
shank = {
'x': x,
'y': y,
'z': np.NaN,
'z': np.nan,
'phi': ref_shank['phi'],
'theta': ref_shank['theta'],
'depth': ref_shank['depth'],
Expand Down
99 changes: 64 additions & 35 deletions iblrig/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
import sounddevice as sd
from annotated_types import Ge, Le
from pydantic import PositiveFloat, PositiveInt, validate_call
from serial.serialutil import SerialException
from serial.tools import list_ports

from iblrig.pydantic_definitions import HardwareSettingsRotaryEncoder
from iblrig.tools import static_vars
from iblutil.util import Bunch
from pybpod_rotaryencoder_module.module import RotaryEncoder
from pybpod_rotaryencoder_module.module_api import RotaryEncoderModule
from pybpod_rotaryencoder_module.module import RotaryEncoder as PybpodRotaryEncoder
from pybpod_rotaryencoder_module.module_api import RotaryEncoderModule as PybpodRotaryEncoderModule
from pybpodapi.bpod.bpod_io import BpodIO
from pybpodapi.bpod_modules.bpod_module import BpodModule
from pybpodapi.state_machine import StateMachine
Expand Down Expand Up @@ -193,7 +195,9 @@ def define_rotary_encoder_actions(self, module: BpodModule | None = None) -> Non
{
'rotary_encoder_reset': (
module_port,
self._define_message(module, [RotaryEncoder.COM_SETZEROPOS, RotaryEncoder.COM_ENABLE_ALLTHRESHOLDS]),
self._define_message(
module, [PybpodRotaryEncoder.COM_SETZEROPOS, PybpodRotaryEncoder.COM_ENABLE_ALLTHRESHOLDS]
),
),
'bonsai_hide_stim': (module_port, self._define_message(module, [ord('#'), 1])),
'bonsai_show_stim': (module_port, self._define_message(module, [ord('#'), 8])),
Expand All @@ -206,9 +210,9 @@ def define_rotary_encoder_actions(self, module: BpodModule | None = None) -> Non
def get_ambient_sensor_reading(self):
if self.ambient_module is None:
return {
'Temperature_C': np.NaN,
'AirPressure_mb': np.NaN,
'RelativeHumidity': np.NaN,
'Temperature_C': np.nan,
'AirPressure_mb': np.nan,
'RelativeHumidity': np.nan,
}
self.ambient_module.start_module_relay()
self.bpod_modules.module_write(self.ambient_module, 'R')
Expand Down Expand Up @@ -320,35 +324,60 @@ def register_softcodes(self, softcode_dict: dict[int, Callable]) -> None:
self.softcode_handler_function = lambda code: softcode_dict[code]()


class MyRotaryEncoder:
def __init__(self, all_thresholds, gain, com, connect=False):
self.RE_PORT = com
self.WHEEL_PERIM = 31 * 2 * np.pi # = 194,778744523
self.deg_mm = 360 / self.WHEEL_PERIM
self.mm_deg = self.WHEEL_PERIM / 360
self.factor = 1 / (self.mm_deg * gain)
self.SET_THRESHOLDS = [x * self.factor for x in all_thresholds]
self.ENABLE_THRESHOLDS = [(x != 0) for x in self.SET_THRESHOLDS]
# ENABLE_THRESHOLDS needs 8 bools even if only 2 thresholds are set
while len(self.ENABLE_THRESHOLDS) < 8:
self.ENABLE_THRESHOLDS.append(False)

# Names of the RE events generated by Bpod
self.ENCODER_EVENTS = [f'RotaryEncoder1_{x}' for x in list(range(1, len(all_thresholds) + 1))]
# Dict mapping threshold crossings with name ov RE event
self.THRESHOLD_EVENTS = dict(zip(all_thresholds, self.ENCODER_EVENTS, strict=False))
if connect:
self.connect()

def connect(self):
if self.RE_PORT == 'COM#':
return
m = RotaryEncoderModule(self.RE_PORT)
m.set_zero_position() # Not necessarily needed
m.set_thresholds(self.SET_THRESHOLDS)
m.enable_thresholds(self.ENABLE_THRESHOLDS)
m.enable_evt_transmission()
m.close()
class RotaryEncoderModule(PybpodRotaryEncoderModule):
_name = 'Rotary Encoder Module'

ENCODER_EVENTS = list()
THRESHOLD_EVENTS = dict()

def __init__(self, settings: HardwareSettingsRotaryEncoder, thresholds_deg: list[float], gain: float):
super().__init__()
self.settings = settings

self._wheel_degree_per_mm = 360.0 / (self.settings.WHEEL_DIAMETER_MM * np.pi)
self.thresholds_deg = thresholds_deg
self.gain = gain
self.ENCODER_EVENTS = [f'RotaryEncoder1_{x + 1}' for x in range(len(thresholds_deg))]
self.THRESHOLD_EVENTS = dict(zip(thresholds_deg, self.ENCODER_EVENTS, strict=False))

def open(self, _=None):
if self.settings.COM_ROTARY_ENCODER is None:
raise ValueError(
'The value for device_rotary_encoder:COM_ROTARY_ENCODER in settings/hardware_settings.yaml is null. '
'Please provide a valid port name.'
)
try:
super().open(self.settings.COM_ROTARY_ENCODER)
except SerialException as e:
raise SerialException(
f'The {self._name} on port {self.settings.COM_ROTARY_ENCODER} is already in use. This is '
f'usually due to a Bonsai process running on the computer. Make sure all Bonsai windows are closed '
f'prior to running the task.'
) from e
except Exception as e:
raise Exception(f'The {self._name} on port {self.settings.COM_ROTARY_ENCODER} did not return the handshake.') from e
log.debug(f'Successfully opened serial connection to {self._name} on port {self.settings.COM_ROTARY_ENCODER}')

def write_parameters(self):
scaled_thresholds_deg = [x / self.gain * self._wheel_degree_per_mm for x in self.thresholds_deg]
enabled_thresholds = [(x < len(scaled_thresholds_deg)) for x in range(8)]

log.info(
f'Thresholds for {self._name} scaled to {", ".join([f"{x:0.2f}" for x in scaled_thresholds_deg])} '
f'using gain of {self.gain:0.1f} deg/mm and wheel diameter of {self.settings.WHEEL_DIAMETER_MM:0.1f} mm.'
)
self.set_zero_position()
self.set_thresholds(scaled_thresholds_deg)
self.enable_thresholds(enabled_thresholds)
self.enable_evt_transmission()

def close(self):
if hasattr(self, 'arcom'):
log.debug(f'Closing serial connection to {self._name} on port {self.settings.COM_ROTARY_ENCODER}')
super().close()

def __del__(self):
self.close()


def sound_device_factory(output: Literal['xonar', 'harp', 'hifi', 'sysdefault'] = 'sysdefault', samplerate: int | None = None):
Expand Down
2 changes: 1 addition & 1 deletion iblrig/online_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def update_trial(self, trial_data, bpod_data) -> None:
# update psychometrics using online statistics method
indexer = (trial_data.stim_probability_left, signed_contrast)
if indexer not in self.psychometrics.index:
self.psychometrics.loc[indexer, :] = np.NaN
self.psychometrics.loc[indexer, :] = np.nan
self.psychometrics.loc[indexer, ('count')] = 0
self.psychometrics.loc[indexer, ('count')] += 1
self.psychometrics.loc[indexer, ('response_time')], self.psychometrics.loc[indexer, ('response_time_std')] = online_std(
Expand Down
1 change: 1 addition & 0 deletions iblrig/pydantic_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class HardwareSettingsFrame2TTL(BunchModel):

class HardwareSettingsRotaryEncoder(BunchModel):
COM_ROTARY_ENCODER: str | None
WHEEL_DIAMETER_MM: float = 62.0


class HardwareSettingsScreen(BunchModel):
Expand Down
6 changes: 3 additions & 3 deletions iblrig/test/tasks/test_passive_choice_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ def test_pipeline(self) -> None:
In order for this to work we must add an external sync to the experiment description as
Bpod only passive choice world is currently not supported.
"""
self.task.experiment_description['sync'] = dyn.get_acquisition_description('choice_world_recording')['sync']
self.task.experiment_description['sync'] = dyn.get_acquisition_description('ephys')['sync']
self.task.create_session()
pipeline = dyn.make_pipeline(self.task.paths.SESSION_FOLDER)
self.assertIn('PassiveRegisterRaw_00', pipeline.tasks)
self.assertIn('PassiveTaskNidq_00', pipeline.tasks)
self.assertIsInstance(pipeline.tasks['PassiveTaskNidq_00'], PassiveTaskNidq)
self.assertIn('Trials_PassiveTaskNidq_00', pipeline.tasks)
self.assertIsInstance(pipeline.tasks['Trials_PassiveTaskNidq_00'], PassiveTaskNidq)
4 changes: 2 additions & 2 deletions iblrig/test/test_hardware_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ def test_rotary_encoder_mixin(self):
"""
Instantiates a bare session with the rotary encoder mixin
"""
session = self.session
RotaryEncoderMixin.init_mixin_rotary_encoder(session)
RotaryEncoderSession = type('RotaryEncoderSession', (EmptyHardwareSession, RotaryEncoderMixin), {}) # noqa: N806
session = RotaryEncoderSession(task_parameter_file=ChoiceWorldSession.base_parameters_file, **TASK_KWARGS)
assert session.device_rotary_encoder.ENCODER_EVENTS == [
'RotaryEncoder1_1',
'RotaryEncoder1_2',
Expand Down
Loading

0 comments on commit 60403a3

Please sign in to comment.