def update_region(self, view_range): # region.setRegion appears to signal twice (once per side?), # so do this in two stages to prevent double signaling. orig_range = self.region.getRegion() # possibilities for region morphing: # 1) sliding / expanding right: right side increases, left side may or may not increase # 2) sliding / expanding left: left side decreases, right side may or may not decrease # 3) expanding in place: left side decreases and right side increases # 4) shrinking in place: left side increases and right side decreases # # (1), (3) and (4) move right side first # (2) move left side first: condition is that new_left < old_left and new_right <= old_right if view_range[0] < orig_range[0] and view_range[1] <= orig_range[1]: first_region = [view_range[0], orig_range[1]] else: first_region = [orig_range[0], view_range[1]] # do right-side while blocked (force block in case we're already in a blocking context) info = parallel_context.get_logger().info with block_signals(self.region, forced_state=True): info('setting first range with block: {} {}'.format( self.region.signalsBlocked(), timestamp())) self.region.setRegion(first_region) # do both sides while emitting (or not) info('setting both sides with block: {} {}'.format( self.region.signalsBlocked(), timestamp())) self.region.setRegion(view_range)
def unset_external_data(self, redraw=False, visible=False): info('Unsetting external (visible {}) and updating curve data {}'. format(visible, timestamp())) if not visible: # Revert to raw slices and reset visible data self._use_raw_slice = True if redraw: self.updatePlotData()
def jump_nav(self, evt): if QtGui.QApplication.keyboardModifiers() != QtCore.Qt.ShiftModifier: return pos = evt.scenePos() if not self.p2.sceneBoundingRect().contains(pos): return newX = self.p2.vb.mapSceneToView(pos).x() minX, maxX = self.region.getRegion() rng = 0.5 * (maxX - minX) info('Jump-updating region box {}'.format(timestamp())) self.update_region([newX - rng, newX + rng])
def set_mean_image(self): if self.curve_manager.heatmap_curve.y_visible is None: return image = np.nanstd(self.curve_manager.heatmap_curve.y_visible, axis=1) chan_map = self.curve_manager.heatmap_curve.map_curves(self.chan_map) x_vis = self.curve_manager.heatmap_curve.x_visible[0] x_avg = 0.5 * (x_vis[0] + x_vis[-1]) frame = embed_frame(chan_map, image) info('setting RMS frame {}'.format(timestamp())) # self.img.setImage(frame, autoLevels=False) self.img.setImage(frame, autoLevels=False) self.frame_text.setText('Time ~ {:.3f}s'.format(x_avg))
def set_image_frame(self, x=None, frame_vec=(), move_vline=True): if not len(frame_vec): if x is None: # can't do anything! return idx = self.curve_manager.heatmap_curve.x_visible[0].searchsorted(x) frame_vec = self.curve_manager.heatmap_curve.y_visible[:, idx] chan_map = self.curve_manager.heatmap_curve.map_curves(self.chan_map) frame = embed_frame(chan_map, frame_vec) if self.frame_filter: frame = self.frame_filter(frame) info('Setting voltage frame {}'.format(timestamp())) # self.img.setImage(frame, autoLevels=False) self.img.setImage(frame, autoLevels=False) self.frame_text.setText('Time {:.3f}s'.format(x)) if move_vline and x is not None: self.vline.setPos(x)
def apply_callback(self, *emitting): """ Apply the real callback Parameters ---------- emitting: QtObject The emitting object """ # if the signal time stack is only one deep, then that signal was already dispatched if len(self._signal_times) == 1: return info = parallel_context.get_logger().info info('applying callback {} {} {}'.format(self.callback, emitting, timestamp())) self.callback(*emitting) self._signal_times = list()
def _set_data(self): r = self._view_range() if not r: self.y_visible = None self.x_visible = None return start, stop = r if self._cslice is None: needs_slice = True else: cstart = self._cslice.start cstop = self._cslice.stop # If the any view limit is outside the old slice bounds, update the loaded data needs_slice = start < cstart or stop > cstop if needs_slice: # When this condition, automatically reset state to raw slices. # If anything is watching this data to change, it will need access to the raw slice. info('Slicing from HDF5 {} and emitting data_changed'.format( timestamp())) N = stop - start L = self.hdf_data.shape[1] # load this many points before and after the view limits so that small updates # can grab pre-loaded data extra = int(self.load_extra / 2 * N) sl = np.s_[max(0, start - extra):min(L, stop + extra)] self._use_raw_slice = True self._raw_slice = self.hdf_data[(self.plot_channels, sl)] * self.dy # self._raw_slice = self.hdf5[sl] * self._yscale self._cslice = sl self.data_changed.emit(self) # can retreive data within the pre-load x0 = self._cslice.start y_start = start - x0 y_stop = y_start + (stop - start) self.y_visible = self.y_slice[:, y_start:y_stop] self.x_visible = np.tile( np.arange(start, stop) * self.dx, (len(self.plot_channels), 1)) info('{}: x,y arrays set: {} {}'.format(type(self), self.y_visible.shape, self.x_visible.shape))
def set_text_position(self, selection): # the number of rows in x- and y-visible is equal to the number of active channels x_visible = self.x_visible y_visible = self.y_visible if y_visible is not None: y_visible = y_visible + self.y_offset label_row = 0 for i, text in enumerate(self.texts): text.setVisible(self._active_channels[i]) if not text.isVisible(): continue # put text 20% from the left x_pos = 0.8 * x_visible[label_row, 0] + 0.2 * x_visible[label_row, -1] # set height to the max in a 10% window around x_pos wid = max(1, int(0.05 * y_visible.shape[1])) n_pos = int(0.2 * y_visible.shape[1]) y_pos = y_visible[label_row, n_pos - wid:n_pos + wid].max() info('Setting text {}, {}: {}'.format(x_pos, y_pos, timestamp())) # print('Setting text {}, {}: {}'.format(x_pos, y_pos, timestamp())) text.setPos(x_pos, y_pos) label_row += 1
def update_region_callback(self, window, view_range, *args): info = parallel_context.get_logger().info with block_signals(self.region): info('region callback called with block: {} {}'.format( self.region.signalsBlocked(), timestamp())) self.update_region(view_range)
def update_zoom_callback(self, *args): # in callback mode, disable the signal from the zoom plot with block_signals(self.p1): info('zoom callback called with block: {} {}'.format( self.p1.signalsBlocked(), timestamp())) self.update_zoom_from_region()
def updatePlotData(self, data_ready=False): # Use data_ready=True to indicate that there's pre-defined x- and y-visible data ready to plot if self.hdf_data is None or not len(self.plot_channels): self.setData([]) self.y_visible = None self.x_visible = None return if not data_ready: self._set_data() r = self._view_range() if not r: return start, stop = r # Decide by how much we should downsample ds = int((stop - start) / self.limit) + 1 if ds == 1: # Small enough to display with no intervention. visible = self.y_visible x_visible = self.x_visible else: # Here convert data into a down-sampled # array suitable for visualizing. N = self.y_visible.shape[1] # This downsample does min/max interleaving # -- does not squash noise # 1st axis: number of display points # 2nd axis: number of raw points per display point # ds = ds * 2 # Nsub = max(5, N // ds) # y = self.y_visible[:Nsub * ds].reshape(Nsub, ds) # visible = np.empty(Nsub * 2, dtype='d') # visible[0::2] = y.min(axis=1) # visible[1::2] = y.max(axis=1) # This downsample picks 2 points regularly spaced in the segment ds = ds * 2 Nsub = max(5, N // ds) p1 = ds // 4 p2 = (3 * ds) // 4 n_rows = self.y_visible.shape[0] y = self.y_visible[:, :Nsub * ds].reshape(n_rows, Nsub, ds) visible = np.empty((n_rows, Nsub * 2), dtype='d') visible[:, 0::2] = y[..., p1] visible[:, 1::2] = y[..., p2] # This downsample does block averaging # -- this flattens noisy segments # Nsub = max(5, N // ds) # y = self.y_visible[:Nsub * ds].reshape(Nsub, ds) # visible = y.mean(axis=1) x_samp = np.linspace(ds // 2, self.x_visible.shape[1] - ds // 2, Nsub * 2).astype('i') if x_samp[-1] >= self.x_visible.shape[1]: x_samp[-1] = N - 1 x_visible = np.take(self.x_visible, x_samp, axis=1) info('{}: Update hdf5 lines called: external data {} {}'.format( type(self), data_ready, timestamp())) visible = visible + self.y_offset connects = np.ones(visible.shape, dtype='>i4') connects[:, -1] = 0 # Set the data as one block without connections from row to row info('{}: x shape {}, y shape {}, mask shape {}'.format( type(self), x_visible.shape, visible.shape, connects.shape)) self.setData(x=x_visible.ravel(), y=visible.ravel(), connect=connects.ravel()) # update_zoom_callback the plot # TODO: is this needed? self.resetTransform() # self.set_text_position() # mark these start, stop positions to match against future view-change signals self._cx1 = start self._cx2 = stop vis_rate = round(1 / (x_visible[0, 1] - x_visible[0, 0])) if vis_rate != self._current_vis_rate: self._current_vis_rate = vis_rate self.vis_rate_changed.emit(float(vis_rate)) self.plot_changed.emit(self)
def viewRangeChanged(self): # only look for x-range changes that actually require loading different data if self._view_really_changed(): info('Zoom plot ranged actually changed, redrawing: {} {}'.format( self._view_range(), timestamp())) self.updatePlotData()