Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7f8e169
Update .pre-commit-config.yaml
bimac Jul 21, 2025
d729a06
Update README.md
bimac Jul 21, 2025
b3eaa48
refactor/gui (#832)
bimac Jul 22, 2025
f4a5ff3
Merge branch 'iblrigv8dev' of github.com:int-brain-lab/iblrig into ib…
bimac Jul 23, 2025
00e5ca9
allow user specified grouping variable for plotter
mdmelin Jul 23, 2025
26ad571
a few refinements
bimac Jul 23, 2025
6869746
Update online_plots.py
bimac Jul 23, 2025
95a5b6f
Update online_plots.py
bimac Jul 23, 2025
6c85be8
Update online_plots.py
bimac Jul 23, 2025
431eb54
Add new functions to plotter as new grouping variables come in
mdmelin Jul 23, 2025
92267f1
bugfix. move file across drives
mdmelin Jul 24, 2025
d8e44aa
ruff
bimac Jul 24, 2025
1e262c1
Update online_plots.py
bimac Jul 24, 2025
49c5b7c
Merge pull request #835 from mdmelin/iblrigv8dev
bimac Jul 29, 2025
1b288b5
add getter/setter for extractor tasks
bimac May 26, 2025
4827fc5
Merge pull request #838 from int-brain-lab/extractor_tasks_2
bimac Jul 29, 2025
619df31
refactoring: sound (#837)
bimac Jul 29, 2025
ff44131
add gain_db parameter to `make_sound()`
bimac Jul 29, 2025
5544eff
ruff
bimac Jul 29, 2025
17da3fb
Merge pull request #834 from int-brain-lab/feat/plot-grouping
bimac Jul 30, 2025
12a515c
move saving of ambient data to separate method in bpod mixin
bimac Jul 30, 2025
120e51d
Revert "move saving of ambient data to separate method in bpod mixin"
bimac Jul 30, 2025
a2b660f
add deprecating warning for xonar soundcard
bimac Jul 30, 2025
b7836ba
Update sound.py
bimac Jul 30, 2025
b0f87ec
add hardware setting for overriding default sound channels
bimac Jul 30, 2025
31fdd20
Update hardware_settings_template.yaml
bimac Jul 30, 2025
b0024eb
Update sound.py
bimac Jul 30, 2025
8a8780e
Update base_choice_world.py
bimac Aug 18, 2025
be269da
Update test_training_choice_world.py
bimac Aug 19, 2025
a4a6843
Update test_training_choice_world.py
bimac Aug 19, 2025
6728c16
Update test_training_choice_world.py
bimac Aug 19, 2025
8b31194
typo fix in file name for photometry
grg2rsr Sep 12, 2025
92921ac
typo fix in file name for photometry - part 2
grg2rsr Sep 12, 2025
d4ad37e
update dependencies
bimac Sep 25, 2025
76b9f65
Revert "update dependencies"
bimac Sep 25, 2025
b2eb451
Merge pull request #848 from int-brain-lab/typo_fix
bimac Sep 25, 2025
f3cacca
Snapshot of Online Plots (#857)
bimac Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ repos:
- id: check-xml

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.7
rev: v0.12.4
hooks:
- id: ruff
types_or: [ python, pyi ]
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changelog
=========

8.30.0
------
* added: Online Plots are stored as PNG and added to Alyx as a Session Note

8.29.0
------
* added: GUI settings for changing MAIN_SYNC
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Links
[![License](https://img.shields.io/github/license/int-brain-lab/iblrig)](https://github.com/int-brain-lab/iblrig/blob/iblrigv8/LICENSE)
[![GitHub tag](https://img.shields.io/github/v/tag/int-brain-lab/iblrig)](https://github.com/int-brain-lab/iblrig/tags)
[![GitHub Discussions](https://img.shields.io/github/discussions/int-brain-lab/iblrig)](https://github.com/int-brain-lab/iblrig/discussions)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
135 changes: 104 additions & 31 deletions iblrig/base_choice_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import enum
import logging
import math
import multiprocessing
import random
import subprocess
import shutil
import time
from pathlib import Path
from re import split as re_split
Expand All @@ -18,11 +19,14 @@
from pydantic import NonNegativeFloat, NonNegativeInt

import iblrig.base_tasks
from ibllib.plots.snapshot import Snapshot
from iblrig import choiceworld, misc
from iblrig.gui.online_plots import online_plots_app
from iblrig.hardware import DTYPE_AMBIENT_SENSOR_BIN, SOFTCODE
from iblrig.pydantic_definitions import TrialDataModel
from iblutil.io import binary, jsonable
from iblutil.util import Bunch
from one.api import OneAlyx
from pybpodapi.com.messaging.trial import Trial
from pybpodapi.protocol import StateMachine

Expand Down Expand Up @@ -790,43 +794,77 @@ class ActiveChoiceWorldSession(ChoiceWorldSession):
The ActiveChoiceWorldSession is a base class for protocols where the mouse is actively making decisions
by turning the wheel. It has the following characteristics

- it is trial based
- it is decision based
- left and right simulus are equiprobable: there is no biased block
- a trial can either be correct / error / no_go depending on the side of the stimulus and the response
- it has a quantifiable performance by computing the proportion of correct trials of passive stimulations protocols or
habituation protocols.
- it is trial based
- it is decision based
- left and right simulus are equiprobable: there is no biased block
- a trial can either be correct / error / no_go depending on the side of the stimulus and the response
- it has a quantifiable performance by computing the proportion of correct trials of passive stimulations protocols or
habituation protocols.

The TrainingChoiceWorld, BiasedChoiceWorld are all subclasses of this class
"""

TrialDataModel = ActiveChoiceWorldTrialData
plot_subprocess: subprocess.Popen | None = None
stop_event = multiprocessing.Event()
plot_process: multiprocessing.Process | None = None

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.trials_table['stim_probability_left'] = np.zeros(NTRIALS_INIT, dtype=np.float64)

def _run(self):
# starts online plotting
# start online plotting
if self.interactive:
log.info('Starting subprocess: online plots')
self.plot_subprocess = subprocess.Popen(
['view_session', str(self.paths['SESSION_RAW_DATA_FOLDER'])],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
self.plot_process = multiprocessing.Process(
target=online_plots_app,
name='online_plots_app',
kwargs={
'session': self.paths['SESSION_RAW_DATA_FOLDER'],
'stop_event': self.stop_event,
'live': True,
},
)
self.plot_process.start()

# run super method
super()._run()

def _finalize(self):
if isinstance(self.plot_subprocess, subprocess.Popen) and self.plot_subprocess.poll() is None:
log.info('Terminating subprocess: online plots')
self.plot_subprocess.terminate()
try:
self.plot_subprocess.wait(timeout=5)
except subprocess.TimeoutExpired:
# stop online plotting
if self.interactive:
log.info('Signaling online plots process to exit')
self.stop_event.set()
self.plot_process.join(timeout=5)
if self.plot_process.is_alive():
log.warning('Process did not terminate within 5 seconds - killing it.')
self.plot_subprocess.kill()
self.plot_process.kill()
self.plot_process = None
self.stop_event.clear()

def register_to_alyx(self) -> dict | None:
# move online plots to session folder
register_snapshot = False
path_plot: Path = self.paths['SESSION_RAW_DATA_FOLDER'] / 'online_plots.png'
suffix = self.paths['SESSION_RAW_DATA_FOLDER'].name[-2:]
if path_plot.exists():
new_path = self.paths['SESSION_FOLDER'] / f'online_plots_{suffix}.png'
if not new_path.exists():
register_snapshot = True
shutil.move(path_plot, new_path)
path_plot = new_path

# call register_to_alyx of super class
session = super().register_to_alyx()

# register online plot as session note
try:
if register_snapshot and session is not None and isinstance(self.one, OneAlyx) and self.one.alyx.is_logged_in:
snapshot = Snapshot(object_id=session['id'], content_type='session', one=self.one)
snapshot.register_image(image_file=path_plot, text=f'Snapshot of Online Plots #{suffix}', width='orig')
except Exception as e:
log.error('Failed to register online plots as session note', exc_info=e)

return session

def show_trial_log(self, extra_info: dict[str, Any] | None = None, log_level: int = logging.INFO):
# construct info dict
Expand Down Expand Up @@ -893,6 +931,9 @@ def trial_completed(self, bpod_data: dict) -> None:
raise e

# record the trial's outcome in the trials_table
# NOTE: response_side corresponds to the direction of the wheel turn, i.e.
# - +1 for wheel movement to the right / CW
# - -1 for wheel movement to the left / CCW
self.trials_table.at[self.trial_num, 'trial_correct'] = 'correct' in outcome
if 'correct' in outcome:
self.session_info.NTRIALS_CORRECT += 1
Expand Down Expand Up @@ -1094,7 +1135,31 @@ def next_trial(self):
position = self.task_params.STIM_POSITIONS[int(np.sign(signed_contrast) == 1)]
contrast = np.abs(signed_contrast)

# debiasing: if the previous trial was incorrect, not a no-go and easy
# DEBIASING
#
# Appendix 2 - IBL protocol for mice training:
#
# Repeat trials occur if the response was incorrect and the Gabor patch contrast was easy (>= 50%). On repeat trials
# the previous contrast is repeated and the side on which the Gabor patch is presented is not randomly selected, but
# rather drawn from a normal distribution with a standard deviation of 0.5 and mean of the fraction of the previous 10
# responses that were 'rightward' (that is, when the wheel was turned clockwise from the point of view of the mouse).
#
# For example, if the last 10 trials were:
# L-R-R-L-R-L-L-R-R-L, where R means 'rightward' responses, the fraction(R) = 5/10 = 0.5
#
# If the sampled value is strictly less than 0.5, the Gabor patch is presented on the left, otherwise on the right.
# This is a form of soft counter biasing where the more the mouse turns in one direction, the more likely it is that
# this will be incorrect on the next trial, meaning the correct movement would be to the opposite side (again, on
# repeat trials only).
#
#
# A trial qualifies for debiasing if:
# 1. the debiasing flag is set in the task parameters,
# 2. the trial number is greater than or equal to 1 (i.e. not the first trial),
# 3. the training phase is less than 5 (i.e. not the last training phase),
# 4. the previous trial was incorrect,
# 5. the previous trial was not a no-go trial, and
# 6. the previous trial was easy (i.e. contrast >= 0.5).
if self.task_params.DEBIAS and self.trial_num >= 1 and self.training_phase < 5:
last_contrast = self.trials_table.loc[self.trial_num - 1, 'contrast']
do_debias_trial = (
Expand All @@ -1103,19 +1168,27 @@ def next_trial(self):
and last_contrast >= 0.5
)
self.trials_table.at[self.trial_num, 'debias_trial'] = do_debias_trial

if do_debias_trial:
# indices of trials that had a response
# Identify indices of trials with valid responses, excluding no-go trials
iresponse = np.logical_and(self.trials_table['response_side'].notna(), self.trials_table['response_side'] != 0)
iresponse = iresponse.index[iresponse]

# takes the average of right responses over last 10 response trials
average_right = (self.trials_table['response_side'][iresponse[-np.minimum(10, iresponse.size) :]] == 1).mean()

# the probability of the next stimulus being on the left is a draw from a normal distribution centered
# on the average right with sigma 0.5 - if it is less than 0.5 the next stimulus will be on the left.
position = self.task_params.STIM_POSITIONS[int(np.random.normal(average_right, 0.5) >= 0.5)]

# contrast is the last contrast
# Calculate the mean movement direction across the last 10 response trials:
# - a value of 0.5 indicates equal proportions of leftward and rightward wheel movements,
# - a value larger than 0.5 indicates a bias towards rightward / CW wheel movements,
# - a value smaller than 0.5 indicates a bias towards leftward / CCW wheel movements.
mean_direction = (self.trials_table['response_side'][iresponse[-np.minimum(10, iresponse.size) :]] == 1).mean()

# The position of the next stimulus (and hence the required movement direction) is determined by drawing from a
# normal distribution centered on the mean response direction (see above) with a standard deviation of 0.5:
# - if the drawn value is LESS THAN 0.5, the next stimulus will be displayed on the LEFT SIDE of the screen,
# requiring a RIGHTWARD / CW MOVEMENT to center the stimulus (countering a LEFTWARD MOVEMENT BIAS).
# - if the drawn value is GREATER THAN OR EQUAL TO 0.5, the stimulus will be displayed on the RIGHT SIDE of the
# screen, requiring a LEFTWARD / CCW MOVEMENT to center the stimulus (countering a RIGHTWARD MOVEMENT BIAS).
position = self.task_params.STIM_POSITIONS[int(np.random.normal(mean_direction, 0.5) >= 0.5)]

# The contrast of the debiasing trial is identical to the contrast of the previous trial.
contrast = last_contrast
else:
self.trials_table.at[self.trial_num, 'debias_trial'] = False
Expand Down
Loading
Loading