Skip to content

Commit 83570ae

Browse files
authored
Merge pull request #1376 from AllenInstitute/refactor-visbeh-sync
Refactor visbeh sync
2 parents 6a647ad + f6f635b commit 83570ae

File tree

4 files changed

+327
-71
lines changed

4 files changed

+327
-71
lines changed

allensdk/brain_observatory/behavior/sync/__init__.py

Lines changed: 224 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,64 +4,236 @@
44
@author: marinag
55
"""
66
from itertools import chain
7-
from typing import Dict, Any
8-
from .process_sync import filter_digital, calculate_delay # NOQA: E402
7+
from typing import Dict, Any, Optional, List, Union
8+
from allensdk.brain_observatory.behavior.sync.process_sync import (
9+
filter_digital, calculate_delay) # NOQA: E402
910
from allensdk.brain_observatory.sync_dataset import Dataset as SyncDataset # NOQA: E402
1011
import numpy as np
1112
import scipy.stats as sps
1213

13-
def get_sync_data(sync_path):
14+
15+
def get_raw_stimulus_frames(
16+
dataset: SyncDataset,
17+
permissive: bool = False
18+
) -> np.ndarray:
19+
""" Report the raw timestamps of each stimulus frame. This corresponds to
20+
the time at which the psychopy window's flip method returned, but not
21+
necessarily to the time at which the stimulus frame was displayed.
22+
23+
Parameters
24+
----------
25+
dataset : describes experiment timing
26+
permissive : If True, None will be returned if timestamps are not found. If
27+
False, a KeyError will be raised
28+
29+
Returns
30+
-------
31+
array of timestamps (floating point; seconds; relative to experiment start).
32+
33+
"""
34+
try:
35+
return dataset.get_edges("falling",'stim_vsync', "seconds")
36+
except KeyError:
37+
if not permissive:
38+
raise
39+
return
40+
41+
42+
def get_ophys_frames(
43+
dataset: SyncDataset,
44+
permissive: bool = False
45+
) -> np.ndarray:
46+
""" Report the timestamps of each optical physiology video frame
47+
48+
Parameters
49+
----------
50+
dataset : describes experiment timing
51+
52+
Returns
53+
-------
54+
array of timestamps (floating point; seconds; relative to experiment start).
55+
permissive : If True, None will be returned if timestamps are not found. If
56+
False, a KeyError will be raised
57+
58+
Notes
59+
-----
60+
use rising edge for Scientifica, falling edge for Nikon
61+
http://confluence.corp.alleninstitute.org/display/IT/Ophys+Time+Sync
62+
This function uses rising edges
63+
64+
"""
65+
try:
66+
return dataset.get_edges("rising", '2p_vsync', "seconds")
67+
except KeyError:
68+
if not permissive:
69+
raise
70+
return
71+
72+
73+
def get_lick_times(
74+
dataset: SyncDataset,
75+
permissive: bool = False
76+
) -> Optional[np.ndarray]:
77+
""" Report the timestamps of each detected lick
78+
79+
Parameters
80+
----------
81+
dataset : describes experiment timing
82+
permissive : If True, None will be returned if timestamps are not found. If
83+
False, a KeyError will be raised
84+
85+
Returns
86+
-------
87+
array of timestamps (floating point; seconds; relative to experiment start)
88+
or None. If None, no lick timestamps were found in this sync
89+
dataset.
90+
91+
"""
92+
return dataset.get_edges(
93+
"rising", ["lick_times", "lick_sensor"], "seconds", permissive)
1494

15-
sync_dataset = SyncDataset(sync_path)
16-
meta_data = sync_dataset.meta_data
17-
sample_freq = meta_data['ni_daq']['counter_output_freq']
95+
96+
def get_stim_photodiode(
97+
dataset: SyncDataset,
98+
permissive: bool = False
99+
) -> Optional[List[float]]:
100+
""" Report the timestamps of each detected sync square transition (both
101+
black -> white and white -> black) in this experiment.
102+
103+
Parameters
104+
----------
105+
dataset : describes experiment timing
106+
permissive : If True, None will be returned if timestamps are not found. If
107+
False, a KeyError will be raised
108+
109+
Returns
110+
-------
111+
array of timestamps (floating point; seconds; relative to experiment start)
112+
or None. If None, no photodiode timestamps were found in this sync
113+
dataset.
114+
115+
"""
116+
return dataset.get_edges(
117+
"all", ["stim_photodiode", "photodiode"], "seconds", permissive)
118+
119+
120+
def get_trigger(
121+
dataset: SyncDataset,
122+
permissive: bool = False
123+
) -> Optional[np.ndarray]:
124+
""" Returns (as a 1-element array) the time at which optical physiology
125+
acquisition was started.
126+
127+
Parameters
128+
----------
129+
dataset : describes experiment timing
130+
permissive : If True, None will be returned if timestamps are not found. If
131+
False, a KeyError will be raised
132+
133+
Returns
134+
-------
135+
timestamps (floating point; seconds; relative to experiment start)
136+
or None. If None, no timestamps were found in this sync dataset.
137+
138+
Notes
139+
-----
140+
Ophys frame timestamps can be recorded before acquisition start when
141+
experimenters are setting up the recording session. These do not
142+
correspond to acquired ophys frames.
143+
144+
"""
145+
return dataset.get_edges(
146+
"rising", ["2p_trigger", "acq_trigger"], "seconds", permissive)
147+
148+
149+
def get_eye_tracking(
150+
dataset: SyncDataset,
151+
permissive: bool = False
152+
) -> Optional[np.ndarray]:
153+
""" Report the timestamps of each frame of the eye tracking video
154+
155+
Parameters
156+
----------
157+
dataset : describes experiment timing
158+
permissive : If True, None will be returned if timestamps are not found. If
159+
False, a KeyError will be raised
160+
161+
Returns
162+
-------
163+
array of timestamps (floating point; seconds; relative to experiment start)
164+
or None. If None, no eye tracking timestamps were found in this sync
165+
dataset.
166+
167+
"""
168+
return dataset.get_edges(
169+
"rising", ["cam2_exposure", "eye_tracking"], "seconds", permissive)
170+
171+
172+
def get_behavior_monitoring(
173+
dataset: SyncDataset,
174+
permissive: bool = False
175+
) -> Optional[np.ndarray]:
176+
""" Report the timestamps of each frame of the behavior
177+
monitoring video
178+
179+
Parameters
180+
----------
181+
dataset : describes experiment timing
182+
permissive : If True, None will be returned if timestamps are not found. If
183+
False, a KeyError will be raised
184+
185+
Returns
186+
-------
187+
array of timestamps (floating point; seconds; relative to experiment start)
188+
or None. If None, no behavior monitoring timestamps were found in this
189+
sync dataset.
190+
191+
"""
192+
return dataset.get_edges(
193+
"rising", ["cam1_exposure", "behavior_monitoring"], "seconds",
194+
permissive)
195+
196+
197+
def get_sync_data(
198+
sync_path: str,
199+
permissive: bool = False
200+
) -> Dict[str, Union[List, np.ndarray, None]]:
201+
""" Convenience function for extracting several timestamp arrays from a
202+
sync file.
203+
204+
Parameters
205+
----------
206+
sync_path : The hdf5 file here ought to be a Visual Behavior sync output
207+
file. See allensdk.brain_observatory.sync_dataset for more details of
208+
this format.
209+
permissive : If True, None will be returned if timestamps are not found. If
210+
False, a KeyError will be raised
18211
19-
# use rising edge for Scientifica, falling edge for Nikon http://confluence.corp.alleninstitute.org/display/IT/Ophys+Time+Sync
20-
# 2P vsyncs
21-
vs2p_r = sync_dataset.get_rising_edges('2p_vsync')
22-
vs2p_f = sync_dataset.get_falling_edges('2p_vsync') # new sync may be able to do units = 'sec', so conversion can be skipped
23-
frames_2p = vs2p_r / sample_freq
24-
vs2p_fsec = vs2p_f / sample_freq
25-
26-
stimulus_times_no_monitor_delay = sync_dataset.get_falling_edges('stim_vsync') / sample_freq
27-
28-
if 'lick_times' in meta_data['line_labels']:
29-
lick_times = sync_dataset.get_rising_edges('lick_1') / sample_freq
30-
elif 'lick_sensor' in meta_data['line_labels']:
31-
lick_times = sync_dataset.get_rising_edges('lick_sensor') / sample_freq
32-
else:
33-
lick_times = None
34-
if '2p_trigger' in meta_data['line_labels']:
35-
trigger = sync_dataset.get_rising_edges('2p_trigger') / sample_freq
36-
elif 'acq_trigger' in meta_data['line_labels']:
37-
trigger = sync_dataset.get_rising_edges('acq_trigger') / sample_freq
38-
if 'stim_photodiode' in meta_data['line_labels']:
39-
a = sync_dataset.get_rising_edges('stim_photodiode') / sample_freq
40-
b = sync_dataset.get_falling_edges('stim_photodiode') / sample_freq
41-
stim_photodiode = sorted(list(a)+list(b))
42-
elif 'photodiode' in meta_data['line_labels']:
43-
a = sync_dataset.get_rising_edges('photodiode') / sample_freq
44-
b = sync_dataset.get_falling_edges('photodiode') / sample_freq
45-
stim_photodiode = sorted(list(a)+list(b))
46-
if 'cam2_exposure' in meta_data['line_labels']:
47-
eye_tracking = sync_dataset.get_rising_edges('cam2_exposure') / sample_freq
48-
elif 'eye_tracking' in meta_data['line_labels']:
49-
eye_tracking = sync_dataset.get_rising_edges('eye_tracking') / sample_freq
50-
if 'cam1_exposure' in meta_data['line_labels']:
51-
behavior_monitoring = sync_dataset.get_rising_edges('cam1_exposure') / sample_freq
52-
elif 'behavior_monitoring' in meta_data['line_labels']:
53-
behavior_monitoring = sync_dataset.get_rising_edges('behavior_monitoring') / sample_freq
54-
55-
sync_data = {'ophys_frames': frames_2p,
56-
'lick_times': lick_times,
57-
'ophys_trigger': trigger,
58-
'eye_tracking': eye_tracking,
59-
'behavior_monitoring': behavior_monitoring,
60-
'stim_photodiode': stim_photodiode,
61-
'stimulus_times_no_delay': stimulus_times_no_monitor_delay,
62-
}
63-
64-
return sync_data
212+
Returns
213+
-------
214+
A dictionary with the following keys. All timestamps in seconds:
215+
ophys_frames : timestamps of each optical physiology frame
216+
lick_times : timestamps of each detected lick
217+
ophys_trigger : The time at which ophys acquisition was started
218+
eye_tracking : timestamps of each eye tracking video frame
219+
behavior_monitoring : timestamps of behavior monitoring video frame
220+
stim_photodiode : timestamps of each photodiode transition
221+
stimulus_times_no_delay : raw stimulus frame timestamps
222+
Some values may be None. This indicates that the corresponding timestamps
223+
were not located in this sync file.
224+
225+
"""
226+
227+
sync_dataset = SyncDataset(sync_path)
228+
return {
229+
'ophys_frames': get_ophys_frames(sync_dataset, permissive),
230+
'lick_times': get_lick_times(sync_dataset, permissive),
231+
'ophys_trigger': get_trigger(sync_dataset, permissive),
232+
'eye_tracking': get_eye_tracking(sync_dataset, permissive),
233+
'behavior_monitoring': get_behavior_monitoring(sync_dataset, permissive),
234+
'stim_photodiode': get_stim_photodiode(sync_dataset, permissive),
235+
'stimulus_times_no_delay': get_raw_stimulus_frames(sync_dataset, permissive)
236+
}
65237

66238

67239
def frame_time_offset(data: Dict[str, Any]) -> float:

allensdk/brain_observatory/sync_dataset.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
1616
"""
1717
import collections
18+
from typing import Union, Sequence, Optional
1819

