class AnalogInputChannelViewer(traits.HasTraits): index = traits.Array(dtype=np.float) data = traits.Array(dtype=np.float) device_channel_num = traits.Int(label='ADC') traits_view = View( Group( Item('device_channel_num', style='readonly'), ChacoPlotItem( 'index', 'data', #x_label = "elapsed time (sec)", x_label="index", y_label="data", show_label=False, y_bounds=(-1, 2**10 + 1), y_auto=False, resizable=True, title='Analog input', ), ), resizable=True, width=800, height=200, )
class Box(HasTraits): # left, bottom, width, height bounds = traits.Array('d', (4, )) left = traits.Property(Float) bottom = traits.Property(Float) width = traits.Property(Float) height = traits.Property(Float) right = traits.Property(Float) # read only top = traits.Property(Float) # read only def _bounds_default(self): return [0.0, 0.0, 1.0, 1.0] def _get_left(self): return self.bounds[0] def _set_left(self, left): oldbounds = self.bounds[:] self.bounds[0] = left self.trait_property_changed('bounds', oldbounds, self.bounds) def _get_bottom(self): return self.bounds[1] def _set_bottom(self, bottom): oldbounds = self.bounds[:] self.bounds[1] = bottom self.trait_property_changed('bounds', oldbounds, self.bounds) def _get_width(self): return self.bounds[2] def _set_width(self, width): oldbounds = self.bounds[:] self.bounds[2] = width self.trait_property_changed('bounds', oldbounds, self.bounds) def _get_height(self): return self.bounds[2] def _set_height(self, height): oldbounds = self.bounds[:] self.bounds[2] = height self.trait_property_changed('bounds', oldbounds, self.bounds) def _get_right(self): return self.left + self.width def _get_top(self): return self.bottom + self.height def _bounds_changed(self, old, new): pass
class DataAxis(t.HasTraits): name = t.Str() units = t.Str() scale = t.Float() offset = t.Float() size = t.Int() index_in_array = t.Int() low_value = t.Float() high_value = t.Float() value = t.Range('low_value', 'high_value') low_index = t.Int(0) high_index = t.Int() slice = t.Instance(slice) slice_bool = t.Bool(False) index = t.Range('low_index', 'high_index') axis = t.Array() def __init__(self, size, index_in_array, name='', scale=1., offset=0., units='undefined', slice_bool=False): super(DataAxis, self).__init__() self.name = name self.units = units self.scale = scale self.offset = offset self.size = size self.high_index = self.size - 1 self.low_index = 0 self.index = 0 self.index_in_array = index_in_array self.update_axis() self.on_trait_change(self.update_axis, ['scale', 'offset', 'size']) self.on_trait_change(self.update_value, 'index') self.on_trait_change(self.set_index_from_value, 'value') self.on_trait_change(self._update_slice, 'slice_bool') self.on_trait_change(self.update_index_bounds, 'size') self.slice_bool = slice_bool def __repr__(self): if self.name is not None: return self.name + ' index: ' + str(self.index_in_array) def update_index_bounds(self): self.high_index = self.size - 1 def update_axis(self): self.axis = generate_axis(self.offset, self.scale, self.size) self.low_value, self.high_value = self.axis.min(), self.axis.max() # self.update_value() def _update_slice(self, value): if value is True: self.slice = slice(None) else: self.slice = None def get_axis_dictionary(self): adict = { 'name': self.name, 'scale': self.scale, 'offset': self.offset, 'size': self.size, 'units': self.units, 'index_in_array': self.index_in_array, 'slice_bool': self.slice_bool } return adict def update_value(self): self.value = self.axis[self.index] def value2index(self, value): """Return the closest index to the given value if between the limits, otherwise it will return either the upper or lower limits Parameters ---------- value : float Returns ------- int """ if value is None: return None else: index = int(round((value - self.offset) / \ self.scale)) if self.size > index >= 0: return index elif index < 0: messages.warning("The given value is below the axis limits") return 0 else: messages.warning("The given value is above the axis limits") return int(self.size - 1) def index2value(self, index): return self.axis[index] def set_index_from_value(self, value): self.index = self.value2index(value) # If the value is above the limits we must correct the value self.value = self.index2value(self.index) def calibrate(self, value_tuple, index_tuple, modify_calibration=True): scale = (value_tuple[1] - value_tuple[0]) /\ (index_tuple[1] - index_tuple[0]) offset = value_tuple[0] - scale * index_tuple[0] if modify_calibration is True: self.offset = offset self.scale = scale else: return offset, scale traits_view = \ tui.View( tui.Group( tui.Group( tui.Item(name = 'name'), tui.Item(name = 'size', style = 'readonly'), tui.Item(name = 'index_in_array', style = 'readonly'), tui.Item(name = 'index'), tui.Item(name = 'value', style = 'readonly'), tui.Item(name = 'units'), tui.Item(name = 'slice_bool', label = 'slice'), show_border = True,), tui.Group( tui.Item(name = 'scale'), tui.Item(name = 'offset'), label = 'Calibration', show_border = True,), label = "Data Axis properties", show_border = True,), )
class LiveTimestampModelerWithAnalogInput(LiveTimestampModeler): view_AIN = traits.Button(label='view analog input (AIN)') viewer = traits.Instance(AnalogInputViewer) # the actual analog data (as a wordstream) ain_data_raw = traits.Array(dtype=np.uint16, transient=True) old_data_raw = traits.Array(dtype=np.uint16, transient=True) timer3_top = traits.Property( ) # necessary to calculate precise timestamps for AIN data channel_names = traits.Property() Vcc = traits.Property(depends_on='_trigger_device') ain_overflowed = traits.Int( 0, transient=True) # integer for display (boolean readonly editor ugly) ain_wordstream_buffer = traits.Any() traits_view = View( Group( Item('synchronize', show_label=False), Item('view_time_model_plot', show_label=False), Item('ain_overflowed', style='readonly'), Item( name='gain', style='readonly', editor=TextEditor(evaluate=float, format_func=myformat), ), Item( name='offset', style='readonly', editor=TextEditor(evaluate=float, format_func=myformat2), ), Item( name='residual_error', style='readonly', editor=TextEditor(evaluate=float, format_func=myformat), ), Item('view_AIN', show_label=False), ), title='Timestamp modeler', ) @traits.cached_property def _get_Vcc(self): return self._trigger_device.Vcc def _get_timer3_top(self): return self._trigger_device.timer3_top def _get_channel_names(self): return self._trigger_device.enabled_channel_names def update_analog_input(self): """call this function frequently to avoid overruns""" new_data_raw = self._trigger_device.get_analog_input_buffer_rawLE() data_raw = np.hstack((new_data_raw, self.old_data_raw)) self.ain_data_raw = new_data_raw newdata_all = [] chan_all = [] any_overflow = False #cum_framestamps = [] while len(data_raw): result = cDecode.process(data_raw) (N, samples, channels, did_overflow, framestamp) = result if N == 0: # no data was able to be processed break data_raw = data_raw[N:] newdata_all.append(samples) chan_all.append(channels) if did_overflow: any_overflow = True # Save framestamp data. # This is not done yet: ## if framestamp is not None: ## cum_framestamps.append( framestamp ) self.old_data_raw = data_raw # save unprocessed data for next run if any_overflow: # XXX should move to logging the error. self.ain_overflowed = 1 raise AnalogDataOverflowedError() if len(chan_all) == 0: # no data return chan_all = np.hstack(chan_all) newdata_all = np.hstack(newdata_all) USB_channel_numbers = np.unique(chan_all) #print len(newdata_all),'new samples on channels',USB_channel_numbers ## F_OSC = 8000000.0 # 8 MHz ## adc_prescaler = 128 ## downsample = 20 # maybe 21? ## n_chan = 3 ## F_samp = F_OSC/adc_prescaler/downsample/n_chan ## dt=1.0/F_samp ## ## print '%.1f Hz sampling. %.3f msec dt'%(F_samp,dt*1e3) ## MAXLEN_SEC=0.3 ## #MAXLEN = int(MAXLEN_SEC/dt) MAXLEN = 5000 #int(MAXLEN_SEC/dt) ## ## print 'MAXLEN',MAXLEN ## ## print for USB_chan in USB_channel_numbers: vi = self.viewer.usb_device_number2index[USB_chan] cond = chan_all == USB_chan newdata = newdata_all[cond] oldidx = self.viewer.channels[vi].index olddata = self.viewer.channels[vi].data if len(oldidx): baseidx = oldidx[-1] + 1 else: baseidx = 0.0 newidx = np.arange(len(newdata), dtype=np.float) + baseidx tmpidx = np.hstack((oldidx, newidx)) tmpdata = np.hstack((olddata, newdata)) if len(tmpidx) > MAXLEN: # clip to MAXLEN self.viewer.channels[vi].index = tmpidx[-MAXLEN:] self.viewer.channels[vi].data = tmpdata[-MAXLEN:] else: self.viewer.channels[vi].index = tmpidx self.viewer.channels[vi].data = tmpdata def _view_AIN_fired(self): self.viewer.edit_traits()
class LiveTimestampModeler(traits.HasTraits): _trigger_device = traits.Instance(ttrigger.DeviceModel) sync_interval = traits.Float(2.0) has_ever_synchronized = traits.Bool(False, transient=True) frame_offset_changed = traits.Event timestamps_framestamps = traits.Array(shape=(None, 2), dtype=np.float) timestamp_data = traits.Any() block_activity = traits.Bool(False, transient=True) synchronize = traits.Button(label='Synchronize') synchronizing_info = traits.Any(None) gain_offset_residuals = traits.Property( depends_on=['timestamps_framestamps']) residual_error = traits.Property(depends_on='gain_offset_residuals') gain = traits.Property(depends_on='gain_offset_residuals') offset = traits.Property(depends_on='gain_offset_residuals') frame_offsets = traits.Dict() last_frame = traits.Dict() view_time_model_plot = traits.Button traits_view = View( Group( Item( name='gain', style='readonly', editor=TextEditor(evaluate=float, format_func=myformat), ), Item( name='offset', style='readonly', editor=TextEditor(evaluate=float, format_func=myformat2), ), Item( name='residual_error', style='readonly', editor=TextEditor(evaluate=float, format_func=myformat), ), Item('synchronize', show_label=False), Item('view_time_model_plot', show_label=False), ), title='Timestamp modeler', ) def _block_activity_changed(self): if self.block_activity: print('Do not change frame rate or AIN parameters. ' 'Automatic prevention of doing ' 'so is not currently implemented.') else: print('You may change frame rate again') def _view_time_model_plot_fired(self): raise NotImplementedError('') def _synchronize_fired(self): if self.block_activity: print('Not synchronizing because activity is blocked. ' '(Perhaps because you are saving data now.') return orig_fps = self._trigger_device.frames_per_second_actual self._trigger_device.set_frames_per_second_approximate(0.0) self._trigger_device.reset_framecount_A = True # trigger reset event self.synchronizing_info = (time.time() + self.sync_interval + 0.1, orig_fps) @traits.cached_property def _get_gain(self): result = self.gain_offset_residuals if result is None: # not enought data return None gain, offset, residuals = result return gain @traits.cached_property def _get_offset(self): result = self.gain_offset_residuals if result is None: # not enought data return None gain, offset, residuals = result return offset @traits.cached_property def _get_residual_error(self): result = self.gain_offset_residuals if result is None: # not enought data return None gain, offset, residuals = result if residuals is None or len(residuals) == 0: # not enought data return None assert len(residuals) == 1 return residuals[0] @traits.cached_property def _get_gain_offset_residuals(self): if self.timestamps_framestamps is None: return None timestamps = self.timestamps_framestamps[:, 0] framestamps = self.timestamps_framestamps[:, 1] if len(timestamps) < 2: return None # like model_remote_to_local in flydra.analysis remote_timestamps = framestamps local_timestamps = timestamps a1 = remote_timestamps[:, np.newaxis] a2 = np.ones((len(remote_timestamps), 1)) A = np.hstack((a1, a2)) b = local_timestamps[:, np.newaxis] x, resids, rank, s = np.linalg.lstsq(A, b) gain = x[0, 0] offset = x[1, 0] return gain, offset, resids def set_trigger_device(self, device): self._trigger_device = device self._trigger_device.on_trait_event( self._on_trigger_device_reset_AIN_overflow_fired, name='reset_AIN_overflow') def _on_trigger_device_reset_AIN_overflow_fired(self): self.ain_overflowed = 0 def _get_now_framestamp(self, max_error_seconds=0.003, full_output=False): count = 0 while count <= 10: now1 = time.time() try: results = self._trigger_device.get_framestamp( full_output=full_output) except ttrigger.NoDataError: raise ImpreciseMeasurementError('no data available') now2 = time.time() if full_output: framestamp, framecount, tcnt = results else: framestamp = results count += 1 measurement_error = abs(now2 - now1) if framestamp % 1.0 < 0.1: warnings.warn('workaround of TCNT race condition on MCU...') continue if measurement_error < max_error_seconds: break time.sleep(0.01) # wait 10 msec before trying again if not measurement_error < max_error_seconds: raise ImpreciseMeasurementError( 'could not obtain low error measurement') if framestamp % 1.0 < 0.1: raise ImpreciseMeasurementError('workaround MCU bug') now = (now1 + now2) * 0.5 if full_output: results = now, framestamp, now1, now2, framecount, tcnt else: results = now, framestamp return results def clear_samples(self, call_update=True): self.timestamps_framestamps = np.empty((0, 2)) if call_update: self.update() def update(self, return_last_measurement_info=False): """call this function fairly often to pump information from the USB device""" if self.synchronizing_info is not None: done_time, orig_fps = self.synchronizing_info # suspended trigger pulses to re-synchronize if time.time() >= done_time: # we've waited the sync duration, restart self._trigger_device.set_frames_per_second_approximate( orig_fps) self.clear_samples(call_update=False) # avoid recursion self.synchronizing_info = None self.has_ever_synchronized = True results = self._get_now_framestamp( full_output=return_last_measurement_info) now, framestamp = results[:2] if return_last_measurement_info: start_timestamp, stop_timestamp, framecount, tcnt = results[2:] self.timestamps_framestamps = np.vstack( (self.timestamps_framestamps, [now, framestamp])) # If more than 100 samples, if len(self.timestamps_framestamps) > 100: # keep only the most recent 50. self.timestamps_framestamps = self.timestamps_framestamps[-50:] if return_last_measurement_info: return start_timestamp, stop_timestamp, framecount, tcnt def get_frame_offset(self, id_string): return self.frame_offsets[id_string] def register_frame(self, id_string, framenumber, frame_timestamp, full_output=False): """note that a frame happened and return start-of-frame time""" # This may get called from another thread (e.g. the realtime # image processing thread). # An important note about locking and thread safety: This code # relies on the Python interpreter to lock data structures # across threads. To do this internally, a lock would be made # for each variable in this instance and acquired before each # access. Because the data structures are simple Python # objects, I believe the operations are atomic and thus this # function is OK. # Don't trust camera drivers with giving a good timestamp. We # only use this to reset our framenumber-to-time data # gathering, anyway. frame_timestamp = time.time() if frame_timestamp is not None: last_frame_timestamp = self.last_frame.get(id_string, -np.inf) this_interval = frame_timestamp - last_frame_timestamp did_frame_offset_change = False if this_interval > self.sync_interval: if self.block_activity: print( 'changing frame offset is disallowed, but you attempted to do it. ignoring.' ) else: # re-synchronize camera # XXX need to figure out where frame offset of two comes from: self.frame_offsets[id_string] = framenumber - 2 did_frame_offset_change = True self.last_frame[id_string] = frame_timestamp if did_frame_offset_change: self.frame_offset_changed = True # fire any listeners result = self.gain_offset_residuals if result is None: # not enough data if full_output: results = None, None, did_frame_offset_change else: results = None return results gain, offset, residuals = result corrected_framenumber = framenumber - self.frame_offsets[id_string] trigger_timestamp = corrected_framenumber * gain + offset if full_output: results = trigger_timestamp, corrected_framenumber, did_frame_offset_change else: results = trigger_timestamp return results