Source code for ccpn.plugins.development.ExportTalos

#=========================================================================================
# 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: Ed Brooksbank $"
__dateModified__ = "$dateModified: 2021-09-13 19:21:22 +0100 (Mon, September 13, 2021) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: Chris Spronk $"
__date__ = "$Date: 2017-11-28 10:28:42 +0000 (Tue, Nov 28, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================


#=========================================================================================
# TODO
# Catch exceptions in case no valid spectra are in the project, now the plugin doesn't open
# when no spectra are in the project
# Output path selection ? If not default
# Tsar installation path ? if not default
# support for 4D and 5D data sets, including tolerance and nucleus selection
# import of Tsar results (assigned peak lists in nef format)
#=========================================================================================




import os,copy,json,pprint,math,shutil,pandas,operator
from collections import OrderedDict as OD
from PyQt5 import QtCore, QtGui, QtWidgets
from ccpn.framework.lib.Plugin import Plugin
from ccpn.ui.gui.modules.PluginModule import PluginModule
from ccpn.ui.gui.widgets.FileDialog import LineEditButtonDialog
from ccpn.ui.gui.widgets.Label import Label
from ccpn.ui.gui.widgets.LineEdit import LineEdit
from ccpn.ui.gui.widgets.TextEditor import TextEditor
from ccpn.ui.gui.widgets.DoubleSpinbox import DoubleSpinbox
from ccpn.ui.gui.widgets.Spinbox import Spinbox
from ccpn.ui.gui.widgets.RadioButtons import RadioButtons
from ccpn.ui.gui.widgets.Button import Button
from ccpn.ui.gui.widgets.ButtonList import ButtonList
from ccpn.ui.gui.widgets.MessageDialog import showYesNoWarning, showWarning, showMessage,showYesNo
from ccpn.ui.gui.widgets.ProjectTreeCheckBoxes import ProjectTreeCheckBoxes
from ccpn.ui.gui.widgets.CheckBox import CheckBox
from ccpn.ui.gui.widgets.PulldownList import PulldownList
from ccpn.ui.gui.widgets.Spacer import Spacer
from ccpn.ui.gui.widgets.ScrollArea import ScrollArea
from ccpn.ui.gui.widgets.Frame import Frame
from ccpn.util.nef import GenericStarParser, StarIo
from ccpn.util.nef.GenericStarParser import SaveFrame, DataBlock, DataExtent, Loop, LoopRow
from functools import partial

############
# Settings #
############

# Widths in pixels for nice widget alignments, length depends on the number of fixed columns in the plugin.
# Variable number of columns will take the last value in the list
columnWidths = [150,100,75,100,75]

# Set some tooltip texts
help = {'Run name': 'Name of the run directory. Spaces will be converted to "_"',
        'Chains': 'Check chains from which to calculate structures',
        'Chemical shift list': 'Select chemical shift list.',
        'Export': 'Sets up the project directory structure and exports Talos shifts.',
        'Write': 'Writes the current settings in JSON and human readable format to the project notes, with the run name as title',
        }

# Talos Nuclei map
nuclei = {'H':    'HN',
          'HA':   'HA',
          'HAx': 'HA2',
          'HAy': 'HA3',
          'HA2': 'HA2',
          'HA3': 'HA3',
          'HA%': 'HA2,HA3',
          'C':    'C',
          'CA':   'CA',
          'CB':   'CB',
          'N':    'N'
          }

