Create a GUI app in Python with PySide6#

This example shows how to create a graphical user interface (GUI) app in Python that uses PyMAPDL to compute the deflection of a square beam.

Simulation setup#

The following script launches a graphical app using PySide6:

import sys

from PySide6 import QtWidgets
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QApplication,
    QGridLayout,
    QLabel,
    QLineEdit,
    QMainWindow,
    QPushButton,
    QSlider,
    QVBoxLayout,
    QWidget,
)


class MainWindow(QMainWindow):
    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self._setup_ui()

    def _setup_ui(self) -> None:
        # General settings for the window
        self.setWindowTitle("PyMAPDL example application")
        self.resize(1000, 500)
        self._widget = QWidget()
        self._layout = QVBoxLayout()
        self._layout.setContentsMargins(0, 0, 0, 0)

        # Create the tabs
        self._tab_widget = QtWidgets.QTabWidget()

        self._tab_preprocessing = QtWidgets.QWidget()
        self._tab_widget.addTab(self._tab_preprocessing, "Preprocessing")
        self._setup_tab_preprocessing()

        self._tab_solver = QtWidgets.QWidget()
        self._tab_widget.addTab(self._tab_solver, "Solver")
        self._setup_tab_solver()

        self._tab_postprocessing = QtWidgets.QWidget()
        self._tab_widget.addTab(self._tab_postprocessing, "Postprocessing")
        self._setup_tab_postprocessing()

        self._layout.addWidget(self._tab_widget)
        self._widget.setLayout(self._layout)
        self.setCentralWidget(self._widget)

    def _setup_tab_preprocessing(self) -> None:
        container_layout = QGridLayout()
        max_qlineedit_width = 250
        self._tab_preprocessing.setLayout(container_layout)

        # Poisson's ration input
        poisson_ratio_label = QLabel("Poisson's ratio: ")
        container_layout.addWidget(poisson_ratio_label, 0, 0)
        self._poisson_ratio_input = QLineEdit()
        self._poisson_ratio_input.setPlaceholderText("Poisson's ratio (PRXY)")
        self._poisson_ratio_input.setText("0.3")
        self._poisson_ratio_input.setMaximumWidth(max_qlineedit_width)

        # Young modulus input
        young_modulus_label = QLabel("Young's modulus: ")
        container_layout.addWidget(young_modulus_label, 1, 0)
        self._young_modulus_input = QLineEdit()
        self._young_modulus_input.setPlaceholderText(
            "Young's modulus in the x direction"
        )
        self._young_modulus_input.setText("210e3")
        self._young_modulus_input.setMaximumWidth(max_qlineedit_width)

        # beam length input
        length_label = QLabel("Length: ")
        container_layout.addWidget(length_label, 2, 0)
        self._length_input = QLineEdit()
        self._length_input.setPlaceholderText("Length")
        self._length_input.setMaximumWidth(max_qlineedit_width)

        # Input for the force exerced on the beam
        force_label = QLabel("Force: ")
        container_layout.addWidget(force_label, 3, 0)
        self._force_input = QLineEdit()
        self._force_input.setPlaceholderText("Load force")
        self._force_input.setMaximumWidth(max_qlineedit_width)

        # Slider for the number of nodes (between 3 and 9)
        number_of_nodes_label = QLabel("Number of nodes: ")
        container_layout.addWidget(number_of_nodes_label, 4, 0)
        self._number_of_nodes_input = QSlider(orientation=Qt.Orientation.Horizontal)
        self._number_of_nodes_input.setMinimum(3)
        self._number_of_nodes_input.setMaximum(9)
        self._number_of_nodes_input.setValue(5)
        self._number_of_nodes_input.setSingleStep(2)
        self._number_of_nodes_input.setPageStep(2)
        self._number_of_nodes_input.setMaximumWidth(max_qlineedit_width - 50)
        self._number_of_node_label = QLabel(
            f"{self._number_of_nodes_input.value()} nodes"
        )
        self._number_of_nodes_input.valueChanged.connect(
            lambda _: self._number_of_node_label.setText(
                f"{self._number_of_nodes_input.value()} nodes"
            )
        )

        # Button to run the preprocessor
        self._run_preprocessor_button = QPushButton(text="Run preprocessor")

        container_layout.addWidget(self._poisson_ratio_input, 0, 1, 1, 2)
        container_layout.addWidget(self._young_modulus_input, 1, 1, 1, 2)
        container_layout.addWidget(self._length_input, 2, 1, 1, 2)
        container_layout.addWidget(self._force_input, 3, 1, 1, 2)
        container_layout.addWidget(self._number_of_nodes_input, 4, 1, 1, 1)
        container_layout.addWidget(self._number_of_node_label, 4, 2, 1, 1)
        container_layout.addWidget(self._run_preprocessor_button, 5, 0, 1, 3)

    def _setup_tab_solver(self) -> None:
        container_layout = QGridLayout()
        self._tab_solver.setLayout(container_layout)

        # Button to run the solver
        self._solve_button = QPushButton(text="Solve")

        container_layout.addWidget(self._solve_button)

    def _setup_tab_postprocessing(self) -> None:
        container_layout = QtWidgets.QVBoxLayout()
        self._tab_postprocessing.setLayout(container_layout)
        self._deflection_label = QLabel("Deflection: ")
        container_layout.addWidget(self._deflection_label)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

The Preprocessing tab contains input fields for Poisson’s ratio, Young modulus, beam length, and a number of simulation nodes.

../../../_images/base_app.png

Add a PyVista plotting frame in the window#

Start by importing the QtInteractor class from the pyvistaqt package and the MapdlTheme class from the ansys-mapdl-core package:

from pyvistaqt import QtInteractor
from ansys.mapdl.core import MapdlTheme

Then, add a plotter on the first tab:

