Source code for schrodinger.application.jaguar.gui.edit_dialog

import re
from copy import copy

import schrodinger
from schrodinger.application.jaguar.input import JaguarInput
from schrodinger.infra.mmcheck import MmException
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import appframework as af1
from schrodinger.ui.qt import filedialog

from . import utils as gui_utils
from .ui import edit_dialog_ui
from .utils import JaguarSettingError

maestro = schrodinger.get_maestro()

IPKASITES = 'ipkasites'
IPKASEARCH = 'ipkasearch'


[docs]class EditDialog(QtWidgets.QMainWindow): """ A dialog that allows JaguarInput objects to be directly edited. :cvar accepted: A signal emitted when the user clicks OK. It is emitted with two arguments: - A `schrodinger.application.jaguar.input.JaguarInput` object representing the current dialog contents. - An integer flag indicating whether the structure in the `schrodinger.application.jaguar.input.JaguarInput` object is new. The flag is one of: - `SAME_STRUCTURE`: If neither the structure nor the workspace contents have changed - `NEW_STRUCTURE`: If the structure has changed. - `RELOAD_STRUCTURE`: If the structure has not changed, but the workspace contents have been changed via a preview. In these cases, the workspace contents should be reset. :vartype accepted: `PyQt5.QtCore.pyqtSignal` :cvar run_requested: A signal emitted when the user selects Run from the File menu. This signal is emitted with one argument, a `schrodinger.application.jaguar.input.JaguarInput` object to run. Actually running the job is the responsibility of the parent widget. :vartype run_requested: `PyQt5.QtCore.pyqtSignal` :cvar restore_workspace: A signal emitted when the user clicks Cancel after modifying the workspace via a preview. The parent widget is responsible for setting the workspace back to its previous state. :vartype restore_workspace: `PyQt5.QtCore.pyqtSignal` :ivar _orig_text: The text to revert back to if the user clicks Revert :vartype _orig_text: str :ivar _full_text: The full text of the input file being edited. Note that this variable is not kept fully up to date. The full text should always be retrieved via `_getFullText`. :vartype _full_text: str :ivar _orig_struc: The structure that was originally read into the dialog. Used to determine if the user has changed the structure. :vartype _orig_struc: `schrodinger.structure.Structure` :ivar _cur_mode: Whether the dialog is in "Input File" mode (`WHOLE_FILE`) or "Structure" mode `SINGLE_STRUC` :vartype _cur_mode: int :ivar _preview_ran: Whether or not the user has replaced the workspace via a Preview since the dialog was created. :vartype _preview_ran: bool """ # The "Standardize Z-matrix Format" option in the Structure menu was not # ported from C++. The menu item was permanently disabled in the C++ GUI # and there was no implementation code. # The "Assign Unique Atom Labels" option in the Structure menu was not # ported from C++. It's functionality is redundant with features built # into C++ and there was no way to get the GUI into a state where "Assign # Unique Atom Labels" had an effect. accepted = QtCore.pyqtSignal(JaguarInput, int) run_requested = QtCore.pyqtSignal(JaguarInput) restore_workspace = QtCore.pyqtSignal() (WHOLE_FILE, SINGLE_STRUC) = list(range(2)) (RELOAD_STRUCTURE, SAME_STRUCTURE, NEW_STRUCTURE) = (-1, 0, 1)
[docs] def __init__(self, parent, jag_input=None): """ Create the dialog and load the specified JaguarInput object, if any. :param parent: The Qt parent widget :type parent: `PyQt5.QtWidgets.QWidget` :param jag_input: The JaguarInput object to edit. If not given, no text will be displayed. :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` """ super(EditDialog, self).__init__(parent, Qt.Dialog) self.setWindowModality(Qt.WindowModal) self.ui = edit_dialog_ui.Ui_MainWindow() self.ui.setupUi(self) self.ui.structure_menu.menuAction().setVisible(False) self._orig_text = "" self._full_text = "" self._orig_struc = None self._cur_mode = self.WHOLE_FILE self._preview_ran = False if jag_input is not None: self.loadMmJag(jag_input)
[docs] def loadMmJag(self, jag_input): """ Load the specified JaguarInput object. :param jag_input: The JaguarInput object to edit. :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` """ # Copy jag_input so we can modify it without changing the variable in # the calling scope jag_input = copy(jag_input) # Clear the MAEFILE directive so it doesn't appear in the text edit jag_input.setDirective("MAEFILE", "") # Store the original structure coordinates so we can detect if the user # has changed the structure self._orig_struc = jag_input.getStructure() self._cur_mode = self.WHOLE_FILE self.ui.input_file_rb.setChecked(True) self.ui.structure_menu.menuAction().setVisible(False) in_text = jag_input.getInputText() self._orig_text = in_text self.ui.text_edit.setPlainText(in_text)
[docs] def modeChanged(self): """ Respond to the user switching between "Input file" and "Structure" via the radio buttons """ if self.ui.input_file_rb.isChecked(): # We've switched from Structure to Input file new_text = self._getFullText() self.ui.text_edit.setPlainText(new_text) self.ui.structure_menu.menuAction().setVisible(False) self._cur_mode = self.WHOLE_FILE else: # We've switched from Input file to Structure self._full_text = self._getCurrentText() struc_text = self._getStrucText(self._full_text) self.ui.text_edit.setPlainText(struc_text) self.ui.structure_menu.menuAction().setVisible(True) self._cur_mode = self.SINGLE_STRUC
def _getFullText(self): """ Update and return the current full text of the input file. :return: The updated input file text :rtype: str """ if self._cur_mode == self.SINGLE_STRUC: struc_text = self._getCurrentText() new_text = self._replaceZMatText(self._full_text, struc_text) elif self._cur_mode == self.WHOLE_FILE: new_text = self._getCurrentText() return new_text def _getStrucText(self, text, struc_num=1, block="zmat"): """ Get the text block corresponding to the requested structure :param text: The input file text to retrieve the structure from :type text: str :param struc_num: The Z-matrix number to retrieve the structure for. May be 1, 2, or 3. :type struc_num: int :param block: The block of text to retrieve. This may be "zmat" or "zvar". :type block: str :return: The requested text block. If no such block is present, an empty string will be returned. :rtype: str """ regex = self._genRegex(struc_num, block) re_result = re.search(regex, text, re.MULTILINE | re.DOTALL) if re_result: struc_text = re_result.group(2) return struc_text.strip() else: return "" def _replaceZMatText(self, text, new_struc, struc_num=1): """ Replace the zmat block for the specified structure. If there is no zmat block in `text`, one will be added at the end. :param text: The input file text to replace the zmat block in :type text: str :param new_struc: The text of the new zmat block to insert :type new_struc: str :param struc_num: The Z-matrix number to replace. May be 1, 2, or 3. :type struc_num: int :return: The input file text with the requested replacement :rtype: str """ text = text.strip() + "\n" new_struc = new_struc.strip() + "\n" (new_text, matched) = self._replaceStrucText(text, new_struc, struc_num) if not matched: struc_str = str(struc_num) if struc_num != 1 else "" new_text = "%s\n&zmat%s\n%s&\n" % (text, struc_str, new_struc) return new_text def _replaceStrucText(self, text, new_struc, struc_num=1, block="zmat"): """ Replace the specified structure block. If the specified block does not yet exist, no replacement will be made. :param text: The input file text :type text: str :param new_struc: The new text for the specified block :type new_struc: str :param struc_num: The Z-matrix number to replace the structure for. May be 1, 2, or 3. :type struc_num: int :param block: The block of text to replace. This may be "zmat" or "zvar". :type block: str :return: The input file text with the requested replacement :rtype: str """ regex = self._genRegex(struc_num, block) repl = r"\1\n%s\3" % new_struc (new_text, matched) = re.subn(regex, repl, text, 1, re.MULTILINE | re.DOTALL) return (new_text, matched) def _replaceZVarText(self, text, new_struc, struc_num=1): """ Replace the zvar block for the specified structure. If there is no zvar block in `text`, one will be added after the corresponding zmat block. If there is no zmat block, then a blank zmat block and the new zvar block will be added at the end. :param text: The input file text to replace the zvar block in :type text: str :param new_struc: The text of the new zvar block to insert :type new_struc: str :param struc_num: The Z-matrix number to replace. May be 1, 2, or 3. :type struc_num: int :return: The input file text with the requested replacement :rtype: str """ text = text.strip() + "\n" new_struc = new_struc.strip() + "\n" (new_text, matched) = self._replaceStrucText(text, new_struc, struc_num, "zvar") if not matched: # If there's no zvar block to replace, try to place it after the # corresponding zmat block struc_str = str(struc_num) if struc_num != 1 else "" zmat_regex = self._genRegex(struc_num, "zmat") zmat_repl = r"\g<0>\n&zvar%s\n%s&\n" % (struc_str, new_struc) (new_text, matched) = re.subn(zmat_regex, zmat_repl, text, 1, re.MULTILINE | re.DOTALL) if not matched: # If there's no corresponding zmat block, then put a blank zmat # block and the new zvar block at the end of the file new_text = "%s\n&zmat%s\n&\n&zvar%s%s&\n" % (text, struc_str, struc_str, new_struc) return new_text def _removeZVarText(self, text, struc_num=1): """ Remove the zvar block for the specified structure :param text: The input file text to remove the zvar block from :type text: str :param struc_num: The Z-matrix number to remove. May be 1, 2, or 3. :type struc_num: int :return: The input file text with the requested block removed :rtype: str """ regex = self._genRegex(struc_num, "zvar") new_text = re.sub(regex, "", text, 1, re.MULTILINE | re.DOTALL) return new_text def _genRegex(self, struc_num=1, block="zmat"): """ Create a regular expression that will match the specified block. :param struc_num: The Z-matrix number to match. May be 1, 2, or 3. If the Z-matrix number is one, the number is optional, i.e. the regex will match "&zmat" or "&zmat1". :type struc_num: int :param block: The block of text to match. This may be "zmat" or "zvar". :type block: str :return: The requested regular expression. Group 1 of the regular expression is the block header (e.g. "&zmat2" or "&zvar"). Group 2 of the regular expression is the block itself (i.e. the coordinates). Group 3 of the regular expression is the trailing ampersand. :rtype: str """ num_match = str(struc_num) if struc_num == 1: num_match += "?" regex = r"(^&%s%s\s*$)(.*?)(^&)" % (block, num_match) return regex def _getCurrentText(self): """ Get the text from the text edit and convert it to a Python string :return: The text from the text edit :rtype: str """ text = self.ui.text_edit.toPlainText() text = str(text) return text
[docs] @gui_utils.catch_jag_errors def accept(self): """ Accept the edits and update the Jaguar panel. :note: This function must not close this dialog, since we have to wait on the Jaguar panel to validate the emitted `schrodinger.application.jaguar.input.JaguarInput` object first. If the object passes validation, it is the responsibility of the Jaguar panel to close this dialog. :note: This function doesn't run a preflight check before emiting the `schrodinger.application.jaguar.input.JaguarInput` object, since the user may still change options before running the job. """ new_input, text = self._getJagInput(False) # Figure out if the user has changed the structure new_struc = new_input.getStructure() if self._compareStrucs(self._orig_struc, new_struc): if self._preview_ran: new_struc_flag = self.RELOAD_STRUCTURE else: new_struc_flag = self.SAME_STRUCTURE else: new_struc_flag = self.NEW_STRUCTURE self.accepted.emit(new_input, new_struc_flag)
[docs] def close(self): """ Close the dialog without saving it's contents. If the user has changed the workspace, the restore_workspace signal will be emitted. """ if self._preview_ran: self.restore_workspace.emit() super(EditDialog, self).close()
[docs] @gui_utils.catch_jag_errors def preview(self): """Display the current structure in the workspace""" if not maestro: self.error("Preview not available outside of Maestro.") return jag_input = self._createJagInputForStrucConversion() struc = jag_input.getStructure() ws_struc = maestro.workspace_get() if not self._compareStrucs(struc, ws_struc): # Don't bother to run the Preview if the current structure is # already in the workspace, since running a Preview means we have to # worry about restoring the workspace when the dialog is closed. maestro.workspace_set(struc) self._preview_ran = True
[docs] def help(self): """Launch help for this dialog""" af1.help_dialog("JAGUAR_TOPIC_EDIT_JOB", parent=self)
[docs] @gui_utils.catch_jag_errors def write(self): """Write the input file to disk""" new_input, text = self._getJagInput(True) filename = filedialog.get_save_file_name(self, filter="Jaguar Input (*.in)", default_suffix="in") if filename: # Save the text directly rather than using new_input.SaveAs() so # that we preserve formatting with open(filename, "w", newline="\n") as fh: fh.write(text)
def _getJagInput(self, run_preflight): """ Create a JaguarInput object containing the currently specified job settings. :param run_preflight: Whether the preflight check should be run on the JaguarInput object after it's generated :type run_preflight: bool :return: A tuple of - A JaguarInput object containing the currently specified job settings - The full text of the input file :raise JaguarSettingError: If the preflight check catches an error. Or if there are unknown keywords present in the input file and the user does not want to continue. In this case, the user will already have been warned about the unknown keywords and should not be prompted again. As such, the error text will be blank. """ text = self._getFullText() new_input = JaguarInput(text=text) if run_preflight: preflight_err = new_input.preflight() if preflight_err: raise JaguarSettingError(preflight_err) if not gui_utils.warn_about_mmjag_unknowns(new_input, self): raise JaguarSettingError() return new_input, text
[docs] def revert(self): """Revert the input script to its original state""" if self._cur_mode == self.WHOLE_FILE: self.ui.text_edit.setPlainText(self._orig_text) elif self._cur_mode == self.SINGLE_STRUC: self._full_text = self._orig_text struc_text = self._getStrucText(self._full_text) self.ui.text_edit.setPlainText(struc_text)
[docs] @gui_utils.catch_jag_errors def convertToZMatrix(self): """ Convert the structure currently in the text edit to internal coordinates (i.e. a Z-matrix). """ temp_jag_input = self._createJagInputForStrucConversion() temp_jag_input.makeInternalCoords() new_struc_text = temp_jag_input.getZmatText() zmat_text = self._getStrucText(new_struc_text) self.ui.text_edit.setPlainText(zmat_text) # Get the zvar block and add it to the full input file zvar_text = self._getStrucText(new_struc_text, block="zvar") self._full_text = self._replaceZVarText(self._full_text, zvar_text)
[docs] @gui_utils.catch_jag_errors def convertToCartesian(self): """ Convert the structure currently in the text edit to Coordinates coordinates """ temp_jag_input = self._createJagInputForStrucConversion() temp_jag_input.makeCartesianCoords() new_struc_text = temp_jag_input.getZmatText() zmat_text = self._getStrucText(new_struc_text) self.ui.text_edit.setPlainText(zmat_text) # Remove the zvar block from the full input file self._full_text = self._removeZVarText(self._full_text)
def _createJagInputForStrucConversion(self): """ Create a JaguarInput object containing only the current structure. Both the zmat block and the zvar block (if present) will be included. :return: A JaguarInput object containing only the current structure :rtype: `schrodinger.application.jaguar.input.JaguarInput` """ if self._cur_mode == self.SINGLE_STRUC: zmat_text = self._getCurrentText() zvar_text = self._getStrucText(self._full_text, block="zvar") elif self._cur_mode == self.WHOLE_FILE: # If we're in "Input file" mode, still parse out the zmat and zvar # blocks in case there are syntax errors elsewhere that we don't # care about now whole_text = self._getCurrentText() zmat_text = self._getStrucText(whole_text, block="zmat") zvar_text = self._getStrucText(whole_text, block="zvar") struc_text = "&zmat\n%s\n&\n" % zmat_text if zvar_text.strip(): struc_text += "&zvar\n%s\n&\n" % zvar_text try: temp_jag_input = JaguarInput(text=struc_text) except MmException as err: msg = ("The structure could not be parsed:\n%s\n\n\n%s" % (struc_text, str(err))) raise JaguarSettingError(msg) return temp_jag_input
[docs] @gui_utils.catch_jag_errors def runJob(self): """ Emit the run_requested signal with the current job settings. """ new_input, text = self._getJagInput(True) self.run_requested.emit(new_input)
[docs] def viewAtomLabels(self, enabled): """ Toggle showing atom labels. :param enabled: Whether atom labels should be turned on or off :type enabled: bool """ if not maestro: self.ui.view_atom_labels_mi.setEnabled(False) err = "Atom labels cannot be shown outside of Maestro." self.error(err) # Run Preview first to make sure that the structure is up-to-date in the # workspace self.preview() onoff = "on" if enabled else "off" cmd = "labelatom anum=off element=off atomname=%s all" % onoff maestro.command(cmd)
[docs] def error(self, msg): """ Display the specified error. This function is required for the `schrodinger.application.jaguar.gui.utils.catch_jag_errors` decorator. :param msg: The error to display :type msg: str """ QtWidgets.QMessageBox.critical(self, 'Error', msg)
def _compareStrucs(self, struc1, struc2): """ Determine if two structures have different coordinates or atom names. :param struc1: :type struc1: `schrodinger.structure.Structure` :param struc2: :type struc2: `schrodinger.structure.Structure` :note: This function is noticeably less "picky" than `schrodinger.structure.Structure.isEquivalent` """ if struc1.atom_total != struc2.atom_total: return False for (atom1, atom2) in zip(struc1.atom, struc2.atom): if atom1.name.strip() != atom2.name.strip(): return False for atom1_coord, atom2_coord in zip(atom1.xyz, atom2.xyz): if abs(atom1_coord - atom2_coord) > 1e-13: # The editor defaults to displaying 12 figures after the # decimal place, so assume that any differences smaller than # that are rounding errors. return False return True
[docs]class PkaEditDialog(EditDialog): """ Extension of EditDialog to remove spaces from certain pka keywords in the text edit. """ def _getFullText(self): """ Extend _getFullText() to remove spaces in the atom lists of pka keywords `ipkasites` or `ipkasearch`. E.g. a text edit that has "ipkasites=N7,H13, H12" in it will become "ipkasites=N7,H13,H12" while keeping everything else constant. See parent class for full documentation. """ new_text = super()._getFullText() keyword_match = re.search(rf'{IPKASITES}.*|{IPKASEARCH}.*', new_text) if not keyword_match: return new_text # Guaranteed to have either ipkasites or ipkasearch at any given time, # so it's safe to only take a single match orig_input_str = keyword_match.group(0) new_input_str = orig_input_str.replace(' ', '') return new_text.replace(orig_input_str, new_input_str)