Source code for ccpn.ui.gui.lib.GuiSpectrumViewNd

"""Module Documentation here

"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2021"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
               "Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See http://www.ccpn.ac.uk/v3-software/downloads/license")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
                 "CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
                 "J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Geerten Vuister $"
__dateModified__ = "$dateModified: 2021-12-23 13:15:21 +0000 (Thu, December 23, 2021) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================

import numpy as np
from itertools import product
from collections import namedtuple
from PyQt5 import QtCore
from ccpn.ui.gui.lib.GuiSpectrumView import GuiSpectrumView
from ccpn.util import Colour
from ccpn.util.Logging import getLogger
from ccpn.core.Spectrum import MAXALIASINGRANGE
from ccpnc.contour import Contourer2d


# _NEWCOMPILEDCONTOURS = True

AxisPlaneData = namedtuple('AxisPlaneData', 'startPoint endPoint pointCount')


def _getLevels(count: int, base: float, factor: float) -> list:
    """return a list with contour levels"""
    levels = []
    if count > 0:
        levels = [base]
        for n in range(count - 1):
            levels.append(np.float32(factor * levels[-1]))
    return levels


[docs]class GuiSpectrumViewNd(GuiSpectrumView): ###PeakListItemClass = PeakListNdItem #def __init__(self, guiSpectrumDisplay, apiSpectrumView, dimMapping=None, region=None, **kwds): def __init__(self): """ guiSpectrumDisplay is the parent apiSpectrumView is the (API) SpectrumView object """ self.setAcceptedMouseButtons = QtCore.Qt.LeftButton self.posLevelsPrev = [] self.negLevelsPrev = [] self.zRegionPrev = None self.posDisplayLists = [] self.negDisplayLists = [] self._traceScale = None # For now: Initialised by CcpOpenGl._getSliceData self.okDataFile = True # used to keep track of warning message that data file does not exist dimensionCount = len(self.strip.axisCodes) self.previousRegion = dimensionCount * [None] # have to have this set before _setupBorderItem called self._application = self.strip.spectrumDisplay.mainWindow.application GuiSpectrumView.__init__(self) self.setZValue(-1) # this is so that the contours are drawn on the bottom self.buildContours = True self.buildContoursOnly = False def _turnOnPhasing(self): """ # CCPN INTERNAL - called by turnOnPhasing method of GuiStrip. """ phasingFrame = self.strip.spectrumDisplay.phasingFrame if phasingFrame.isVisible(): direction = phasingFrame.getDirection() traces = self.hPhaseTraces if direction == 0 else self.vPhaseTraces for trace, line in traces: trace.setVisible(True) line.setVisible(True) def _turnOffPhasing(self): """ # CCPN INTERNAL - called by turnOffPhasing method of GuiStrip. """ for traces in self.hPhaseTraces, self.vPhaseTraces: for trace, line in traces: trace.setVisible(False) line.setVisible(False) def _changedPhasingDirection(self): phasingFrame = self.strip.spectrumDisplay.phasingFrame direction = phasingFrame.getDirection() if direction == 0: for trace, line in self.hPhaseTraces: trace.setVisible(True) line.setVisible(True) for trace, line in self.vPhaseTraces: trace.setVisible(False) line.setVisible(False) else: for trace, line in self.hPhaseTraces: trace.setVisible(False) line.setVisible(False) for trace, line in self.vPhaseTraces: trace.setVisible(True) line.setVisible(True) self._updatePhasing() def _updatePhasing(self): """ # CCPN INTERNAL - called in _updatePhasing method of GuiStrip """ if not self.isDisplayed: return @property def traceScale(self) -> float: """Scale for trace in this spectrumView""" return self._traceScale @traceScale.setter def traceScale(self, value): """Setter for scale for trace in this spectrumView""" self._traceScale = value self.strip._updateTraces() self._updatePhasing() def _printContourData(self, printer, contourData, colour, xTile0, xTile1, yTile0, yTile1, xTranslate, xScale, xTotalPointCount, yTranslate, yScale, yTotalPointCount): for xTile in range(xTile0, xTile1): for yTile in range(yTile0, yTile1): for contour in contourData: n = len(contour) // 2 contour = contour.copy() contour = contour.reshape((n, 2)) contour[:, 0] += xTotalPointCount * xTile contour[:, 0] *= xScale contour[:, 0] += xTranslate contour[:, 1] += yTotalPointCount * yTile contour[:, 1] *= yScale contour[:, 1] += yTranslate printer.writePolyline(contour, colour) def _buildGLContours(self, glList): """Build the contour arrays """ if self.spectrum.positiveContourBase is None or self.spectrum.positiveContourBase == 0.0: raise RuntimeError('Positive Contour Base is not defined') if self.spectrum.negativeContourBase is None or self.spectrum.negativeContourBase == 0.0: raise RuntimeError('Negative Contour Base is not defined') if self.spectrum.includePositiveContours: # .displayPositiveContours: self.posLevels = _getLevels(self.positiveContourCount, self.positiveContourBase, self.positiveContourFactor) else: self.posLevels = [] if self.spectrum.includeNegativeContours: # .displayNegativeContours: self.negLevels = _getLevels(self.negativeContourCount, self.negativeContourBase, self.negativeContourFactor) else: self.negLevels = [] colName = self._getColour('positiveContourColour') or '#E00810' if not colName.startswith('#'): # get the colour from the gradient table or a single red colListPos = tuple(Colour.scaledRgba(col) for col in Colour.colorSchemeTable[colName]) if colName in Colour.colorSchemeTable else ((1, 0, 0, 1),) else: colListPos = (Colour.scaledRgba(colName),) colName = self._getColour('negativeContourColour') or '#E00810' if not colName.startswith('#'): # get the colour from the gradient table or a single red colListNeg = tuple(Colour.scaledRgba(col) for col in Colour.colorSchemeTable[colName]) if colName in Colour.colorSchemeTable else ((1, 0, 0, 1),) else: colListNeg = (Colour.scaledRgba(colName),) glList.posColours = self.posColours = colListPos glList.negColours = self.negColours = colListNeg try: self._constructContours(self.posLevels, self.negLevels, glList=glList) except FileNotFoundError: self._project._logger.warning("No data file found for %s" % self) return def _constructContours(self, posLevels, negLevels, glList=None): """Construct the contours for this spectrum using an OpenGL display list The way this is done here, any change in contour level needs to call this function. """ posLevelsArray = np.array(posLevels, np.float32) negLevelsArray = np.array(negLevels, np.float32) self.posLevelsPrev = list(posLevels) self.negLevelsPrev = list(negLevels) self.zRegionPrev = tuple([tuple(axis.region) for axis in self.strip.orderedAxes[2:]]) posContoursAll = negContoursAll = None numDims = self.spectrum.dimensionCount # if _NEWCOMPILEDCONTOURS: # new code for the recompiled glList # get the positive/negative contour colour lists _posColours = self._interpolateColours(self.posColours, posLevels) _negColours = self._interpolateColours(self.negColours, negLevels) contourList = None try: if True: # numDims < 3 or self._application.preferences.general.generateSinglePlaneContours: dataArrays = tuple() for position, dataArray in self._getPlaneData(): # overlay all the planes into a single plane dataArrays += (dataArray,) # break # moved to C Code # if len(dataArrays) > 1 and not self._application.preferences.general.generateSinglePlaneContours: # sum = dataArrays[0] # for ii in range(1, len(dataArrays)): # sum = np.max(sum, dataArrays[ii].clip(0.0, 1e16)) + np.min(sum, dataArrays[ii].clip(-1e16, 0.0)) # dataArrays = (sum,) # build the contours contourList = Contourer2d.contourerGLList(dataArrays, posLevelsArray, negLevelsArray, np.array(_posColours, dtype=np.float32), np.array(_negColours, dtype=np.float32), not self._application.preferences.general.generateSinglePlaneContours) except Exception as es: getLogger().warning(f'Contouring error: {es}') finally: if contourList and contourList[1] > 0: # set the contour arrays for the GL object glList.numVertices = contourList[1] glList.indices = contourList[2] glList.vertices = contourList[3] glList.colors = contourList[4] else: # clear the arrays glList.numVertices = 0 glList.indices = np.array((), dtype=np.uint32) glList.vertices = np.array((), dtype=np.float32) glList.colors = np.array((), dtype=np.float32) def _interpolateColours(self, colourList, levels): colours = [] stepX = len(levels) - 1 step = stepX stepY = len(colourList) - 1 jj = 0 if stepX > 0: for ii in range(stepX + 1): _interp = (stepX - step) / stepX _intCol = Colour.interpolateColourRgba(colourList[min(jj, stepY)], colourList[min(jj + 1, stepY)], _interp, alpha=1.0) colours.extend(_intCol) step -= stepY while step < 0: step += stepX jj += 1 else: colours = colourList[0] return colours def _getPlaneData(self): spectrum = self.spectrum dimensionCount = spectrum.dimensionCount dimIndices = self.dimensionIndices xDim = dimIndices[0] yDim = dimIndices[1] orderedAxes = self.strip.axes if dimensionCount == 2: planeData = spectrum.getPlaneData(xDim=xDim + 1, yDim=yDim + 1) position = [1, 1] yield position, planeData elif dimensionCount == 3: # start with the simple case axisData = self._getAxisInfo(orderedAxes, 2) if not axisData: return position = dimensionCount * [1] # if axisData.startPoint <= axisData.endPoint: # need to check after other bits working # _loopArgs = range(axisData.startPoint, axisData.endPoint) # else: # _loopArgs = range(axisData.startPoint - 1, axisData.endPoint - 1, -1) # for z in _loopArgs: for z in range(axisData.startPoint, axisData.endPoint, 1 if axisData.endPoint > axisData.startPoint else -1): position[dimIndices[2]] = (z % axisData.pointCount) + 1 planeData = spectrum.getPlaneData(position, xDim=xDim + 1, yDim=yDim + 1) yield position, planeData elif dimensionCount >= 4: # get the axis information axes = [self._getAxisInfo(orderedAxes, dim) for dim in range(2, dimensionCount)] if None in axes: return # create a tuple of the ranges for the planes _loopArgs = tuple(range(axis.startPoint, axis.endPoint, 1 if axis.endPoint > axis.startPoint else -1) for axis in axes) position = dimensionCount * [1] _offset = dimensionCount - len(_loopArgs) # should always be 2? # iterate over all the axes for _plane in product(*_loopArgs): # get the axis position and put into the position vector for dim, pos in enumerate(_plane): _axis = axes[dim] position[dimIndices[dim + _offset]] = (pos % axes[dim].pointCount) + 1 # get the plane data planeData = spectrum.getPlaneData(position, xDim=xDim + 1, yDim=yDim + 1) yield position, planeData def _getAxisInfo(self, orderedAxes, axisIndex): """Get the information for the required axis """ indx = self.dimensionIndices[axisIndex] if indx is None: return # get the axis region zPosition = orderedAxes[axisIndex].position width = orderedAxes[axisIndex].width axisCode = self.strip.spectrumDisplay.axisCodes[axisIndex] # get the ppm range zPointCount = (self.spectrum.pointCounts)[indx] zRegionValue = (zPosition + 0.5 * width, zPosition - 0.5 * width) # Note + and - (axis backwards) # clip to the aliasingLimits of the spectrum - ignore if both greater/less than limits aliasing = (self.spectrum.aliasingLimits)[indx] if all(val <= aliasing[0] for val in zRegionValue) or all(val >= aliasing[1] for val in zRegionValue): return zRegionValue = tuple(float(np.clip(val, *aliasing)) for val in zRegionValue) # convert ppm- to point-range zPointFloat0 = self.spectrum.ppm2point(zRegionValue[0], axisCode=axisCode) - 1.0 zPointFloat1 = self.spectrum.ppm2point(zRegionValue[1], axisCode=axisCode) - 1.0 # convert to integers zPointInt0, zPointInt1 = (int(zPointFloat0 + (1 if zPointFloat0 >= 0 else 0)), # this gives first and 1+last integer in range int(zPointFloat1 + (1 if zPointFloat1 >= 0 else 0))) # and takes into account negative ppm2Point if zPointInt0 == zPointInt1: # only one plane visible, need to 2 points for range() if zPointFloat0 - (zPointInt0 - 1) < zPointInt1 - zPointFloat1: # which is closest to an integer zPointInt0 -= 1 else: zPointInt1 += 1 elif (zPointInt1 - zPointInt0) >= zPointCount: # range is more than range of planes, set to maximum zPointInt0 = 0 zPointInt1 = zPointCount return AxisPlaneData(zPointInt0, zPointInt1, zPointCount) def _getVisiblePlaneList(self, firstVisible=None, minimumValuePerPoint=None): spectrum = self.spectrum dimensionCount = spectrum.dimensionCount dimIndices = self.dimensionIndices orderedAxes = self._apiStripSpectrumView.strip.orderedAxes if dimensionCount <= 2: return None, None, [] planeList = () planePointValues = () for dim in range(2, dimensionCount): indx = self.dimensionIndices[dim] if indx is None: return # make sure there is always a spectrumView to base visibility on # useFirstVisible = firstVisible if firstVisible else self zPosition = orderedAxes[dim].position axisCode = orderedAxes[dim].code # get the plane count from the widgets planeCount = self.strip.planeAxisBars[dim - 2].planeCount # .planeToolbar.planeCounts[dim - 2].value() zPointCount = (self.spectrum.pointCounts)[indx] zValuePerPoint = (self.spectrum.ppmPerPoints)[indx] # minSpectrumFrequency, maxSpectrumFrequency = (self.spectrum.spectrumLimits)[index] # pass in a smaller valuePerPoint - if there are differences in the z-resolution, otherwise just use local valuePerPoint minZWidth = 3 * zValuePerPoint zWidth = (planeCount + 2) * minimumValuePerPoint[dim - 2] if minimumValuePerPoint else (planeCount + 2) * zValuePerPoint zWidth = max(zWidth, minZWidth) zRegionValue = (zPosition + 0.5 * zWidth, zPosition - 0.5 * zWidth) # Note + and - (axis backwards) # ppm position to point range zPointFloat0 = self.spectrum.ppm2point(zRegionValue[0], axisCode=axisCode) - 1 zPointFloat1 = self.spectrum.ppm2point(zRegionValue[1], axisCode=axisCode) - 1 # convert to integers zPointInt0, zPointInt1 = (int(zPointFloat0 + (1 if zPointFloat0 >= 0 else 0)), # this gives first and 1+last integer in range int(zPointFloat1 + (1 if zPointFloat1 >= 0 else 0))) # and takes into account negative ppm2Point if zPointInt0 == zPointInt1: # only one plane visible, need to 2 points for range() if zPointFloat0 - (zPointInt0 - 1) < zPointInt1 - zPointFloat1: # which is closest to an integer zPointInt0 -= 1 else: zPointInt1 += 1 # check that the point values are not outside the maximum aliasing limits zPointInt0 = max(zPointInt0, -MAXALIASINGRANGE * zPointCount) zPointInt1 = min(zPointInt1, (MAXALIASINGRANGE + 1) * zPointCount) _temp = (tuple((zz % zPointCount) for zz in range(zPointInt0, zPointInt1)), 0, zPointCount) planeList = planeList + (_temp,) # need to add 0.5 for the indexing in the api planePointValues = () # not sure tha the ppm's are needed here # planePointValues = planePointValues + ((tuple(self.spectrum.ppm2point(zz + 0.5, axisCode=axisCode) # for zz in range(zPointInt0, zPointInt1 + 1)), zPointOffset, zPointCount),) return planeList, planePointValues, dimIndices
[docs] def getVisibleState(self, dimensionCount=None): """Get the visible state for the X/Y axes """ if not dimensionCount: dimensionCount = self.spectrum.dimensionCount return tuple(self._getSpectrumViewParams(vParam) for vParam in range(0, dimensionCount))
[docs] def refreshData(self): # spawn a rebuild in the openGL strip self.buildContours = True from ccpn.ui.gui.lib.OpenGL.CcpnOpenGL import GLNotifier GLSignals = GLNotifier(parent=self) GLSignals.emitPaintEvent()