def _setup_tab_preprocessing(self) -> None:
    ...
    container_layout.addWidget(self._run_preprocessor_button, 5, 0, 1, 3)

    # PyVista frame in the window
    self._preprocessing_plotter = QtInteractor(theme=MapdlTheme())
    container_layout.addWidget(self._preprocessing_plotter, 0, 4, 6, 50)

Add another plotter on the second tab:

def _setup_tab_postprocessing(self) -> None:
    container_layout = QtWidgets.QVBoxLayout()
    self._tab_postprocessing.setLayout(container_layout)
    self._postprocessing_plotter = QtInteractor(theme=MapdlTheme())
    container_layout.addWidget(self._postprocessing_plotter)
    self._deflection_label = QLabel("Deflection: ")
    container_layout.addWidget(self._deflection_label)

Finally, make sure to correctly close the VTK widgets when closing the app:

def closeEvent(self, event) -> None:
    self._preprocessing_plotter.close()
    self._postprocessing_plotter.close()
    event.accept()  # let the window close

Launch an MAPDL instance in your window#

Add an attribute to your MainWindow for the MAPDL instance and import the launch_mapdl package.

# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

import sys

from PySide6 import QtWidgets
from PySide6.QtCore import Qt
        self._mapdl.slashsolu()
        self._mapdl.solve()
        self._mapdl.finish()

        # run postprocessing
        self._mapdl.post1()

Develop the logic#

Connect each button to a function that contains the logic:

def _setup_tab_preprocessing(self) -> None:
    ...
    # Button to run the preprocessor
    self._run_preprocessor_button = QPushButton(text="Run preprocessor")
    self._run_preprocessor_button.clicked.connect(self._run_preprocessor)
    ...


def _setup_tab_solver(self) -> None:
    container_layout = QGridLayout()
    self._tab_solver.setLayout(container_layout)

    self._solve_button = QPushButton(text="Solve")
    self._solve_button.clicked.connect(self._run_solver)

    container_layout.addWidget(self._solve_button)

You can now write the related functions:

        container_layout.addWidget(self._poisson_ratio_input, 0, 1, 1, 2)
        container_layout.addWidget(self._young_modulus_input, 1, 1, 1, 2)
        container_layout.addWidget(self._length_input, 2, 1, 1, 2)
        container_layout.addWidget(self._force_input, 3, 1, 1, 2)
        container_layout.addWidget(self._number_of_nodes_input, 4, 1, 1, 1)
        container_layout.addWidget(self._number_of_node_label, 4, 2, 1, 1)
        container_layout.addWidget(self._run_preprocessor_button, 5, 0, 1, 3)

        # PyVista frame in the window
        self._preprocessing_plotter = QtInteractor(theme=MapdlTheme())
        container_layout.addWidget(self._preprocessing_plotter, 0, 4, 6, 50)

    def _setup_tab_solver(self) -> None:
        container_layout = QGridLayout()
        self._tab_solver.setLayout(container_layout)

        # Button to run the solver
        self._solve_button = QPushButton(text="Solve")
        self._solve_button.clicked.connect(self._run_solver)

        container_layout.addWidget(self._solve_button)

    def _setup_tab_postprocessing(self) -> None:
        container_layout = QtWidgets.QVBoxLayout()
        self._tab_postprocessing.setLayout(container_layout)
        # Add a PyVista frame
        self._postprocessing_plotter = QtInteractor(theme=MapdlTheme())
        container_layout.addWidget(self._postprocessing_plotter)
        self._deflection_label = QLabel("Deflection: ")
        container_layout.addWidget(self._deflection_label)

    def _run_preprocessor(self) -> None:
        try:
            poisson_ratio = float(self._poisson_ratio_input.text())
            young_modulus = float(self._young_modulus_input.text())
            length = float(self._length_input.text())
            force = float(self._force_input.text())
        except Exception:
            msg_box = QMessageBox()
            msg_box.setText("Expecting a number")
            msg_box.exec()
            return

        poisson_ratio = float(self._poisson_ratio_input.text())
        young_modulus = float(self._young_modulus_input.text())
        length = float(self._length_input.text())
        force = float(self._force_input.text())

        self._mapdl.clear()
        self._mapdl.verify()
        self._mapdl.prep7()
        self._mapdl.antype("STATIC")
        # create element type
        self._mapdl.et(1, "BEAM188")

        # Create material
        self._mapdl.mp("PRXY", 1, poisson_ratio)
        self._mapdl.mp("EX", 1, young_modulus)
        self._mapdl.sectype(1, "BEAM", "RECT")
        self._mapdl.secdata("10", "10")

        self._number_of_nodes = self._number_of_nodes_input.value()

        # Create the nodes
        for node_num in range(1, self._number_of_nodes + 1):
            self._mapdl.n(
                node_num, (node_num - 1) * length / (self._number_of_nodes - 1), 0, 0
            )

        # Create the elements
        for elem_num in range(1, self._number_of_nodes):
            self._mapdl.e(elem_num, elem_num + 1)

        # Fix beam ends
        self._mapdl.d(1, lab="ALL")
        self._mapdl.d(self._number_of_nodes, lab="ALL")

        #  Apply the force to the node in the middle
        self._mapdl.f(self._number_of_nodes // 2 + 1, lab="FY", value=force)

        # Get the pv.Plotter object from mapdl.eplot function
        # to plot in the window
        preprocessing_plotter = self._mapdl.eplot(
            show_node_numbering=True, show_edges=True, cpos="xy", return_plotter=True
        )

        self._preprocessing_plotter.GetRenderWindow().AddRenderer(
../../../_images/final_app_preprocessing.png ../../../_images/final_app_solver.png ../../../_images/final_app_postprocessing.png

Additional files#

The example files can be downloaded using these links: