class NI_DAQmxAcquisitionWorker(Worker): MAX_READ_INTERVAL = 0.2 MAX_READ_PTS = 10000 def init(self): # Prevent interference between the read callback and the shutdown code: self.tasklock = threading.RLock() # Assigned on a per-task basis and cleared afterward: self.read_array = None self.task = None # Assigned on a per-shot basis and cleared afterward: self.buffered_mode = False self.h5_file = None self.acquired_data = None self.buffered_rate = None self.buffered_chans = None # Hard coded for now. Perhaps we will add functionality to enable # and disable inputs in manual mode, and adjust the rate: self.manual_mode_chans = ['ai%d' % i for i in range(self.num_AI)] self.manual_mode_rate = 1000 # An event for knowing when the wait durations are known, so that we may use # them to chunk up acquisition data: self.wait_durations_analysed = Event('wait_durations_analysed') # Start task for manual mode self.start_task(self.manual_mode_chans, self.manual_mode_rate) def shutdown(self): if self.task is not None: self.stop_task() def read(self, task_handle, event_type, num_samples, callback_data=None): """Called as a callback by DAQmx while task is running. Also called by us to get remaining data just prior to stopping the task. Since the callback runs in a separate thread, we need to serialise access to instance variables""" samples_read = int32() with self.tasklock: if self.task is None or task_handle != self.task.taskHandle.value: # Task stopped already. return 0 self.task.ReadAnalogF64( num_samples, -1, DAQmx_Val_GroupByScanNumber, self.read_array, self.read_array.size, samples_read, None, ) # Select only the data read, and downconvert to 32 bit: data = self.read_array[: int(samples_read.value), :].astype(np.float32) if self.buffered_mode: # Append to the list of acquired data: self.acquired_data.append(data) else: # TODO: Send it to the broker thingy. pass return 0 def start_task(self, chans, rate): """Set up a task that acquires data with a callback every MAX_READ_PTS points or MAX_READ_INTERVAL seconds, whichever is faster. NI DAQmx calls callbacks in a separate thread, so this method returns, but data acquisition continues until stop_task() is called. Data is appended to self.acquired_data if self.buffered_mode=True, or (TODO) sent to the [whatever the AI server broker is called] if self.buffered_mode=False.""" if self.task is not None: raise RuntimeError('Task already running') if chans is None: return # Get data MAX_READ_PTS points at a time or once every MAX_READ_INTERVAL # seconds, whichever is faster: num_samples = min(self.MAX_READ_PTS, int(rate * self.MAX_READ_INTERVAL)) self.read_array = np.zeros((num_samples, len(chans)), dtype=np.float64) self.task = Task() for chan in chans: self.task.CreateAIVoltageChan( self.MAX_name + '/' + chan, "", DAQmx_Val_RSE, self.AI_range[0], self.AI_range[1], DAQmx_Val_Volts, None, ) self.task.CfgSampClkTiming( "", rate, DAQmx_Val_Rising, DAQmx_Val_ContSamps, num_samples ) if self.buffered_mode: self.task.CfgDigEdgeStartTrig(self.clock_terminal, DAQmx_Val_Rising) # This must not be garbage collected until the task is: self.task.callback_ptr = DAQmxEveryNSamplesEventCallbackPtr(self.read) self.task.RegisterEveryNSamplesEvent( DAQmx_Val_Acquired_Into_Buffer, num_samples, 0, self.task.callback_ptr, 100 ) self.task.StartTask() def stop_task(self): with self.tasklock: if self.task is None: raise RuntimeError('Task not running') # Read remaining data: self.read(self.task, None, -1) # Stop the task: self.task.StopTask() self.task.ClearTask() self.task = None self.read_array = None def transition_to_buffered(self, device_name, h5file, initial_values, fresh): self.logger.debug('transition_to_buffered') # read channels, acquisition rate, etc from H5 file with h5py.File(h5file, 'r') as f: group = f['/devices/' + device_name] if 'AI' not in group: # No acquisition return {} AI_table = group['AI'][:] device_properties = properties.get(f, device_name, 'device_properties') chans = [_ensure_str(c) for c in AI_table['connection']] # Remove duplicates and sort: if chans: self.buffered_chans = sorted(set(chans), key=split_conn_AI) self.h5_file = h5file self.buffered_rate = device_properties['acquisition_rate'] self.acquired_data = [] # Stop the manual mode task and start the buffered mode task: self.stop_task() self.buffered_mode = True self.start_task(self.buffered_chans, self.buffered_rate) return {} def transition_to_manual(self, abort=False): self.logger.debug('transition_to_manual') # If we were doing buffered mode acquisition, stop the buffered mode task and # start the manual mode task. We might not have been doing buffered mode # acquisition if abort() was called when we are not in buffered mode, or if # there were no acuisitions this shot. if not self.buffered_mode: return True if self.buffered_chans is not None: self.stop_task() self.buffered_mode = False self.logger.info('transitioning to manual mode, task stopped') self.start_task(self.manual_mode_chans, self.manual_mode_rate) if abort: self.acquired_data = None self.buffered_chans = None self.h5_file = None self.buffered_rate = None return True with h5py.File(self.h5_file, 'a') as hdf5_file: data_group = hdf5_file['data'] data_group.create_group(self.device_name) waits_in_use = len(hdf5_file['waits']) > 0 if self.buffered_chans is not None and not self.acquired_data: msg = """No data was acquired. Perhaps the acquisition task was not triggered to start, is the device connected to a pseudoclock?""" raise RuntimeError(dedent(msg)) # Concatenate our chunks of acquired data and recast them as a structured # array with channel names: if self.acquired_data: start_time = time.time() dtypes = [(chan, np.float32) for chan in self.buffered_chans] raw_data = np.concatenate(self.acquired_data).view(dtypes) raw_data = raw_data.reshape((len(raw_data),)) self.acquired_data = None self.buffered_chans = None self.extract_measurements(raw_data, waits_in_use) self.h5_file = None self.buffered_rate = None msg = 'data written, time taken: %ss' % str(time.time() - start_time) else: msg = 'No acquisitions in this shot.' self.logger.info(msg) return True def extract_measurements(self, raw_data, waits_in_use): self.logger.debug('extract_measurements') if waits_in_use: # There were waits in this shot. We need to wait until the other process has # determined their durations before we proceed: self.wait_durations_analysed.wait(self.h5_file) with h5py.File(self.h5_file, 'a') as hdf5_file: if waits_in_use: # get the wait start times and durations waits = hdf5_file['/data/waits'] wait_times = waits['time'] wait_durations = waits['duration'] try: acquisitions = hdf5_file['/devices/' + self.device_name + '/AI'] except KeyError: # No acquisitions! return try: measurements = hdf5_file['/data/traces'] except KeyError: # Group doesn't exist yet, create it: measurements = hdf5_file.create_group('/data/traces') t0 = self.AI_start_delay for connection, label, t_start, t_end, _, _, _ in acquisitions: connection = _ensure_str(connection) label = _ensure_str(label) if waits_in_use: # add durations from all waits that start prior to t_start of # acquisition t_start += wait_durations[(wait_times < t_start)].sum() # compare wait times to t_end to allow for waits during an # acquisition t_end += wait_durations[(wait_times < t_end)].sum() i_start = int(np.ceil(self.buffered_rate * (t_start - t0))) i_end = int(np.floor(self.buffered_rate * (t_end - t0))) # np.ceil does what we want above, but float errors can miss the # equality: if t0 + (i_start - 1) / self.buffered_rate - t_start > -2e-16: i_start -= 1 # We want np.floor(x) to yield the largest integer < x (not <=): if t_end - t0 - i_end / self.buffered_rate < 2e-16: i_end -= 1 t_i = t0 + i_start / self.buffered_rate t_f = t0 + i_end / self.buffered_rate times = np.linspace(t_i, t_f, i_end - i_start + 1, endpoint=True) values = raw_data[connection][i_start : i_end + 1] dtypes = [('t', np.float64), ('values', np.float32)] data = np.empty(len(values), dtype=dtypes) data['t'] = times data['values'] = values measurements.create_dataset(label, data=data) def abort_buffered(self): return self.transition_to_manual(True) def abort_transition_to_buffered(self): return self.transition_to_manual(True) def program_manual(self, values): return {}
class Plugin(object): def __init__(self, initial_settings): self.menu = None self.notifications = {} self.initial_settings = initial_settings self.BLACS = None self.command_queue = Queue() self.master_pseudoclock = None self.shot_start_time = None self.stop_time = None self.markers = None self.waits = None self.time_spent_waiting = None self.next_wait_index = None self.next_marker_index = None self.bar_text_prefix = None self.h5_filepath = None self.wait_completed_events_supported = False self.wait_completed = Event('wait_completed', type='wait') self.mainloop_thread = threading.Thread(target=self.mainloop) self.mainloop_thread.daemon = True self.mainloop_thread.start() def plugin_setup_complete(self, BLACS): self.BLACS = BLACS self.ui = UiLoader().load(os.path.join(PLUGINS_DIR, module, 'controls.ui')) self.bar = self.ui.bar self.style = QtWidgets.QStyleFactory.create('Fusion') if self.style is None: # If we're on Qt4, fall back to Plastique style: self.style = QtWidgets.QStyleFactory.create('Plastique') if self.style is None: # Not sure what's up, but fall back to app's default style: self.style = QtWidgets.QApplication.style() self.bar.setStyle(self.style) self.bar.setMaximum(BAR_MAX) self.bar.setAlignment(QtCore.Qt.AlignCenter) # Add our controls to the BLACS gui: BLACS['ui'].queue_status_verticalLayout.insertWidget(0, self.ui) # We need to know the name of the master pseudoclock so we can look up # the duration of each shot: self.master_pseudoclock = self.BLACS['experiment_queue'].master_pseudoclock # Check if the wait monitor device, if any, supports wait completed events: with h5py.File(self.BLACS['connection_table_h5file'], 'r') as f: if 'waits' in f: acq_device = f['waits'].attrs['wait_monitor_acquisition_device'] acq_device = _ensure_str(acq_device) if acq_device: props = properties.get(f, acq_device, 'connection_table_properties') if props.get('wait_monitor_supports_wait_completed_events', False): self.wait_completed_events_supported = True self.ui.wait_warning.hide() def get_save_data(self): return {} def get_callbacks(self): return {'science_over': self.on_science_over, 'science_starting': self.on_science_starting} @callback(priority=100) def on_science_starting(self, h5_filepath): # Tell the mainloop that we're starting a shot: self.command_queue.put(('start', h5_filepath)) @callback(priority=5) def on_science_over(self, h5_filepath): # Tell the mainloop we're done with this shot: self.command_queue.put(('stop', None)) @inmain_decorator(True) def clear_bar(self): self.bar.setEnabled(False) self.bar.setFormat('No shot running') self.bar.setValue(0) self.bar.setPalette(self.style.standardPalette()) self.ui.wait_warning.hide() def get_next_thing(self): """Figure out what's going to happen next: a wait, a time marker, or a regular update. Return a string saying which, and a float saying how long from now it will occur. If the thing has already happened but not been taken into account by our processing yet, then return zero for the time.""" if self.waits is not None and self.next_wait_index < len(self.waits): next_wait_time = self.waits['time'][self.next_wait_index] else: next_wait_time = np.inf if self.markers is not None and self.next_marker_index < len(self.markers): next_marker_time = self.markers['time'][self.next_marker_index] else: next_marker_time = np.inf assert self.shot_start_time is not None assert self.time_spent_waiting is not None labscript_time = time.time() - self.shot_start_time - self.time_spent_waiting next_update_time = labscript_time + UPDATE_INTERVAL if next_update_time < next_wait_time and next_update_time < next_marker_time: return 'update', UPDATE_INTERVAL elif next_wait_time < next_marker_time: return 'wait', max(0, next_wait_time - labscript_time) else: return 'marker', max(0, next_marker_time - labscript_time) @inmain_decorator(True) def update_bar_style(self, marker=False, wait=False, previous=False): """Update the bar's style to reflect the next marker or wait, according to self.next_marker_index or self.next_wait_index. If previous=True, instead update to reflect the current marker or wait.""" assert not (marker and wait) # Ignore requests to reflect markers or waits if there are no markers # or waits in this shot: marker = marker and self.markers is not None and len(self.markers) > 0 wait = wait and self.waits is not None and len(self.waits) > 0 if marker: marker_index = self.next_marker_index if previous: marker_index -= 1 assert marker_index >= 0 label, _, color = self.markers[marker_index] self.bar_text_prefix = '[%s] ' % _ensure_str(label) r, g, b = color[0] # Black is the default colour in labscript.add_time_marker. # Don't change the bar colour if the marker colour is black. if (r, g, b) != (0,0,0): bar_color = QtGui.QColor(r, g, b) if black_has_good_contrast(r, g, b): highlight_text_color = QtCore.Qt.black else: highlight_text_color = QtCore.Qt.white else: bar_color = None highlight_text_color = None regular_text_color = None # use default elif wait: wait_index = self.next_wait_index if previous: wait_index -= 1 assert wait_index >= 0 label = self.waits[wait_index]['label'] self.bar_text_prefix = '-%s- ' % _ensure_str(label) highlight_text_color = regular_text_color = QtGui.QColor(192, 0, 0) bar_color = QtCore.Qt.gray if marker or wait: palette = QtGui.QPalette() if bar_color is not None: palette.setColor(QtGui.QPalette.Highlight, bar_color) # Ensure the colour of the text on the filled in bit of the progress # bar has good contrast: if highlight_text_color is not None: palette.setColor(QtGui.QPalette.HighlightedText, highlight_text_color) if regular_text_color is not None: palette.setColor(QtGui.QPalette.Text, regular_text_color) self.bar.setPalette(palette) else: self.bar_text_prefix = None # Default palette: self.bar.setPalette(self.style.standardPalette()) @inmain_decorator(True) def update_bar_value(self, marker=False, wait=False): """Update the progress bar with the current time elapsed. If marker or wait is true, then use the exact time at which the next marker or wait is defined, rather than the current time as returned by time.time()""" thinspace = u'\u2009' self.bar.setEnabled(True) assert not (marker and wait) if marker: labscript_time = self.markers['time'][self.next_marker_index] elif wait: labscript_time = self.waits['time'][self.next_wait_index] else: labscript_time = time.time() - self.shot_start_time - self.time_spent_waiting value = int(round(labscript_time / self.stop_time * BAR_MAX)) self.bar.setValue(value) text = u'%.2f%ss / %.2f%ss (%%p%s%%)' text = text % (labscript_time, thinspace, self.stop_time, thinspace, thinspace) if self.bar_text_prefix is not None: text = self.bar_text_prefix + text self.bar.setFormat(text) def _start(self, h5_filepath): """Called from the mainloop when starting a shot""" self.h5_filepath = h5_filepath # Get the stop time, any waits and any markers from the shot: with h5py.File(h5_filepath, 'r') as f: props = properties.get(f, self.master_pseudoclock, 'device_properties') self.stop_time = props['stop_time'] try: self.markers = f['time_markers'][:] self.markers.sort(order=(bytes if PY2 else str)('time')) except KeyError: self.markers = None try: self.waits = f['waits'][:] self.waits.sort(order=(bytes if PY2 else str)('time')) except KeyError: self.waits = None self.shot_start_time = time.time() self.time_spent_waiting = 0 self.next_marker_index = 0 self.next_wait_index = 0 def _stop(self): """Called from the mainloop when ending a shot""" self.h5_filepath = None self.shot_start_time = None self.stop_time = None self.markers = None self.waits = None self.time_spent_waiting = None self.next_wait_index = None self.next_marker_index = None self.bar_text_prefix = None def mainloop(self): running = False self.clear_bar() while True: try: if running: # How long until the next thing of interest occurs, and # what is it? It can be either a wait, a marker, or a # regular update. next_thing, timeout = self.get_next_thing() try: command, _ = self.command_queue.get(timeout=timeout) except Empty: if next_thing == 'update': self.update_bar_value() if next_thing == 'marker': self.update_bar_style(marker=True) self.update_bar_value(marker=True) self.next_marker_index += 1 elif next_thing == 'wait': wait_start_time = time.time() self.update_bar_style(wait=True) self.update_bar_value(wait=True) self.next_wait_index += 1 # wait for the wait to complete, but abandon # processing if the command queue is non-empty, # i.e. if a stop command is sent. while self.command_queue.empty(): try: # Wait for only 0.1 sec at a time, so that # we can check if the queue is empty in between: self.wait_completed.wait(self.h5_filepath, timeout=0.1) except TimeoutError: # Only wait for wait completed events if the wait # monitor device supports them. Otherwise, skip # after this first timeout, and it will just look # like the wait had 0.1 sec duration. if self.wait_completed_events_supported: # The wait is still in progress: continue # The wait completed (or completion events are not # supported): self.time_spent_waiting += time.time() - wait_start_time # Set the bar style back to whatever the # previous marker was, if any: self.update_bar_style(marker=True, previous=True) self.update_bar_value() break continue else: command, h5_filepath = self.command_queue.get() if command == 'close': break elif command == 'start': assert not running running = True self._start(h5_filepath) self.update_bar_value() if ( self.waits is not None and len(self.waits) > 0 and not self.wait_completed_events_supported ): inmain(self.ui.wait_warning.show) elif command == 'stop': assert running self.clear_bar() running = False self._stop() else: raise ValueError(command) except Exception: logger.exception("Exception in mainloop, ignoring.") # Stop processing of the current shot, if any. self.clear_bar() inmain(self.bar.setFormat, "Error in progress bar plugin") running = False self._stop() def close(self): self.command_queue.put(('close', None)) self.mainloop_thread.join() # The rest of these are boilerplate: def get_menu_class(self): return None def get_notification_classes(self): return [] def get_setting_classes(self): return [] def set_menu_instance(self, menu): self.menu = menu def set_notification_instances(self, notifications): self.notifications = notifications