11"""Mesoscope (timeline) data extraction."""
22import logging
3+ from itertools import chain
34
45import numpy as np
56from scipy .signal import find_peaks
@@ -625,7 +626,7 @@ def __init__(self, session_path, n_FOVs):
625626 self .var_names = [f'{ x } _{ y .lower ()} ' for x in self .var_names for y in fov ]
626627 self .save_names = [f'{ y } /{ x } ' for x in self .save_names for y in fov ]
627628
628- def _extract (self , sync = None , chmap = None , device_collection = 'raw_imaging_data' , events = None ):
629+ def _extract (self , sync = None , chmap = None , device_collection = 'raw_imaging_data' , events = None , use_volume_counter = False ):
629630 """
630631 Extract the frame timestamps for each individual field of view (FOV) and the time offsets
631632 for each line scan.
@@ -636,6 +637,10 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
636637 rawImagingData.times file). The field of view (FOV) shifts are then applied to these
637638 timestamps for each field of view and provided together with the line shifts.
638639
640+ Note that for single plane sessions, the 'neural_frames' and 'volume_counter' channels are
641+ identical. For multi-depth sessions, 'neural_frames' contains the frame times for each
642+ depth acquired.
643+
639644 Parameters
640645 ----------
641646 sync : one.alf.io.AlfBunch
@@ -649,14 +654,22 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
649654 events : pandas.DataFrame
650655 A table of software events, with columns {'time_timeline' 'name_timeline',
651656 'event_timeline'}.
657+ use_volume_counter : bool
658+ If True, use the 'volume_counter' channel to extract the frame times. On the scale of
659+ calcium dynamics, it shouldn't matter whether we read specifically the timing of each
660+ slice, or assume that they are equally spaced between the volume_counter pulses. But
661+ in cases where each depth doesn't have the same nr of FOVs / scanlines, some depths
662+ will be faster than others, so it would be better to read out the neural frames for
663+ the purpose of computing the correct timeshifts per line. This can be set to True
664+ for legacy extractions.
652665
653666 Returns
654667 -------
655668 list of numpy.array
656669 A list of timestamps for each FOV and the time offsets for each line scan.
657670 """
671+ volume_times = sync ['times' ][sync ['channels' ] == chmap ['volume_counter' ]]
658672 frame_times = sync ['times' ][sync ['channels' ] == chmap ['neural_frames' ]]
659-
660673 # imaging_start_time = datetime.datetime(*map(round, self.rawImagingData.meta['acquisitionStartTime']))
661674 if isinstance (device_collection , str ):
662675 device_collection = [device_collection ]
@@ -671,10 +684,11 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
671684 # Calculate line shifts
672685 _ , fov_time_shifts , line_time_shifts = self .get_timeshifts (imaging_data ['meta' ])
673686 assert len (fov_time_shifts ) == self .n_FOVs , f'unexpected number of FOVs for { collection } '
687+ vts = volume_times [np .logical_and (volume_times >= tmin , volume_times <= tmax )]
674688 ts = frame_times [np .logical_and (frame_times >= tmin , frame_times <= tmax )]
675- assert ts .size >= imaging_data [
676- 'times_scanImage' ]. size , ( f" fewer DAQ timestamps for { collection } than expected: "
677- f" DAQ/frames = { ts .size } /{ imaging_data [' times_scanImage' ].size } " )
689+ assert ts .size >= imaging_data ['times_scanImage' ]. size , \
690+ ( f' fewer DAQ timestamps for { collection } than expected: '
691+ f' DAQ/frames = { ts .size } /{ imaging_data [" times_scanImage" ].size } ' )
678692 if ts .size > imaging_data ['times_scanImage' ].size :
679693 _logger .warning (
680694 'More DAQ frame times detected for %s than were found in the raw image data.\n '
@@ -683,8 +697,32 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
683697 'when image data is corrupt, or when frames are not written to file.' ,
684698 collection , ts .size , imaging_data ['times_scanImage' ].size )
685699 _logger .info ('Dropping last %i frame times for %s' , ts .size - imaging_data ['times_scanImage' ].size , collection )
700+ vts = vts [vts < ts [imaging_data ['times_scanImage' ].size ]]
686701 ts = ts [:imaging_data ['times_scanImage' ].size ]
687- fov_times .append ([ts + offset for offset in fov_time_shifts ])
702+
703+ # A 'slice_id' is a ScanImage 'ROI', comprising a collection of 'scanfields' a.k.a. slices at different depths
704+ # The total number of 'scanfields' == len(imaging_data['meta']['FOV'])
705+ slice_ids = np .array ([x ['slice_id' ] for x in imaging_data ['meta' ]['FOV' ]])
706+ unique_areas , slice_counts = np .unique (slice_ids , return_counts = True )
707+ n_unique_areas = len (unique_areas )
708+
709+ if use_volume_counter :
710+ # This is the simple, less accurate way of extrating imaging times
711+ fov_times .append ([vts + offset for offset in fov_time_shifts ])
712+ else :
713+ if len (np .unique (slice_counts )) != 1 :
714+ # A different number of depths per FOV may no longer be an issue with this new method
715+ # of extracting imaging times, but the below assertion is kept as it's not tested and
716+ # not implemented for a different number of scanlines per FOV
717+ _logger .warning (
718+ 'different number of slices per area (i.e. scanfields per ROI) (%s).' ,
719+ ' vs ' .join (map (str , slice_counts )))
720+ # This gets the imaging times for each FOV, respecting the order of the scanfields in multidepth imaging
721+ fov_times .append (list (chain .from_iterable (
722+ [ts [i ::n_unique_areas ][:vts .size ] + offset for offset in fov_time_shifts [:n_depths ]]
723+ for i , n_depths in enumerate (slice_counts )
724+ )))
725+
688726 if not line_shifts :
689727 line_shifts = line_time_shifts
690728 else : # The line shifts should be the same across all imaging bouts
@@ -693,7 +731,7 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
693731 # Concatenate imaging timestamps across all bouts for each field of view
694732 fov_times = list (map (np .concatenate , zip (* fov_times )))
695733 n_fov_times , = set (map (len , fov_times ))
696- if n_fov_times != frame_times .size :
734+ if n_fov_times != volume_times .size :
697735 # This may happen if an experimenter deletes a raw_imaging_data folder
698736 _logger .debug ('FOV timestamps length does not match neural frame count; imaging bout(s) likely missing' )
699737 return fov_times + line_shifts
@@ -782,7 +820,7 @@ def get_timeshifts(raw_imaging_meta):
782820 Calculate the time shifts for each field of view (FOV) and the relative offsets for each
783821 scan line.
784822
785- For a 2 scan field , 2 depth recording (so 4 FOVs):
823+ For a 2 area (i.e. 'ROI') , 2 depth recording (so 4 FOVs):
786824
787825 Frame 1, lines 1-512 correspond to FOV_00
788826 Frame 1, lines 551-1062 correspond to FOV_01
@@ -791,6 +829,13 @@ def get_timeshifts(raw_imaging_meta):
791829 Frame 3, lines 1-512 correspond to FOV_00
792830 ...
793831
832+ All areas are acquired for each depth such that...
833+
834+ FOV_00 = area 1, depth 1
835+ FOV_01 = area 2, depth 1
836+ FOV_02 = area 1, depth 2
837+ FOV_03 = area 2, depth 2
838+
794839 Parameters
795840 ----------
796841 raw_imaging_meta : dict
0 commit comments