[docs]class TalosGuiPlugin(PluginModule): className = 'TALOS' def __init__(self, mainWindow=None, plugin=None, application=None, **kwds): super(TalosGuiPlugin, self) PluginModule.__init__(self, mainWindow=mainWindow, plugin=plugin, application=application) # Import some functionality for placing widgets on the grid and setting their properties from .pluginAddons import _addRow, _addColumn, _addVerticalSpacer, _setWidth, _setWidgetProperties self.scrollArea = ScrollArea(self.mainWidget,grid=(0,0)) self.scrollArea.setWidgetResizable(True) self.scrollAreaLayout = Frame(None, setLayout=True) self.scrollArea.setWidget(self.scrollAreaLayout) self.scrollAreaLayout.setContentsMargins(20, 20, 20, 20) # self.userPluginPath = self.application.preferences.general.userPluginPath # self.outputPath = self.application.preferences.general.dataPath self.pluginPath = os.path.join(self.project.path,TalosGuiPlugin.className) # Create ORDERED dictionary to store all parameters for the run # deepcopy doesn't work on dictionaries with the qt widgets, so to get a separation of gui widgets and values for storage # it is needed to create the two dictionaries alongside each other self.guiDict = OD([('General',OD()), ('Chains',OD()), ('Chemical shift list',OD())]) self.settings = OD([('General',OD()), ('Chains',OD()), ('Chemical shift list',OD())]) # Input check validInput = self._inputDataCheck() if not validInput: return # Run name grid = 0,0 widget = Label(self.scrollAreaLayout,text='Run name', grid=grid,tipText=help['Run name']) _setWidgetProperties(widget,_setWidth(columnWidths,grid)) grid = _addColumn(grid) widget = LineEdit(self.scrollAreaLayout, text = 'Run_1', grid=grid,tipText=help['Run name']) _setWidgetProperties(widget,_setWidth(columnWidths,grid)) self.guiDict['General']['Run name'] = widget self.settings['General']['Run name'] = self._getValue(widget) # Set molecular chains, use pulldown, Talos only allows one chain at a time grid = _addVerticalSpacer(self.scrollAreaLayout,grid) grid = _addRow(grid) widget = Label(self.scrollAreaLayout,text='Molecular chains', grid=grid) _setWidgetProperties(widget,_setWidth(columnWidths,grid)) grid = _addColumn(grid) widget = PulldownList(self.scrollAreaLayout, grid=grid, tipText=help['Chains']) _setWidgetProperties(widget,_setWidth(columnWidths,grid)) self.chains = [chain.id for chain in sorted(self.project.chains)] widget.setData(self.chains) self.guiDict['Chains'] = widget self.settings['Chains'] = self._getValue(widget) # Set Chemical shift list, use checkboxes with own labels grid = _addVerticalSpacer(self.scrollAreaLayout,grid) grid = _addRow(grid) widget = Label(self.scrollAreaLayout,text='Chemical shift list', grid=grid) _setWidgetProperties(widget,_setWidth(columnWidths,grid)) grid = _addColumn(grid) widget = PulldownList(self.scrollAreaLayout, grid=grid, tipText=help['Chemical shift list']) _setWidgetProperties(widget,_setWidth(columnWidths,grid)) self.shiftLists = [shiftList.id for shiftList in sorted(self.project.chemicalShiftLists)] widget.setData(self.shiftLists) self.guiDict['Chemical shift list'] = widget self.settings['Chemical shift list'] = self._getValue(widget) # Action buttons: Set up, Run, import grid = _addVerticalSpacer(self.scrollAreaLayout, grid) grid = _addColumn(grid) texts = ['Export Talos', 'Write settings'] tipTexts = [help['Export'], help['Write']] callbacks = [self._exportTalos, self._writeSettings] widget = ButtonList(parent=self.scrollAreaLayout, texts=texts, callbacks=callbacks, tipTexts=tipTexts, grid=grid, gridSpan=(1,4)) _setWidgetProperties(widget,_setWidth(columnWidths,grid),heightType='Minimum') Spacer(self.scrollAreaLayout, 5, 5, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding, grid=(grid[0]+1, 10), gridSpan=(1, 1)) def _createTalosSequence(self, chain): # Create a sequence string first_resid = chain.residues[0].sequenceCode talosSequence = 'DATA FIRST_RESID {0}\n'.format(first_resid) sequence = '' for r in chain.residues: resName = r.shortName if r.shortName == 'C' and r.residueVariant == '-HG': resName = 'c' sequence += resName # Split the sequence into chunks of 10 sequence = [sequence[i:i + 10] for i in range(0, len(sequence), 10)] # Add up to 5 chunks of 10 residues per SEQUENCE line for i in range(1+int(len(sequence)/5)): talosSequence += '\nDATA SEQUENCE ' for j in range(0,5): try: talosSequence += sequence[5*i+j] + ' ' except IndexError: pass return talosSequence def _getSortedShifts(self, shiftList, molecularChain): shifts = [] cnt = 0 for cs in shiftList.chemicalShifts: # Shifts have to match Talos nuclei and the selected molecular chain if not cs.nmrAtom.name in nuclei: continue if not cs.nmrAtom.nmrResidue.nmrChain.chain == molecularChain: continue # Get the assignments shift = cs.value atomName = cs.nmrAtom.name resName = cs.nmrAtom.nmrResidue.residue.shortName resId = int(cs.nmrAtom.nmrResidue.sequenceCode) if cs.nmrAtom.nmrResidue.residue.shortName == 'C' and cs.nmrAtom.nmrResidue.residue.residueVariant == '-HG': resName = 'c' # Convert to Talos atom name if not ',' in nuclei[atomName]: atomName = nuclei[atomName] shifts.append([resId, resName, atomName, shift]) else: atomNames = nuclei[atomName].split(',') for atomName in atomNames: shifts.append([resId, resName, atomName, shift]) # Sort the chemical shifts on residue number and atom name sortedShifts = sorted(shifts, key=operator.itemgetter(0,2)) return sortedShifts def _writeTalos(self): chainId = self.settings['Chains'] shiftListId = self.settings['Chemical shift list'] chain = self.project.getByPid('MC:'+chainId) shiftList = self.project.getByPid('CL:' + shiftListId) talosSequence = self._createTalosSequence(chain) sortedShifts = self._getSortedShifts(shiftList,chain) out = 'REMARK Chemical shift table.\n' out += 'REMARK Chain: {0}\n'.format(chainId) out += 'REMARK Shift list: {0}\n\n'.format(shiftListId) out += '{0}\n\n'.format(talosSequence) out += 'VARS RESID RESNAME ATOMNAME SHIFT\n' out += 'FORMAT %4d %1s %4s %8.3f\n\n' for shift in sortedShifts: out += '{0:>4d} {1:1s} {2:>4s} {3:>8.3f}\n'.format(shift[0],shift[1],shift[2],shift[3]) with open(os.path.join(self.runPath,'{0}_talos.tab'.format(self.settings['General']['Run name'])),'w') as output: output.write(out) def _inputDataCheck(self): # Checks available input data at plugin start return True inputWarning = '' if len(self.project.chains) == 0: inputWarning += 'No molecular chains found in the project\n' if len(self.project.chemicalShiftLists) == 0: inputWarning += 'No Chemical shift list found in the project\n' if inputWarning != '': showWarning('Warning',inputWarning) #self._closeModule() self.deleteLater() return False return True def _runDataCheck(self): # Checks data before run time inputWarning = '' if len(self.settings['General']['Run name'].strip()) == 0: inputWarning += 'No run name specified\n' if len(self.settings['General']['Run name'].split()) > 1: inputWarning += 'Run name may not contain spaces\n' if inputWarning != '': showWarning('Warning',inputWarning) setupComplete = False else: setupComplete = True return setupComplete # def _checkTsarPath(self): # self.TsarPath = self.userPluginPath+'Tsar/' # if not os.path.exists(self.TsarPath): # os.makedirs(self.TsarPath) def _getValue(self,widget): # Get the current value of the widget: if isinstance(widget,str): value = widget if not isinstance(widget, Button) and not isinstance(widget, ButtonList): if hasattr(widget,'get'): value = widget.get() elif hasattr(widget, 'isChecked'): value = widget.isChecked() elif hasattr(widget, 'value'): value = widget.value() return value def _updateSettings(self, inputDict, outputDict): # Gets all the values from a dictionary that contains widgets # and writes values to the dictionary with the same structure for k, v in inputDict.items(): if isinstance(v, dict): self._updateSettings(v, outputDict[k]) else: outputDict[k] = self._getValue(v) return outputDict def _writeSettings(self): # Get values from the GUI widgets and save in the settings self._updateSettings(self.guiDict, self.settings) # # Create a data block of the regular CCPN objects, which would normally be exported directly. # self._createPidList() # Check if all input requirements are met setupComplete = self._runDataCheck() if setupComplete == True: # Creates a JSON string and a cleaned up human readable string, both added to notes, in a single note jsonString = json.dumps(self.settings,indent=4) s = pprint.pformat(jsonString) n = '' maxChars = 0 for line in s.split('\n'): for char in ['"','{','}',',',"'",'(',')','\\']: line = line.replace(char,'') # Remove preceding 5 spaces and \n without removing newline line = line[5:-1].rstrip(' ') + '\n' if len(line.split(':')[0]) > maxChars: maxChars = len(line) # Don't write unnecessary empty newlines if not len(line.split()) == 0: n += line # Justify key and values nicely formatted = '' for line in n.split('\n'): try: k,v = line.split(':') line = '{0}{1}\n'.format((k + ':').ljust(maxChars + 4), v) # For non mono spaced fonts we need to convert spaces to tabs, but tab locations in the notes are different # from the editor, where this method works: # nTabs = math.ceil((maxChars - len(k))/4) # line = '{0}{1}{2} {3} {4} {5}\n'.format(k+':',nTabs*'\t', str(v).strip(), len(k), maxChars, nTabs) except ValueError: pass formatted += line noteTitle = '{0} - Export Talos'.format(self.settings['General']['Run name']) note = self.project.newNote(noteTitle) note.text = formatted + '\n\nJSON format:\n\n' + jsonString showMessage('Message', 'Created project note {0}'.format(noteTitle)) def _exportTalos(self): # Get values from the GUI widgets and save in the settings self._updateSettings(self.guiDict, self.settings) # Set the run path self.runPath = os.path.join(self.pluginPath, self.settings['General']['Run name']) # Check if all input requirements are met setupComplete = self._runDataCheck() if setupComplete == True: # Check if tree exists, and if to overwrite overwrite = False if os.path.exists(self.runPath): overwrite = showYesNo('Warning', 'Run {0} exists. Overwrite?'.format(self.settings['General']['Run name'])) if overwrite == False: showMessage('Message', 'Project set up aborted') return if overwrite == True: shutil.rmtree(self.runPath) # Create a (new) directory tree self._createRunTree() self._writeTalos() showMessage('Message', 'Talos export complete') def _createRunTree(self): if not os.path.exists(self.pluginPath): os.makedirs(self.pluginPath) if not os.path.exists(self.runPath): os.makedirs(self.runPath)
[docs]class TalosPlugin(Plugin): PLUGINNAME = 'Export Talos' guiModule = TalosGuiPlugin CCPNPLUGIN = True
[docs] def run(self, **kwargs): """ Insert here the script for running Tsar """ print('Running Talos', kwargs)
# TalosPlugin.register() # Registers the pipe in the pluginList # Set tolerances from project.spectrum.tolerances by default