Пример #1
0
class ConsoleInput(QLineEdit, object):
    def __init__(self, commands=[], parent=None):
        super(ConsoleInput, self).__init__(parent=parent)

        self._commands = commands

        self._model = QStringListModel()
        self._model.setStringList(self._commands)
        self._completer = QCompleter(self)
        self._completer.setModel(self._model)
        self._completer.setCompletionMode(QCompleter.PopupCompletion)
        self._completer.setCaseSensitivity(Qt.CaseInsensitive)
        self.setCompleter(self._completer)
        self.setFont(QFont('Arial', 9, QFont.Bold, False))
Пример #2
0
class HypnotoadGui(QMainWindow, Ui_Hypnotoad):
    """A graphical interface for Hypnotoad"""
    def __init__(self):
        super().__init__(None)
        self.setupUi(self)

        try:
            self.menu_File.setToolTipsVisible(True)
            self.menu_Mesh.setToolTipsVisible(True)
            self.menu_Help.setToolTipsVisible(True)
        except AttributeError:
            pass

        self.plot_widget = MatplotlibWidget(self.plottingArea)

        def set_triggered(widget, function):
            widget.triggered.connect(function)
            if hasattr(function, "__doc__"):
                widget.setToolTip(function.__doc__.strip())

        def set_clicked(widget, function):
            widget.clicked.connect(function)
            if hasattr(function, "__doc__"):
                widget.setToolTip(function.__doc__.strip())

        set_clicked(self.geqdsk_file_browse_button, self.select_geqdsk_file)
        self.geqdsk_file_line_edit.editingFinished.connect(self.read_geqdsk)

        set_clicked(self.options_file_browse_button, self.select_options_file)
        self.options_file_line_edit.editingFinished.connect(self.read_options)

        set_clicked(self.run_button, self.run)
        set_triggered(self.action_Run, self.run)

        set_clicked(self.write_grid_button, self.write_grid)
        set_triggered(self.action_Write_grid, self.write_grid)
        self.write_grid_button.setEnabled(False)

        self.nonorthogonal_box.stateChanged.connect(self.set_nonorthogonal)

        set_clicked(self.regrid_button, self.regrid)
        set_triggered(self.action_Regrid, self.regrid)

        set_triggered(self.action_Revert, self.revert_options)
        set_triggered(self.action_Save, self.save_options)
        set_triggered(self.action_Save_as, self.save_options_as)
        set_triggered(self.action_New, self.new_options)
        set_triggered(self.action_Open, self.select_options_file)
        set_triggered(self.action_About, self.help_about)
        set_triggered(self.action_Preferences, self.open_preferences)

        self.action_Quit.triggered.connect(self.close)

        self.options = DEFAULT_OPTIONS
        self.gui_options = DEFAULT_GUI_OPTIONS
        self.filename = DEFAULT_OPTIONS_FILENAME

        self.search_bar.setPlaceholderText("Search options...")
        self.search_bar.textChanged.connect(self.search_options_form)
        self.search_bar.setToolTip(self.search_options_form.__doc__.strip())
        option_names = (
            set(BoutMesh.user_options_factory.defaults.keys()).union(
                set(tokamak.TokamakEquilibrium.user_options_factory.defaults.
                    keys())).union(
                        set(tokamak.TokamakEquilibrium.
                            nonorthogonal_options_factory.defaults.keys(
                            )  # noqa: E501
                            )))
        self.search_bar_completer = QCompleter(option_names)
        self.search_bar_completer.setCaseSensitivity(Qt.CaseInsensitive)
        self.search_bar.setCompleter(self.search_bar_completer)

        self.options_form.cellChanged.connect(self.options_form_changed)
        self.options_form.itemDoubleClicked.connect(_table_item_edit_display)
        self.update_options_form()

    def close(self):
        # Delete and garbage-collect hypnotoad objects here so that any ParallelMap
        # instances get deleted if they exists. ParallelMap.__del__() calls
        # terminate on the worker processes. Needs to happen before program exits
        # or program will hang waiting for parallel workers to finish (which they
        # never do because they are waiting for another job in
        # ParallelMap.worker_run()).
        if hasattr(self, "eq"):
            del self.eq
        if hasattr(self, "mesh"):
            del self.mesh
        gc.collect()
        super().close()

    def help_about(self):
        """About Hypnotoad"""

        about_text = __doc__.strip()
        about_text += f"\nVersion : {__version__}"

        about_box = QMessageBox(self)
        about_box.setText(about_text)
        about_box.exec_()

    def open_preferences(self):
        """GUI preferences and settings"""
        preferences_window = Preferences(self)
        preferences_window.exec_()

    def revert_options(self):
        """Revert the current options to the loaded file, or defaults if no
        file loaded

        """

        self.statusbar.showMessage("Reverting options", 2000)
        self.options = DEFAULT_OPTIONS

        options_filename = self.options_file_line_edit.text()

        if options_filename:
            self.read_options()
        else:
            self.options_form.setRowCount(0)
            self.update_options_form()

    def new_options(self):
        """New set of options"""

        self.options = DEFAULT_OPTIONS
        self.options_form.setRowCount(0)
        self.update_options_form()

    def save_options(self):
        """Save options to file"""

        self.statusbar.showMessage("Saving...", 2000)

        if not self.filename or self.filename == DEFAULT_OPTIONS_FILENAME:
            self.save_options_as()

        if not self.filename:
            self.filename = DEFAULT_OPTIONS_FILENAME
            return

        self.options_file_line_edit.setText(self.filename)

        options_to_save = self.options
        if self.gui_options["save_full_yaml"]:
            mesh_options = BoutMesh.user_options_factory.create(self.options)
            eq_options = tokamak.TokamakEquilibrium.user_options_factory.create(
                self.options)
            nonorth_options = (tokamak.TokamakEquilibrium.
                               nonorthogonal_options_factory.create(
                                   self.options))

            # This converts any numpy types to native Python using the tolist()
            # method of any numpy objects/types. Note this does return a scalar
            # and not a list for values that aren't arrays. Also remove any
            # private/magic keys
            options_to_save = {}
            for key, value in mesh_options.items():
                options_to_save[key] = getattr(value, "tolist",
                                               lambda: value)()
            for key, value in eq_options.items():
                options_to_save[key] = getattr(value, "tolist",
                                               lambda: value)()
            for key, value in nonorth_options.items():
                options_to_save[key] = getattr(value, "tolist",
                                               lambda: value)()

        with open(self.filename, "w") as f:
            yaml.dump(options_to_save, f)

    def save_options_as(self):
        """Save options to file with new filename"""

        if not self.filename:
            self.filename = DEFAULT_OPTIONS_FILENAME

        self.filename, _ = QFileDialog.getSaveFileName(self,
                                                       "Save grid to file",
                                                       self.filename,
                                                       filter=YAML_FILTER)

        if not self.filename:
            return

        # If there was no extension, add one, unless the file already exists
        path = pathlib.Path(self.filename)
        if not path.exists() and path.suffix == "":
            self.filename += ".yml"

        self.save_options()

    def update_options_form(self):
        """Update the widget values in the options form, based on the current
        values in self.options

        """

        filtered_options = copy.deepcopy(self.options)

        filtered_defaults = dict(BoutMesh.user_options_factory.defaults)
        filtered_defaults.update(
            tokamak.TokamakEquilibrium.user_options_factory.defaults)
        filtered_defaults.update(
            tokamak.TokamakEquilibrium.nonorthogonal_options_factory.defaults)

        # evaluate filtered_defaults using the values in self.options, so that any
        # expressions get evaluated
        filtered_default_values = dict(
            BoutMesh.user_options_factory.create(self.options))
        try:
            filtered_default_values.update(
                tokamak.TokamakEquilibrium.user_options_factory.create(
                    self.options))
            if not hasattr(self, "eq"):
                filtered_default_values.update(
                    tokamak.TokamakEquilibrium.nonorthogonal_options_factory.
                    create(self.options))
            else:
                # Use the object if it exists because some defaults are updated when the
                # Equilibrium is created
                filtered_default_values.update(
                    self.eq.nonorthogonal_options_factory.create(self.options))
        except (ValueError, TypeError) as e:
            self._popup_error_message(e)
            return

        # Skip options handled specially elsewhere
        filtered_options.pop("orthogonal", None)
        del filtered_defaults["orthogonal"]

        self.options_form.setSortingEnabled(False)
        self.options_form.cellChanged.disconnect(self.options_form_changed)
        self.options_form.setRowCount(len(filtered_defaults))

        for row, (key, value) in enumerate(sorted(filtered_defaults.items())):
            item0 = QTableWidgetItem(key)
            item0.setFlags(item0.flags() & ~Qt.ItemIsEditable)
            item0.setToolTip(value.doc)
            self.options_form.setItem(row, 0, item0)
            if key in filtered_options:
                value_to_set = str(filtered_options[key])
            else:
                value_to_set = f"{filtered_default_values[key]} (default)"
            item1 = QTableWidgetItem(value_to_set)
            item1.setToolTip(textwrap.fill(value.doc))
            self.options_form.setItem(row, 1, item1)

        self.options_form.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        self.options_form.setSortingEnabled(True)
        self.options_form.cellChanged.connect(self.options_form_changed)

    def options_form_changed(self, row, column):
        """Change the options form from the widget table"""

        item = self.options_form.item(row, column)

        if column == 0:
            # column 0 is not editable, so this should not be possible
            raise ValueError("Not allowed to change option names")
        else:
            key = self.options_form.item(row, 0).text()

            if item.text() == "":
                # Reset to default
                # Might be better to just keep the old value if nothing is passed, but
                # don't know how to get that
                if key in self.options:
                    del self.options[key]
                return

            self.options[key] = ast.literal_eval(item.text())

        self.update_options_form()

    def search_options_form(self, text):
        """Search for specific options"""

        for i in range(self.options_form.rowCount()):
            row = self.options_form.item(i, 0)

            matches = text.lower() in row.text().lower()
            self.options_form.setRowHidden(i, not matches)

    def select_options_file(self):
        """Choose a Hypnotoad options file to load"""

        filename, _ = QFileDialog.getOpenFileName(self,
                                                  "Open options file",
                                                  ".",
                                                  filter=YAML_FILTER)

        if (filename is None) or (filename == ""):
            return  # Cancelled
        if not os.path.exists(filename):
            self.write("Could not find " + filename)
            return

        self.options_file_line_edit.setText(filename)
        self.filename = filename
        self.read_options()
        self.nonorthogonal_box.setChecked(
            not self.options.get("orthogonal", True))

    def read_options(self):
        """Read the options file"""

        self.statusbar.showMessage("Reading options", 2000)
        options_filename = self.options_file_line_edit.text()

        # Save the existing options in case there is an error loading the options file
        original_options = self.options

        if options_filename:
            with open(options_filename, "r") as f:
                self.options = yaml.safe_load(f)

        possible_options = ([
            opt
            for opt in tokamak.TokamakEquilibrium.user_options_factory.defaults
        ] + [
            opt for opt in tokamak.TokamakEquilibrium.
            nonorthogonal_options_factory.defaults  # noqa: E501
        ] + [opt for opt in BoutMesh.user_options_factory.defaults])
        unused_options = [
            opt for opt in self.options if opt not in possible_options
        ]
        if unused_options != []:
            short_filename = pathlib.Path(options_filename).parts[-1]
            self._popup_error_message(
                f"Error: There were options in the input file that are not used: "
                f"{unused_options}. Cannot load {short_filename}.")
            self.options = original_options

        self.options_form.setRowCount(0)
        self.update_options_form()

    def select_geqdsk_file(self):
        """Choose a "geqdsk" equilibrium file to open"""

        filename, _ = QFileDialog.getOpenFileName(self, "Open geqdsk file",
                                                  ".")

        if (filename is None) or (filename == ""):
            return  # Cancelled
        if not os.path.exists(filename):
            self.write("Could not find " + filename)
            self.geqdsk_file_line_edit.setStyleSheet(
                f"QLineEdit {{ background-color: {COLOURS['red']} }}")
            return

        self.geqdsk_file_line_edit.setText(filename)
        self.geqdsk_file_line_edit.setStyleSheet("")

        self.read_geqdsk()

    def read_geqdsk(self):
        """Read the equilibrium file"""

        self.statusbar.showMessage("Reading geqdsk", 2000)
        geqdsk_filename = self.geqdsk_file_line_edit.text()

        if not os.path.exists(geqdsk_filename):
            self.geqdsk_file_line_edit.setStyleSheet(
                f"QLineEdit {{ background-color : {COLOURS['red']} }}")
            self.statusbar.showMessage(
                f"Could not find equilibrium file '{geqdsk_filename}'")
            return

        try:
            with open(geqdsk_filename, "rt") as f:
                self.eq = tokamak.read_geqdsk(
                    f,
                    settings=copy.deepcopy(self.options),
                    nonorthogonal_settings=copy.deepcopy(self.options),
                )
        except (ValueError, RuntimeError, func_timeout.FunctionTimedOut) as e:
            self._popup_error_message(e)
            return

        self.update_options_form()

        # Delete mesh if it exists, since we have a new self.eq object
        if hasattr(self, "mesh"):
            del self.mesh
        self.regrid_button.setEnabled(False)
        self.action_Regrid.setEnabled(False)

        self.plot_grid()

        self.nonorthogonal_box.setChecked(not self.options["orthogonal"])

    def run(self):
        """Run Hypnotoad and generate the grid"""

        if not hasattr(self, "eq"):
            self.statusbar.showMessage("Missing equilibrium file!")
            self.geqdsk_file_line_edit.setStyleSheet(
                f"QLineEdit {{ background-color: {COLOURS['red']} }}")
            return

        # Call read_geqdsk to recreate self.eq object in case any settings needed in
        # __init__ have been changed
        self.read_geqdsk()

        self.statusbar.showMessage("Running...")
        try:
            self.mesh = BoutMesh(self.eq, self.options)
        except (ValueError, SolutionError, func_timeout.FunctionTimedOut) as e:
            self._popup_error_message(e)
            return

        self.mesh.calculateRZ()
        self.statusbar.showMessage("Done!", 2000)

        self.plot_grid()

        self.write_grid_button.setEnabled(True)
        self.regrid_button.setEnabled(not self.options["orthogonal"])
        self.action_Regrid.setEnabled(not self.options["orthogonal"])

    def set_nonorthogonal(self, state):
        state = bool(state)
        self.options["orthogonal"] = not state
        self.update_options_form()

    def regrid(self):
        """Regrid a nonorthogonal grid after spacing settings are changed"""

        if not hasattr(self, "mesh"):
            self.statusbar.showMessage("Generate grid first!")
            self.geqdsk_file_line_edit.setStyleSheet(
                f"QLineEdit {{ background-color: {COLOURS['red']} }}")
            return

        self.statusbar.showMessage("Running...")

        try:
            self.mesh.redistributePoints(self.options)
            self.mesh.calculateRZ()
        except (ValueError, TypeError, func_timeout.FunctionTimedOut) as e:
            self._popup_error_message(e)
            return

        self.statusbar.showMessage("Done!", 2000)

        self.plot_grid(keep_limits=True)

    def write_grid(self):
        """Write generated mesh to file"""

        # Create all the geometrical quantities
        try:
            self.mesh.geometry()
        except (
                ValueError,
                TypeError,
                func_timeout.FunctionTimedOut,
                SolutionError,
        ) as e:
            self._popup_error_message(e)
            return

        if not hasattr(self, "mesh"):
            flags = QMessageBox.StandardButton.Ok
            QMessageBox.critical(self, "Error",
                                 "Can't write mesh to file; no mesh found!",
                                 flags)
            return

        filename, _ = QFileDialog.getSaveFileName(
            self,
            "Save grid to file",
            self.gui_options["grid_file"],
            filter=NETCDF_FILTER,
        )

        if not filename:
            return

        # If there was no extension, add one, unless the file already exists
        path = pathlib.Path(self.filename)
        if not path.exists() and path.suffix == "":
            self.filename += ".nc"

        self.mesh.writeGridfile(filename)

    def plot_grid(self, *, keep_limits=False):
        self.plot_widget.clear(keep_limits=keep_limits)

        if hasattr(self, "eq"):
            self.eq.plotPotential(ncontours=40, axis=self.plot_widget.axes)
            self.eq.plotWall(axis=self.plot_widget.axes)

        if hasattr(self, "mesh"):
            # mesh exists, so plot the grid points
            self.mesh.plotPoints(
                xlow=self.gui_options["plot_xlow"],
                ylow=self.gui_options["plot_ylow"],
                corners=self.gui_options["plot_corners"],
                ax=self.plot_widget.axes,
            )
        elif hasattr(self, "eq"):
            # no mesh, but do have equilibrium, so plot separatrices
            for region in self.eq.regions.values():
                self.plot_widget.axes.plot(
                    [p.R for p in region.points],
                    [p.Z for p in region.points],
                    marker="o",
                )
            self.plot_widget.axes.plot(*self.eq.x_points[0], "rx")

        self.plot_widget.canvas.draw()

    def _popup_error_message(self, error):
        error_message = QErrorMessage()
        error_message.showMessage(
            str(error) + "<br><br>" + traceback.format_exc())
        error_message.exec_()