Skip to content

Commit

Permalink
Merge pull request #546 from int-brain-lab/iblrigv8dev
Browse files Browse the repository at this point in the history
8.12.4
  • Loading branch information
bimac authored Nov 3, 2023
2 parents 800eeeb + 685289b commit c582e33
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 31 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changelog
---------

8.12.4
------
* updated online-plots

8.12.3
------
* bugfix: getting training status of subject not present on local server
Expand Down
2 changes: 1 addition & 1 deletion iblrig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.3'
__version__ = '8.12.4'

# 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)
Expand Down
10 changes: 6 additions & 4 deletions iblrig/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def truncated_exponential(scale: float = 0.35, min_value: float = 0.2, max_value
if min_value <= x <= max_value:
return x
else:
return truncated_exponential(scale=scale, min_value=min_value, max_value=max_value)
return truncated_exponential(scale, min_value, max_value)


def get_biased_probs(n: int, idx: int = -1, p_idx: float = 0.5) -> list[float]:
Expand All @@ -186,13 +186,15 @@ def get_biased_probs(n: int, idx: int = -1, p_idx: float = 0.5) -> list[float]:
Raises
------
IndexError
If `idx` is out of range
ValueError
If `idx` is outside the valid range [-1, n), or if `p_idx` is 0.
If `p_idx` is 0.
"""
if idx < -1 or idx >= n:
raise ValueError("Invalid index. Index should be in the range [-1, n).")
if n == 1:
return [1.0]
if idx not in range(-n, n):
raise IndexError("`idx` is out of range.")
if p_idx == 0:
raise ValueError("Probability must be larger than 0.")
z = n - 1 + p_idx
Expand Down
95 changes: 71 additions & 24 deletions iblrig/online_plots.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from pathlib import Path
import datetime
import time
Expand All @@ -10,6 +11,7 @@

import one.alf.io

from iblrig.choiceworld import get_subject_training_info
from iblrig.misc import online_std
from iblrig.raw_data_loaders import load_task_jsonable
from iblutil.util import Bunch
Expand All @@ -21,7 +23,7 @@
PROBABILITY_SET = np.array([.2, .5, .8])
# if the mouse does less than 400 trials in the first 45mins it's disengaged
ENGAGED_CRITIERION = {'secs': 45 * 60, 'trial_count': 400}
sns.set_style('white')
sns.set_style("darkgrid")


class DataModel(object):
Expand All @@ -33,6 +35,8 @@ class DataModel(object):
- a last trials dataframe that contains 20 trials worth of data for the timeline view
- various counters such as ntrials and water delivered
"""
task_settings = None

def __init__(self, task_file):
"""
Can be instantiated empty or from an existing jsonable file from any rig version
Expand All @@ -42,6 +46,7 @@ def __init__(self, task_file):
self.last_trials = pd.DataFrame(
columns=['correct', 'signed_contrast', 'stim_on', 'play_tone', 'reward_time', 'error_time', 'response_time'],
index=np.arange(NTRIALS_PLOT))

if task_file is None or not Path(task_file).exists():
self.psychometrics = pd.DataFrame(
columns=['count', 'response_time', 'choice', 'response_time_std', 'choice_std'],
Expand All @@ -51,10 +56,14 @@ def __init__(self, task_file):
self.trials_table = pd.DataFrame(columns=['response_time'], index=np.arange(NTRIALS_INIT))
self.ntrials = 0
self.ntrials_correct = 0
self.ntrials_nan = np.nan
self.percent_correct = np.nan
self.percent_error = np.nan
self.water_delivered = 0
self.time_elapsed = 0
self.ntrials_engaged = 0 # those are the trials happening within the first 400s
else:
self.get_task_settings(Path(task_file).parent)
trials_table, bpod_data = load_task_jsonable(task_file)
# here we take the end time of the first trial as reference to avoid factoring in the delay
self.time_elapsed = bpod_data[-1]['Trial end timestamp'] - bpod_data[0]['Trial end timestamp']
Expand All @@ -74,6 +83,8 @@ def __init__(self, task_file):
)
self.ntrials = trials_table.shape[0]
self.ntrials_correct = np.sum(trials_table.trial_correct)
self.ntrials_nan = self.ntrials if self.ntrials > 0 else np.nan
self.percent_correct = self.ntrials_correct / self.ntrials_nan * 100
# agg.water_delivered = trials_table.water_delivered.iloc[-1]
self.water_delivered = trials_table.reward_amount.sum()
# init the last trials table
Expand All @@ -92,7 +103,7 @@ def __init__(self, task_file):
# we keep only a single column as buffer
self.trials_table = trials_table[['response_time']]
# for the trials plots this is the background image showing green if correct, red if incorrect
self.rgb_background = np.zeros((NTRIALS_PLOT, 1, 3), dtype=np.uint8)
self.rgb_background = np.ones((NTRIALS_PLOT, 1, 3), dtype=np.uint8) * 229
self.rgb_background[self.last_trials.correct == False, 0, 0] = 255 # noqa
self.rgb_background[self.last_trials.correct == True, 0, 1] = 255 # noqa
# keep the last contrasts as a 20 by 2 array
Expand All @@ -102,6 +113,13 @@ def __init__(self, task_file):
self.last_contrasts[ileft, 0] = np.abs(self.last_trials.signed_contrast[ileft])
self.last_contrasts[iright, 1] = np.abs(self.last_trials.signed_contrast[iright])

def get_task_settings(self, session_directory: str | Path) -> None:
task_settings_file = Path(session_directory).joinpath('_iblrig_taskSettings.raw.json')
if not task_settings_file.exists():
return
with open(task_settings_file, 'r') as fid:
self.task_settings = json.load(fid)

def update_trial(self, trial_data, bpod_data) -> None:
# update counters
self.time_elapsed = bpod_data['Trial end timestamp'] - bpod_data['Bpod start timestamp']
Expand Down Expand Up @@ -147,6 +165,8 @@ def update_trial(self, trial_data, bpod_data) -> None:
self.last_contrasts[-1, :] = 0
self.last_contrasts[-1, int(self.last_trials.signed_contrast.iloc[-1] > 0)] = abs(
self.last_trials.signed_contrast.iloc[-1])
self.ntrials_nan = self.ntrials if self.ntrials > 0 else np.nan
self.percent_correct = self.ntrials_correct / self.ntrials_nan * 100

def compute_end_session_criteria(self):
"""
Expand Down Expand Up @@ -180,12 +200,15 @@ class OnlinePlots(object):
Use ctrl + Z to interrupt
>>> OnlinePlots().run(task_file)
"""

def __init__(self, task_file=None):
self.data = DataModel(task_file=task_file)

# create figure and axes
h = Bunch({})
h.fig = plt.figure(constrained_layout=True, figsize=(10, 8))
h.fig_title = h.fig.suptitle(f"{self._session_string}", fontweight='bold')
self._set_session_string()
h.fig_title = h.fig.suptitle(f"{self._session_string}")
nc = 9
hc = nc // 2
h.gs = h.fig.add_gridspec(2, nc)
Expand All @@ -194,29 +217,35 @@ def __init__(self, task_file=None):
h.ax_performance = h.fig.add_subplot(h.gs[0, nc - 1])
h.ax_reaction = h.fig.add_subplot(h.gs[1, hc:nc - 1])
h.ax_water = h.fig.add_subplot(h.gs[1, nc - 1])
h.ax_psych.set(title='psychometric curve', xlim=[-1.01, 1.01], ylim=[0, 1.01])
h.ax_reaction.set(title='reaction times', xlim=[-1.01, 1.01], ylim=[0, 4], xlabel='signed contrast')

h.ax_psych.set(title='psychometric curve', xlim=[-1, 1], ylim=[0, 1])
h.ax_reaction.set(title='reaction times', xlim=[-1, 1], ylim=[0, 4], xlabel='signed contrast')
xticks = np.arange(-1, 1.1, .25)
xticklabels = np.array([f'{x:g}' for x in xticks])
xticklabels[1::2] = ''
h.ax_psych.set_xticks(xticks, xticklabels)
h.ax_reaction.set_xticks(xticks, xticklabels)

h.ax_trials.set(yticks=[], title='trials timeline', xlim=[-5, 30], xlabel='time (s)')
h.ax_performance.set(xticks=[], xlim=[-1.01, 1.01], title='# trials')
h.ax_water.set(xticks=[], xlim=[-1.01, 1.01], ylim=[0, 1000], title='water (uL)')
h.ax_trials.set_xticks(h.ax_trials.get_xticks(), [''] + h.ax_trials.get_xticklabels()[1::])
h.ax_performance.set(xticks=[], xlim=[-0.6, 0.6], ylim=[0, 100], title='performance')
h.ax_water.set(xticks=[], xlim=[-0.6, 0.6], ylim=[0, 1000], title='reward')

# create psych curves
h.curve_psych = {}
h.curve_reaction = {}
for i, p in enumerate(PROBABILITY_SET):
h.curve_psych[p] = h.ax_psych.plot(
self.data.psychometrics.loc[p].index, self.data.psychometrics.loc[p]['choice'], '.-')
self.data.psychometrics.loc[p].index, self.data.psychometrics.loc[p]['choice'], 'k.-', zorder=10, clip_on=False)
h.curve_reaction[p] = h.ax_reaction.plot(
self.data.psychometrics.loc[p].index, self.data.psychometrics.loc[p]['response_time'], '.-')
self.data.psychometrics.loc[p].index, self.data.psychometrics.loc[p]['response_time'], 'k.-')

# create the two bars on the right side
h.bar_correct = h.ax_performance.bar(0, self.data.ntrials_correct, label='correct', color='g')
h.bar_error = h.ax_performance.bar(
0, self.data.ntrials - self.data.ntrials_correct, label='error', color='r', bottom=self.data.ntrials_correct)
h.bar_correct = h.ax_performance.bar(0, self.data.percent_correct, label='correct', color='k')
h.bar_water = h.ax_water.bar(0, self.data.water_delivered, label='water delivered', color='b')

# create the trials timeline view in a single axis
xpos = np.tile([[-4, -1.5]], (NTRIALS_PLOT, 1)).T.flatten()
xpos = np.tile([[-3.75, -1.25]], (NTRIALS_PLOT, 1)).T.flatten()
ypos = np.tile(np.arange(NTRIALS_PLOT), 2)
h.im_trials = h.ax_trials.imshow(
self.data.rgb_background, alpha=.2, extent=[-10, 50, -.5, NTRIALS_PLOT - .5], aspect='auto', origin='lower')
Expand All @@ -233,16 +262,24 @@ def __init__(self, task_file=None):
}
h.scatter_contrast = h.ax_trials.scatter(xpos, ypos, s=250, c=self.data.last_contrasts.T.flatten(),
alpha=1, marker='o', vmin=0.0, vmax=1, cmap='Greys')
xticks = np.arange(-1, 1.1, .25)
xticklabels = np.array([f'{x:g}' for x in xticks])
xticklabels[1::2] = ''
h.ax_psych.set_xticks(xticks, xticklabels)

self.h = h
self.update_titles()
plt.show(block=False)
plt.draw()

def update_titles(self):
self.h.fig_title.set_text(
f"{self._session_string} time elapsed: {str(datetime.timedelta(seconds=int(self.data.time_elapsed)))}")
self.h.ax_water.title.set_text(f"water \n {self.data.water_delivered:.2f} (uL)")
self.h.ax_performance.title.set_text(f" correct/tot \n {self.data.ntrials_correct} / {self.data.ntrials}")
protocol = (self.data.task_settings["PYBPOD_PROTOCOL"] if self.data.task_settings else '').replace('_', r'\_')
spacer = r'\ \ ·\ \ '
main_title = r'$\mathbf{' + protocol + fr'{spacer}{self.data.ntrials}\ trials{spacer}time\ elapsed:\ ' \
fr'{str(datetime.timedelta(seconds=int(self.data.time_elapsed)))}' + r'}$'
self.h.fig_title.set_text(main_title + '\n' + self._session_string)
self.h.ax_water.title.set_text(f"total reward\n{self.data.water_delivered:.1f}μL")
self.h.ax_performance.title.set_text(f"performance\n{self.data.percent_correct:.0f}%")

def update_trial(self, trial_data, bpod_data):
"""
Expand Down Expand Up @@ -273,24 +310,34 @@ def update_graphics(self, pupdate: float | None = None):
h.lines_trials[k][0].set(xdata=self.data.last_trials[k])
self.h.scatter_contrast.set_array(self.data.last_contrasts.T.flatten())
# update barplots
self.h.bar_correct[0].set(height=self.data.ntrials_correct)
self.h.bar_error[0].set(height=self.data.ntrials - self.data.ntrials_correct, y=self.data.ntrials_correct)
self.h.bar_correct[0].set(height=self.data.percent_correct)
self.h.bar_water[0].set(height=self.data.water_delivered)
h.ax_performance.set(ylim=[0, (self.data.ntrials // 50 + 1) * 50])

@property
def _session_string(self) -> str:
return ' - '.join(self.data.session_path.parts[-3:]) if self.data.session_path != "" else ""
def _set_session_string(self) -> None:
if isinstance(self.data.task_settings, dict):
training_info, _ = get_subject_training_info(subject_name=self.data.task_settings["SUBJECT_NAME"],
task_name=self.data.task_settings["PYBPOD_PROTOCOL"],
lab=self.data.task_settings["ALYX_LAB"])
self._session_string = f'subject: {self.data.task_settings["SUBJECT_NAME"]} · ' \
f'weight: {self.data.task_settings["SUBJECT_WEIGHT"]}g · ' \
f'training phase: {training_info["training_phase"]} · ' \
f'stimulus gain: {self.data.task_settings["STIM_GAIN"]} · ' \
f'reward amount: {self.data.task_settings["REWARD_AMOUNT_UL"]}µl'
else:
self._session_string = ''

def run(self, task_file: Path | str) -> None:
"""
This methods is for online use, it will watch for a file in conjunction with an iblrigv8 running task
:param task_file:
:return:
"""
task_file = Path(task_file)
self.data.get_task_settings(task_file.parent)
self._set_session_string()
self.update_titles()
self.h.fig.canvas.flush_events()
self.real_time = Bunch({'fseek': 0, 'time_last_check': 0})
task_file = Path(task_file)
flag_file = task_file.parent.joinpath('new_trial.flag')

while True:
Expand Down
4 changes: 2 additions & 2 deletions iblrig/test/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class TestMisc(unittest.TestCase):
def test_draw_contrast(self):
n_draws = 1000
n_draws = 5000
n_contrasts = 10
contrast_set = np.linspace(0, 1, n_contrasts)

Expand All @@ -30,7 +30,7 @@ def assert_distribution(values: list[int], f_exp: list[float] | None = None) ->
assert_distribution(contrasts, expected)

self.assertRaises(ValueError, misc.draw_contrast, [], "incorrect_type") # assert exception for incorrect type
self.assertRaises(ValueError, misc.draw_contrast, [0, 1], "biased", 2) # assert exception for out-of-range index
self.assertRaises(IndexError, misc.draw_contrast, [0, 1], "biased", 2) # assert exception for out-of-range index

def test_online_std(self):
n = 41
Expand Down

0 comments on commit c582e33

Please sign in to comment.