Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: gui-extended-example #3555

Merged
merged 12 commits into from
Nov 19, 2024
1 change: 1 addition & 0 deletions doc/changelog.d/3555.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fix: gui-extended-example
Binary file not shown.
250 changes: 73 additions & 177 deletions doc/source/examples/extended_examples/gui/executable.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,163 +6,27 @@ 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:

.. code-block:: python

import sys

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

Application layout
==================

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)
The :download:`gui_app.py <gui_app.py>` script launches a graphical app using PySide6.

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

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

.. image:: final_app_preprocessing.png

The **Preprocessing** tab contains input fields for Poisson's ratio, Young modulus, beam length, and a number of simulation nodes.
The **Postprocessing** tab shows the deformation plot.

.. image:: final_app_postprocessing.png

.. image:: base_app.png

Add a PyVista plotting frame in the window
Add a PyVista plotting frame to the window
==========================================

Start by importing the `QtInteractor <https://qtdocs.pyvista.org/api_reference.html#qtinteractor>`_
class from the ``pyvistaqt`` package and the :class:`MapdlTheme <ansys.mapdl.core.plotting.theme.MapdlTheme>`
class from the ``ansys-mapdl-core`` package:
class from the `pyvistaqt <https://github.com/pyvista/pyvistaqt>`_ package and
the :class:`MapdlTheme <ansys.mapdl.core.plotting.theme.MapdlTheme>`
class from the `ansys-mapdl-core <pymapdl_repo_>`_ package:

.. code:: python

Expand All @@ -177,8 +41,6 @@ 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)
Expand All @@ -190,12 +52,23 @@ Add another plotter on the second tab:
.. code:: python

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)

The plotter can be updated with a PyMAPDL plotter object as follow:

.. code:: python

# Getting PyMAPDL plotter object
nodal_disp_plotter = self._mapdl.post_processing.plot_nodal_displacement(
"norm", show_node_numbering=True, cpos="xy", return_plotter=True
)

# Updating widget
self._postprocessing_plotter.GetRenderWindow().AddRenderer(
nodal_disp_plotter.scene.renderer
)

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

Expand All @@ -209,10 +82,42 @@ Finally, make sure to correctly close the VTK widgets when closing the app:
Launch an MAPDL instance in your window
=======================================

Add an attribute to your MainWindow for the MAPDL instance and import the ``launch_mapdl`` package.
In this example, the MAPDL instance is launched outside the ``MainWindow`` object,
and it passed to it as an argument.

.. code:: python

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

The ``MainWindow`` object stores the :class:`Mapdl <ansys.mapdl.core.mapdl.MapdlBase>` object internally:

.. code:: python

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


Simulation setup
================

The model is built in ``build_model`` method:

.. literalinclude:: gui_app.py
:lines: 189-216

And solved in ``run_solver``:

.. literalinclude:: gui_app.py
:lines: 19, 22-26, 231-236
:lines: 218-246


Develop the logic
=================
Expand All @@ -222,41 +127,32 @@ Connect each button to a function that contains the logic:
.. vale off

.. code-block:: python
:emphasize-lines: 5,14
:emphasize-lines: 5

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)

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

container_layout.addWidget(self._solve_button)
self._solve_button.clicked.connect(self.run_solver)
container_layout.addWidget(self._solve_button, 5, 0, 1, 3)
...

.. vale on

You can now write the related functions:
Run the app
===========

.. literalinclude:: gui_app.py
:lines: 137-223
You can run the app as a normal python script:

.. image:: final_app_preprocessing.png
.. code:: console

.. image:: final_app_solver.png
$ python gui_app.py

.. image:: final_app_postprocessing.png

Additional files
================

The example files can be downloaded using these links:
The example files can be downloaded using this link:

* Original :download:`gui_app.py <gui_app.py>` script
* :download:`gui_app.py <gui_app.py>`: Complete Python script.
* :download:`requirements.txt <requirements.txt>`: Python libraries requirements.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Loading