class ArrayVolumePlot(qt.QWidget): """ Widget for plotting a n-D array (n >= 3) as a 3D scalar field. Three axis arrays can be provided to calibrate the axes. The signal array can have an arbitrary number of dimensions, the only limitation being that the last 3 dimensions must have the same length as the axes arrays. Sliders are provided to select indices on the first (n - 3) dimensions of the signal array, and the plot is updated to load the stack corresponding to the selection. """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayVolumePlot, self).__init__(parent) self.__signal = None self.__signal_name = None # the Z, Y, X axes apply to the last three dimensions of the signal # (in that order) self.__z_axis = None self.__z_axis_name = None self.__y_axis = None self.__y_axis_name = None self.__x_axis = None self.__x_axis_name = None from ._VolumeWindow import VolumeWindow self._view = VolumeWindow(self) self._hline = qt.QFrame(self) self._hline.setFrameStyle(qt.QFrame.HLine) self._hline.setFrameShadow(qt.QFrame.Sunken) self._legend = qt.QLabel(self) self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False layout = qt.QVBoxLayout() layout.addWidget(self._view) layout.addWidget(self._hline) layout.addWidget(self._legend) layout.addWidget(self._selector) self.setLayout(layout) def getVolumeView(self): """Returns the plot used for the display :rtype: SceneWindow """ return self._view def setData(self, signal, x_axis=None, y_axis=None, z_axis=None, signal_name=None, xlabel=None, ylabel=None, zlabel=None, title=None): """ :param signal: n-D dataset, whose last 3 dimensions are used as the 3D stack values. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. :param z_axis: 1-D dataset used as the image's z. If provided, its lengths must be equal to the length of the 3rd to last dimension of ``signal``. :param signal_name: Label used in the legend :param xlabel: Label for X axis :param ylabel: Label for Y axis :param zlabel: Label for Z axis :param title: Graph title """ if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateVolume) self.__selector_is_connected = False self.__signal = signal self.__signal_name = signal_name or "" self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel self.__z_axis = z_axis self.__z_axis_name = zlabel self._selector.setData(signal) self._selector.setAxisNames(["Y", "X", "Z"]) self._updateVolume() # the legend label shows the selection slice producing the volume # (only interesting for ndim > 3) if signal.ndim > 3: self._selector.setVisible(True) self._legend.setVisible(True) self._hline.setVisible(True) else: self._selector.setVisible(False) self._legend.setVisible(False) self._hline.setVisible(False) if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateVolume) self.__selector_is_connected = True def _updateVolume(self): """Update displayed stack according to the current axes selector data.""" x_axis = self.__x_axis y_axis = self.__y_axis z_axis = self.__z_axis offset = [] scale = [] for axis in [x_axis, y_axis, z_axis]: if axis is None: calibration = NoCalibration() elif len(axis) == 2: calibration = LinearCalibration(y_intercept=axis[0], slope=axis[1]) else: calibration = ArrayCalibration(axis) if not calibration.is_affine(): _logger.warning("Axis has not linear values, ignored") offset.append(0.) scale.append(1.) else: offset.append(calibration(0)) scale.append(calibration.get_slope()) legend = self.__signal_name + "[" for sl in self._selector.selection(): if sl == slice(None): legend += ":, " else: legend += str(sl) + ", " legend = legend[:-2] + "]" self._legend.setText("Displayed data: " + legend) # Update SceneWidget data = self._selector.selectedData() volumeView = self.getVolumeView() volumeView.setData(data, offset=offset, scale=scale) volumeView.setAxesLabels(self.__x_axis_name, self.__y_axis_name, self.__z_axis_name) def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self.getVolumeView().clear()
class ArrayComplexImagePlot(qt.QWidget): """ Widget for plotting an image of complex from a multi-dimensional signal array and two 1D axes array. The signal array can have an arbitrary number of dimensions, the only limitation being that the last two dimensions must have the same length as the axes arrays. Sliders are provided to select indices on the first (n - 2) dimensions of the signal array, and the plot is updated to show the image corresponding to the selection. If one or both of the axes does not have regularly spaced values, the the image is plotted as a coloured scatter plot. """ def __init__(self, parent=None, colormap=None): """ :param parent: Parent QWidget """ super(ArrayComplexImagePlot, self).__init__(parent) self.__signals = None self.__signals_names = None self.__x_axis = None self.__x_axis_name = None self.__y_axis = None self.__y_axis_name = None self._plot = ComplexImageView(self) if colormap is not None: for mode in (ComplexImageView.ComplexMode.ABSOLUTE, ComplexImageView.ComplexMode.SQUARE_AMPLITUDE, ComplexImageView.ComplexMode.REAL, ComplexImageView.ComplexMode.IMAGINARY): self._plot.setColormap(colormap, mode) self._plot.getPlot().getIntensityHistogramAction().setVisible(True) self._plot.setKeepDataAspectRatio(True) # not closable self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self._selector.selectionChanged.connect(self._updateImage) self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) self._auxSigSlider.setMinimum(0) self._auxSigSlider.setValue(0) self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) self._auxSigSlider.setToolTip("Select auxiliary signals") layout = qt.QVBoxLayout() layout.addWidget(self._plot) layout.addWidget(self._selector) layout.addWidget(self._auxSigSlider) self.setLayout(layout) def _sliderIdxChanged(self, value): self._updateImage() def getPlot(self): """Returns the plot used for the display :rtype: PlotWidget """ return self._plot.getPlot() def setImageData(self, signals, x_axis=None, y_axis=None, signals_names=None, xlabel=None, ylabel=None, title=None): """ :param signals: list of n-D datasets, whose last 2 dimensions are used as the image's values, or list of 3D datasets interpreted as RGBA image. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. :param signals_names: Names for each image, used as subtitle and legend. :param xlabel: Label for X axis :param ylabel: Label for Y axis :param title: Graph title """ self._selector.selectionChanged.disconnect(self._updateImage) self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) self.__signals = signals self.__signals_names = signals_names self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel self.__title = title self._selector.clear() self._selector.setAxisNames(["Y", "X"]) self._selector.setData(signals[0]) if len(signals[0].shape) <= 2: self._selector.hide() else: self._selector.show() self._auxSigSlider.setMaximum(len(signals) - 1) if len(signals) > 1: self._auxSigSlider.show() else: self._auxSigSlider.hide() self._auxSigSlider.setValue(0) self._updateImage() self._plot.getPlot().resetZoom() self._selector.selectionChanged.connect(self._updateImage) self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) def _updateImage(self): selection = self._selector.selection() auxSigIdx = self._auxSigSlider.value() images = [img[selection] for img in self.__signals] image = images[auxSigIdx] x_axis = self.__x_axis y_axis = self.__y_axis if x_axis is None and y_axis is None: xcalib = NoCalibration() ycalib = NoCalibration() else: if x_axis is None: # no calibration x_axis = numpy.arange(image.shape[1]) elif numpy.isscalar(x_axis) or len(x_axis) == 1: # constant axis x_axis = x_axis * numpy.ones((image.shape[1], )) elif len(x_axis) == 2: # linear calibration x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] if y_axis is None: y_axis = numpy.arange(image.shape[0]) elif numpy.isscalar(y_axis) or len(y_axis) == 1: y_axis = y_axis * numpy.ones((image.shape[0], )) elif len(y_axis) == 2: y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] xcalib = ArrayCalibration(x_axis) ycalib = ArrayCalibration(y_axis) self._plot.setData(image) if xcalib.is_affine(): xorigin, xscale = xcalib(0), xcalib.get_slope() else: _logger.warning("Unsupported complex image X axis calibration") xorigin, xscale = 0., 1. if ycalib.is_affine(): yorigin, yscale = ycalib(0), ycalib.get_slope() else: _logger.warning("Unsupported complex image Y axis calibration") yorigin, yscale = 0., 1. self._plot.setOrigin((xorigin, yorigin)) self._plot.setScale((xscale, yscale)) if self.__title: title = self.__title if len(self.__signals_names) > 1: # Append dataset name only when there is many datasets title += '\n' + self.__signals_names[auxSigIdx] else: title = self.__signals_names[auxSigIdx] self._plot.setGraphTitle(title) self._plot.getXAxis().setLabel(self.__x_axis_name) self._plot.getYAxis().setLabel(self.__y_axis_name) def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self._plot.setData(None)
class ArrayStackPlot(qt.QWidget): """ Widget for plotting a n-D array (n >= 3) as a stack of images. Three axis arrays can be provided to calibrate the axes. The signal array can have an arbitrary number of dimensions, the only limitation being that the last 3 dimensions must have the same length as the axes arrays. Sliders are provided to select indices on the first (n - 3) dimensions of the signal array, and the plot is updated to load the stack corresponding to the selection. """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayStackPlot, self).__init__(parent) self.__signal = None self.__signal_name = None # the Z, Y, X axes apply to the last three dimensions of the signal # (in that order) self.__z_axis = None self.__z_axis_name = None self.__y_axis = None self.__y_axis_name = None self.__x_axis = None self.__x_axis_name = None self._stack_view = StackView(self) self._hline = qt.QFrame(self) self._hline.setFrameStyle(qt.QFrame.HLine) self._hline.setFrameShadow(qt.QFrame.Sunken) self._legend = qt.QLabel(self) self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False layout = qt.QVBoxLayout() layout.addWidget(self._stack_view) layout.addWidget(self._hline) layout.addWidget(self._legend) layout.addWidget(self._selector) self.setLayout(layout) def getStackView(self): """Returns the plot used for the display :rtype: StackView """ return self._stack_view def setStackData(self, signal, x_axis=None, y_axis=None, z_axis=None, signal_name=None, xlabel=None, ylabel=None, zlabel=None, title=None): """ :param signal: n-D dataset, whose last 3 dimensions are used as the 3D stack values. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. :param z_axis: 1-D dataset used as the image's z. If provided, its lengths must be equal to the length of the 3rd to last dimension of ``signal``. :param signal_name: Label used in the legend :param xlabel: Label for X axis :param ylabel: Label for Y axis :param zlabel: Label for Z axis :param title: Graph title """ if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateStack) self.__selector_is_connected = False self.__signal = signal self.__signal_name = signal_name or "" self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel self.__z_axis = z_axis self.__z_axis_name = zlabel self._selector.setData(signal) self._selector.setAxisNames(["Y", "X", "Z"]) self._stack_view.setGraphTitle(title or "") # by default, the z axis is the image position (dimension not plotted) self._stack_view.getPlotWidget().getXAxis().setLabel(self.__x_axis_name or "X") self._stack_view.getPlotWidget().getYAxis().setLabel(self.__y_axis_name or "Y") self._updateStack() ndims = len(signal.shape) self._stack_view.setFirstStackDimension(ndims - 3) # the legend label shows the selection slice producing the volume # (only interesting for ndim > 3) if ndims > 3: self._selector.setVisible(True) self._legend.setVisible(True) self._hline.setVisible(True) else: self._selector.setVisible(False) self._legend.setVisible(False) self._hline.setVisible(False) if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateStack) self.__selector_is_connected = True @staticmethod def _get_origin_scale(axis): """Assuming axis is a regularly spaced 1D array, return a tuple (origin, scale) where: - origin = axis[0] - scale = (axis[n-1] - axis[0]) / (n -1) :param axis: 1D numpy array :return: Tuple (axis[0], (axis[-1] - axis[0]) / (len(axis) - 1)) """ return axis[0], (axis[-1] - axis[0]) / (len(axis) - 1) def _updateStack(self): """Update displayed stack according to the current axes selector data.""" stk = self._selector.selectedData() x_axis = self.__x_axis y_axis = self.__y_axis z_axis = self.__z_axis calibrations = [] for axis in [z_axis, y_axis, x_axis]: if axis is None: calibrations.append(NoCalibration()) elif len(axis) == 2: calibrations.append( LinearCalibration(y_intercept=axis[0], slope=axis[1])) else: calibrations.append(ArrayCalibration(axis)) legend = self.__signal_name + "[" for sl in self._selector.selection(): if sl == slice(None): legend += ":, " else: legend += str(sl) + ", " legend = legend[:-2] + "]" self._legend.setText("Displayed data: " + legend) self._stack_view.setStack(stk, calibrations=calibrations) self._stack_view.setLabels(labels=[ self.__z_axis_name, self.__y_axis_name, self.__x_axis_name ]) def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self._stack_view.clear()
class ArrayImagePlot(qt.QWidget): """ Widget for plotting an image from a multi-dimensional signal array and two 1D axes array. The signal array can have an arbitrary number of dimensions, the only limitation being that the last two dimensions must have the same length as the axes arrays. Sliders are provided to select indices on the first (n - 2) dimensions of the signal array, and the plot is updated to show the image corresponding to the selection. If one or both of the axes does not have regularly spaced values, the the image is plotted as a coloured scatter plot. """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayImagePlot, self).__init__(parent) self.__signals = None self.__signals_names = None self.__x_axis = None self.__x_axis_name = None self.__y_axis = None self.__y_axis_name = None self._plot = Plot2D(self) self._plot.setDefaultColormap( Colormap(name="viridis", vmin=None, vmax=None, normalization=Colormap.LINEAR)) self._plot.getIntensityHistogramAction().setVisible(True) self._plot.setKeepDataAspectRatio(True) # not closable self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self._selector.selectionChanged.connect(self._updateImage) self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) self._auxSigSlider.setMinimum(0) self._auxSigSlider.setValue(0) self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) self._auxSigSlider.setToolTip("Select auxiliary signals") layout = qt.QVBoxLayout() layout.addWidget(self._plot) layout.addWidget(self._selector) layout.addWidget(self._auxSigSlider) self.setLayout(layout) def _sliderIdxChanged(self, value): self._updateImage() def getPlot(self): """Returns the plot used for the display :rtype: Plot2D """ return self._plot def setImageData(self, signals, x_axis=None, y_axis=None, signals_names=None, xlabel=None, ylabel=None, title=None, isRgba=False, xscale=None, yscale=None): """ :param signals: list of n-D datasets, whose last 2 dimensions are used as the image's values, or list of 3D datasets interpreted as RGBA image. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. :param signals_names: Names for each image, used as subtitle and legend. :param xlabel: Label for X axis :param ylabel: Label for Y axis :param title: Graph title :param isRgba: True if data is a 3D RGBA image :param str xscale: Scale of X axis in (None, 'linear', 'log') :param str yscale: Scale of Y axis in (None, 'linear', 'log') """ self._selector.selectionChanged.disconnect(self._updateImage) self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) self.__signals = signals self.__signals_names = signals_names self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel self.__title = title self._selector.clear() if not isRgba: self._selector.setAxisNames(["Y", "X"]) img_ndim = 2 else: self._selector.setAxisNames(["Y", "X", "RGB(A) channel"]) img_ndim = 3 self._selector.setData(signals[0]) if len(signals[0].shape) <= img_ndim: self._selector.hide() else: self._selector.show() self._auxSigSlider.setMaximum(len(signals) - 1) if len(signals) > 1: self._auxSigSlider.show() else: self._auxSigSlider.hide() self._auxSigSlider.setValue(0) self._axis_scales = xscale, yscale self._updateImage() self._plot.resetZoom() self._selector.selectionChanged.connect(self._updateImage) self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) def _updateImage(self): selection = self._selector.selection() auxSigIdx = self._auxSigSlider.value() legend = self.__signals_names[auxSigIdx] images = [img[selection] for img in self.__signals] image = images[auxSigIdx] x_axis = self.__x_axis y_axis = self.__y_axis if x_axis is None and y_axis is None: xcalib = NoCalibration() ycalib = NoCalibration() else: if x_axis is None: # no calibration x_axis = numpy.arange(image.shape[1]) elif numpy.isscalar(x_axis) or len(x_axis) == 1: # constant axis x_axis = x_axis * numpy.ones((image.shape[1], )) elif len(x_axis) == 2: # linear calibration x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] if y_axis is None: y_axis = numpy.arange(image.shape[0]) elif numpy.isscalar(y_axis) or len(y_axis) == 1: y_axis = y_axis * numpy.ones((image.shape[0], )) elif len(y_axis) == 2: y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] xcalib = ArrayCalibration(x_axis) ycalib = ArrayCalibration(y_axis) self._plot.remove(kind=( "scatter", "image", )) if xcalib.is_affine() and ycalib.is_affine(): # regular image xorigin, xscale = xcalib(0), xcalib.get_slope() yorigin, yscale = ycalib(0), ycalib.get_slope() origin = (xorigin, yorigin) scale = (xscale, yscale) self._plot.getXAxis().setScale('linear') self._plot.getYAxis().setScale('linear') self._plot.addImage(image, legend=legend, origin=origin, scale=scale, replace=True, resetzoom=False) else: xaxisscale, yaxisscale = self._axis_scales if xaxisscale is not None: self._plot.getXAxis().setScale('log' if xaxisscale == 'log' else 'linear') if yaxisscale is not None: self._plot.getYAxis().setScale('log' if yaxisscale == 'log' else 'linear') scatterx, scattery = numpy.meshgrid(x_axis, y_axis) # fixme: i don't think this can handle "irregular" RGBA images self._plot.addScatter(numpy.ravel(scatterx), numpy.ravel(scattery), numpy.ravel(image), legend=legend) if self.__title: title = self.__title if len(self.__signals_names) > 1: # Append dataset name only when there is many datasets title += '\n' + self.__signals_names[auxSigIdx] else: title = self.__signals_names[auxSigIdx] self._plot.setGraphTitle(title) self._plot.getXAxis().setLabel(self.__x_axis_name) self._plot.getYAxis().setLabel(self.__y_axis_name) def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self._plot.clear()
class ArrayCurvePlot(qt.QWidget): """ Widget for plotting a curve from a multi-dimensional signal array and a 1D axis array. The signal array can have an arbitrary number of dimensions, the only limitation being that the last dimension must have the same length as the axis array. The widget provides sliders to select indices on the first (n - 1) dimensions of the signal array, and buttons to add/replace selected curves to the plot. This widget also handles simple 2D or 3D scatter plots (third dimension displayed as colour of points). """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayCurvePlot, self).__init__(parent) self.__signals = None self.__signals_names = None self.__signal_errors = None self.__axis = None self.__axis_name = None self.__x_axis_errors = None self.__values = None self._plot = Plot1D(self) self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False self._plot.sigActiveCurveChanged.connect( self._setYLabelFromActiveLegend) layout = qt.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._plot) layout.addWidget(self._selector) self.setLayout(layout) def getPlot(self): """Returns the plot used for the display :rtype: Plot1D """ return self._plot def setCurvesData(self, ys, x=None, yerror=None, xerror=None, ylabels=None, xlabel=None, title=None, xscale=None, yscale=None): """ :param List[ndarray] ys: List of arrays to be represented by the y (vertical) axis. It can be multiple n-D array whose last dimension must have the same length as x (and values must be None) :param ndarray x: 1-D dataset used as the curve's x values. If provided, its lengths must be equal to the length of the last dimension of ``y`` (and equal to the length of ``value``, for a scatter plot). :param ndarray yerror: Single array of errors for y (same shape), or None. There can only be one array, and it applies to the first/main y (no y errors for auxiliary_signals curves). :param ndarray xerror: 1-D dataset of errors for x, or None :param str ylabels: Labels for each curve's Y axis :param str xlabel: Label for X axis :param str title: Graph title :param str xscale: Scale of X axis in (None, 'linear', 'log') :param str yscale: Scale of Y axis in (None, 'linear', 'log') """ self.__signals = ys self.__signals_names = ylabels or (["Y"] * len(ys)) self.__signal_errors = yerror self.__axis = x self.__axis_name = xlabel self.__x_axis_errors = xerror if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateCurve) self.__selector_is_connected = False self._selector.setData(ys[0]) self._selector.setAxisNames(["Y"]) if len(ys[0].shape) < 2: self._selector.hide() else: self._selector.show() self._plot.setGraphTitle(title or "") if xscale is not None: self._plot.getXAxis().setScale('log' if xscale == 'log' else 'linear') if yscale is not None: self._plot.getYAxis().setScale('log' if yscale == 'log' else 'linear') self._updateCurve() if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateCurve) self.__selector_is_connected = True def _updateCurve(self): selection = self._selector.selection() ys = [sig[selection] for sig in self.__signals] y0 = ys[0] len_y = len(y0) x = self.__axis if x is None: x = numpy.arange(len_y) elif numpy.isscalar(x) or len(x) == 1: # constant axis x = x * numpy.ones_like(y0) elif len(x) == 2 and len_y != 2: # linear calibration a + b * x x = x[0] + x[1] * numpy.arange(len_y) self._plot.remove(kind=("curve", )) for i in range(len(self.__signals)): legend = self.__signals_names[i] # errors only supported for primary signal in NXdata y_errors = None if i == 0 and self.__signal_errors is not None: y_errors = self.__signal_errors[self._selector.selection()] self._plot.addCurve(x, ys[i], legend=legend, xerror=self.__x_axis_errors, yerror=y_errors) if i == 0: self._plot.setActiveCurve(legend) self._plot.resetZoom() self._plot.getXAxis().setLabel(self.__axis_name) self._plot.getYAxis().setLabel(self.__signals_names[0]) def _setYLabelFromActiveLegend(self, previous_legend, new_legend): for ylabel in self.__signals_names: if new_legend is not None and new_legend == ylabel: self._plot.getYAxis().setLabel(ylabel) break def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self._plot.clear()
class ArrayCurvePlot(qt.QWidget): """ Widget for plotting a curve from a multi-dimensional signal array and a 1D axis array. The signal array can have an arbitrary number of dimensions, the only limitation being that the last dimension must have the same length as the axis array. The widget provides sliders to select indices on the first (n - 1) dimensions of the signal array, and buttons to add/replace selected curves to the plot. This widget also handles simple 2D or 3D scatter plots (third dimension displayed as colour of points). """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayCurvePlot, self).__init__(parent) self.__signals = None self.__signals_names = None self.__signal_errors = None self.__axis = None self.__axis_name = None self.__x_axis_errors = None self.__values = None self._plot = Plot1D(self) self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False self._plot.sigActiveCurveChanged.connect(self._setYLabelFromActiveLegend) layout = qt.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._plot) layout.addWidget(self._selector) self.setLayout(layout) def getPlot(self): """Returns the plot used for the display :rtype: Plot1D """ return self._plot def setCurvesData(self, ys, x=None, yerror=None, xerror=None, ylabels=None, xlabel=None, title=None): """ :param List[ndarray] ys: List of arrays to be represented by the y (vertical) axis. It can be multiple n-D array whose last dimension must have the same length as x (and values must be None) :param ndarray x: 1-D dataset used as the curve's x values. If provided, its lengths must be equal to the length of the last dimension of ``y`` (and equal to the length of ``value``, for a scatter plot). :param ndarray yerror: Single array of errors for y (same shape), or None. There can only be one array, and it applies to the first/main y (no y errors for auxiliary_signals curves). :param ndarray xerror: 1-D dataset of errors for x, or None :param str ylabels: Labels for each curve's Y axis :param str xlabel: Label for X axis :param str title: Graph title """ self.__signals = ys self.__signals_names = ylabels or (["Y"] * len(ys)) self.__signal_errors = yerror self.__axis = x self.__axis_name = xlabel self.__x_axis_errors = xerror if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateCurve) self.__selector_is_connected = False self._selector.setData(ys[0]) self._selector.setAxisNames(["Y"]) if len(ys[0].shape) < 2: self._selector.hide() else: self._selector.show() self._plot.setGraphTitle(title or "") self._updateCurve() if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateCurve) self.__selector_is_connected = True def _updateCurve(self): selection = self._selector.selection() ys = [sig[selection] for sig in self.__signals] y0 = ys[0] len_y = len(y0) x = self.__axis if x is None: x = numpy.arange(len_y) elif numpy.isscalar(x) or len(x) == 1: # constant axis x = x * numpy.ones_like(y0) elif len(x) == 2 and len_y != 2: # linear calibration a + b * x x = x[0] + x[1] * numpy.arange(len_y) self._plot.remove(kind=("curve",)) for i in range(len(self.__signals)): legend = self.__signals_names[i] # errors only supported for primary signal in NXdata y_errors = None if i == 0 and self.__signal_errors is not None: y_errors = self.__signal_errors[self._selector.selection()] self._plot.addCurve(x, ys[i], legend=legend, xerror=self.__x_axis_errors, yerror=y_errors) if i == 0: self._plot.setActiveCurve(legend) self._plot.resetZoom() self._plot.getXAxis().setLabel(self.__axis_name) self._plot.getYAxis().setLabel(self.__signals_names[0]) def _setYLabelFromActiveLegend(self, previous_legend, new_legend): for ylabel in self.__signals_names: if new_legend is not None and new_legend == ylabel: self._plot.getYAxis().setLabel(ylabel) break def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self._plot.clear()
class ArrayStackPlot(qt.QWidget): """ Widget for plotting a n-D array (n >= 3) as a stack of images. Three axis arrays can be provided to calibrate the axes. The signal array can have an arbitrary number of dimensions, the only limitation being that the last 3 dimensions must have the same length as the axes arrays. Sliders are provided to select indices on the first (n - 3) dimensions of the signal array, and the plot is updated to load the stack corresponding to the selection. """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayStackPlot, self).__init__(parent) self.__signal = None self.__signal_name = None # the Z, Y, X axes apply to the last three dimensions of the signal # (in that order) self.__z_axis = None self.__z_axis_name = None self.__y_axis = None self.__y_axis_name = None self.__x_axis = None self.__x_axis_name = None self._stack_view = StackView(self) self._hline = qt.QFrame(self) self._hline.setFrameStyle(qt.QFrame.HLine) self._hline.setFrameShadow(qt.QFrame.Sunken) self._legend = qt.QLabel(self) self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False layout = qt.QVBoxLayout() layout.addWidget(self._stack_view) layout.addWidget(self._hline) layout.addWidget(self._legend) layout.addWidget(self._selector) self.setLayout(layout) def getStackView(self): """Returns the plot used for the display :rtype: StackView """ return self._stack_view def setStackData(self, signal, x_axis=None, y_axis=None, z_axis=None, signal_name=None, xlabel=None, ylabel=None, zlabel=None, title=None): """ :param signal: n-D dataset, whose last 3 dimensions are used as the 3D stack values. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. :param z_axis: 1-D dataset used as the image's z. If provided, its lengths must be equal to the length of the 3rd to last dimension of ``signal``. :param signal_name: Label used in the legend :param xlabel: Label for X axis :param ylabel: Label for Y axis :param zlabel: Label for Z axis :param title: Graph title """ if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateStack) self.__selector_is_connected = False self.__signal = signal self.__signal_name = signal_name or "" self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel self.__z_axis = z_axis self.__z_axis_name = zlabel self._selector.setData(signal) self._selector.setAxisNames(["Y", "X", "Z"]) self._stack_view.setGraphTitle(title or "") # by default, the z axis is the image position (dimension not plotted) self._stack_view.getPlot().getXAxis().setLabel(self.__x_axis_name or "X") self._stack_view.getPlot().getYAxis().setLabel(self.__y_axis_name or "Y") self._updateStack() ndims = len(signal.shape) self._stack_view.setFirstStackDimension(ndims - 3) # the legend label shows the selection slice producing the volume # (only interesting for ndim > 3) if ndims > 3: self._selector.setVisible(True) self._legend.setVisible(True) self._hline.setVisible(True) else: self._selector.setVisible(False) self._legend.setVisible(False) self._hline.setVisible(False) if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateStack) self.__selector_is_connected = True @staticmethod def _get_origin_scale(axis): """Assuming axis is a regularly spaced 1D array, return a tuple (origin, scale) where: - origin = axis[0] - scale = (axis[n-1] - axis[0]) / (n -1) :param axis: 1D numpy array :return: Tuple (axis[0], (axis[-1] - axis[0]) / (len(axis) - 1)) """ return axis[0], (axis[-1] - axis[0]) / (len(axis) - 1) def _updateStack(self): """Update displayed stack according to the current axes selector data.""" stk = self._selector.selectedData() x_axis = self.__x_axis y_axis = self.__y_axis z_axis = self.__z_axis calibrations = [] for axis in [z_axis, y_axis, x_axis]: if axis is None: calibrations.append(NoCalibration()) elif len(axis) == 2: calibrations.append( LinearCalibration(y_intercept=axis[0], slope=axis[1])) else: calibrations.append(ArrayCalibration(axis)) legend = self.__signal_name + "[" for sl in self._selector.selection(): if sl == slice(None): legend += ":, " else: legend += str(sl) + ", " legend = legend[:-2] + "]" self._legend.setText("Displayed data: " + legend) self._stack_view.setStack(stk, calibrations=calibrations) self._stack_view.setLabels( labels=[self.__z_axis_name, self.__y_axis_name, self.__x_axis_name]) def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self._stack_view.clear()
class ArrayVolumePlot(qt.QWidget): """ Widget for plotting a n-D array (n >= 3) as a 3D scalar field. Three axis arrays can be provided to calibrate the axes. The signal array can have an arbitrary number of dimensions, the only limitation being that the last 3 dimensions must have the same length as the axes arrays. Sliders are provided to select indices on the first (n - 3) dimensions of the signal array, and the plot is updated to load the stack corresponding to the selection. """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayVolumePlot, self).__init__(parent) self.__signal = None self.__signal_name = None # the Z, Y, X axes apply to the last three dimensions of the signal # (in that order) self.__z_axis = None self.__z_axis_name = None self.__y_axis = None self.__y_axis_name = None self.__x_axis = None self.__x_axis_name = None from silx.gui.plot3d.ScalarFieldView import ScalarFieldView from silx.gui.plot3d import SFViewParamTree self._view = ScalarFieldView(self) def computeIsolevel(data): data = data[numpy.isfinite(data)] if len(data) == 0: return 0 else: return numpy.mean(data) + numpy.std(data) self._view.addIsosurface(computeIsolevel, '#FF0000FF') # Create a parameter tree for the scalar field view options = SFViewParamTree.TreeView(self._view) options.setSfView(self._view) # Add the parameter tree to the main window in a dock widget dock = qt.QDockWidget() dock.setWidget(options) self._view.addDockWidget(qt.Qt.RightDockWidgetArea, dock) self._hline = qt.QFrame(self) self._hline.setFrameStyle(qt.QFrame.HLine) self._hline.setFrameShadow(qt.QFrame.Sunken) self._legend = qt.QLabel(self) self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False layout = qt.QVBoxLayout() layout.addWidget(self._view) layout.addWidget(self._hline) layout.addWidget(self._legend) layout.addWidget(self._selector) self.setLayout(layout) def getVolumeView(self): """Returns the plot used for the display :rtype: ScalarFieldView """ return self._view def normalizeComplexData(self, data): """ Converts a complex data array to its amplitude, if necessary. :param data: the data to normalize :return: """ if hasattr(data, "dtype"): isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) else: isComplex = isinstance(data, numbers.Complex) if isComplex: data = numpy.absolute(data) return data def setData(self, signal, x_axis=None, y_axis=None, z_axis=None, signal_name=None, xlabel=None, ylabel=None, zlabel=None, title=None): """ :param signal: n-D dataset, whose last 3 dimensions are used as the 3D stack values. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. :param z_axis: 1-D dataset used as the image's z. If provided, its lengths must be equal to the length of the 3rd to last dimension of ``signal``. :param signal_name: Label used in the legend :param xlabel: Label for X axis :param ylabel: Label for Y axis :param zlabel: Label for Z axis :param title: Graph title """ signal = self.normalizeComplexData(signal) if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateVolume) self.__selector_is_connected = False self.__signal = signal self.__signal_name = signal_name or "" self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel self.__z_axis = z_axis self.__z_axis_name = zlabel self._selector.setData(signal) self._selector.setAxisNames(["Y", "X", "Z"]) self._view.setAxesLabels(self.__x_axis_name or 'X', self.__y_axis_name or 'Y', self.__z_axis_name or 'Z') self._updateVolume() # the legend label shows the selection slice producing the volume # (only interesting for ndim > 3) if signal.ndim > 3: self._selector.setVisible(True) self._legend.setVisible(True) self._hline.setVisible(True) else: self._selector.setVisible(False) self._legend.setVisible(False) self._hline.setVisible(False) if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateVolume) self.__selector_is_connected = True def _updateVolume(self): """Update displayed stack according to the current axes selector data.""" data = self._selector.selectedData() x_axis = self.__x_axis y_axis = self.__y_axis z_axis = self.__z_axis offset = [] scale = [] for axis in [x_axis, y_axis, z_axis]: if axis is None: calibration = NoCalibration() elif len(axis) == 2: calibration = LinearCalibration(y_intercept=axis[0], slope=axis[1]) else: calibration = ArrayCalibration(axis) if not calibration.is_affine(): _logger.warning("Axis has not linear values, ignored") offset.append(0.) scale.append(1.) else: offset.append(calibration(0)) scale.append(calibration.get_slope()) legend = self.__signal_name + "[" for sl in self._selector.selection(): if sl == slice(None): legend += ":, " else: legend += str(sl) + ", " legend = legend[:-2] + "]" self._legend.setText("Displayed data: " + legend) self._view.setData(data, copy=False) self._view.setScale(*scale) self._view.setTranslation(*offset) self._view.setAxesLabels(self.__x_axis_name, self.__y_axis_name, self.__z_axis_name) def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self._view.setData(None)
class ArrayImagePlot(qt.QWidget): """ Widget for plotting an image from a multi-dimensional signal array and two 1D axes array. The signal array can have an arbitrary number of dimensions, the only limitation being that the last two dimensions must have the same length as the axes arrays. Sliders are provided to select indices on the first (n - 2) dimensions of the signal array, and the plot is updated to show the image corresponding to the selection. If one or both of the axes does not have regularly spaced values, the the image is plotted as a coloured scatter plot. """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayImagePlot, self).__init__(parent) self.__signals = None self.__signals_names = None self.__x_axis = None self.__x_axis_name = None self.__y_axis = None self.__y_axis_name = None self._plot = Plot2D(self) self._plot.setDefaultColormap(Colormap(name="viridis", vmin=None, vmax=None, normalization=Colormap.LINEAR)) self._plot.getIntensityHistogramAction().setVisible(True) # not closable self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self._selector.selectionChanged.connect(self._updateImage) self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) self._auxSigSlider.setMinimum(0) self._auxSigSlider.setValue(0) self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) self._auxSigSlider.setToolTip("Select auxiliary signals") layout = qt.QVBoxLayout() layout.addWidget(self._plot) layout.addWidget(self._selector) layout.addWidget(self._auxSigSlider) self.setLayout(layout) def _sliderIdxChanged(self, value): self._updateImage() def getPlot(self): """Returns the plot used for the display :rtype: Plot2D """ return self._plot def setImageData(self, signals, x_axis=None, y_axis=None, signals_names=None, xlabel=None, ylabel=None, title=None, isRgba=False): """ :param signals: list of n-D datasets, whose last 2 dimensions are used as the image's values, or list of 3D datasets interpreted as RGBA image. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. :param signals_names: Names for each image, used as subtitle and legend. :param xlabel: Label for X axis :param ylabel: Label for Y axis :param title: Graph title :param isRgba: True if data is a 3D RGBA image """ self._selector.selectionChanged.disconnect(self._updateImage) self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) self.__signals = signals self.__signals_names = signals_names self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel self.__title = title self._selector.clear() if not isRgba: self._selector.setAxisNames(["Y", "X"]) img_ndim = 2 else: self._selector.setAxisNames(["Y", "X", "RGB(A) channel"]) img_ndim = 3 self._selector.setData(signals[0]) if len(signals[0].shape) <= img_ndim: self._selector.hide() else: self._selector.show() self._auxSigSlider.setMaximum(len(signals) - 1) if len(signals) > 1: self._auxSigSlider.show() else: self._auxSigSlider.hide() self._auxSigSlider.setValue(0) self._updateImage() self._selector.selectionChanged.connect(self._updateImage) self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) def _updateImage(self): selection = self._selector.selection() auxSigIdx = self._auxSigSlider.value() legend = self.__signals_names[auxSigIdx] images = [img[selection] for img in self.__signals] image = images[auxSigIdx] x_axis = self.__x_axis y_axis = self.__y_axis if x_axis is None and y_axis is None: xcalib = NoCalibration() ycalib = NoCalibration() else: if x_axis is None: # no calibration x_axis = numpy.arange(image.shape[1]) elif numpy.isscalar(x_axis) or len(x_axis) == 1: # constant axis x_axis = x_axis * numpy.ones((image.shape[1], )) elif len(x_axis) == 2: # linear calibration x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] if y_axis is None: y_axis = numpy.arange(image.shape[0]) elif numpy.isscalar(y_axis) or len(y_axis) == 1: y_axis = y_axis * numpy.ones((image.shape[0], )) elif len(y_axis) == 2: y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] xcalib = ArrayCalibration(x_axis) ycalib = ArrayCalibration(y_axis) self._plot.remove(kind=("scatter", "image",)) if xcalib.is_affine() and ycalib.is_affine(): # regular image xorigin, xscale = xcalib(0), xcalib.get_slope() yorigin, yscale = ycalib(0), ycalib.get_slope() origin = (xorigin, yorigin) scale = (xscale, yscale) self._plot.addImage(image, legend=legend, origin=origin, scale=scale, replace=True) else: scatterx, scattery = numpy.meshgrid(x_axis, y_axis) # fixme: i don't think this can handle "irregular" RGBA images self._plot.addScatter(numpy.ravel(scatterx), numpy.ravel(scattery), numpy.ravel(image), legend=legend) title = "" if self.__title: title += self.__title if not title.strip().endswith(self.__signals_names[auxSigIdx]): title += "\n" + self.__signals_names[auxSigIdx] self._plot.setGraphTitle(title) self._plot.getXAxis().setLabel(self.__x_axis_name) self._plot.getYAxis().setLabel(self.__y_axis_name) self._plot.resetZoom() def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self._plot.clear()
class XpadVisualisation(QWidget): unfoldButtonClicked = pyqtSignal() def __init__(self): super(QWidget, self).__init__() self.layout = QVBoxLayout(self) self.raw_data = None self.flatfield_image = None self.path = None self.diagram_data_array = [] self.angles = [] # Initialize tab screen self.tabs = QTabWidget() self.raw_data_tab = QWidget() # Create an unfolding data tab self.unfolded_data_tab = UnfoldingDataTab(self) self.diagram_tab = QWidget() self.fitting_data_tab = QWidget() # Create raw data display tab self.raw_data_tab.layout = QVBoxLayout(self.raw_data_tab) self.raw_data_viewer = RawDataViewer(self.raw_data_tab) # Create diagram plot data tab self.diagram_tab.layout = QVBoxLayout(self.diagram_tab) self.diagram_data_plot = Plot1D(self.diagram_tab) # Create fitting curve tab self.fitting_data_tab.layout = QVBoxLayout(self.fitting_data_tab) self.fitting_data_selector = NumpyAxesSelector(self.fitting_data_tab) self.fitting_data_plot = Plot1D(self.fitting_data_tab) self.fitting_widget = self.fitting_data_plot.getFitAction() self.fit_action = FitAction(plot=self.fitting_data_plot, parent=self.fitting_data_plot) self.toolbar = QToolBar("New") # Create automatic fitting tab self.automatic_fit_tab = FittingDataTab(self) self.unfolded_data_tab.viewer.get_unfold_with_flatfield_action( ).unfoldWithFlatfieldClicked.connect(self.get_calibration) self.unfolded_data_tab.viewer.get_unfold_action( ).unfoldClicked.connect(self.get_calibration) self.unfolded_data_tab.unfoldingFinished.connect( self.create_diagram_array) self.init_UI() def init_UI(self): self.tabs.resize(400, 300) # Add tabs self.tabs.addTab(self.raw_data_tab, "Raw data") self.tabs.addTab(self.unfolded_data_tab, "Unfolded data") self.tabs.addTab(self.diagram_tab, "Diffraction diagram") self.tabs.addTab(self.fitting_data_tab, "Fitted data") self.tabs.addTab(self.automatic_fit_tab, "Automatic fit") self.raw_data_tab.layout.addWidget(self.raw_data_viewer) self.diagram_tab.layout.addWidget(self.diagram_data_plot) self.diagram_data_plot.setGraphTitle(f"Diagram diffraction") self.diagram_data_plot.setGraphXLabel("two-theta (°)") self.diagram_data_plot.setGraphYLabel("intensity") self.diagram_data_plot.setYAxisLogarithmic(True) self.fitting_data_selector.setNamedAxesSelectorVisibility(False) self.fitting_data_selector.setVisible(True) self.fitting_data_selector.setAxisNames("12") self.fitting_data_plot.setYAxisLogarithmic(True) self.fitting_data_plot.setGraphXLabel("two-theta (°)") self.fitting_data_plot.setGraphYLabel("intensity") self.fitting_data_plot.getRoiAction().trigger() self.fitting_widget.setXRangeUpdatedOnZoom(False) self.toolbar.addAction(self.fit_action) self.fit_action.setVisible(True) self.fitting_data_plot.addToolBar(self.toolbar) self.fitting_data_tab.layout.addWidget(self.fitting_data_plot) self.fitting_data_tab.layout.addWidget(self.fitting_data_selector) # Add tabs to widget self.layout.addWidget(self.tabs) # self.unfold_timer.timeout.connect(self.unfold_data) self.fitting_data_selector.selectionChanged.connect(self.fitting_curve) self.fitting_data_plot.getCurvesRoiWidget().sigROIWidgetSignal.connect( self.get_roi_list) self.unfolded_data_tab.viewer.scatter_selector.selectionChanged.connect( self.synchronize_visualisation) @pyqtSlot() def on_click(self): print("\n") for currentQTableWidgetItem in self.tableWidget.selectedItems(): print(currentQTableWidgetItem.row(), currentQTableWidgetItem.column(), currentQTableWidgetItem.text()) def set_data(self, path: str) -> None: self.path = path with File(os.path.join(path), mode='r') as h5file: self.raw_data = get_dataset(h5file, DataPath.IMAGE_INTERPRETATION.value)[:] # We put the raw data in the dataviewer self.raw_data_viewer.set_movie(self.raw_data, self.flatfield_image) self.unfolded_data_tab.images = self.raw_data self.unfolded_data_tab.path = self.path # We allocate a number of view in the stack of unfolded data and fitting data self.unfolded_data_tab.viewer.set_stack_slider(self.raw_data.shape[0]) self.fitting_data_selector.setData( numpy.zeros((self.raw_data.shape[0], 1, 1))) def set_calibration(self, calibration): # Check if there is a empty list of coordinate in the direct beam calibration if not [] in [value for value in calibration.values()]: self.unfolded_data_tab.set_calibration(calibration) if self.unfolded_data_tab.images is not None: self.unfolded_data_tab.start_unfolding() else: print("Direct beam not calibrated yet.") def get_calibration(self): self.unfoldButtonClicked.emit() def create_diagram_array(self): self.diagram_data_array = [] numpy.seterr(divide='ignore', invalid='ignore') for image in self.unfolded_data_tab.viewer.get_scatter_items(): self.diagram_data_array.append( extract_diffraction_diagram( image[0], image[1], image[2], 1.0 / self.unfolded_data_tab.geometry["calib"], -100, 100, patch_data_flag=True)) self.plot_diagram() self.automatic_fit_tab.set_data_to_fit(self.diagram_data_array) self.fitting_data_selector.selectionChanged.emit() set_plot_limits(self.diagram_data_plot, self.diagram_data_plot.getActiveCurve()) def plot_diagram(self, images_to_remove=[-1]): self.diagram_data_plot.setGraphTitle( f"Diagram diffraction of {self.path.split('/')[-1]}") for index, curve in enumerate(self.diagram_data_array): if index not in images_to_remove: self.diagram_data_plot.addCurve(curve[0], curve[1], f'Data of image {index}', color="#0000FF", replace=False, symbol='o') def get_flatfield(self, flat_img: numpy.ndarray): self.flatfield_image = flat_img self.raw_data_viewer.get_action_flatfield().set_flatfield( self.flatfield_image) self.unfolded_data_tab.flatfield = flat_img def synchronize_visualisation(self): # When user change the unfolded view, it set the raw image to the same frame self.raw_data_viewer.setFrameNumber( self.unfolded_data_tab.viewer.scatter_selector.selection()[0]) def fitting_curve(self): if len(self.diagram_data_array) > 0: self.clear_plot_fitting_widget() curve = self.diagram_data_array[ self.fitting_data_selector.selection()[0]] self.fitting_data_plot.addCurve(curve[0], curve[1], symbol='o') set_plot_limits(self.fitting_data_plot, curve) def get_roi_list(self, events: dict): self.rois_list = list(events["roilist"]) def clear_plot_fitting_widget(self): self.fitting_data_plot.clear() self.fitting_data_plot.clearMarkers()
class ArrayCurvePlot(qt.QWidget): """ Widget for plotting a curve from a multi-dimensional signal array and a 1D axis array. The signal array can have an arbitrary number of dimensions, the only limitation being that the last dimension must have the same length as the axis array. The widget provides sliders to select indices on the first (n - 1) dimensions of the signal array, and buttons to add/replace selected curves to the plot. This widget also handles simple 2D or 3D scatter plots (third dimension displayed as colour of points). """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayCurvePlot, self).__init__(parent) self.__signal = None self.__signal_name = None self.__signal_errors = None self.__axis = None self.__axis_name = None self.__axis_errors = None self.__values = None self.__first_curve_added = False self._plot = Plot1D(self) self._plot.setDefaultColormap( # for scatters {"name": "viridis", "vmin": 0., "vmax": 1., # ignored (autoscale) but mandatory "normalization": "linear", "autoscale": True}) self.selectorDock = qt.QDockWidget("Data selector", self._plot) # not closable self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable | qt.QDockWidget.DockWidgetFloatable) self._selector = NumpyAxesSelector(self.selectorDock) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False self.selectorDock.setWidget(self._selector) self._plot.addTabbedDockWidget(self.selectorDock) layout = qt.QGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._plot, 0, 0) self.setLayout(layout) def setCurveData(self, y, x=None, values=None, yerror=None, xerror=None, ylabel=None, xlabel=None, title=None): """ :param y: dataset to be represented by the y (vertical) axis. For a scatter, this must be a 1D array and x and values must be 1-D arrays of the same size. In other cases, it can be a n-D array whose last dimension must have the same length as x (and values must be None) :param x: 1-D dataset used as the curve's x values. If provided, its lengths must be equal to the length of the last dimension of ``y`` (and equal to the length of ``value``, for a scatter plot). :param values: Values, to be provided for a x-y-value scatter plot. This will be used to compute the color map and assign colors to the points. :param yerror: 1-D dataset of errors for y, or None :param xerror: 1-D dataset of errors for x, or None :param ylabel: Label for Y axis :param xlabel: Label for X axis :param title: Graph title """ self.__signal = y self.__signal_name = ylabel self.__signal_errors = yerror self.__axis = x self.__axis_name = xlabel self.__axis_errors = xerror self.__values = values if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateCurve) self.__selector_is_connected = False self._selector.setData(y) self._selector.setAxisNames([ylabel or "Y"]) if len(y.shape) < 2: self.selectorDock.hide() else: self.selectorDock.show() self._plot.setGraphTitle(title or "") self._plot.setGraphXLabel(self.__axis_name or "X") self._plot.setGraphYLabel(self.__signal_name or "Y") self._updateCurve() if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateCurve) self.__selector_is_connected = True def _updateCurve(self): y = self._selector.selectedData() x = self.__axis if x is None: x = numpy.arange(len(y)) elif numpy.isscalar(x) or len(x) == 1: # constant axis x = x * numpy.ones_like(y) elif len(x) == 2 and len(y) != 2: # linear calibration a + b * x x = x[0] + x[1] * numpy.arange(len(y)) legend = self.__signal_name + "[" for sl in self._selector.selection(): if sl == slice(None): legend += ":, " else: legend += str(sl) + ", " legend = legend[:-2] + "]" if self.__signal_errors is not None: y_errors = self.__signal_errors[self._selector.selection()] else: y_errors = None self._plot.remove(kind=("curve", "scatter")) # values: x-y-v scatter if self.__values is not None: self._plot.addScatter(x, y, self.__values, legend=legend, xerror=self.__axis_errors, yerror=y_errors) # x monotonically increasing: curve elif numpy.all(numpy.diff(x) > 0): self._plot.addCurve(x, y, legend=legend, xerror=self.__axis_errors, yerror=y_errors) # scatter else: self._plot.addScatter(x, y, value=numpy.ones_like(y), legend=legend, xerror=self.__axis_errors, yerror=y_errors) self._plot.resetZoom() self._plot.setGraphXLabel(self.__axis_name) self._plot.setGraphYLabel(self.__signal_name) def clear(self): self._plot.clear()
class ArrayImagePlot(qt.QWidget): """ Widget for plotting an image from a multi-dimensional signal array and two 1D axes array. The signal array can have an arbitrary number of dimensions, the only limitation being that the last two dimensions must have the same length as the axes arrays. Sliders are provided to select indices on the first (n - 2) dimensions of the signal array, and the plot is updated to show the image corresponding to the selection. If one or both of the axes does not have regularly spaced values, the the image is plotted as a coloured scatter plot. """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayImagePlot, self).__init__(parent) self.__signal = None self.__signal_name = None self.__x_axis = None self.__x_axis_name = None self.__y_axis = None self.__y_axis_name = None self._plot = Plot2D(self) self._plot.setDefaultColormap({ "name": "viridis", "vmin": 0., "vmax": 1., # ignored (autoscale) but mandatory "normalization": "linear", "autoscale": True }) self.selectorDock = qt.QDockWidget("Data selector", self._plot) # not closable self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable | qt.QDockWidget.DockWidgetFloatable) self._legend = qt.QLabel(self) self._selector = NumpyAxesSelector(self.selectorDock) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False layout = qt.QVBoxLayout() layout.addWidget(self._plot) layout.addWidget(self._legend) self.selectorDock.setWidget(self._selector) self._plot.addTabbedDockWidget(self.selectorDock) self.setLayout(layout) def setImageData(self, signal, x_axis=None, y_axis=None, signal_name=None, xlabel=None, ylabel=None, title=None): """ :param signal: n-D dataset, whose last 2 dimensions are used as the image's values. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. :param signal_name: Label used in the legend :param xlabel: Label for X axis :param ylabel: Label for Y axis :param title: Graph title """ if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateImage) self.__selector_is_connected = False self.__signal = signal self.__signal_name = signal_name or "" self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel self._selector.setData(signal) self._selector.setAxisNames([ylabel or "Y", xlabel or "X"]) if len(signal.shape) < 3: self.selectorDock.hide() else: self.selectorDock.show() self._plot.setGraphTitle(title or "") self._plot.setGraphXLabel(self.__x_axis_name or "X") self._plot.setGraphYLabel(self.__y_axis_name or "Y") self._updateImage() if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateImage) self.__selector_is_connected = True def _updateImage(self): legend = self.__signal_name + "[" for sl in self._selector.selection(): if sl == slice(None): legend += ":, " else: legend += str(sl) + ", " legend = legend[:-2] + "]" self._legend.setText("Displayed data: " + legend) img = self._selector.selectedData() x_axis = self.__x_axis y_axis = self.__y_axis if x_axis is None and y_axis is None: xcalib = NoCalibration() ycalib = NoCalibration() else: if x_axis is None: # no calibration x_axis = numpy.arange(img.shape[-1]) elif numpy.isscalar(x_axis) or len(x_axis) == 1: # constant axis x_axis = x_axis * numpy.ones((img.shape[-1], )) elif len(x_axis) == 2: # linear calibration x_axis = x_axis[0] * numpy.arange(img.shape[-1]) + x_axis[1] if y_axis is None: y_axis = numpy.arange(img.shape[-2]) elif numpy.isscalar(y_axis) or len(y_axis) == 1: y_axis = y_axis * numpy.ones((img.shape[-2], )) elif len(y_axis) == 2: y_axis = y_axis[0] * numpy.arange(img.shape[-2]) + y_axis[1] xcalib = ArrayCalibration(x_axis) ycalib = ArrayCalibration(y_axis) self._plot.remove(kind=("scatter", "image")) if xcalib.is_affine() and ycalib.is_affine(): # regular image xorigin, xscale = xcalib(0), xcalib.get_slope() yorigin, yscale = ycalib(0), ycalib.get_slope() origin = (xorigin, yorigin) scale = (xscale, yscale) self._plot.addImage(img, legend=legend, origin=origin, scale=scale) else: scatterx, scattery = numpy.meshgrid(x_axis, y_axis) self._plot.addScatter(numpy.ravel(scatterx), numpy.ravel(scattery), numpy.ravel(img), legend=legend) self._plot.setGraphXLabel(self.__x_axis_name) self._plot.setGraphYLabel(self.__y_axis_name) self._plot.resetZoom() def clear(self): self._plot.clear()
class FittingDataTab(QWidget): def __init__(self, parent, data_to_fit=None): super().__init__(parent) self._data_to_fit = data_to_fit self._fitted_data = [] self.layout = QVBoxLayout(self) self.automatic_plot = Plot1D(self) self.fitting_data_selector = NumpyAxesSelector(self) self.fit = FitManager() self.fit.addtheory("pearson7", function=pearson7bg, parameters=[ 'backgr', 'slopeLin', 'amplitude', 'center', 'fwhm', 'exposant' ], estimate=estimate_pearson7) """ self.fitting_widget = self.fitting_data_plot.getFitAction() self.fit_action = FitAction(plot=self.fitting_data_plot, parent=self.fitting_data_plot) self.toolbar = QToolBar("New") """ self.init_ui() def init_ui(self): self.setLayout(self.layout) self.layout.addWidget(self.automatic_plot) self.layout.addWidget(self.fitting_data_selector) self.fitting_data_selector.setNamedAxesSelectorVisibility(False) self.fitting_data_selector.setVisible(True) self.fitting_data_selector.setAxisNames("12") # self.fitting_data_selector.selectionChanged.connect(self.automatic_plot_fit) def set_data_to_fit(self, data_to_fit): self._data_to_fit = data_to_fit self.fitting_data_selector.setData( numpy.zeros((len(data_to_fit), 1, 1))) self.start_automatic_fit() def automatic_fit(self): if self._data_to_fit is not None: prominence = max( self._data_to_fit[0][1][~numpy.isnan(self._data_to_fit[0][1])]) print(prominence) peaks, _ = find_peaks(self._data_to_fit[0][1], prominence=prominence / 3.0) plt.plot(peaks, self._data_to_fit[0][1][peaks], "xr") plt.plot(self._data_to_fit[0][1]) plt.legend(['Test detection with prominence']) plt.show() def plot_fit(self): if len(self._fitted_data) > 0 and len(self._data_to_fit) > 0: self.automatic_plot.addCurve( self._data_to_fit[self.fitting_data_selector.selection()[0]] [0], self._data_to_fit[self.fitting_data_selector.selection() [0]][1], 'Data to fit') self.automatic_plot.addCurve( self._fitted_data[self.fitting_data_selector.selection()[0]] [0], self._fitted_data[self.fitting_data_selector.selection() [0]][1], 'Fitted data') def start_automatic_fit(self): self.fit.settheory("pearson7") print("Start fitting...") for data in self._data_to_fit: # copy the data arrays, with all the values even the nan ones to keep the size # the copy is used to not erase data on arrays ploted in the last panel x = data[0].copy() y = data[1].copy() # get the max of the y array without any nan value (it would be the maximum) maximum = max(y[~numpy.isnan(y)]) # current maximum / peak we are looking for (for the first iteration it will be the max) current_maximum = max(y[~numpy.isnan(y)]) try: cpt_peak = 0 print("Searching peak and fitting it...") # plot the original curve, were we are going to plot each fitted peak. self.automatic_plot.addCurve(x, y, "Data to fit") print("Max : ", maximum, " Current max : ", current_maximum) # this threshold means that we only want peaks that are at least at 1/4 distance in y axis of the max. while current_maximum > (maximum / 4.0): peak = numpy.where(y == current_maximum)[0][0] left = peak - 35 if peak - 35 > 0 else 0 right = peak + 35 if peak + 35 < len(x) else len(x) - 1 x_peak = x[left:right] y_peak = y[left:right] # set the data to fit, i.e only the peak without all the curve self.fit.setdata(x=x_peak, y=y_peak) # use the estimate function we made to make a first guess of the parameters of the function that will fit our peak. self.fit.estimate() # fit the function. self.fit.runfit() # draw the resulted function of the fit. self.automatic_plot.addCurve( x_peak, pearson7bg( x_peak, *(param['fitresult'] for param in self.fit.fit_results)), f"Peak number {cpt_peak}") self._fitted_data.append( self.automatic_plot.getActiveCurve()) backgr = self.fit.fit_results[0]['fitresult'] fwhm = self.fit.fit_results[4]['fitresult'] # erase the peak to make it easier to found other peaks. y[left:right] = statistics.mean(y[~numpy.isnan(y)]) current_maximum = max(y[~numpy.isnan(y)]) cpt_peak += 1 print("new current_max : ", current_maximum) except (numpy.linalg.LinAlgError, TypeError): print( "Singular matrix error: fit is impossible with the given parameters" )
class UnfoldedDataViewer(QWidget): def __init__(self, parent): super().__init__(parent=parent) self.scatter_view = ScatterView(self) colormap = Colormap('viridis', normalization='log') self.scatter_view.setGraphTitle("Stack of unfolded data") self.scatter_view.setColormap(colormap) self.plot = self.scatter_view.getPlotWidget() self.plot.setGraphXLabel("two-theta (°)") self.plot.setGraphYLabel("psi (°)") self.plot.setKeepDataAspectRatio(False) self.plot.setYAxisInverted(True) self.scatter_selector = NumpyAxesSelector(self) # Prevent user from changing dimensions for the plot self.scatter_selector.setNamedAxesSelectorVisibility(False) self.scatter_selector.setVisible(True) self.scatter_selector.setAxisNames("12") self.layout = QVBoxLayout(self) self.layout.addWidget(self.scatter_view) self.layout.addWidget(self.scatter_selector) self.stack = [] self.initial_data_flag = True self.toolbar = QToolBar("Custom toolbar 1") self.scatter_view.addToolBar(self.toolbar) self.action_unfold = Unfold(self.plot, parent=self) self.action_unfold_with_flatfield = UnfoldWithFlatfield(self.plot, parent=self) self.action_save = SaveAction(self.plot, parent=self) self.toolbar.addAction(self.action_unfold) self.toolbar.addAction(self.action_unfold_with_flatfield) self.toolbar.addAction(self.action_save) self.scatter_selector.selectionChanged.connect( self.change_displayed_data) def add_scatter(self, scatter_image: tuple, scatter_factor: int): # Add an image to the stack. If it is the first, emit the selectionChanged signal to plot the first image self.stack.append((scatter_image[0][::scatter_factor], scatter_image[1][::scatter_factor], scatter_image[2][::scatter_factor])) if self.initial_data_flag: self.scatter_selector.selectionChanged.emit() self.initial_data_flag = False def set_stack_slider(self, nb_images: int): # Set the size of the sliderbar that will let the user navigate the images self.clear_scatter_view() self.scatter_selector.setData(numpy.zeros((nb_images, 1, 1))) def change_displayed_data(self): # If there is at least one unfolded image, clear the view, unpack the data and plot a scatter view of the image if len(self.stack) > 0: self.clear_scatter_view() tth_array, psi_array, intensity = self.stack[ self.scatter_selector.selection()[0]] self.plot.setGraphXLimits( min(tth_array) - 0.0, max(tth_array) + 0.0) self.plot.setGraphYLimits( min(psi_array) - 5.0, max(psi_array) + 5.0) start = time.time() self.scatter_view.setData(tth_array, psi_array, intensity, copy=False) end = time.time() print("Setting the data took :", (end - start) * 1000.0, " ms") def clear_scatter_view(self): self.scatter_view.setData(None, None, None) def reset_scatter_view(self): self.clear_scatter_view() self.stack = [] self.initial_data_flag = True def get_scatter_item(self, index: int) -> tuple: return self.stack[index] def get_scatter_items(self) -> list: return self.stack def get_unfold_action(self): return self.action_unfold def get_unfold_with_flatfield_action(self): return self.action_unfold_with_flatfield