1920
import h5py as h5
2021
import numpy as np
@@ -294,8 +295,37 @@ def get_rising_edges(self, line, units='samples'):
294295
changes = self.get_bit_changes(bit)
295296
return self.get_all_times(units)[np.where(changes == 1)]
296297

297-
def get_edges(self, kind, keys, units='seconds'):
298+
def get_edges(
299+
self,
300+
kind: str,
301+
keys: Union[str, Sequence[str]],
302+
units: str = "seconds",
303+
permissive: bool = False
304+
) -> Optional[np.ndarray]:
298305
""" Utility function for extracting edge times from a line
306+
307+
Parameters
308+
----------
309+
kind : One of "rising", "falling", or "all". Should this method return
310+
timestamps for rising, falling or both edges on the appropriate
311+
line
312+
keys : These will be checked in sequence. Timestamps will be returned
313+
for the first which is present in the line labels
314+
units : one of "seconds", "samples", or "indices". The returned
315+
"time"stamps will be given in these units.
316+
raise_missing : If True and no matching line is found, a KeyError will
317+
be raised
318+
319+
Returns
320+
-------
321+
An array of edge times. If raise_missing is False and none of the keys
322+
were found, returns None.
323+
324+
Raises
325+
------
326+
KeyError : none of the provided keys were found among this dataset's
327+
line labels
328+
299329
"""
300330
if kind == 'falling':
301331
fn = self.get_falling_edges
@@ -316,7 +346,9 @@ def get_edges(self, kind, keys, units='seconds'):
316346
except ValueError:
317347
continue
318348

319-
raise KeyError(f"none of {keys} were found in this dataset's line labels")
349+
if not permissive:
350+
raise KeyError(
351+
f"none of {keys} were found in this dataset's line labels")
320352

321353
def get_falling_edges(self, line, units='samples'):
322354
"""

0 commit comments

Comments
 (0)