|
1 | | -import numpy as np |
2 | | -import pandas as pd |
3 | | -from pybpodapi.protocol import StateMachine |
| 1 | +from pathlib import Path |
| 2 | +from typing import Annotated |
| 3 | + |
| 4 | +import yaml |
| 5 | +from annotated_types import Interval |
4 | 6 |
|
5 | 7 | import iblrig.misc |
6 | | -from iblrig.base_choice_world import BiasedChoiceWorldSession |
| 8 | +from iblrig.base_choice_world import BiasedChoiceWorldSession, BiasedChoiceWorldTrialData |
7 | 9 | from iblrig.hardware import SOFTCODE |
8 | 10 | from iblutil.util import setup_logger |
| 11 | +from pybpodapi.protocol import StateMachine |
9 | 12 |
|
10 | 13 | log = setup_logger(__name__) |
11 | 14 |
|
12 | | -INTERACTIVE_DELAY = 1.0 |
13 | | -NTRIALS_INIT = 2000 |
| 15 | +# read defaults from task_parameters.yaml |
| 16 | +with open(Path(__file__).parent.joinpath('task_parameters.yaml')) as f: |
| 17 | + DEFAULTS = yaml.safe_load(f) |
14 | 18 |
|
15 | 19 |
|
16 | | -class Session(BiasedChoiceWorldSession): |
| 20 | +class CuedBiasedChoiceWorldTrialData(BiasedChoiceWorldTrialData): |
| 21 | + """Pydantic Model for Trial Data, extended from :class:`~.iblrig.base_choice_world.BiasedChoiceWorldTrialData`.""" |
17 | 22 |
|
| 23 | + play_audio_cue: bool |
| 24 | + |
| 25 | + |
| 26 | +class Session(BiasedChoiceWorldSession): |
18 | 27 | protocol_name = 'samuel_cuedBiasedChoiceWorld' |
| 28 | + TrialDataModel = CuedBiasedChoiceWorldTrialData |
19 | 29 |
|
20 | | - def __init__(self, *args, **kwargs): |
| 30 | + def __init__(self, *args, probability_audio_cue: float = 1.0, **kwargs): |
21 | 31 | super().__init__(**kwargs) |
22 | 32 |
|
| 33 | + # store parameters to task_params |
| 34 | + self.session_info['PROBABILITY_AUDIO_CUE'] = probability_audio_cue |
| 35 | + |
23 | 36 | # loads in the settings in order to determine the main sync and thus the pipeline extractor tasks |
24 | 37 | is_main_sync = self.hardware_settings.get('MAIN_SYNC', False) |
25 | 38 | trials_task = 'CuedBiasedTrials' if is_main_sync else 'CuedBiasedTrialsTimeline' |
26 | 39 | self.extractor_tasks = ['TrialRegisterRaw', trials_task, 'TrainingStatus'] |
| 40 | + |
27 | 41 | # Update experiment description which was created by superclass init |
28 | 42 | self.experiment_description['tasks'][-1][self.protocol_name]['extractors'] = self.extractor_tasks |
29 | 43 |
|
30 | | - # init behaviour data |
31 | | - self.movement_left = self.device_rotary_encoder.THRESHOLD_EVENTS[ |
32 | | - self.task_params.QUIESCENCE_THRESHOLDS[0]] |
33 | | - self.movement_right = self.device_rotary_encoder.THRESHOLD_EVENTS[ |
34 | | - self.task_params.QUIESCENCE_THRESHOLDS[1]] |
35 | | - # init counter variables |
36 | | - self.trial_num = -1 |
37 | | - self.block_num = -1 |
38 | | - self.block_trial_num = -1 |
39 | | - # init the tables, there are 2 of them: a trials table and a ambient sensor data table |
40 | | - self.trials_table = pd.DataFrame({ |
41 | | - 'contrast': np.zeros(NTRIALS_INIT) * np.NaN, |
42 | | - 'position': np.zeros(NTRIALS_INIT) * np.NaN, |
43 | | - 'quiescent_period': np.zeros(NTRIALS_INIT) * np.NaN, |
44 | | - 'response_side': np.zeros(NTRIALS_INIT, dtype=np.int8), |
45 | | - 'response_time': np.zeros(NTRIALS_INIT) * np.NaN, |
46 | | - 'reward_amount': np.zeros(NTRIALS_INIT) * np.NaN, |
47 | | - 'reward_valve_time': np.zeros(NTRIALS_INIT) * np.NaN, |
48 | | - 'stim_angle': np.zeros(NTRIALS_INIT) * np.NaN, |
49 | | - 'stim_freq': np.zeros(NTRIALS_INIT) * np.NaN, |
50 | | - 'stim_gain': np.zeros(NTRIALS_INIT) * np.NaN, |
51 | | - 'stim_phase': np.zeros(NTRIALS_INIT) * np.NaN, |
52 | | - 'stim_reverse': np.zeros(NTRIALS_INIT, dtype=bool), |
53 | | - 'stim_sigma': np.zeros(NTRIALS_INIT) * np.NaN, |
54 | | - 'trial_correct': np.zeros(NTRIALS_INIT, dtype=bool), |
55 | | - 'trial_num': np.zeros(NTRIALS_INIT, dtype=np.int16), |
56 | | - }) |
57 | | - |
58 | 44 | def get_state_machine_trial(self, i): |
59 | 45 | sma = StateMachine(self.bpod) |
60 | 46 | if i == 0: # First trial exception start camera |
61 | | - session_delay_start = self.task_params.get("SESSION_DELAY_START", 0) |
62 | | - log.info("First trial initializing, will move to next trial only if:") |
63 | | - log.info("1. camera is detected") |
64 | | - log.info(f"2. {session_delay_start} sec have elapsed") |
| 47 | + session_delay_start = self.task_params.get('SESSION_DELAY_START', 0) |
| 48 | + log.info('First trial initializing, will move to next trial only if:') |
| 49 | + log.info('1. camera is detected') |
| 50 | + log.info(f'2. {session_delay_start} sec have elapsed') |
65 | 51 | sma.add_state( |
66 | | - state_name="trial_start", |
| 52 | + state_name='trial_start', |
67 | 53 | state_timer=0, |
68 | | - state_change_conditions={"Port1In": "delay_initiation"}, |
69 | | - output_actions=[("SoftCode", SOFTCODE.TRIGGER_CAMERA), ("BNC1", 255)], |
| 54 | + state_change_conditions={'Port1In': 'delay_initiation'}, |
| 55 | + output_actions=[('SoftCode', SOFTCODE.TRIGGER_CAMERA), ('BNC1', 255)], |
70 | 56 | ) # start camera |
71 | 57 | sma.add_state( |
72 | | - state_name="delay_initiation", |
| 58 | + state_name='delay_initiation', |
73 | 59 | state_timer=session_delay_start, |
74 | 60 | output_actions=[], |
75 | | - state_change_conditions={"Tup": "reset_rotary_encoder"}, |
| 61 | + state_change_conditions={'Tup': 'reset_rotary_encoder'}, |
76 | 62 | ) |
77 | 63 | else: |
78 | 64 | sma.add_state( |
79 | | - state_name="trial_start", |
| 65 | + state_name='trial_start', |
80 | 66 | state_timer=0, # ~100µs hardware irreducible delay |
81 | | - state_change_conditions={"Tup": "reset_rotary_encoder"}, |
82 | | - output_actions=[self.bpod.actions.stop_sound, ("BNC1", 255)], |
| 67 | + state_change_conditions={'Tup': 'reset_rotary_encoder'}, |
| 68 | + output_actions=[self.bpod.actions.stop_sound, ('BNC1', 255)], |
83 | 69 | ) # stop all sounds |
84 | 70 |
|
85 | 71 | sma.add_state( |
86 | | - state_name="reset_rotary_encoder", |
| 72 | + state_name='reset_rotary_encoder', |
87 | 73 | state_timer=0, |
88 | 74 | output_actions=[self.bpod.actions.rotary_encoder_reset], |
89 | | - state_change_conditions={"Tup": "quiescent_period"}, |
| 75 | + state_change_conditions={'Tup': 'quiescent_period'}, |
90 | 76 | ) |
91 | 77 |
|
92 | 78 | sma.add_state( # '>back' | '>reset_timer' |
93 | | - state_name="quiescent_period", |
| 79 | + state_name='quiescent_period', |
94 | 80 | state_timer=self.quiescent_period, |
95 | 81 | output_actions=[], |
96 | 82 | state_change_conditions={ |
97 | | - "Tup": "play_tone", |
98 | | - self.movement_left: "reset_rotary_encoder", |
99 | | - self.movement_right: "reset_rotary_encoder", |
| 83 | + 'Tup': 'play_tone', |
| 84 | + self.movement_left: 'reset_rotary_encoder', |
| 85 | + self.movement_right: 'reset_rotary_encoder', |
100 | 86 | }, |
101 | 87 | ) |
102 | 88 | # play tone, move on to next state if sound is detected, with a time-out of 0.1s |
103 | 89 | # SP how can we make sure the delay between play_tone and stim_on is always exactly 1s? |
104 | 90 | sma.add_state( |
105 | | - state_name="play_tone", |
| 91 | + state_name='play_tone', |
106 | 92 | state_timer=0.1, # SP is this necessary?? |
107 | 93 | output_actions=[self.bpod.actions.play_tone], |
108 | 94 | state_change_conditions={ |
109 | | - "Tup": "interactive_delay", |
110 | | - "BNC2High": "interactive_delay", |
| 95 | + 'Tup': 'interactive_delay', |
| 96 | + 'BNC2High': 'interactive_delay', |
111 | 97 | }, |
112 | 98 | ) |
113 | 99 | # this will add a delay between auditory cue and visual stimulus |
114 | 100 | # this needs to be precise and accurate based on the parameter |
115 | 101 | sma.add_state( |
116 | | - state_name="interactive_delay", |
| 102 | + state_name='interactive_delay', |
117 | 103 | state_timer=self.task_params.INTERACTIVE_DELAY, |
118 | 104 | output_actions=[], |
119 | | - state_change_conditions={"Tup": "stim_on"}, |
| 105 | + state_change_conditions={'Tup': 'stim_on'}, |
120 | 106 | ) |
121 | 107 | # show stimulus, move on to next state if a frame2ttl is detected, with a time-out of 0.1s |
122 | 108 | sma.add_state( |
123 | | - state_name="stim_on", |
| 109 | + state_name='stim_on', |
124 | 110 | state_timer=0.1, |
125 | 111 | output_actions=[self.bpod.actions.bonsai_show_stim], |
126 | 112 | state_change_conditions={ |
127 | | - "Tup": "reset2_rotary_encoder", |
128 | | - "BNC1High": "reset2_rotary_encoder", |
129 | | - "BNC1Low": "reset2_rotary_encoder", |
| 113 | + 'Tup': 'reset2_rotary_encoder', |
| 114 | + 'BNC1High': 'reset2_rotary_encoder', |
| 115 | + 'BNC1Low': 'reset2_rotary_encoder', |
130 | 116 | }, |
131 | 117 | ) |
| 118 | + |
132 | 119 | sma.add_state( |
133 | | - state_name="reset2_rotary_encoder", |
| 120 | + state_name='reset2_rotary_encoder', |
134 | 121 | state_timer=0.05, # the delay here is to avoid race conditions in the bonsai flow |
135 | 122 | output_actions=[self.bpod.actions.rotary_encoder_reset], |
136 | | - state_change_conditions={"Tup": "closed_loop"}, |
| 123 | + state_change_conditions={'Tup': 'closed_loop'}, |
137 | 124 | ) |
138 | 125 |
|
139 | 126 | sma.add_state( |
140 | | - state_name="closed_loop", |
| 127 | + state_name='closed_loop', |
141 | 128 | state_timer=self.task_params.RESPONSE_WINDOW, |
142 | 129 | output_actions=[self.bpod.actions.bonsai_closed_loop], |
143 | 130 | state_change_conditions={ |
144 | | - "Tup": "no_go", |
145 | | - self.event_error: "freeze_error", |
146 | | - self.event_reward: "freeze_reward", |
| 131 | + 'Tup': 'no_go', |
| 132 | + self.event_error: 'freeze_error', |
| 133 | + self.event_reward: 'freeze_reward', |
147 | 134 | }, |
148 | 135 | ) |
149 | 136 |
|
150 | 137 | sma.add_state( |
151 | | - state_name="no_go", |
| 138 | + state_name='no_go', |
152 | 139 | state_timer=self.task_params.FEEDBACK_NOGO_DELAY_SECS, |
153 | 140 | output_actions=[self.bpod.actions.bonsai_hide_stim, self.bpod.actions.play_noise], |
154 | | - state_change_conditions={"Tup": "exit_state"}, |
| 141 | + state_change_conditions={'Tup': 'exit_state'}, |
155 | 142 | ) |
156 | 143 |
|
157 | 144 | sma.add_state( |
158 | | - state_name="freeze_error", |
| 145 | + state_name='freeze_error', |
159 | 146 | state_timer=0, |
160 | 147 | output_actions=[self.bpod.actions.bonsai_freeze_stim], |
161 | | - state_change_conditions={"Tup": "error"}, |
| 148 | + state_change_conditions={'Tup': 'error'}, |
162 | 149 | ) |
163 | 150 |
|
164 | 151 | sma.add_state( |
165 | | - state_name="error", |
| 152 | + state_name='error', |
166 | 153 | state_timer=self.task_params.FEEDBACK_ERROR_DELAY_SECS, |
167 | 154 | output_actions=[self.bpod.actions.play_noise], |
168 | | - state_change_conditions={"Tup": "hide_stim"}, |
| 155 | + state_change_conditions={'Tup': 'hide_stim'}, |
169 | 156 | ) |
170 | 157 |
|
171 | 158 | sma.add_state( |
172 | | - state_name="freeze_reward", |
| 159 | + state_name='freeze_reward', |
173 | 160 | state_timer=0, |
174 | 161 | output_actions=[self.bpod.actions.bonsai_freeze_stim], |
175 | | - state_change_conditions={"Tup": "reward"}, |
| 162 | + state_change_conditions={'Tup': 'reward'}, |
176 | 163 | ) |
177 | 164 |
|
178 | 165 | sma.add_state( |
179 | | - state_name="reward", |
| 166 | + state_name='reward', |
180 | 167 | state_timer=self.reward_time, |
181 | | - output_actions=[("Valve1", 255), ("BNC1", 255)], |
182 | | - state_change_conditions={"Tup": "correct"}, |
| 168 | + output_actions=[('Valve1', 255), ('BNC1', 255)], |
| 169 | + state_change_conditions={'Tup': 'correct'}, |
183 | 170 | ) |
184 | 171 |
|
185 | 172 | sma.add_state( |
186 | | - state_name="correct", |
| 173 | + state_name='correct', |
187 | 174 | state_timer=self.task_params.FEEDBACK_CORRECT_DELAY_SECS, |
188 | 175 | output_actions=[], |
189 | | - state_change_conditions={"Tup": "hide_stim"}, |
| 176 | + state_change_conditions={'Tup': 'hide_stim'}, |
190 | 177 | ) |
191 | 178 |
|
192 | 179 | sma.add_state( |
193 | | - state_name="hide_stim", |
| 180 | + state_name='hide_stim', |
194 | 181 | state_timer=0.1, |
195 | 182 | output_actions=[self.bpod.actions.bonsai_hide_stim], |
196 | 183 | state_change_conditions={ |
197 | | - "Tup": "exit_state", |
198 | | - "BNC1High": "exit_state", |
199 | | - "BNC1Low": "exit_state", |
| 184 | + 'Tup': 'exit_state', |
| 185 | + 'BNC1High': 'exit_state', |
| 186 | + 'BNC1Low': 'exit_state', |
200 | 187 | }, |
201 | 188 | ) |
202 | 189 |
|
203 | 190 | sma.add_state( |
204 | | - state_name="exit_state", |
| 191 | + state_name='exit_state', |
205 | 192 | state_timer=self.task_params.ITI_DELAY_SECS, |
206 | | - output_actions=[("BNC1", 255)], |
207 | | - state_change_conditions={"Tup": "exit"}, |
| 193 | + output_actions=[('BNC1', 255)], |
| 194 | + state_change_conditions={'Tup': 'exit'}, |
208 | 195 | ) |
209 | 196 | return sma |
210 | 197 |
|
| 198 | + @staticmethod |
| 199 | + def extra_parser(): |
| 200 | + parser = super(Session, Session).extra_parser() |
| 201 | + parser.add_argument( |
| 202 | + '--probability_audio_cue', |
| 203 | + option_strings=['--probability_audio_cue'], |
| 204 | + dest='probability_audio_cue', |
| 205 | + default=DEFAULTS.get('PROBABILITY_AUDIO_CUE', 1.0), |
| 206 | + type=float, |
| 207 | + help='defines the probability of the audio cue to be played', |
| 208 | + ) |
| 209 | + return parser |
| 210 | + |
211 | 211 |
|
212 | 212 | if __name__ == '__main__': # pragma: no cover |
213 | | - kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) |
214 | | - sess = Session(**kwargs) |
| 213 | + task_kwargs = iblrig.misc.get_task_arguments(parents=[Session.extra_parser()]) |
| 214 | + sess = Session(**task_kwargs) |
215 | 215 | sess.run() |
0 commit comments