diff --git a/DevDoc.md b/DevDoc.md new file mode 100644 index 0000000..324fe5d --- /dev/null +++ b/DevDoc.md @@ -0,0 +1,57 @@ +# Developer Documentation + +## Files and their purpose. + +- GUIMainWindow.py + + This file contains the code responsible for rendering the main Window. + +- preferences.py + + This file contains a list of variables that can be changed by the user as they prefer. For example plot line colors, line styles, default plot limits etc. + +- profoil_canvas.py + + canvas is the container which contains all the plots. main data structure is just a simple matplotlib figure. when binding in to the pyqt main window this will be casted from matplotlib.figure.Figure in to matplotlib.backends.backend_qt5agg.FigureCanvasQTAgg + +- profoil_interface.py + + Profoil_interface provides list of functions to extract information from text files generated on each PROFOIL run. extract_all_data() function is particularly interesting as it handles all the heavy lifting and returns all the information back into profoil_ui at once. + +- profoil_ui.py + + profoil_ui is the entry point of the program. Initializing, Subclassing the classes from previously listed files, and setting up the program happens here. + +## Class Hierarchy and WorkFlow + +As shown below, profoil UI inherits from multiple classes. UI_MainWindow is the class responsible for generating the main window. Profoil_UI extends the base class with call back methods for button clicks etc. ProfoilCanvas is the main figure canvas where all the graphs being plotted. + +Additionally Profoil_UI inherits from QtWidgets::QMainWindow class to return properly typed instance when selecting a file through file explorer window. The cleanest way probably to do this is to extend UI_MainWindow from QMainWindow but that makes it difficult to make changes to the GUI.ui file later on as outlined in the next paragraph. To keep the sanity of completly isolated GUI, multiple inheritance path is chosen here. + +#### + +### Isolation of GUI components + +The file GUIMainWindow.py is a computer generated file. This file is being generated by pyuic5 consuming the GUI.ui file. GUI.ui is an xml file outlining the layout of the buttons menu etc and its been generated by QTDesigner. + +So for any modificaitons with regards to the easthetics, layout, colors fonts etc one can easily modify the provided GUI.ui file and generate GUIMainWindow.py by running below command. Once done without manually modifying any code, you should be able to run profoil_ui. + +```bash +pyuic5 GUI.ui -o GUIMainWindow.py +``` + +## Data Flow inside the application + +As outlined in the user manual after each run of profoil new set of files will be generated by PROFOIL. profoil_interface.py is the module that interfaces with these files mainly. extract_all_data() function in this module extracts all the relevent information from these files. Non-dimensionalized velocity, nu markers and airfoil contuour is directly been plotted in to matplotlib axis as per the standard. + +But nu-alpha plots are done differently because of the requirement to facilitate surface switching. Approach I've taken is to plot nu_alpha related data in to matplotlib Line2D objects which act as suitable "containers" rather than plotting them directly on to a graph. When user switches the surface these Line2D objects will be placed on the actual matplotlib axis. More information can be found in plot_ue(), plot_xy() and plot_nu_alfa() functions. surface switching is happening on select_surface() function. + + + +Most of the details outlining the implementations are inserted to file headers as well for easy reference. Also doxygen generated callflow charts are inserted as a quick reference. But use it with cautios because as the project progresses documentation could be outdated. + +## Contributing to the documentation + +Documentation files (DevDoc and UserManual) are first written in markdown. I used MarkText for this and the files were later been converted to HTML format for offline use. So please use the same workflow so that it ensures both .md and .html files are in sync. + + diff --git a/GUI.ui b/GUI.ui new file mode 100644 index 0000000..aee00c7 --- /dev/null +++ b/GUI.ui @@ -0,0 +1,420 @@ + + + MainWindow + + + + 0 + 0 + 1320 + 775 + + + + + 1320 + 775 + + + + PROFOIL-UI + + + + + + + + 8 + + + + 0 + + + + Design View + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + Grid + + + true + + + + + + + History + + + true + + + + + + + + + + + Surface + + + + + + + + Upper + + + + + Lower + + + + + + + + + + + 8 + + + + + + + Start Edits + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 8 + + + + Cancel + + + + + + + + + + 8 + + + + Apply Edits + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 8 + + + + Undo + + + + + + + + + + 8 + + + + Plot From File + + + + + + + + 8 + + + + background-color:yellowgreen; + + + Run Profoil + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 8 + + + + Revert + + + + + + + + + + Courier + 8 + + + + <html><head/><body><p><br/></p></body></html> + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + File View + + + + + + + Courier + 8 + + + + true + + + + + + + + + + + Courier + 8 + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save + + + + + + + + + Profoil.in + + + + + + + Profoil.log + + + + + + + + + + + + + 0 + 0 + 1320 + 18 + + + + + File + + + + + + + Overlay + + + + + + + + + + + + Open + + + + + Save + + + + + Geometry from *.xy file (no header line) + + + + + Clear Overlay + + + + + Geometry from *.dat file (1 header line) + + + + + MSES *.dat File + + + + + + diff --git a/GUIMainWindow.py b/GUIMainWindow.py new file mode 100644 index 0000000..2c3444e --- /dev/null +++ b/GUIMainWindow.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'GUI.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(1320, 775) + MainWindow.setMinimumSize(QtCore.QSize(1320, 775)) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget) + self.horizontalLayout.setObjectName("horizontalLayout") + self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) + font = QtGui.QFont() + font.setPointSize(8) + self.tabWidget.setFont(font) + self.tabWidget.setObjectName("tabWidget") + self.widget = QtWidgets.QWidget() + self.widget.setObjectName("widget") + self.horizontalLayout_10 = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout_10.setObjectName("horizontalLayout_10") + self.verticalLayout_canvas = QtWidgets.QVBoxLayout() + self.verticalLayout_canvas.setObjectName("verticalLayout_canvas") + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.verticalLayout_canvas.addItem(spacerItem) + self.horizontalLayout_10.addLayout(self.verticalLayout_canvas) + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.checkBox_grid = QtWidgets.QCheckBox(self.widget) + self.checkBox_grid.setChecked(True) + self.checkBox_grid.setObjectName("checkBox_grid") + self.horizontalLayout_3.addWidget(self.checkBox_grid) + self.checkBox_history = QtWidgets.QCheckBox(self.widget) + self.checkBox_history.setChecked(True) + self.checkBox_history.setObjectName("checkBox_history") + self.horizontalLayout_3.addWidget(self.checkBox_history) + self.verticalLayout.addLayout(self.horizontalLayout_3) + self.horizontalLayout_9 = QtWidgets.QHBoxLayout() + self.horizontalLayout_9.setObjectName("horizontalLayout_9") + self.label_4 = QtWidgets.QLabel(self.widget) + self.label_4.setObjectName("label_4") + self.horizontalLayout_9.addWidget(self.label_4) + self.combo_switch_surface = QtWidgets.QComboBox(self.widget) + self.combo_switch_surface.setObjectName("combo_switch_surface") + self.combo_switch_surface.addItem("") + self.combo_switch_surface.addItem("") + self.horizontalLayout_9.addWidget(self.combo_switch_surface) + self.horizontalLayout_9.setStretch(0, 1) + self.horizontalLayout_9.setStretch(1, 3) + self.verticalLayout.addLayout(self.horizontalLayout_9) + self.btn_start_edits = QtWidgets.QPushButton(self.widget) + font = QtGui.QFont() + font.setPointSize(8) + self.btn_start_edits.setFont(font) + self.btn_start_edits.setStyleSheet("") + self.btn_start_edits.setObjectName("btn_start_edits") + self.verticalLayout.addWidget(self.btn_start_edits) + self.horizontalLayout_8 = QtWidgets.QHBoxLayout() + self.horizontalLayout_8.setObjectName("horizontalLayout_8") + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_8.addItem(spacerItem1) + self.btn_cancel = QtWidgets.QPushButton(self.widget) + font = QtGui.QFont() + font.setPointSize(8) + self.btn_cancel.setFont(font) + self.btn_cancel.setObjectName("btn_cancel") + self.horizontalLayout_8.addWidget(self.btn_cancel) + self.verticalLayout.addLayout(self.horizontalLayout_8) + self.btn_apply_edits = QtWidgets.QPushButton(self.widget) + font = QtGui.QFont() + font.setPointSize(8) + self.btn_apply_edits.setFont(font) + self.btn_apply_edits.setObjectName("btn_apply_edits") + self.verticalLayout.addWidget(self.btn_apply_edits) + self.horizontalLayout_6 = QtWidgets.QHBoxLayout() + self.horizontalLayout_6.setObjectName("horizontalLayout_6") + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_6.addItem(spacerItem2) + self.btn_undo = QtWidgets.QPushButton(self.widget) + font = QtGui.QFont() + font.setPointSize(8) + self.btn_undo.setFont(font) + self.btn_undo.setObjectName("btn_undo") + self.horizontalLayout_6.addWidget(self.btn_undo) + self.verticalLayout.addLayout(self.horizontalLayout_6) + self.btn_plot_from_file = QtWidgets.QPushButton(self.widget) + font = QtGui.QFont() + font.setPointSize(8) + self.btn_plot_from_file.setFont(font) + self.btn_plot_from_file.setObjectName("btn_plot_from_file") + self.verticalLayout.addWidget(self.btn_plot_from_file) + self.btn_run_profoil = QtWidgets.QPushButton(self.widget) + font = QtGui.QFont() + font.setPointSize(8) + self.btn_run_profoil.setFont(font) + self.btn_run_profoil.setStyleSheet("background-color:yellowgreen;") + self.btn_run_profoil.setObjectName("btn_run_profoil") + self.verticalLayout.addWidget(self.btn_run_profoil) + self.horizontalLayout_7 = QtWidgets.QHBoxLayout() + self.horizontalLayout_7.setObjectName("horizontalLayout_7") + spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_7.addItem(spacerItem3) + self.btn_revert = QtWidgets.QPushButton(self.widget) + font = QtGui.QFont() + font.setPointSize(8) + self.btn_revert.setFont(font) + self.btn_revert.setObjectName("btn_revert") + self.horizontalLayout_7.addWidget(self.btn_revert) + self.verticalLayout.addLayout(self.horizontalLayout_7) + self.lbl_summary = QtWidgets.QLabel(self.widget) + font = QtGui.QFont() + font.setFamily("Courier") + font.setPointSize(8) + self.lbl_summary.setFont(font) + self.lbl_summary.setWordWrap(True) + self.lbl_summary.setObjectName("lbl_summary") + self.verticalLayout.addWidget(self.lbl_summary) + spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem4) + self.horizontalLayout_10.addLayout(self.verticalLayout) + self.horizontalLayout_10.setStretch(0, 6) + self.horizontalLayout_10.setStretch(1, 1) + self.tabWidget.addTab(self.widget, "") + self.widget1 = QtWidgets.QWidget() + self.widget1.setObjectName("widget1") + self.gridLayout = QtWidgets.QGridLayout(self.widget1) + self.gridLayout.setObjectName("gridLayout") + self.plainTextEdit_profoil_log = QtWidgets.QPlainTextEdit(self.widget1) + font = QtGui.QFont() + font.setFamily("Courier") + font.setPointSize(8) + self.plainTextEdit_profoil_log.setFont(font) + self.plainTextEdit_profoil_log.setReadOnly(True) + self.plainTextEdit_profoil_log.setPlainText("") + self.plainTextEdit_profoil_log.setObjectName("plainTextEdit_profoil_log") + self.gridLayout.addWidget(self.plainTextEdit_profoil_log, 1, 1, 1, 1) + self.plainTextEdit_profoil_in = QtWidgets.QPlainTextEdit(self.widget1) + font = QtGui.QFont() + font.setFamily("Courier") + font.setPointSize(8) + self.plainTextEdit_profoil_in.setFont(font) + self.plainTextEdit_profoil_in.setPlainText("") + self.plainTextEdit_profoil_in.setObjectName("plainTextEdit_profoil_in") + self.gridLayout.addWidget(self.plainTextEdit_profoil_in, 1, 0, 1, 1) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem5) + self.btn_save_profoil_in = QtWidgets.QPushButton(self.widget1) + self.btn_save_profoil_in.setObjectName("btn_save_profoil_in") + self.horizontalLayout_2.addWidget(self.btn_save_profoil_in) + self.gridLayout.addLayout(self.horizontalLayout_2, 2, 0, 1, 1) + self.label = QtWidgets.QLabel(self.widget1) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 1) + self.label_2 = QtWidgets.QLabel(self.widget1) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 0, 1, 1, 1) + self.tabWidget.addTab(self.widget1, "") + self.horizontalLayout.addWidget(self.tabWidget) + self.horizontalLayout.setStretch(0, 4) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1320, 18)) + self.menubar.setObjectName("menubar") + self.menuFile = QtWidgets.QMenu(self.menubar) + self.menuFile.setObjectName("menuFile") + self.menuOverlay = QtWidgets.QMenu(self.menubar) + self.menuOverlay.setObjectName("menuOverlay") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + self.actionOpen = QtWidgets.QAction(MainWindow) + self.actionOpen.setObjectName("actionOpen") + self.actionSave = QtWidgets.QAction(MainWindow) + self.actionSave.setObjectName("actionSave") + self.actionProfoil_dat_File = QtWidgets.QAction(MainWindow) + self.actionProfoil_dat_File.setObjectName("actionProfoil_dat_File") + self.actionClear_Overlay = QtWidgets.QAction(MainWindow) + self.actionClear_Overlay.setObjectName("actionClear_Overlay") + self.actionXFoil_dat_File = QtWidgets.QAction(MainWindow) + self.actionXFoil_dat_File.setObjectName("actionXFoil_dat_File") + self.actionMSES_dat_File = QtWidgets.QAction(MainWindow) + self.actionMSES_dat_File.setObjectName("actionMSES_dat_File") + self.menuFile.addAction(self.actionOpen) + self.menuFile.addAction(self.actionSave) + self.menuOverlay.addAction(self.actionProfoil_dat_File) + self.menuOverlay.addAction(self.actionXFoil_dat_File) + self.menuOverlay.addAction(self.actionClear_Overlay) + self.menubar.addAction(self.menuFile.menuAction()) + self.menubar.addAction(self.menuOverlay.menuAction()) + + self.retranslateUi(MainWindow) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "PROFOIL-UI")) + self.checkBox_grid.setText(_translate("MainWindow", "Grid")) + self.checkBox_history.setText(_translate("MainWindow", "History")) + self.label_4.setText(_translate("MainWindow", "Surface")) + self.combo_switch_surface.setItemText(0, _translate("MainWindow", "Upper")) + self.combo_switch_surface.setItemText(1, _translate("MainWindow", "Lower")) + self.btn_start_edits.setText(_translate("MainWindow", "Start Edits")) + self.btn_cancel.setText(_translate("MainWindow", "Cancel")) + self.btn_apply_edits.setText(_translate("MainWindow", "Apply Edits")) + self.btn_undo.setText(_translate("MainWindow", "Undo")) + self.btn_plot_from_file.setText(_translate("MainWindow", "Plot From File")) + self.btn_run_profoil.setText(_translate("MainWindow", "Run Profoil")) + self.btn_revert.setText(_translate("MainWindow", "Revert")) + self.lbl_summary.setText(_translate("MainWindow", "


")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.widget), _translate("MainWindow", "Design View")) + self.btn_save_profoil_in.setText(_translate("MainWindow", "Save")) + self.label.setText(_translate("MainWindow", "Profoil.in")) + self.label_2.setText(_translate("MainWindow", "Profoil.log")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.widget1), _translate("MainWindow", "File View")) + self.menuFile.setTitle(_translate("MainWindow", "File")) + self.menuOverlay.setTitle(_translate("MainWindow", "Overlay")) + self.actionOpen.setText(_translate("MainWindow", "Open")) + self.actionSave.setText(_translate("MainWindow", "Save")) + self.actionProfoil_dat_File.setText(_translate("MainWindow", "Geometry from *.xy file (no header line)")) + self.actionClear_Overlay.setText(_translate("MainWindow", "Clear Overlay")) + self.actionXFoil_dat_File.setText(_translate("MainWindow", "Geometry from *.dat file (1 header line)")) + self.actionMSES_dat_File.setText(_translate("MainWindow", "MSES *.dat File")) diff --git a/Manual.html b/Manual.html index c78b2f4..bd2a200 100644 --- a/Manual.html +++ b/Manual.html @@ -1,223 +1,309 @@ - + README

PROFOIL-UI

+.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #9a6e3a; + /* This background color was intended by the author of this theme. */ + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function, +.token.class-name { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +/*# sourceURL=webpack://./node_modules/prismjs/themes/prism.css */ +/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8uL25vZGVfbW9kdWxlcy9wcmlzbWpzL3RoZW1lcy9wcmlzbS5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7RUFJRTs7QUFFRjs7Q0FFQyxZQUFZO0NBQ1osZ0JBQWdCO0NBQ2hCLHdCQUF3QjtDQUN4QixzRUFBc0U7Q0FDdEUsY0FBYztDQUNkLGdCQUFnQjtDQUNoQixnQkFBZ0I7Q0FDaEIsb0JBQW9CO0NBQ3BCLGtCQUFrQjtDQUNsQixpQkFBaUI7Q0FDakIsZ0JBQWdCOztDQUVoQixnQkFBZ0I7Q0FDaEIsY0FBYztDQUNkLFdBQVc7O0NBRVgscUJBQXFCO0NBQ3JCLGtCQUFrQjtDQUNsQixpQkFBaUI7Q0FDakIsYUFBYTtBQUNkOztBQUVBOztDQUVDLGlCQUFpQjtDQUNqQixtQkFBbUI7QUFDcEI7O0FBRUE7O0NBRUMsaUJBQWlCO0NBQ2pCLG1CQUFtQjtBQUNwQjs7QUFFQTtDQUNDOztFQUVDLGlCQUFpQjtDQUNsQjtBQUNEOztBQUVBLGdCQUFnQjtBQUNoQjtDQUNDLFlBQVk7Q0FDWixjQUFjO0NBQ2QsY0FBYztBQUNmOztBQUVBOztDQUVDLG1CQUFtQjtBQUNwQjs7QUFFQSxnQkFBZ0I7QUFDaEI7Q0FDQyxhQUFhO0NBQ2IsbUJBQW1CO0NBQ25CLG1CQUFtQjtBQUNwQjs7QUFFQTs7OztDQUlDLGdCQUFnQjtBQUNqQjs7QUFFQTtDQUNDLFdBQVc7QUFDWjs7QUFFQTtDQUNDLFdBQVc7QUFDWjs7QUFFQTs7Ozs7OztDQU9DLFdBQVc7QUFDWjs7QUFFQTs7Ozs7O0NBTUMsV0FBVztBQUNaOztBQUVBOzs7OztDQUtDLGNBQWM7Q0FDZCxvRUFBb0U7Q0FDcEUsaUNBQWlDO0FBQ2xDOztBQUVBOzs7Q0FHQyxXQUFXO0FBQ1o7O0FBRUE7O0NBRUMsY0FBYztBQUNmOztBQUVBOzs7Q0FHQyxXQUFXO0FBQ1o7O0FBRUE7O0NBRUMsaUJBQWlCO0FBQ2xCO0FBQ0E7Q0FDQyxrQkFBa0I7QUFDbkI7O0FBRUE7Q0FDQyxZQUFZO0FBQ2IiLCJzb3VyY2VSb290IjoiIn0= */ + + + + + + + +

PROFOIL-UI

A Python based graphical user interface for the multi-point inverse airfoil design code PROFOIL.

-

What is PROFOIL-UI

+

What is PROFOIL-UI

PROFOIL-UI is a minimalistic GUI front-end for PROFOIL. In PROFOIL, an airfoil is chiefly defined by its α*(ϕ) distribution. The new PROFOIL-UI can be used to edit an α*(ϕ) distribution and thereby define a new airfoil through iteration. Mouse cursor edits to the α*(ϕ) distribution are made through the UI. The UI can be used to display both the current and prior airfoil to better examine how the airfoil evolves iteratively through the design process.

The diagrams below illustrate the typical PROFOIL work-flow along with the PROFOIL-UI work-flow to show how the two programs fit together.

-

PROFOIL Work-flow

-

PROFOIL-UI Work-flow

+

PROFOIL Work-flow

+

PROFOIL-UI Work-flow

Note that the interfacing between PROFOIL and PROFOIL-UI is done via text files. Users may chose to edit the α*(ϕ) distribution via cursor edits while making other possible modifications into the profoil.in file as indicated by the dotted line in the figure above. (For example adding a new FOIL line or modifying NEWT* lines).

-

Pre-requisites / Assumptions

+

Pre-requisites / Assumptions

Users are assumed to have some basic theoretical understanding of the multi-point airfoil design methodology proposed by Eppler, Selig et al. Some exposure to PROFOILs functionality and keywords will certainly be helpful. Also an error free profoil.in file is required to start a session.

-

Installation

+

Installation

If you have a Python development environment like Anaconda and PyQt5 already installed, it should be sufficient to run PROFOIL-UI straight away. Otherwise a new Python environment can be created, and all the dependencies can be installed via:

-
pip install -r requirements.txt
-
- +
pip install -r requirements.txt

The necessary folders and files for a PROFOIL install are shown below (graphics files in ./doc_media are not shown).

-

Directory Structure

-

Starting PROFOIL-UI

+

Directory Structure

+

Starting PROFOIL-UI

Per above, in a typical distribution, PROFOIL-UI is conveniently placed in the ./ui sub-folder. It can either be run by double clicking PROFOIL-UI.bat file or triggering a Python shell by navigating to the ./ui folder and typing the line below into the Windows command prompt.

-
python profoil_ui.py
-
- -

Loading an Airfoil

+
python profoil_ui.py
+

Loading an Airfoil

Upon starting, users can load an airfoil by choosing the input file (a valid PROFOIL .in file) through the File menu. The installation includes the sample files in the ./runs folder. Choosing one of the samples, e.g. ../runs/examples/example1.in will load the file into the program to run the executable PROFOIL (./bin/profoil.exe). Below screen-shot shows the initial plot followed by descriptions on each pane and widget.

-

Initial Plot

+

Initial Plot

Velocity Distribution

This graph shows the non-dimensionalized velocity distribution over the airfoil surface. Each line corresponds to each angle of attack specified in the .in file through the ALFASP line. Each velocity distribution curve has upward and downward pointing triangles (markers) to indicate the segment ϕ values on upper and lower surfaces respectively. Note that the triangles on upper and lower surfaces near the trailing edge indicate the upper and lower ϕS closure arc limits.

Airfoil Shape

@@ -1100,29 +1278,20 @@

Loading an AirfoilRunning

+

Upper and Lower surface selector
Selects the active surface for α*(ϕ) cursor edits. Users may choose to switch back and forth during an edit.

+

Grid Check Box
Toggles the grid on the α*(ϕ) distribution

+

History Check Box
Toggles the visibility of the plots corresponding to the previous case mainly used for reference purposes.

+

Running

Once the airfoil is first loaded and initial plots were made, subsequent changes can be done to the airfoil velocity distribution using the button/mouse actions described below.

-

Buttons are arranged in pairs such that one main action is followed by a possible reverse action for easy reference. For example “Start Edits” button immediately followed by “Cancel” meaning any edits which did not turn out to be as expected can be discarded. The same logic is applicable for “Apply Edits” -> “Undo” and “Run Profoil” -> “Revert” button pairs.

-

Start Edits Button
-This button initiates cursor edit functionality. “Edit mode” will be activated once clicked and it will be toggled on the subsequent clicks. Active “Edit Mode” is indicated by red text on the button. Once in “Edit mode”, successive left mouse button clicks on the α*(ϕ) graph introduces a new red cursor edit line. This cursor edit line prescribes the altered α*(ϕ) distribution which will later be applied on to the existing α*(ϕ) distribution. Please note that this new α*(ϕ) distribution can be introduced by clicking anywhere in the α*(ϕ) graph in any order and does not have to span through the complete original distribution. For example, if a section of the airfoil to be altered is ranging between 15<ϕ<25, this red cursor edit line does not have to go beyond those segment limits.

-

Cursor Edit

-

Cancel Button
-Cancels the cursor edit lines introduced in the previous step (red curve).

-

Apply Edits Button
-Previously introduced α*(ϕ) distribution indicated by the red cursor edit line, will be applied to the current α*(ϕ) distribution shown in green by triggering this button. Upon triggering, the cursor edit α*(ϕ) distribution will be linearly interpolated and applied, such that the α* values of the existing green curve will be altered if the corresponding ϕ values falls within the limits of the edits. (This is way easier to understand in practice than it sounds). The modified α*(ϕ) distribution will then be saved to profoil.in file making the program ready to run PROFOIL. Before each save, backup file of profoil.in will be generated as buffer.in in ./work directory.

-

Apply Edits

-

Undo Button
-Undo Edits button is used to undo all the changes that were introduced to the α*(ϕ) distribution in the “Apply Edits” step BEFORE running PROFOIL.

-

Plot From File Button
-As indicated above in the PROFOIL-UI work flow, changes can be done to the profoil.in file manually as required. These changes could generally be in any form including addition of FOIL lines. Before running PROFOIL, these newly introduced FOIL lines could be inspected prior to running PROFOIL using this button.

-

Run PROFOIL Button
-The “Run Profoil” button runs PROFOIL as the name implies. +

Buttons are arranged in pairs such that one main action is followed by a possible reverse action for easy reference. For example "Start Edits" button immediately followed by "Cancel" meaning any edits which did not turn out to be as expected can be discarded. The same logic is applicable for "Apply Edits" -> "Undo" and "Run Profoil" -> "Revert" button pairs.

+

Start Edits Button
This button initiates cursor edit functionality. "Edit mode" will be activated once clicked and it will be toggled on the subsequent clicks. Active "Edit Mode" is indicated by red text on the button. Once in "Edit mode", successive left mouse button clicks on the α*(ϕ) graph introduces a new red cursor edit line. This cursor edit line prescribes the altered α*(ϕ) distribution which will later be applied on to the existing α*(ϕ) distribution. Please note that this new α*(ϕ) distribution can be introduced by clicking anywhere in the α*(ϕ) graph in any order and does not have to span through the complete original distribution. For example, if a section of the airfoil to be altered is ranging between 15<ϕ<25, this red cursor edit line does not have to go beyond those segment limits.

+

Cursor Edit

+

Cancel Button
Cancels the cursor edit lines introduced in the previous step (red curve).

+

Apply Edits Button
Previously introduced α*(ϕ) distribution indicated by the red cursor edit line, will be applied to the current α*(ϕ) distribution shown in green by triggering this button. Upon triggering, the cursor edit α*(ϕ) distribution will be linearly interpolated and applied, such that the α* values of the existing green curve will be altered if the corresponding ϕ values falls within the limits of the edits. (This is way easier to understand in practice than it sounds). The modified α*(ϕ) distribution will then be saved to profoil.in file making the program ready to run PROFOIL. Before each save, backup file of profoil.in will be generated as buffer.in in ./work directory.

+

Apply Edits

+

Undo Button
Undo Edits button is used to undo all the changes that were introduced to the α*(ϕ) distribution in the "Apply Edits" step BEFORE running PROFOIL.

+

Plot From File Button
As indicated above in the PROFOIL-UI work flow, changes can be done to the profoil.in file manually as required. These changes could generally be in any form including addition of FOIL lines. Before running PROFOIL, these newly introduced FOIL lines could be inspected prior to running PROFOIL using this button.

+

Run PROFOIL Button
The "Run Profoil" button runs PROFOIL as the name implies. This button operates by taking the following actions under the hood.

\ No newline at end of file +SOFTWARE. +
+ + \ No newline at end of file diff --git a/README.md b/README.md index ea73a41..9792d8f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # PROFOIL-UI -A Python based graphical user interface for the generalized multi-point inverse airfoil design code [PROFOIL](https://github.com/m-selig/PROFOIL). + +A Python based graphical user interface for the multi-point inverse airfoil design code PROFOIL. ## What is PROFOIL-UI @@ -19,43 +20,15 @@ Users are assumed to have some basic theoretical understanding of the multi-poin ## Installation -Assuming PROFOIL is already been installed, PROFOIL-UI installation could be completed by downloading the files in to the "ui" folder in the PROFOIL installation. If you have a Python development environment like Anaconda and PyQt5 already installed, it should be sufficient to run PROFOIL-UI straight away. Otherwise a new Python environment can be created, and all the dependencies can be installed via: +If you have a Python development environment like Anaconda and PyQt5 already installed, it should be sufficient to run PROFOIL-UI straight away. Otherwise a new Python environment can be created, and all the dependencies can be installed via: ```sh pip install -r requirements.txt ``` -The necessary folders and files for a PROFOIL install are shown below (png files in **./doc_media** and .in files in the **./runs/examples** folder are denoted by * to indicate multiple files). - -``` -. -├── bin -│   └── profoil.exe -├── docs -│   └── PROFOIL-User-Guide-2022.pdf -├── license -│   └── license.txt -├── runs -│   └── examples -│   ├── example1.in -│   ├── example2.in -│   └── *.in -├── ui -│   ├── Manual.html -│   ├── PROFOIL-UI.bat -│   ├── README.md -│   ├── README.txt -│   ├── doc_media -│   │   └── *.png -│   ├── preferences.py -│   ├── profoil_canvas.py -│   ├── profoil_interface.py -│   ├── profoil_ui.py -│   ├── pyqt_designer.ui -│   └── requirements.txt -└── work -``` +The necessary folders and files for a PROFOIL install are shown below (graphics files in **./doc_media** are not shown). +![Directory Structure](./doc_media/3_install-tree.png) ## Starting PROFOIL-UI @@ -65,7 +38,7 @@ Per above, in a typical distribution, PROFOIL-UI is conveniently placed in the * python profoil_ui.py ``` -## Loading an Airfoil +## Loading an Airfoil Upon starting, users can load an airfoil by choosing the input file (a valid PROFOIL .in file) through the File menu. The installation includes the sample files in the **./runs** folder. Choosing one of the samples, e.g. **../runs/examples/example1.in** will load the file into the program to run the executable PROFOIL (**./bin/profoil.exe**). @@ -130,25 +103,27 @@ This button operates by taking the following actions under the hood. There will be 4 lines in total in the α\*(ϕ) plot right after a new airfoil is designed (if the program is successfully executed). -- Black dashed line: shows converged data from the previous run. -- Blue dashed line: shows prescribed α\*(ϕ) curve. -- Purple line: shows converged data from the most recent run. -- Green line: shows modifiable α\*(ϕ) distribution using the cursor edits. +- Black dashed line: shows converged data from the previous run. +- Blue dashed line: shows prescribed α\*(ϕ) curve. +- Purple line: shows converged data from the most recent run. +- Green line: shows modifiable α\*(ϕ) distribution using the cursor edits. ![phi_alpha_lines](./doc_media/7_phi_alfa_lines.png) Notes : + - Black and Blue lines will not be shown in the initial plot because there is no history to show. - Green and purple lines always coincide just after the run. For demonstration purposes, green line was moved away from the purple one in the attached screen-shot. Once PROFOIL execution finishes, the previous design will be shown in dashed-grey lines and the new design will be shown in solid lines. Typically it is assumed that each successful PROFOIL run will be followed by an analysis session through XFoil or some other means to verify if the intended changes were indeed met. Notes : + - If for some reason, the prescribed α\*(ϕ) distribution does not result in successful design, the plots will not be updated and failure will be indicated through a warning message. - Summary statistics (last 14 lines of profoil.log file) will be displayed in the "Summary" section in the right bottom of the window for successful runs and complete log files could further be inspected in File View tab. - In case if the program crashes for some unexpected reason, the buffer.in file which is one iteration behind the current profoil.in file can be found in the **./work** directory. -During this iterative process, geometric overlay could be referenced using the Overlay menu. In this menu \*.dat file refers to any file containing 𝓍,𝓎 coordinates with up to 2 header files. This covers profoil.xy files generated by PROFOIL, XFoil format dat files and MSES blade files. The overlay will be kept in the airfoil plot until they will be manually cleared through Overlay -> Clear Overlay function. +During this iterative process, geometric overlay could be referenced using the Overlay menu. In this menu \*.dat file refers to a file containing 𝓍,𝓎 coordinates with 0 or 1 header line. This covers profoil.xy files generated by PROFOIL and XFoil format dat files. The overlay will be kept in the airfoil plot until they will be manually cleared through Overlay -> Clear Overlay function. ![phi_alpha_lines](./doc_media/8_overlay.png) @@ -159,6 +134,7 @@ File View tab allows users to make required modifications to profoil.in file wit Finally, once the design requirements are met, the most recent profoil.in file in **./work** directory which corresponds to the final design iteration, can be saved into a user specified destination using File -> Save function. ## Special Notes + 1. For symmetrical airfoil with **SYM** flag in the _.in_ file, _profoil.dmp_ file will be generated without the lower surface α\*(ϕ) distribution. This leads to the plots in PROFOIL-UI to show only the upper surface α\*(ϕ) markers accordingly. This aligns very well on how PROFOIL treats **SYM** flag as it reads only the upper surface α\*(ϕ) distribution and discards the FOIL lines past ILE. In practice, if one wants the graphs to show the lower surface α\*(ϕ) markers as well, then **SYM_TOGGLE** flag should be used just before the **DUMP** line in the _profoil.in_ file. Below images show the program output, without SYM_TOGGLE option and with SYM_TOGGLE option for better clarity. @@ -193,4 +169,3 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` - diff --git a/preferences.py b/preferences.py index 64c64ab..57f66b9 100644 --- a/preferences.py +++ b/preferences.py @@ -1,3 +1,23 @@ +# Copyright (c) 2022 Kanishka Jayawardane [kanishkagj@yahoo.com] + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + # This file list out all the preferences # The options listed down here has to be compatible with matplotlib. # Documentation can be found at @@ -7,70 +27,65 @@ #=========================================== DIRECTORIES ============================================ -WORKDIR = "../work" -BINDIR = "../bin" +WORK_DIR = "../work" +BIN_DIR = "../bin" #===================================== CONFIG OF PHI-ALPHA PLOT ===================================== -AN_PREV_LINE_LINESTYLE = '--' # dotted lines for previous plot -AN_CURR_LINE_LINESTYLE = '--' # dotted lines for current plot -AN_PRES_LINE_LINESTYLE = '--' # dotted lines for prescribed plot -AN_MODI_LINE_LINESTYLE = '-' # continuous lines for main modifiable line +AN_PREV_LINE_LINESTYLE = '--' # dotted lines for previous plot +AN_CURR_LINE_LINESTYLE = '--' # dotted lines for current plot +AN_PRES_LINE_LINESTYLE = '--' # dotted lines for prescribed plot +AN_MODI_LINE_LINESTYLE = '-' # continuous lines for main modifiable line -AN_PREV_LINE_COLOR = "black" # color of previous plot -AN_CURR_LINE_COLOR = "darkviolet" # color of current plot -AN_PRES_LINE_COLOR = "blue" # color of prescribed plot -AN_MODI_LINE_COLOR = "green" # color of modifiable line +AN_PREV_LINE_COLOR = "black" # color of previous plot +AN_CURR_LINE_COLOR = "darkviolet" # color of current plot +AN_PRES_LINE_COLOR = "blue" # color of prescribed plot +AN_MODI_LINE_COLOR = "green" # color of modifiable line -AN_SPLN_LINE_LINESTYLE = '--+' # dotted lines with + marks for spline -AN_SPLN_LINE_COLOR = 'red' # dotted lines with + marks for spline +AN_SPLN_LINE_LINESTYLE = '--+' # dotted lines with + marks for spline +AN_SPLN_LINE_COLOR = 'red' # dotted lines with + marks for spline -AN_PREV_LINE_MARKER = "o" # markers on the alfa_nu plot -AN_CURR_LINE_MARKER = "o" # markers on the alfa_nu plot -AN_PRES_LINE_MARKER = "o" # markers on the alfa_nu plot -AN_MODI_LINE_MARKER = "o" # markers on the alfa_nu plot +AN_PREV_LINE_MARKER = "o" # markers on the alfa_nu plot +AN_CURR_LINE_MARKER = "o" # markers on the alfa_nu plot +AN_PRES_LINE_MARKER = "o" # markers on the alfa_nu plot +AN_MODI_LINE_MARKER = "o" # markers on the alfa_nu plot -AN_PREV_LINE_MARKERFACECOLOR = "None" # markers face color on ref plots -AN_CURR_LINE_MARKERFACECOLOR = "None" # markers face color on ref plots -AN_PRES_LINE_MARKERFACECOLOR = "None" # markers face color on ref plots +AN_PREV_LINE_MARKERFACECOLOR = "None" # markers face color on ref plots +AN_CURR_LINE_MARKERFACECOLOR = "None" # markers face color on ref plots +AN_PRES_LINE_MARKERFACECOLOR = "None" # markers face color on ref plots -AN_PLOT_MARKERSIZE = 3 # All markers share the same size. -AN_PLOT_LINEWIDTH = 1 # All lines share the same width. +AN_PLOT_MARKERSIZE = 3 # All markers share the same size. +AN_PLOT_LINEWIDTH = 1 # All lines share the same width. -AN_PLOT_YLIMITS = (-20, 20) # From -20 deg to +20 deg -AN_PLOT_XLIMITS_UPPER = ( 37, 0) # From -20 deg to +20 deg -AN_PLOT_XLIMITS_LOWER = ( 23, 60) # From -20 deg to +20 deg -AN_FLIP_YAXIS_LOWER_SURFACE = True # With this set, y-axis limits will be reversed - # for lower surface +AN_PLOT_YLIMITS = (-20, 20) # From -20 deg to +20 deg +AN_PLOT_XLIMITS_UPPER = ( 37, 0) # From -20 deg to +20 deg +AN_PLOT_XLIMITS_LOWER = ( 23, 60) # From -20 deg to +20 deg +AN_FLIP_YAXIS_LOWER_SURFACE = True # With this set, y-axis limits will be reversed + # for lower surface #============================ CONFIG OF VELOCITY DISTRIBUTION (UE) PLOT ============================= -UE_PLOT_OLD_LINE_COLOR = "darkgrey" # UE Plot , previous reference plot colors -UE_PLOT_OLD_LINE_STYLE = "--" # UE Plot , previous reference plot line-style -UE_PLOT_OLD_MARKER_COLOR = "darkgrey" # UE Plot , previous reference plot marker color +UE_PLOT_OLD_LINE_COLOR = "darkgrey" # UE Plot , previous reference plot colors +UE_PLOT_OLD_LINE_STYLE = "--" # UE Plot , previous reference plot line-style +UE_PLOT_OLD_MARKER_COLOR = "darkgrey" # UE Plot , previous reference plot marker color -UE_PLOT_LINEWIDTH = 1 # UE Plot , line width of the plots -UE_PLOT_COLOR = None # UE Plot , line color of the plots +UE_PLOT_LINEWIDTH = 1 # UE Plot , line width of the plots +UE_PLOT_COLOR = None # UE Plot , line color of the plots # ======================================== CONFIG OF XY PLOT ========================================= -XY_PLOT_LINEWIDTH = 1 # XY Plot line width -XY_PLOT_COLOR = "black" # XY Plot line color +XY_PLOT_LINEWIDTH = 1 # XY Plot line width +XY_PLOT_COLOR = "black" # XY Plot line color -OVERLAY_LINEWIDTH = 1 # Overlay Plot line width -OVERLAY_LINESTYLE = '.--' # Overlay Plot line style -OVERLAY_LINECOLOR = 'red' # Overlay Plot line color -OVERLAY_MARKERSIZE = 5 # Overlay Plot line marker +OVERLAY_LINEWIDTH = 1 # Overlay Plot line width +OVERLAY_LINESTYLE = '.--' # Overlay Plot line style +OVERLAY_LINECOLOR = 'red' # Overlay Plot line color +OVERLAY_MARKERSIZE = 5 # Overlay Plot line marker #================================= CONFIG RELATED TO UE & XY PLOTS ================================== -UPPER_SURFACE_PHI_MARKER = "^" # Upward facing Triangular Phi Marks -LOWER_SURFACE_PHI_MARKER = "v" # Downward facing Triangular Phi Marks -UPPER_SURFACE_PHI_MARKER_SIZE = 8 # Phi marker size - upper Surface -LOWER_SURFACE_PHI_MARKER_SIZE = 8 # Phi marker size - lower Surface - -#===================================CONFIG RELATED TO MAIN WINDOW==================================== - -MAIN_WINDOW_WIDTH = 1320 # This would be the minimum window width as well -MAIN_WINDOW_HEIGHT = 775 # This would be the minimum window height as well +UPPER_SURFACE_PHI_MARKER = "^" # Upward facing Triangular Phi Marks +LOWER_SURFACE_PHI_MARKER = "v" # Downward facing Triangular Phi Marks +UPPER_SURFACE_PHI_MARKER_SIZE = 8 # Phi marker size - upper Surface +LOWER_SURFACE_PHI_MARKER_SIZE = 8 # Phi marker size - lower Surface \ No newline at end of file diff --git a/profoil_canvas.py b/profoil_canvas.py index f9430a7..dbecb12 100644 --- a/profoil_canvas.py +++ b/profoil_canvas.py @@ -1,3 +1,24 @@ +# Copyright (c) 2022 Kanishka Jayawardane [kanishkagj@yahoo.com] + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + # This is the main module of PROFOIL-UI. # From the beginning It was conceptualized as a minimalistic program which allows users to do any modifications as they wish relatively easily. @@ -47,620 +68,665 @@ # More in-depth implementation details follows in each functions doc-strings. - import numpy as np from pathlib import Path from scipy.interpolate import interp1d -import profoil_interface as p_intf from preferences import * +import profoil_interface as p_intf +from profoil_interface import WORKDIR, BINDIR import matplotlib matplotlib.use('Qt5Agg', force=True) import matplotlib.pyplot as plt from matplotlib.figure import Figure -WORKDIR = Path(WORKDIR) -BINDIR = Path(BINDIR) -UI_DIR = Path.cwd() - -class ProfoilCanvas(): - - def __init__(self): - plt.ion() - - self.GRID_ON = True # Grid on the phi-alpha* plot - self.SHOW_PREV_LINES = True # Show previous plots on the Velocity and x,y plots. - - self.active_surface = "Upper" +class ProfoilCanvas: - self.upper_xlim =AN_PLOT_XLIMITS_UPPER - self.upper_ylim =AN_PLOT_YLIMITS - self.lower_xlim =AN_PLOT_XLIMITS_LOWER - self.lower_ylim =tuple(reversed(AN_PLOT_YLIMITS)) if AN_FLIP_YAXIS_LOWER_SURFACE else AN_PLOT_YLIMITS + def __init__(self): + plt.ion() - self.gen_gui_fig() - - # Upper Surface Lines in the phi-alpha* distribution plot - # during the program execution these lines will not be re-plotted. + self.GRID_ON = True # Grid on the phi-alpha* plot + self.SHOW_PREV_LINES = True # Show previous plots on the Velocity and x,y plots. + + self.active_surface = "Upper" - self.upper_nu_alfa_previous = plt.Line2D([],[], linestyle=AN_PREV_LINE_LINESTYLE, marker=AN_PREV_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_PREV_LINE_COLOR, markerfacecolor=AN_PREV_LINE_MARKERFACECOLOR, clip_on=False) - self.upper_nu_alfa_current = plt.Line2D([],[], linestyle=AN_CURR_LINE_LINESTYLE, marker=AN_CURR_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_CURR_LINE_COLOR, markerfacecolor=AN_CURR_LINE_MARKERFACECOLOR, clip_on=False) - self.upper_nu_alfa_prescribed = plt.Line2D([],[], linestyle=AN_PRES_LINE_LINESTYLE, marker=AN_PRES_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_PRES_LINE_COLOR, markerfacecolor=AN_PRES_LINE_MARKERFACECOLOR, clip_on=False) - self.upper_nu_alfa_modi = plt.Line2D([],[], linestyle=AN_MODI_LINE_LINESTYLE, marker=AN_MODI_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_MODI_LINE_COLOR, clip_on=False) + self.upper_xlim =AN_PLOT_XLIMITS_UPPER + self.upper_ylim =AN_PLOT_YLIMITS + self.lower_xlim =AN_PLOT_XLIMITS_LOWER + self.lower_ylim =tuple(reversed(AN_PLOT_YLIMITS)) if AN_FLIP_YAXIS_LOWER_SURFACE else AN_PLOT_YLIMITS - # Lower Surface Lines in the phi-alpha* distribution plot - # during the program execution these lines will not be re-plotted. + self.gen_gui_fig() + + # Upper Surface Lines in the phi-alpha* distribution plot + # during the program execution these lines will not be re-plotted. - self.lower_nu_alfa_previous = plt.Line2D([],[], linestyle=AN_PREV_LINE_LINESTYLE, marker=AN_PREV_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_PREV_LINE_COLOR, markerfacecolor=AN_PREV_LINE_MARKERFACECOLOR, clip_on=False) - self.lower_nu_alfa_current = plt.Line2D([],[], linestyle=AN_CURR_LINE_LINESTYLE, marker=AN_CURR_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_CURR_LINE_COLOR, markerfacecolor=AN_CURR_LINE_MARKERFACECOLOR, clip_on=False) - self.lower_nu_alfa_prescribed = plt.Line2D([],[], linestyle=AN_PRES_LINE_LINESTYLE, marker=AN_PRES_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_PRES_LINE_COLOR, markerfacecolor=AN_PRES_LINE_MARKERFACECOLOR, clip_on=False) - self.lower_nu_alfa_modi = plt.Line2D([],[], linestyle=AN_MODI_LINE_LINESTYLE, marker=AN_MODI_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_MODI_LINE_COLOR, clip_on=False) + self.upper_nu_alfa_previous = plt.Line2D([],[], linestyle=AN_PREV_LINE_LINESTYLE, marker=AN_PREV_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_PREV_LINE_COLOR, markerfacecolor=AN_PREV_LINE_MARKERFACECOLOR, clip_on=False) + self.upper_nu_alfa_current = plt.Line2D([],[], linestyle=AN_CURR_LINE_LINESTYLE, marker=AN_CURR_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_CURR_LINE_COLOR, markerfacecolor=AN_CURR_LINE_MARKERFACECOLOR, clip_on=False) + self.upper_nu_alfa_prescribed = plt.Line2D([],[], linestyle=AN_PRES_LINE_LINESTYLE, marker=AN_PRES_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_PRES_LINE_COLOR, markerfacecolor=AN_PRES_LINE_MARKERFACECOLOR, clip_on=False) + self.upper_nu_alfa_modi = plt.Line2D([],[], linestyle=AN_MODI_LINE_LINESTYLE, marker=AN_MODI_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_MODI_LINE_COLOR, clip_on=False) - self.cursor_edit_line, = self.an_ax.plot([], [], AN_SPLN_LINE_LINESTYLE, picker=True, color=AN_SPLN_LINE_COLOR, linewidth=AN_PLOT_LINEWIDTH) + # Lower Surface Lines in the phi-alpha* distribution plot + # during the program execution these lines will not be re-plotted. - # Flags - self.ready_to_interact = False # holds a flag to signal if program is ready to run. - # used to stop errors from happening by accidental key presses + self.lower_nu_alfa_previous = plt.Line2D([],[], linestyle=AN_PREV_LINE_LINESTYLE, marker=AN_PREV_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_PREV_LINE_COLOR, markerfacecolor=AN_PREV_LINE_MARKERFACECOLOR, clip_on=False) + self.lower_nu_alfa_current = plt.Line2D([],[], linestyle=AN_CURR_LINE_LINESTYLE, marker=AN_CURR_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_CURR_LINE_COLOR, markerfacecolor=AN_CURR_LINE_MARKERFACECOLOR, clip_on=False) + self.lower_nu_alfa_prescribed = plt.Line2D([],[], linestyle=AN_PRES_LINE_LINESTYLE, marker=AN_PRES_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_PRES_LINE_COLOR, markerfacecolor=AN_PRES_LINE_MARKERFACECOLOR, clip_on=False) + self.lower_nu_alfa_modi = plt.Line2D([],[], linestyle=AN_MODI_LINE_LINESTYLE, marker=AN_MODI_LINE_MARKER, linewidth=AN_PLOT_LINEWIDTH, markersize=AN_PLOT_MARKERSIZE, color=AN_MODI_LINE_COLOR, clip_on=False) - self.edit_mode = False + self.cursor_edit_line, = self.an_ax.plot([], [], AN_SPLN_LINE_LINESTYLE, picker=True, color=AN_SPLN_LINE_COLOR, linewidth=AN_PLOT_LINEWIDTH) - def gen_gui_fig(self): - - # Setting up the main window/figure - - self.gui_fig = Figure(figsize=(12, 7)) - grid = plt.GridSpec(4,2, wspace=0.1, hspace=0.2) - - # Setting up the 3 main axes - # ue_ax : velocity distribution axes - # xy_ax : x,y airfoil contour axes - # an_ax : alpha*-nu axes - - self.ue_ax = self.gui_fig.add_subplot(grid[:3, 0]) - self.xy_ax = self.gui_fig.add_subplot(grid[ 3, 0]) - self.an_ax = self.gui_fig.add_subplot(grid[:3, 1]) - - self.setup_axes() - - self.gui_fig.subplots_adjust(left=0.05, right=0.98, top=0.96, bottom=0.08, hspace = 0.02, wspace=0.02) - self.gui_fig.canvas.mpl_connect('button_press_event', self.on_click) - - def setup_axes(self): - """ - initializes the axes - """ - self.ue_ax.n_untouch = 0 - self.xy_ax.n_untouch = 0 - self.an_ax.n_untouch = 1 # spline will be untouched - - self.ue_ax.set_title(r'$Velocity\ Distribution$') - self.ue_ax.set_ylabel(r'$V/V_{\infty}$') - - self.xy_ax.set_xlabel(r"$x/c$") - self.xy_ax.set_ylabel(r"$y/c$") - - self.an_ax.set_title(r'$\alpha^*(\phi) - Upper$') - self.an_ax.set_xlabel(r"$\phi$") - self.an_ax.grid(True) - - self.xy_ax.axes.set_aspect('equal', 'datalim') - # self.an_ax.axes.set_aspect('equal', 'datalim') - - self.setup_axes_limits() - - def setup_axes_limits(self): - """ - initializes the axes limits - """ - self.ue_ax.set_xlim(-0.08, 1.08) - self.ue_ax.set_ylim(0, 2.5) - - self.xy_ax.set_xlim(-0.08, 1.08) - - if self.active_surface == "Lower": - self.an_ax.set_xlim(*AN_PLOT_XLIMITS_LOWER) - self.an_ax.set_ylim(*(tuple(reversed(AN_PLOT_YLIMITS)) if AN_FLIP_YAXIS_LOWER_SURFACE else AN_PLOT_YLIMITS)) - if self.active_surface == "Upper": - self.an_ax.set_xlim(*AN_PLOT_XLIMITS_UPPER) - self.an_ax.set_ylim(*AN_PLOT_YLIMITS) - - def clear_axes(self): - """ - clears all axes data such that new session with new airfoil can be loaded. - """ - self.ue_ax.lines=[] - self.ue_ax.collections=[] - - self.xy_ax.lines=[] - self.xy_ax.collections=[] - - self.clear_an_ax() - - self.upper_nu_alfa_previous.set_data([],[]) - self.upper_nu_alfa_current.set_data([],[]) - self.upper_nu_alfa_prescribed.set_data([],[]) - self.upper_nu_alfa_modi.set_data([],[]) - - self.lower_nu_alfa_previous.set_data([],[]) - self.lower_nu_alfa_current.set_data([],[]) - self.lower_nu_alfa_prescribed.set_data([],[]) - self.lower_nu_alfa_modi.set_data([],[]) - - self.cursor_edit_line.set_data([],[]) - - def reset_toolbar(self): - """ - This function resets the toolbar such that all tool bar items are set to not-checked - """ - try: - for x in self.tool_bar.actions(): - if x.isChecked(): - x.trigger() - except: - pass - - def load_line(self, line): - """ - This function "loads a line" in to the cursor editor. - In summary it references a line object from the child class to be modified - """ - self.cancel_cursor_inputs() - self.nu_alfa = line - self.nu_alfa_points = self.nu_alfa.get_xydata().tolist() - self.gui_fig.canvas.draw() - - - def on_click(self, event): - """ - All the mouse click events goes here - """ - if not self.ready_to_interact: return - if not self.edit_mode: return - if event.inaxes!=self.an_ax: return - - # Left click - if event.button == 1: - self.cursor_edit_line_points.append([event.xdata,event.ydata]) - self.cursor_edit_line_points.sort() - self.cursor_edit_line.set_data(*list(zip(*self.cursor_edit_line_points))) - self.gui_fig.canvas.draw() - - # Right Click and spline is actually built - if event.button == 3: - self.apply_edits(event) - self.gui_fig.canvas.draw() - - def backup_zoomed_limits(self, surface): - """ - save zoomed limits so that switching between upper and lower surfaces cause resetting limits - """ - if surface == self.active_surface: return - if surface == "Upper": - self.lower_xlim = self.an_ax.get_xlim() - self.lower_ylim = self.an_ax.get_ylim() - if surface == "Lower": - self.upper_xlim = self.an_ax.get_xlim() - self.upper_ylim = self.an_ax.get_ylim() - - def select_surface(self, surface, first_time=False): - """ - Callback function which switches the upper and lower surfaces - through the radio buttons. - Depending on the preferences: - y-axis is inverted upon switching. - delta_alpha* which get added in changes the direction - """ - self.reset_toolbar() - self.clear_an_ax() - if surface =="Upper": - # backup zoomed limits if applicable. - if not first_time: - self.backup_zoomed_limits(surface) - self.active_surface = "Upper" - - self.an_ax.set_xlim(*self.upper_xlim) - self.an_ax.set_ylim(*self.upper_ylim) - - self.an_ax.set_title(r'$\alpha^*(\phi) - Upper$') - - # axes transformation between ax limits and pixels are handled - # automatically by ax.add_line(...) - # ax.lines = [..] wouldn't work the same way - - self.an_ax.add_line(self.upper_nu_alfa_previous) - self.an_ax.add_line(self.upper_nu_alfa_current) - self.an_ax.add_line(self.upper_nu_alfa_prescribed) - self.an_ax.add_line(self.upper_nu_alfa_modi) - - # load_line makes the Line2D object which is passed active - # in the interactive plot. - - self.load_line(self.upper_nu_alfa_modi) - - if surface =="Lower": - # backup zoomed limits if applicable. - if not first_time: - self.backup_zoomed_limits(surface) - self.active_surface = "Lower" - - self.an_ax.set_xlim(*self.lower_xlim) - self.an_ax.set_ylim(*self.lower_ylim) - - self.an_ax.set_title(r'$\alpha^*(\phi) - Lower$') - - # axes transformation between pixel and ax limits are handled - # automatically by ax.add_line(...) - # ax.lines = [..] wouldn't work the same way - - self.an_ax.add_line(self.lower_nu_alfa_previous) - self.an_ax.add_line(self.lower_nu_alfa_current) - self.an_ax.add_line(self.lower_nu_alfa_prescribed) - self.an_ax.add_line(self.lower_nu_alfa_modi) - - # load_line makes the Line2D object which is passed active - # in the interactive plot. - - self.load_line(self.lower_nu_alfa_modi) - self.gui_fig.canvas.draw() - - def checkbox_toggle(self, label): - self.reset_toolbar() - if label=="Grid": - self.toggle_grid_lines() - else: - self.toggle_previous_plots() - - def toggle_previous_plots(self, event): - """ - Toggles the visibility of previous lines. - Will make effect from the next plot - not on the current plot - """ - self.SHOW_PREV_LINES = bool(event) - - # Goes through each axis and sets the visibility - # of the preserved lines. - # each ax class will hold a list of preserved lines. - for ax in [self.ue_ax, self.xy_ax]: - for line in ax.preserved_plots: - line.set_visible(self.SHOW_PREV_LINES) - for marker in ax.preserved_markers: - marker.set_visible(self.SHOW_PREV_LINES) - self.gui_fig.canvas.draw() - - def toggle_grid_lines(self, event): - """ - Toggles the grid lines on the an_ax. - Will make effect immediately - """ - self.GRID_ON = bool(event) - self.an_ax.grid(self.GRID_ON) - self.gui_fig.canvas.draw() - - def set_edit_mode_off(self): - self.edit_mode = False - self.btn_start_edits.setStyleSheet('QPushButton {color: black;}') - - def start_cursor_edits(self, event): - self.reset_toolbar() - self.edit_mode = not self.edit_mode - self.btn_start_edits.setStyleSheet('QPushButton {color: red;}' if self.edit_mode else 'QPushButton {color: black;}') - if not self.edit_mode: self.cancel_cursor_inputs() - - def cancel_cursor_inputs(self, event=None): - """ - Resets the cursor edit line in to an empty line. - Simple means to start over with a new cursor edit line. - """ - self.reset_toolbar() - if not self.ready_to_interact: return - self.cursor_edit_line.set_data([],[]) - self.cursor_edit_line_points = self.cursor_edit_line.get_xydata().tolist() - self.set_edit_mode_off() - self.gui_fig.canvas.draw() - - def apply_edits(self, event): - """ - Cursor edits are applied to the green line from the red line - """ - self.reset_toolbar() - # Better protection than if self.cursor_edit_line_points because one point cannot make spline - if len(self.cursor_edit_line_points)>1: - self.set_edit_mode_off() - x_data, y_data = self.nu_alfa.get_data() - spline = interp1d(*np.array(self.cursor_edit_line_points).T, kind='linear', bounds_error=False) - new_y = spline(x_data) - self.nu_alfa.set_ydata(np.where(np.isnan(new_y), y_data, new_y)) - self.save_edits_to_file() - self.cancel_cursor_inputs() - - def save_edits_to_file(self): - """ - Saves green line data in to the profoil.in file - """ - nu_upper, alfa_upper = self.upper_nu_alfa_modi.get_data() - nu_lower, alfa_lower = self.lower_nu_alfa_modi.get_data() - nu_list = list(nu_upper) + list(nu_lower) - alfa_list = list(alfa_upper) + list(alfa_lower) - p_intf.gen_buffer() - p_intf.gen_input_file(nu_list, alfa_list, len(nu_upper)) - - def undo_edits(self, event): - """ - This callback function resets the modifiable phi-alpha* distribution - back to the current converged data from the most recent run. - Typically used when the line edits - has to be reverted. - """ - self.reset_toolbar() - if not self.ready_to_interact: return - self.cancel_cursor_inputs() - self.upper_nu_alfa_modi.set_data(*self.upper_nu_alfa_current.get_data()) - self.lower_nu_alfa_modi.set_data(*self.lower_nu_alfa_current.get_data()) - self.save_edits_to_file() - self.gui_fig.canvas.draw() - - def plot_from_file(self, event): - """ - Reads profoil.in file in the work directory and updates the green line - This action confirms any manual modifications if applicable - """ - self.reset_toolbar() - nu, alfa, ile, phis = p_intf.extract_dmp("profoil.in") - - nu_upper = nu[:ile] - alfa_upper = alfa[:ile] - - nu_lower = nu[ile:] - alfa_lower = alfa[ile:] - - self.upper_nu_alfa_modi.set_data(nu_upper,alfa_upper) - self.lower_nu_alfa_modi.set_data(nu_lower,alfa_lower) - self.gui_fig.canvas.draw() - - def run_profoil(self, event=None): - """ - Executes PROFOIL with the following steps. - 1. Backup the previous line. # For reloading as required - 2. Resets the cursor edit line. # Because after the run cursor edit line should not be there. - 3. Creates buffer.in from the existing profoil.in file. - 4. Creates a new profoil.in file by replacing the FOIL lines with the data in the graph. - 5. Prints out the profoil.log file. - """ - self.reset_toolbar() - if not self.ready_to_interact: return - self.set_edit_mode_off() - self.bkp_previous_line() - self.cancel_cursor_inputs() - self.run_from_profoil_in() - self.gui_fig.canvas.draw() - - def revert(self, event): - """ - This callback function loads the converged data from the previous run - typically used when something goes wrong with the current run. - Equivalent to loading buffer.in instead of profoil.in in the work dir. - """ - self.reset_toolbar() - if not self.ready_to_interact: return - if not self.upper_nu_alfa_previous.get_data()[0]: return - self.set_edit_mode_off() - self.cancel_cursor_inputs() - p_intf.swap_buffer() - self.run_profoil() - self.gui_fig.canvas.draw() - - def proc_make_ax_old(self, ax, preserve_lines=1): - """ - This function accepts an axes object and applies the settings for previous plot. - 1. Keep only last few lines on the axes given by preserve_lines and purges away the rest - 2. Greys/blacks out the lines/markers - 3. Make the lines dashed - 4. Removes the legend - 5. Sets visibility - 6. Makes a list of preserved lines and collections to set the visibility later on through "History" check-box - """ - plots = ax.lines - - ax.preserved_plots = [] # list of references for preserved plots will be stored for clearing history - ax.preserved_markers = [] # list of references for preserved plots will be stored for clearing history - - untouch_plots = plots[:ax.n_untouch] - preserved_plots = plots[ax.n_untouch:][-preserve_lines:] - - for plot in untouch_plots: - plot.set_label('_nolegend_') - - for plot in preserved_plots: - plot.set_color(UE_PLOT_OLD_LINE_COLOR) - plot.set_linestyle(UE_PLOT_OLD_LINE_STYLE) - plot.set_label('_nolegend_') - plot.set_visible(self.SHOW_PREV_LINES) - - markers = ax.collections[-2* preserve_lines:] - for marker in markers: - marker.set_color(UE_PLOT_OLD_MARKER_COLOR) - marker.set_visible(self.SHOW_PREV_LINES) - - ax.preserved_plots = preserved_plots.copy() # list of references for clearing history - ax.preserved_markers = markers.copy() # list of references for clearing history - - ax.lines = untouch_plots + preserved_plots - ax.collections = markers - ax.set_prop_cycle(None) - - def proc_make_line_untouchable(self, ax, plot_index=-1): - """ - plot list is divided into 2 main sections. - untouchable plots ; these hold fixed REF lines - touchable plots ; these hold variable plots. - untouchable plots were pushed towards the beginning of the - plot list such that the remaining plots can be modified with - slicing with ease - """ - plots = ax.lines - reserve_plot = plots[plot_index] - del plots[plot_index] - plots.insert(ax.n_untouch, reserve_plot) - ax.n_untouch += 1 - - def remove_last_untouchable(self, ax): - """ - Pops the last object from the "untouchable plots" section of ax.lines list - """ - plots = ax.lines - del plots[ax.n_untouch -1] - ax.n_untouch -= 1 - - def plot_ue(self, n_prev_plots =1): - """ - Plots velocity distribution data and airfoil contour - """ - n_cols = len(self.ue_lines.keys()) - n_ue_preserve = n_cols * n_prev_plots # alphas give the number of lines - n_xy_preserve = n_prev_plots - - # make the previous plots "old" [greyed out and dashed etc] - self.proc_make_ax_old(self.ue_ax, n_ue_preserve) - self.proc_make_ax_old(self.xy_ax, n_xy_preserve) - - - # plots the velocity distribution - # color of the original plot is extracted back to make the upper and lower markers. - for alpha in sorted(self.ue_lines.keys(), key=float): - p = self.ue_ax.plot(self.ue_lines[alpha]['x'], self.ue_lines[alpha]['v_vinf'], label = "{:5.2f}".format(alpha), lw=UE_PLOT_LINEWIDTH, color=UE_PLOT_COLOR, clip_on=False) - self.ue_ax.scatter(self.upper_vel_markers[alpha]['x'], self.upper_vel_markers[alpha]['v_vinf'], color=p[-1].get_color(), marker=UPPER_SURFACE_PHI_MARKER, s=UPPER_SURFACE_PHI_MARKER_SIZE, clip_on=False) - self.ue_ax.scatter(self.lower_vel_markers[alpha]['x'], self.lower_vel_markers[alpha]['v_vinf'], color=p[-1].get_color(), marker=LOWER_SURFACE_PHI_MARKER, s=LOWER_SURFACE_PHI_MARKER_SIZE, clip_on=False) - - # legend is not used in the current implementation because Alphas are just dummy variables. - # can modify easily in the future if Alphas to be read from the .in file. - # self.ue_ax.legend(fontsize ='small', frameon = False, loc="upper right") - - # plots the airfoil contour - # color of the original plot is extracted back to make the upper and lower markers. - - p = self.xy_ax.plot(self.x, self.y, lw=XY_PLOT_LINEWIDTH, color=XY_PLOT_COLOR, clip_on=False) - self.xy_ax.scatter(self.xy_marker_upper['x'], self.xy_marker_upper['y'], color=p[-1].get_color(), marker=UPPER_SURFACE_PHI_MARKER, s=UPPER_SURFACE_PHI_MARKER_SIZE, clip_on=False) - self.xy_ax.scatter(self.xy_marker_lower['x'], self.xy_marker_lower['y'], color=p[-1].get_color(), marker=LOWER_SURFACE_PHI_MARKER, s=LOWER_SURFACE_PHI_MARKER_SIZE, clip_on=False) - - self.gui_fig.canvas.draw() - - def bkp_previous_line(self): - """ - data from "current" plot sets data on "previous" plot - """ - self.upper_nu_alfa_previous.set_data(*self.upper_nu_alfa_current.get_data()) - self.lower_nu_alfa_previous.set_data(*self.lower_nu_alfa_current.get_data()) - - def clear_an_ax(self): - """ - Removes all lines except the "untouchable plots" section - ax.clear(...) wouldn't work here because it resets all the limits and - de-reference the axes from the cursor editor. - """ - self.an_ax.lines = self.an_ax.lines[:self.an_ax.n_untouch] - - def plot_nu_alfa(self): - """ - As per the overall description on the top, plotting of phi-alpha* distribution - does not generate new plot lines (Line2D objects) using ax.plot(...) - The state of these 8 lines will be maintained with the same ID by changing - the data these lines hold. This method vastly eases off the surface switching. - """ - self.upper_nu_alfa_prescribed.set_data(*self.upper_nu_alfa_modi.get_data()) - self.upper_nu_alfa_modi.set_data(self.nu_upper, self.alfa_upper) - self.upper_nu_alfa_current.set_data(self.nu_upper, self.alfa_upper) - - self.lower_nu_alfa_prescribed.set_data(*self.lower_nu_alfa_modi.get_data()) - self.lower_nu_alfa_modi.set_data(self.nu_lower, self.alfa_lower) - self.lower_nu_alfa_current.set_data(self.nu_lower, self.alfa_lower) - - def overlay_dat(self, datfile): - """ - This function overlays a given dat file contour in the xy plot. - File formats with different number of header lines up to SKIPROWS_LIMIT are supported - """ - SKIPROWS_LIMIT = 3 - skip_n = 0 - while skip_n <= SKIPROWS_LIMIT: - if skip_n == SKIPROWS_LIMIT: - self.overlay_error_dialog() - return - try: - x,y = np.loadtxt(Path(datfile), skiprows=skip_n).T - break - except: - skip_n +=1 - - self.xy_ax.plot(x, y, OVERLAY_LINESTYLE, lw=OVERLAY_LINEWIDTH, color=OVERLAY_LINECOLOR, markersize=OVERLAY_MARKERSIZE, clip_on=False) - self.proc_make_line_untouchable(self.xy_ax) - self.gui_fig.canvas.draw() - - def clear_overlay(self): - """ - This function removes the the overlay in LIFO order - If more than one overlay being added the last one - will be cleared off first - """ - self.remove_last_untouchable(self.xy_ax) - self.gui_fig.canvas.draw() - - - def run_from_profoil_in(self): - """ - Executes PROFOIL when the profoil.in file is ready in the WORKDIR - """ - p_intf.work2bin() - p_intf.exec_profoil() - p_intf.bin2work() - - self.x,self.y, self.xy_marker_upper, self.xy_marker_lower, \ - self.ue_lines, self.upper_vel_markers, self.lower_vel_markers, \ - self.nu_upper, self.alfa_upper, self.nu_lower, self.alfa_lower, self.ile = p_intf.extract_all_data() - - if p_intf.is_design_converged(): - p_intf.bin2work() - self.lbl_summary.setText(p_intf.catfile(BINDIR/"profoil.log", tail=14)) - self.plot_ue() - self.plot_nu_alfa() - - else: - self.failure_error_dialog() - - self.plainTextEdit_profoil_log.setPlainText(p_intf.catfile(BINDIR/"profoil.log", tail=0)) - - - def initial_plot(self): - """ - Initial plot is called once at the initial setup. - Before calling initial_plot its required to have run_from_profoil_in(...) called. - """ - # self.cursor_edit_line, = self.an_ax.plot([], [], AN_SPLN_LINE_LINESTYLE, picker=True, color=AN_SPLN_LINE_COLOR, linewidth=AN_PLOT_LINEWIDTH) - self.select_surface("Upper", first_time =True) - self.gui_fig.canvas.draw() - - def load(self, in_file=None): - """ - Loads a *.in file in to the program. Keeps on calling until a valid file is provided. - 1. Sets the ready_to_interact flag to make sure no errors will occur by pressing a random button. - 2. copies *.in file in to WORKDIR as profoil.in - 3. Runs PROFOIL - """ - in_file = Path(in_file) if in_file else Path(input("Please specify the input file: ")) - self.ready_to_interact = True - p_intf.save2profoil_in(in_file.open().read()) - self.run_from_profoil_in() - self.initial_plot() - - # self.plainTextEdit_profoil_in.setPlainText(p_intf.catfile(WORKDIR/"profoil.in", tail=0)) - - def save_airfoil(self, out_file): - """ - Saves the profoil.in file from the WORKDIR in to a specified location with a given name. - For the ease of use, if the given path does not exist, the program creates the path for you. - """ - file_path = Path(out_file) - file_path.parent.mkdir(parents=True, exist_ok=True) - with file_path.open("w") as f: - f.write(Path(WORKDIR/"profoil.in").open().read()) + # Flags + # ===== + self.ready_to_interact = False + # holds a flag to signal if program is ready to run. + # purpose is to stop errors from occurring by accidental key presses + # prior to loading files. + self.edit_mode = False + + def gen_gui_fig(self): + + # Setting up the main window/figure + + self.gui_fig = Figure(figsize=(12, 7)) + grid = plt.GridSpec(4,2, wspace=0.1, hspace=0.2) + + # Setting up the 3 main axes + # ue_ax : velocity distribution axes + # xy_ax : x,y airfoil contour axes + # an_ax : alpha*-nu axes + + self.ue_ax = self.gui_fig.add_subplot(grid[:3, 0]) + self.xy_ax = self.gui_fig.add_subplot(grid[ 3, 0]) + self.an_ax = self.gui_fig.add_subplot(grid[:3, 1]) + + self.setup_axes() + + self.gui_fig.subplots_adjust(left=0.05, right=0.98, top=0.96, bottom=0.08, hspace = 0.02, wspace=0.02) + self.gui_fig.canvas.mpl_connect('button_press_event', self.on_click) + + def setup_axes(self): + """ + initializes the axes + """ + self.ue_ax.n_untouch = 0 + self.xy_ax.n_untouch = 0 + self.an_ax.n_untouch = 1 # spline will be untouched + + self.ue_ax.set_title(r'$Velocity\ Distribution$') + self.ue_ax.set_ylabel(r'$V/V_{\infty}$') + + self.xy_ax.set_xlabel(r"$x/c$") + self.xy_ax.set_ylabel(r"$y/c$") + + self.an_ax.set_title(r'$\alpha^*(\phi) - Upper$') + self.an_ax.set_xlabel(r"$\phi$") + self.an_ax.grid(True) + + self.xy_ax.axes.set_aspect('equal', 'datalim') + # self.an_ax.axes.set_aspect('equal', 'datalim') + + self.setup_axes_limits() + + def setup_axes_limits(self): + """ + initializes the axes limits + """ + self.ue_ax.set_xlim(-0.08, 1.08) + self.ue_ax.set_ylim(0, 2.5) + + self.xy_ax.set_xlim(-0.08, 1.08) + + if self.active_surface == "Lower": + self.an_ax.set_xlim(*AN_PLOT_XLIMITS_LOWER) + self.an_ax.set_ylim(*(tuple(reversed(AN_PLOT_YLIMITS)) if AN_FLIP_YAXIS_LOWER_SURFACE else AN_PLOT_YLIMITS)) + if self.active_surface == "Upper": + self.an_ax.set_xlim(*AN_PLOT_XLIMITS_UPPER) + self.an_ax.set_ylim(*AN_PLOT_YLIMITS) + + def clear_axes(self): + """ + clears all axes data such that new session with new airfoil can be loaded. + """ + self.ue_ax.lines=[] + self.ue_ax.collections=[] + + self.xy_ax.lines=[] + self.xy_ax.collections=[] + + self.clear_an_ax() + + self.upper_nu_alfa_previous.set_data([],[]) + self.upper_nu_alfa_current.set_data([],[]) + self.upper_nu_alfa_prescribed.set_data([],[]) + self.upper_nu_alfa_modi.set_data([],[]) + + self.lower_nu_alfa_previous.set_data([],[]) + self.lower_nu_alfa_current.set_data([],[]) + self.lower_nu_alfa_prescribed.set_data([],[]) + self.lower_nu_alfa_modi.set_data([],[]) + + self.cursor_edit_line.set_data([],[]) + + def reset_toolbar(self): + """ + This function resets the toolbar such that all tool bar items are set to not-checked + """ + try: + for x in self.tool_bar.actions(): + if x.isChecked(): + x.trigger() + except: + pass + + def load_line(self, line): + """ + This function "loads a line" in to the cursor editor. + In summary it references a line object from the child class to be modified + """ + self.cancel_cursor_inputs() + self.nu_alfa = line + self.nu_alfa_points = self.nu_alfa.get_xydata().tolist() + self.gui_fig.canvas.draw() + + + def on_click(self, event): + """ + All the mouse click events goes here + """ + if not self.ready_to_interact: return + if not self.edit_mode: return + if event.inaxes!=self.an_ax: return + + # Left click + if event.button == 1: + self.cursor_edit_line_points.append([event.xdata,event.ydata]) + self.cursor_edit_line_points.sort() + self.cursor_edit_line.set_data(*list(zip(*self.cursor_edit_line_points))) + self.gui_fig.canvas.draw() + + # Right Click and spline is actually built + if event.button == 3: + self.apply_edits(event) + self.gui_fig.canvas.draw() + + def backup_zoomed_limits(self, surface): + """ + save zoomed limits so that switching between upper and lower surfaces cause resetting limits + """ + if surface == self.active_surface: return + if surface == "Upper": + self.lower_xlim = self.an_ax.get_xlim() + self.lower_ylim = self.an_ax.get_ylim() + if surface == "Lower": + self.upper_xlim = self.an_ax.get_xlim() + self.upper_ylim = self.an_ax.get_ylim() + + def select_surface(self, surface, first_time=False): + """ + Callback function that switches the upper and lower surfaces + through the radio buttons. + Depending on the preferences: + y-axis is inverted upon switching. + """ + self.reset_toolbar() + self.clear_an_ax() + if surface =="Upper": + # backup zoomed limits if applicable. + if not first_time: + self.backup_zoomed_limits(surface) + self.active_surface = "Upper" + + self.an_ax.set_xlim(*self.upper_xlim) + self.an_ax.set_ylim(*self.upper_ylim) + + self.an_ax.set_title(r'$\alpha^*(\phi) - Upper$') + + # axes transformation between ax limits and pixels are handled + # automatically by ax.add_line(...) + # ax.lines = [..] wouldn't work the same way + + self.an_ax.add_line(self.upper_nu_alfa_previous) + self.an_ax.add_line(self.upper_nu_alfa_current) + self.an_ax.add_line(self.upper_nu_alfa_prescribed) + self.an_ax.add_line(self.upper_nu_alfa_modi) + + # load_line makes the Line2D object which is passed active + # in the interactive plot. + + self.load_line(self.upper_nu_alfa_modi) + + if surface =="Lower": + # backup zoomed limits if applicable. + if not first_time: + self.backup_zoomed_limits(surface) + self.active_surface = "Lower" + + self.an_ax.set_xlim(*self.lower_xlim) + self.an_ax.set_ylim(*self.lower_ylim) + + self.an_ax.set_title(r'$\alpha^*(\phi) - Lower$') + + # axes transformation between pixel and ax limits are handled + # automatically by ax.add_line(...) + # ax.lines = [..] wouldn't work the same way + + self.an_ax.add_line(self.lower_nu_alfa_previous) + self.an_ax.add_line(self.lower_nu_alfa_current) + self.an_ax.add_line(self.lower_nu_alfa_prescribed) + self.an_ax.add_line(self.lower_nu_alfa_modi) + + # load_line makes the Line2D object which is passed active + # in the interactive plot. + + self.load_line(self.lower_nu_alfa_modi) + self.gui_fig.canvas.draw() + + def checkbox_toggle(self, label): + self.reset_toolbar() + if label=="Grid": + self.toggle_grid_lines() + else: + self.toggle_previous_plots() + + def toggle_previous_plots(self, event): + """ + Toggles the visibility of previous lines. + Will make effect from the next plot - not on the current plot + """ + self.SHOW_PREV_LINES = bool(event) + + # Goes through each axis and sets the visibility + # of the preserved lines. + # each ax class will hold a list of preserved lines. + for ax in [self.ue_ax, self.xy_ax]: + for line in ax.preserved_plots: + line.set_visible(self.SHOW_PREV_LINES) + for marker in ax.preserved_markers: + marker.set_visible(self.SHOW_PREV_LINES) + self.gui_fig.canvas.draw() + + def toggle_grid_lines(self, event): + """ + Toggles the grid lines on the an_ax. + Will make effect immediately + """ + self.GRID_ON = bool(event) + self.an_ax.grid(self.GRID_ON) + self.gui_fig.canvas.draw() + + def set_edit_mode_off(self): + self.edit_mode = False + self.btn_start_edits.setStyleSheet('QPushButton {color: black;}') + + def start_cursor_edits(self, event): + self.reset_toolbar() + self.edit_mode = not self.edit_mode + self.btn_start_edits.setStyleSheet('QPushButton {color: red;}' if self.edit_mode else 'QPushButton {color: black;}') + if not self.edit_mode: self.cancel_cursor_inputs() + + def cancel_cursor_inputs(self, event=None): + """ + Resets the cursor edit line in to an empty line. + Simple means to start over with a new cursor edit line. + """ + self.reset_toolbar() + if not self.ready_to_interact: return + self.cursor_edit_line.set_data([],[]) + self.cursor_edit_line_points = self.cursor_edit_line.get_xydata().tolist() + self.set_edit_mode_off() + self.gui_fig.canvas.draw() + + def apply_edits(self, event): + """ + Cursor edits are applied to the green line from the red line + """ + self.reset_toolbar() + # Better protection than if self.cursor_edit_line_points because one point cannot make spline + if len(self.cursor_edit_line_points)>1: + self.set_edit_mode_off() + x_data, y_data = self.nu_alfa.get_data() + spline = interp1d(*np.array(self.cursor_edit_line_points).T, + kind='linear', + bounds_error=False) + new_y = spline(x_data) + self.nu_alfa.set_ydata(np.where(np.isnan(new_y), y_data, new_y)) + self.save_edits_to_file() + self.cancel_cursor_inputs() + + def save_edits_to_file(self): + """ + Saves green line data in to the profoil.in file + """ + nu_upper, alfa_upper = self.upper_nu_alfa_modi.get_data() + nu_lower, alfa_lower = self.lower_nu_alfa_modi.get_data() + nu_list = list(nu_upper) + list(nu_lower) + alfa_list = list(alfa_upper) + list(alfa_lower) + p_intf.gen_buffer() + p_intf.gen_input_file(nu_list, alfa_list, len(nu_upper)) + + def undo_edits(self, event): + """ + This callback function resets the modifiable phi-alpha* distribution + back to the current converged data from the most recent run. + Typically used when the line edits + has to be reverted. + """ + self.reset_toolbar() + if not self.ready_to_interact: return + self.cancel_cursor_inputs() + self.upper_nu_alfa_modi.set_data(*self.upper_nu_alfa_current.get_data()) + self.lower_nu_alfa_modi.set_data(*self.lower_nu_alfa_current.get_data()) + self.save_edits_to_file() + self.gui_fig.canvas.draw() + + def plot_from_file(self, event): + """ + Reads profoil.in file in the work directory and updates the green line + This action confirms any manual modifications if applicable + """ + self.reset_toolbar() + nu, alfa, ile, phis = p_intf.extract_dmp("profoil.in") + + nu_upper = nu[:ile] + alfa_upper = alfa[:ile] + + nu_lower = nu[ile:] + alfa_lower = alfa[ile:] + + self.upper_nu_alfa_modi.set_data(nu_upper,alfa_upper) + self.lower_nu_alfa_modi.set_data(nu_lower,alfa_lower) + self.gui_fig.canvas.draw() + + def run_profoil(self, event=None): + """ + Executes PROFOIL with the following steps. + 1. Backup the previous line. # For reloading as required + 2. Resets the cursor edit line. # Because after the run cursor edit line should not be there. + 3. Creates buffer.in from the existing profoil.in file. + 4. Creates a new profoil.in file by replacing the FOIL lines with the data in the graph. + 5. Prints out the profoil.log file. + """ + self.reset_toolbar() + if not self.ready_to_interact: return + self.set_edit_mode_off() + self.bkp_previous_line() + self.cancel_cursor_inputs() + self.run_from_profoil_in() + self.gui_fig.canvas.draw() + + def revert(self, event): + """ + This callback function loads the converged data from the previous run + typically used when something goes wrong with the current run. + Equivalent to loading buffer.in instead of profoil.in in the work dir. + """ + self.reset_toolbar() + if not self.ready_to_interact: return + if not self.upper_nu_alfa_previous.get_data()[0]: return + self.set_edit_mode_off() + self.cancel_cursor_inputs() + p_intf.swap_buffer() + self.run_profoil() + self.gui_fig.canvas.draw() + + def proc_make_ax_old(self, ax, preserve_lines=1): + """ + This function accepts an axes object and applies the settings for previous plot. + 1. Keep only last few lines on the axes given by preserve_lines and purges away the rest + 2. Greys/blacks out the lines/markers + 3. Make the lines dashed + 4. Removes the legend + 5. Sets visibility + 6. Makes a list of preserved lines and collections to set the visibility later on through "History" check-box + """ + plots = ax.lines + + ax.preserved_plots = [] # list of references for preserved plots will be stored for clearing history + ax.preserved_markers = [] # list of references for preserved plots will be stored for clearing history + + untouch_plots = plots[:ax.n_untouch] + preserved_plots = plots[ax.n_untouch:][-preserve_lines:] + + for plot in untouch_plots: + plot.set_label('_nolegend_') + + for plot in preserved_plots: + plot.set_color(UE_PLOT_OLD_LINE_COLOR) + plot.set_linestyle(UE_PLOT_OLD_LINE_STYLE) + plot.set_label('_nolegend_') + plot.set_visible(self.SHOW_PREV_LINES) + + markers = ax.collections[-2* preserve_lines:] + for marker in markers: + marker.set_color(UE_PLOT_OLD_MARKER_COLOR) + marker.set_visible(self.SHOW_PREV_LINES) + + ax.preserved_plots = preserved_plots.copy() # list of references for clearing history + ax.preserved_markers = markers.copy() # list of references for clearing history + + # remove old plots except the untouchable and the ones from last run + for line in ax.lines[ax.n_untouch:-preserve_lines]: + line.remove() + + # remove markers except the markers of last run + for marker in ax.collections[:-2* preserve_lines]: + marker.remove() + + ax.set_prop_cycle(None) + + def proc_make_line_untouchable(self, ax, plot_index=-1): + """ + plot list is divided into 2 main sections. + untouchable plots ; these hold fixed REF lines + touchable plots ; these hold variable plots. + untouchable plots were pushed towards the beginning of the + plot list such that the remaining plots can be modified with + slicing with ease + """ + plots = ax.lines + reserve_plot = plots[plot_index] + del plots[plot_index] + plots.insert(ax.n_untouch, reserve_plot) + ax.n_untouch += 1 + + def remove_last_untouchable(self, ax): + """ + Pops the last object from the "untouchable plots" section of ax.lines list + """ + plots = ax.lines + del plots[ax.n_untouch -1] + ax.n_untouch -= 1 + + def plot_xy(self, n_prev_plots =1): + """ + Plots airfoil contour + """ + n_xy_preserve = n_prev_plots + + # make the previous plots "old" [greyed out and dashed etc] + self.proc_make_ax_old(self.xy_ax, n_xy_preserve) + + # plots the airfoil contour + # color of the original plot is extracted back to make the upper and lower markers. + + p = self.xy_ax.plot(self.x, self.y, lw=XY_PLOT_LINEWIDTH, color=XY_PLOT_COLOR, clip_on=False) + self.xy_ax.scatter(self.xy_marker_upper['x'], self.xy_marker_upper['y'], color=p[-1].get_color(), marker=UPPER_SURFACE_PHI_MARKER, s=UPPER_SURFACE_PHI_MARKER_SIZE, clip_on=False) + self.xy_ax.scatter(self.xy_marker_lower['x'], self.xy_marker_lower['y'], color=p[-1].get_color(), marker=LOWER_SURFACE_PHI_MARKER, s=LOWER_SURFACE_PHI_MARKER_SIZE, clip_on=False) + + self.gui_fig.canvas.draw() + + + def plot_ue(self, n_prev_plots =1): + """ + Plots velocity distribution data + """ + n_cols = len(self.ue_lines.keys()) + n_ue_preserve = n_cols * n_prev_plots # alphas give the number of lines + + # make the previous plots "old" [greyed out and dashed etc] + self.proc_make_ax_old(self.ue_ax, n_ue_preserve) + + # plots the velocity distribution + # color of the original plot is extracted back to make the upper and lower markers. + for alpha in sorted(self.ue_lines.keys(), key=float): + p = self.ue_ax.plot(self.ue_lines[alpha]['x'], self.ue_lines[alpha]['v_vinf'], label = "{:5.2f}".format(alpha), lw=UE_PLOT_LINEWIDTH, color=UE_PLOT_COLOR, clip_on=False) + self.ue_ax.scatter(self.upper_vel_markers[alpha]['x'], self.upper_vel_markers[alpha]['v_vinf'], color=p[-1].get_color(), marker=UPPER_SURFACE_PHI_MARKER, s=UPPER_SURFACE_PHI_MARKER_SIZE, clip_on=False) + self.ue_ax.scatter(self.lower_vel_markers[alpha]['x'], self.lower_vel_markers[alpha]['v_vinf'], color=p[-1].get_color(), marker=LOWER_SURFACE_PHI_MARKER, s=LOWER_SURFACE_PHI_MARKER_SIZE, clip_on=False) + + # legend is not used in the current implementation because Alphas are just dummy variables. + # can modify easily in the future if Alphas to be read from the .in file. + # self.ue_ax.legend(fontsize ='small', frameon = False, loc="upper right") + + self.gui_fig.canvas.draw() + + def bkp_previous_line(self): + """ + data from "current" plot sets data on "previous" plot + """ + self.upper_nu_alfa_previous.set_data(*self.upper_nu_alfa_current.get_data()) + self.lower_nu_alfa_previous.set_data(*self.lower_nu_alfa_current.get_data()) + + def clear_an_ax(self): + """ + Removes all lines except the "untouchable plots" section + ax.clear(...) wouldn't work here because it resets all the limits and + de-reference the axes from the cursor editor. + """ + for line in self.an_ax.lines[self.an_ax.n_untouch:]: + line.remove() + + def plot_nu_alfa(self): + """ + As per the overall description on the top, plotting of phi-alpha* distribution + does not generate new plot lines (Line2D objects) using ax.plot(...) + The state of these 8 lines will be maintained with the same ID by changing + the data these lines hold. This method vastly eases off the surface switching. + """ + self.upper_nu_alfa_prescribed.set_data(*self.upper_nu_alfa_modi.get_data()) + self.upper_nu_alfa_modi.set_data(self.nu_upper, self.alfa_upper) + self.upper_nu_alfa_current.set_data(self.nu_upper, self.alfa_upper) + + self.lower_nu_alfa_prescribed.set_data(*self.lower_nu_alfa_modi.get_data()) + self.lower_nu_alfa_modi.set_data(self.nu_lower, self.alfa_lower) + self.lower_nu_alfa_current.set_data(self.nu_lower, self.alfa_lower) + + def overlay_dat(self, datfile, skiprows): + """ + This function overlays a given dat file contour in the xy plot. + File formats with different number of header are supported + """ + try: + x,y = np.loadtxt(Path(datfile), skiprows=skiprows).T + except: + self.overlay_error_dialog() + return + + self.xy_ax.plot(x, y, OVERLAY_LINESTYLE, lw=OVERLAY_LINEWIDTH, color=OVERLAY_LINECOLOR, markersize=OVERLAY_MARKERSIZE, clip_on=False) + self.proc_make_line_untouchable(self.xy_ax) + self.gui_fig.canvas.draw() + + def clear_overlay(self): + """ + This function removes the the overlay in LIFO order + If more than one overlay being added the last one + will be cleared off first + """ + self.remove_last_untouchable(self.xy_ax) + self.gui_fig.canvas.draw() + + def update_file_view(self): + """ + Updates the text boxes in the File View tab + """ + self.plainTextEdit_profoil_log.setPlainText(p_intf.catfile(BINDIR/"profoil.log", tail=0)) + self.plainTextEdit_profoil_in.setPlainText(p_intf.catfile(BINDIR/"profoil.in", tail=0)) + + def update_summary_text(self): + """ + updates the summary label in the design view + """ + self.lbl_summary.setText(p_intf.catfile(BINDIR/"profoil.log", tail=14)) + + def extract_all_profoil_data(self): + """ + Once the PROFOIL is finished running, the data will be in the BINDIR. + This functions updates all the relevant fields in the UI + from the PROFOIL output files in one go + """ + self.x, \ + self.y, \ + self.xy_marker_upper, \ + self.xy_marker_lower, \ + self.ue_lines, \ + self.upper_vel_markers,\ + self.lower_vel_markers,\ + self.nu_upper, \ + self.alfa_upper, \ + self.nu_lower, \ + self.alfa_lower, \ + self.ile = p_intf.extract_all_data() + + def run_from_profoil_in(self): + """ + Executes PROFOIL when the profoil.in file is ready in the WORKDIR + """ + + # copy profoil.in file from work_directory in to bin directory + # execute profoil + # bring all the output files back in to work_directory + # last step is for conceptual isolation between bin and work + # because the airfoil designer is supposed to work on work_directory + + p_intf.work2bin() + p_intf.exec_profoil() + p_intf.bin2work() + + # profoil run may or may not have been successful. + # either way, file view has to be updated. + # conditional logic follows + + self.update_file_view() + + if p_intf.is_design_converged(): + p_intf.bin2work() + self.extract_all_profoil_data() + self.update_summary_text() + self.plot_ue() + self.plot_xy() + self.plot_nu_alfa() + + else: + self.failure_error_dialog() + + def initial_plot(self): + """ + Initial plot is called once at the initial setup. + Before calling initial_plot its required to have run_from_profoil_in(...) called. + """ + self.select_surface("Upper", first_time =True) + self.gui_fig.canvas.draw() + + def load_in_file(self, in_file=None): + """ + Loads a *.in file in to the program. Keeps on calling until a valid file is provided. + 1. Sets the ready_to_interact flag to make sure no errors will occur by pressing a random button. + 2. copies *.in file in to WORKDIR as profoil.in + 3. Runs PROFOIL + """ + in_file = Path(in_file) if in_file else Path(input("Please specify the input file: ")) + self.ready_to_interact = True + p_intf.save2profoil_in(in_file.open().read()) + self.run_from_profoil_in() + self.initial_plot() + + def save_airfoil(self, out_file): + """ + Saves the profoil.in file from the WORKDIR in to a specified location with a given name. + For the ease of use, if the given path does not exist, the program creates the path for you. + """ + file_path = Path(out_file) + file_path.parent.mkdir(parents=True, exist_ok=True) + with file_path.open("w") as f: + f.write(Path(WORKDIR/"profoil.in").open().read()) \ No newline at end of file diff --git a/profoil_interface.py b/profoil_interface.py index 4b2812a..96cc31f 100644 --- a/profoil_interface.py +++ b/profoil_interface.py @@ -1,3 +1,23 @@ +# Copyright (c) 2022 Kanishka Jayawardane [kanishkagj@yahoo.com] + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + # This modules interfaces PROFOIL-UI with PROFOIL. # One design feature was to have a clear separation between the input and output of PROFOIL has been maintained # meaning profoil.in file has nothing to do with what is been extracted to do the plots and vice versa. @@ -35,234 +55,244 @@ from pathlib import Path import shutil -WORKDIR = Path(WORKDIR) -BINDIR = Path(BINDIR) -UI_DIR = Path.cwd() +WORKDIR = Path(WORK_DIR).resolve() # using absolute paths +BINDIR = Path(BIN_DIR).resolve() # using absolute paths + +# changing the working directory in to BINDIR +# so that its not necessary to switch back and forth between +# WORKDIR and BINDIR on each profoil run + +os.chdir(BINDIR) def extract_alphas(filename="profoil.in"): - """ - Extracts design alpha values from the profoil.in file - """ - alphas = [] - lines = Path(WORKDIR/filename).open().readlines() - for i, line in enumerate(lines): - if line.startswith("ALFASP "): - n = int(re.findall("ALFASP\s+(\d+)", line)[0]) - for j in range(n): - alphas.append(float(lines[i+j+1])) - assert len(alphas) == n - return alphas + """ + Extracts design alpha values from the profoil.in file + This functions is not currently being used but could + be useful if alphas to be included as a legend + in the velocity plot + """ + alphas = [] + lines = Path(WORKDIR/filename).open().readlines() + for i, line in enumerate(lines): + if line.startswith("ALFASP "): + n = int(re.findall("ALFASP\s+(\d+)", line)[0]) + for j in range(n): + alphas.append(float(lines[i+j+1])) + assert len(alphas) == n + return alphas def extract_xy(filename="profoil.xy"): - """ - Extracts x,y data from profoil.xy file - """ - x,y = np.loadtxt(Path(WORKDIR/filename)).T - return x,y + """ + Extracts x,y data from profoil.xy file + """ + x,y = np.loadtxt(Path(WORKDIR/filename)).T + return x,y def extract_vel(filename="profoil.vel"): - """ - Extracts v/v_inf data from profoil.vel file - First column is expected to carry "phi" data - generated with VELDIST 60 - """ - phi, v_vinf= np.loadtxt(Path(WORKDIR/filename)).T - return phi, v_vinf + """ + Extracts v/v_inf data from profoil.vel file + First column is expected to carry "phi" data + generated with VELDIST 60 + """ + phi, v_vinf= np.loadtxt(Path(WORKDIR/filename)).T + return phi, v_vinf def extract_dmp(filename="profoil.dmp"): - """ - Extracts the converged nu-alpha* pairs, LE index, - and recovery parameters from the profoil.dump file. - regex is used exclusively for all extractions - with multi-line flag. - """ - text = Path(WORKDIR/filename).open().read() - foil_section = re.findall(r"^FOIL.*(?:\nFOIL.*)*$", text, flags = re.M)[0] - nu, alfa = np.loadtxt(StringIO(foil_section), usecols=(1,2)).T - ile = int(re.findall(r"^ILE\s+(\d+)", text, flags=re.M)[0]) - phis = [float(i) for i in re.findall(r"^PHIS\s+(.*)", text, flags=re.M)[0].split()] - return nu, alfa, ile, phis + """ + Extracts the converged nu-alpha* pairs, LE index, + and recovery parameters from the profoil.dump file. + regex is used exclusively for all extractions + with multi-line flag. + """ + text = Path(WORKDIR/filename).open().read() + foil_section = re.findall(r"^FOIL.*(?:\nFOIL.*)*$", text, flags = re.M)[0] + nu, alfa = np.loadtxt(StringIO(foil_section), usecols=(1,2)).T + ile = int(re.findall(r"^ILE\s+(\d+)", text, flags=re.M)[0]) + phis = [float(i) for i in re.findall(r"^PHIS\s+(.*)", text, flags=re.M)[0].split()] + return nu, alfa, ile, phis def split_vel(phi, v_vinf): - """ - PROFOIL writes Non-dimensionalized velocities over the airfoil contour - in a continuous stream of numbers without breaks for each AoA. - This continuous stream is split into a list of phi-v/v_inf pairs - using phi==0 (start of each AoA) locators. - - PS: Splitting into fixed length chunks won't work here because depending - on the placement of the LE stagnation point, an additional point may or - may not be added in to the stream. - """ - breaks = np.argwhere(phi==0).flatten()[1:] - phi_list= np.split(phi, breaks) - vel_list= np.split(v_vinf, breaks) - return phi_list, vel_list + """ + PROFOIL writes Non-dimensionalized velocities over the airfoil contour + in a continuous stream of numbers without breaks for each AoA. + This continuous stream is split into a list of phi-v/v_inf pairs + using phi==0 (start of each AoA) locators. + + PS: Splitting into fixed length chunks won't work here because depending + on the placement of the LE stagnation point, an additional point may or + may not be added in to the stream. + """ + breaks = np.argwhere(phi==0).flatten()[1:] + phi_list= np.split(phi, breaks) + vel_list= np.split(v_vinf, breaks) + return phi_list, vel_list def gen_vel_splines(phi_list, vel_list): - """ - Takes a list of phi-v/v_inf pairs and creates a spline for each - phi-v/v_inf distribution. These splines will be used to locate the - phi markers on the upper and lower surfaces. - """ - return [interp1d(phi,v) for phi,v in zip(phi_list, vel_list)] + """ + Takes a list of phi-v/v_inf pairs and creates a spline for each + phi-v/v_inf distribution. These splines will be used to locate the + phi markers on the upper and lower surfaces. + """ + return [interp1d(phi,v) for phi,v in zip(phi_list, vel_list)] def gen_phi2xy_splines(x, y): - """ - Creates 2 splines which maps; - phi->x - phi->y - x,y coordinates are taken from profoil.xy file and equidistant phi distribution is presumed. - """ - phis = np.linspace(0,360, len(x)) - return interp1d(phis,x), interp1d(phis,y) + """ + Creates 2 splines which maps; + phi->x + phi->y + x,y coordinates are taken from profoil.xy file and equidistant phi distribution is presumed. + """ + phis = np.linspace(0,360, len(x)) + return interp1d(phis,x), interp1d(phis,y) def extract_all_data(): - """ - This lengthy function could be somewhat problematic to understand at the first glance; - hence the below diagram for better clarity. + """ + This lengthy function could be somewhat problematic to understand at the first glance; + hence the below diagram for better clarity. - +-----------+ +-----------+ +-----------+ - |profoil.xy | |profoil.dmp| |profoil.vel| - +-----------+ +-----------+ +-----------+ + +-----------+ +-----------+ +-----------+ + |profoil.xy | |profoil.dmp| |profoil.vel| + +-----------+ +-----------+ +-----------+ | | | | | - v v v v v - +--++--+ +--------------------+ +---++---------+ - | x||y | |phi alpha ile phi_S | |phi||v/v_inf | - +--++--+ +--------+-----------+ +---++---------+ + v v v v v + +--++--+ +--------------------+ +---++---------+ + | x||y | |phi alpha ile phi_S | |phi||v/v_inf | + +--++--+ +--------+-----------+ +---++---------+ | | | | - v | v v - +-------+-------+ | +---------++-----------+ - +--+ phi2x |phi2y +---+ | | phi_list||vv_inf_list| - | +-------+-------+ | | ++--------++-----------+ + v | v v + +-------+-------+ | +---------++-----------+ + +--+ phi2x |phi2y +---+ | | phi_list||vv_inf_list| + | +-------+-------+ | | ++--------++-----------+ | | | | | | - | v +--v---v------+ | v - | +-----------+ |lower_markers| | +------------------+ - | | xy_markers| |upper_markers| | | phi2v_spline_list| - | +-----------+ +-------------+ | +------------------+ + | v +--v---v------+ | v + | +-----------+ |lower_markers| | +------------------+ + | | xy_markers| |upper_markers| | | phi2v_spline_list| + | +-----------+ +-------------+ | +------------------+ | | | - | | v - | | +--------+ - +-----------------------------------+---->|ue_lines| - +--------+ - - main point to understand is, whatever the data not represented as f(x) has to be - transformed into f(x), using splines in the form of spl(phi). popular interp1d spline - is used here which appears to work without any issue given phi increases monotonically. - Additionally, for the airfoil contour, x(phi) and y(phi) has to be constructed because - the markers are given in phi. - """ - - # extract row data from output files - phi, vel = extract_vel() - nu, alfa, ile, (phis_upper, phis_lower) = extract_dmp() - x,y = extract_xy() - - # create splines - phi2x_spline, phi2y_spline = gen_phi2xy_splines(x,y) - phi_list, vel_list = split_vel(phi, vel) - phi2v_spline_list = gen_vel_splines(phi_list, vel_list) - - design_alphas = range(len(phi_list)) - # design_alphas is just a dummy alpha list. - # this can be replaced with extract_alphas() if needed - # but this will place a constraint on having alphas listed in the .in file. - # since listing alphas in the plot is not mandatory, a simple range would work here - - # creates x-v/v_inf distribution from phi-v/v_inf distribution using phi2x_spline. - ue_lines = {alfa:{"x": phi2x_spline(phi), "v_vinf": spl(phi)} - for alfa, phi, spl - in zip(design_alphas, phi_list, phi2v_spline_list)} - - # creates a list of phi values corresponding to FOIL lines. - # This is done by transforming the FOIL line phi by a constant factor NU2PHI - # caution; these markers are not sorted - since they will be plotted as scatter. - - NU2PHI = 6 # degrees around the circle / nu_max in the FOIL lines which is 60. - - nu_upper = nu[:ile].tolist() - alfa_upper = alfa[:ile].tolist() - upper_markes_phi = np.array([phis_upper] + nu_upper) * NU2PHI - - nu_lower = nu[ile:].tolist() - alfa_lower = alfa[ile:].tolist() - lower_markes_phi = np.array(nu_lower + [phis_lower]) * NU2PHI - - # Triangular marker positions on upper and lower surfaces are determined using the previously - # transformed phi values. This is done on upper and lower surface velocity distributions - # and upper and lower x,y coordinates. - - upper_vel_markers = {alfa:{"x": phi2x_spline(upper_markes_phi), "v_vinf": spl(upper_markes_phi)} - for alfa, spl - in zip(design_alphas, phi2v_spline_list)} - - lower_vel_markers = {alfa:{"x": phi2x_spline(lower_markes_phi), "v_vinf": spl(lower_markes_phi)} - for alfa, spl - in zip(design_alphas, phi2v_spline_list)} - - xy_marker_upper = {"x" : phi2x_spline(upper_markes_phi) , "y": phi2y_spline(upper_markes_phi)} - xy_marker_lower = {"x" : phi2x_spline(lower_markes_phi) , "y": phi2y_spline(lower_markes_phi)} - - return x,y, xy_marker_upper, xy_marker_lower, \ - ue_lines, upper_vel_markers, lower_vel_markers, \ - nu_upper, alfa_upper, nu_lower, alfa_lower, ile + | | v + | | +--------+ + +-----------------------------------+---->|ue_lines| + +--------+ + + main point to understand is, whatever the data not represented as f(x) has to be + transformed into f(x), using splines in the form of spl(phi). popular interp1d spline + is used here which appears to work without any issue given phi increases monotonically. + Additionally, for the airfoil contour, x(phi) and y(phi) has to be constructed because + the markers are given in phi. + """ + + # extract row data from output files + phi, vel = extract_vel() + nu, alfa, ile, (phis_upper, phis_lower) = extract_dmp() + x,y = extract_xy() + + # create splines + phi2x_spline, phi2y_spline = gen_phi2xy_splines(x,y) + phi_list, vel_list = split_vel(phi, vel) + phi2v_spline_list = gen_vel_splines(phi_list, vel_list) + + design_alphas = range(len(phi_list)) + # design_alphas is just a dummy alpha list. + # this can be replaced with extract_alphas() if needed + # but this will place a constraint on having alphas listed in the .in file. + # since listing alphas in the plot is not mandatory, a simple range would work here + + # creates x-v/v_inf distribution from phi-v/v_inf distribution using phi2x_spline. + ue_lines = {alfa:{"x": phi2x_spline(phi), "v_vinf": spl(phi)} + for alfa, phi, spl + in zip(design_alphas, phi_list, phi2v_spline_list)} + + # creates a list of phi values corresponding to FOIL lines. + # This is done by transforming the FOIL line phi by a constant factor NU2PHI + # caution; these markers are not sorted - since they will be plotted as scatter. + + NU2PHI = 6 # degrees around the circle / nu_max in the FOIL lines which is 60. + + nu_upper = nu[:ile].tolist() + alfa_upper = alfa[:ile].tolist() + upper_markes_phi = np.array([phis_upper] + nu_upper) * NU2PHI + + nu_lower = nu[ile:].tolist() + alfa_lower = alfa[ile:].tolist() + lower_markes_phi = np.array(nu_lower + [phis_lower]) * NU2PHI + + # Triangular marker positions on upper and lower surfaces are determined using the previously + # transformed phi values. This is done on upper and lower surface velocity distributions + # and upper and lower x,y coordinates. + + upper_vel_markers = {alfa:{"x": phi2x_spline(upper_markes_phi), "v_vinf": spl(upper_markes_phi)} + for alfa, spl + in zip(design_alphas, phi2v_spline_list)} + + lower_vel_markers = {alfa:{"x": phi2x_spline(lower_markes_phi), "v_vinf": spl(lower_markes_phi)} + for alfa, spl + in zip(design_alphas, phi2v_spline_list)} + + xy_marker_upper = {"x" : phi2x_spline(upper_markes_phi) , "y": phi2y_spline(upper_markes_phi)} + xy_marker_lower = {"x" : phi2x_spline(lower_markes_phi) , "y": phi2y_spline(lower_markes_phi)} + + return x,y, xy_marker_upper, xy_marker_lower, \ + ue_lines, upper_vel_markers, lower_vel_markers, \ + nu_upper, alfa_upper, nu_lower, alfa_lower, ile def gen_input_template(filename="profoil.in"): - """ - Takes profoil.in file and devoid the FOIL section containing - nu-alpha* block along with ILE line so that these 2 sections - can then be filled up by profoil-ui data - As a precautionary measure, VELDIST will be set to 60 as well. - """ - file_content = Path(WORKDIR/filename).open().read() - foils_blk_devoided = re.sub(r"^FOIL.*(?:\nFOIL.*)*$", r"{}", file_content, flags=re.M) - foils_ile_devoided = re.sub(r"(^ILE\s+)\d+", r"\1{}", foils_blk_devoided, flags=re.M) - foils_ile_vdist_fixed = re.sub(r"^VELDIST\s+\d+", r"VELDIST 60", foils_ile_devoided, flags=re.M) - return foils_ile_vdist_fixed + """ + Takes profoil.in file and devoid the FOIL section containing + nu-alpha* block along with ILE line so that these 2 sections + can then be filled up by profoil-ui data + As a precautionary measure, VELDIST will be set to 60 as well. + """ + file_content = Path(WORKDIR/filename).open().read() + foils_blk_devoided = re.sub(r"^FOIL.*(?:\nFOIL.*)*$", r"{}", file_content, flags=re.M) + foils_ile_devoided = re.sub(r"(^ILE\s+)\d+", r"\1{}", foils_blk_devoided, flags=re.M) + foils_ile_vdist_fixed = re.sub(r"^VELDIST\s+\d+", r"VELDIST 60", foils_ile_devoided, flags=re.M) + return foils_ile_vdist_fixed def gen_foil_line(nu, alpha, i, ile, delta_alpha=0): - """ - generates a foil line with ILE marker for easy REF - """ - return "FOIL{:12.5f}{:12.5f}{:5d}{:4d}{}".format(nu, alpha, i, delta_alpha, - " <--- ILE" if i == ile else "") + """ + generates a foil line with ILE marker for easy REF + """ + return "FOIL{:12.5f}{:12.5f}{:5d}{:4d}{}".format(nu, alpha, i, delta_alpha, + " <--- ILE" if i == ile else "") def gen_input_file(nu_list, alpha_list, ile): - """ - Generates profoil.in file using passed nu-alpha pairs and LE seg. - Only the FOIL section and ILE will be updated - while keeping the rest of the original profoil.in file intact. - """ - file_template = gen_input_template() - foils_section = "\n".join([gen_foil_line(nu, alpha, i, ile) - for i, (nu, alpha) - in enumerate(zip(nu_list,alpha_list), start=1)]) - save2profoil_in(file_template.format(foils_section, ile)) + """ + Generates profoil.in file using passed nu-alpha pairs and LE seg. + Only the FOIL section and ILE will be updated + while keeping the rest of the original profoil.in file intact. + """ + file_template = gen_input_template() + foils_section = "\n".join([gen_foil_line(nu, alpha, i, ile) + for i, (nu, alpha) + in enumerate(zip(nu_list,alpha_list), start=1)]) + save2profoil_in(file_template.format(foils_section, ile)) def save2profoil_in(text, filename=WORKDIR/"profoil.in"): - """ - saves changes to the profoil.in file if the contents were actually altered. - """ - if catfile(WORKDIR/"profoil.in") == text: return - with Path(filename).open("w") as f: - f.write(text) + """ + saves changes to the profoil.in file if the contents were actually altered. + """ + if catfile(WORKDIR/"profoil.in") == text: return + with Path(filename).open("w") as f: + f.write(text) def is_design_converged(filename="profoil.log"): - """ - Upon running PROFOIL.exe this function examines profoil.log file - for successful completion of airfoil design. - """ - return "AIRFOIL DESIGN IS FINISHED" in Path(BINDIR/filename).open().read() + """ + Upon running PROFOIL.exe this function examines profoil.log file + for successful completion of airfoil design. + Note: + Probably its best to check for "STATISTICS" line instead. + Because there could be errors in calculations in VELDIST for ex: + even after the design is converged + """ + return "AIRFOIL DESIGN IS FINISHED" in Path(BINDIR/filename).open().read() def exec_profoil(): - """ - Executes PROFOIL.exe located in the BINDIR - chdir found to be required to do this without using subprocess. - """ - os.chdir(BINDIR) - os.system("profoil.exe > profoil.log") - os.chdir(UI_DIR) + """ + Executes PROFOIL.exe located in the BINDIR + chdir found to be required to do this without using subprocess. + """ + os.system("{} > profoil.log".format("profoil.exe" if os.name == "nt" else "./profoil")) """ Below utility functions are self explanatory. @@ -270,27 +300,27 @@ def exec_profoil(): shutil is used to keep the generality between platforms. """ def work2bin(): - shutil.copy(WORKDIR/"profoil.in", BINDIR/"profoil.in") + shutil.copy(WORKDIR/"profoil.in", BINDIR/"profoil.in") def gen_buffer(): - shutil.copy(WORKDIR/"profoil.in", WORKDIR/"buffer.in") + shutil.copy(WORKDIR/"profoil.in", WORKDIR/"buffer.in") def swap_buffer(): - shutil.copy(WORKDIR/"profoil.in", WORKDIR/"temp.in") - shutil.copy(WORKDIR/"buffer.in", WORKDIR/"profoil.in") - shutil.copy(WORKDIR/"temp.in", WORKDIR/"buffer.in") + shutil.copy(WORKDIR/"profoil.in", WORKDIR/"temp.in") + shutil.copy(WORKDIR/"buffer.in", WORKDIR/"profoil.in") + shutil.copy(WORKDIR/"temp.in", WORKDIR/"buffer.in") def bin2work(): - shutil.copy(BINDIR/"profoil.xy" , WORKDIR/"profoil.xy" ) - shutil.copy(BINDIR/"profoil.vel", WORKDIR/"profoil.vel") - shutil.copy(BINDIR/"profoil.dmp", WORKDIR/"profoil.dmp") - shutil.copy(BINDIR/"profoil.log", WORKDIR/"profoil.log") + shutil.copy(BINDIR/"profoil.xy" , WORKDIR/"profoil.xy" ) + shutil.copy(BINDIR/"profoil.vel", WORKDIR/"profoil.vel") + shutil.copy(BINDIR/"profoil.dmp", WORKDIR/"profoil.dmp") + shutil.copy(BINDIR/"profoil.log", WORKDIR/"profoil.log") def catfile(filename, tail=0): - """ - Equivalent to linux 'cat' command. option 'tail' is to specify - the number of tail lines to show. tail=0 means the whole file - will be displayed. - """ - lines = Path(filename).open().readlines() - return "".join(lines[-tail:]) \ No newline at end of file + """ + Equivalent to linux 'cat' command. option 'tail' is to specify + the number of tail lines to show. tail=0 means the whole file + will be displayed. + """ + lines = Path(filename).open().readlines() + return "".join(lines[-tail:]) \ No newline at end of file diff --git a/profoil_ui.py b/profoil_ui.py index fe7eda8..0c22406 100644 --- a/profoil_ui.py +++ b/profoil_ui.py @@ -1,8 +1,22 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'PROFOIL-UI.ui' -# Created by: PyQt5 UI code generator 5.9.2 -# WARNING! All changes made in this file will be lost! +# Copyright (c) 2022 Kanishka Jayawardane [kanishkagj@yahoo.com] + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtWidgets import QMessageBox @@ -11,15 +25,11 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.backend_bases import key_press_handler +from GUIMainWindow import Ui_MainWindow from profoil_canvas import ProfoilCanvas from preferences import * import profoil_interface as p_intf -from pathlib import Path -WORKDIR = Path(WORKDIR) -BINDIR = Path(BINDIR) -UI_DIR = Path.cwd() - """ Below is a monkey patch to handle a possible bug in matplotlib. regardless of the back-end, matplotlib tool-bar home-button, doesn't appear to redraw even when the frameon=True. @@ -30,358 +40,142 @@ home = NavigationToolbar.home def patched_home(self, *args, **kwargs): - home(self, *args, **kwargs) - ui.setup_axes_limits() + home(self, *args, **kwargs) + ui.setup_axes_limits() NavigationToolbar.home = patched_home -class Ui_MainWindow(QtWidgets.QMainWindow, ProfoilCanvas): - def __init__(self): - QtWidgets.QMainWindow.__init__(self) - ProfoilCanvas.__init__(self) - - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - # MainWindow.resize(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT) - MainWindow.setMinimumSize(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName("centralwidget") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget) - self.horizontalLayout.setObjectName("horizontalLayout") - self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) - font = QtGui.QFont() - font.setPointSize(8) - # font.setFamily("Courier") - self.tabWidget.setFont(font) - self.tabWidget.setObjectName("tabWidget") - self.widget = QtWidgets.QWidget() - self.widget.setObjectName("widget") - self.horizontalLayout_10 = QtWidgets.QHBoxLayout(self.widget) - self.horizontalLayout_10.setObjectName("horizontalLayout_10") - self.verticalLayout_canvas = QtWidgets.QVBoxLayout() - self.verticalLayout_canvas.setObjectName("verticalLayout_canvas") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.verticalLayout_canvas.addItem(spacerItem) - self.horizontalLayout_10.addLayout(self.verticalLayout_canvas) - self.verticalLayout = QtWidgets.QVBoxLayout() - self.verticalLayout.setObjectName("verticalLayout") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.checkBox_grid = QtWidgets.QCheckBox(self.widget) - self.checkBox_grid.setChecked(True) - self.checkBox_grid.setObjectName("checkBox_grid") - self.horizontalLayout_3.addWidget(self.checkBox_grid) - self.checkBox_history = QtWidgets.QCheckBox(self.widget) - self.checkBox_history.setChecked(True) - self.checkBox_history.setObjectName("checkBox_history") - self.horizontalLayout_3.addWidget(self.checkBox_history) - self.verticalLayout.addLayout(self.horizontalLayout_3) - self.horizontalLayout_9 = QtWidgets.QHBoxLayout() - self.horizontalLayout_9.setObjectName("horizontalLayout_9") - self.label_4 = QtWidgets.QLabel(self.widget) - self.label_4.setObjectName("label_4") - self.horizontalLayout_9.addWidget(self.label_4) - self.combo_switch_surface = QtWidgets.QComboBox(self.widget) - self.combo_switch_surface.setObjectName("combo_switch_surface") - self.combo_switch_surface.addItem("") - self.combo_switch_surface.addItem("") - self.horizontalLayout_9.addWidget(self.combo_switch_surface) - self.horizontalLayout_9.setStretch(0, 1) - self.horizontalLayout_9.setStretch(1, 3) - self.verticalLayout.addLayout(self.horizontalLayout_9) - self.btn_start_edits = QtWidgets.QPushButton(self.widget) - font = QtGui.QFont() - font.setPointSize(8) - self.btn_start_edits.setFont(font) - self.btn_start_edits.setObjectName("btn_start_edits") - self.verticalLayout.addWidget(self.btn_start_edits) - self.horizontalLayout_8 = QtWidgets.QHBoxLayout() - self.horizontalLayout_8.setObjectName("horizontalLayout_8") - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_8.addItem(spacerItem1) - self.btn_cancel = QtWidgets.QPushButton(self.widget) - font = QtGui.QFont() - font.setPointSize(8) - self.btn_cancel.setFont(font) - self.btn_cancel.setObjectName("btn_cancel") - self.horizontalLayout_8.addWidget(self.btn_cancel) - self.verticalLayout.addLayout(self.horizontalLayout_8) - self.btn_apply_edits = QtWidgets.QPushButton(self.widget) - font = QtGui.QFont() - font.setPointSize(8) - self.btn_apply_edits.setFont(font) - self.btn_apply_edits.setObjectName("btn_apply_edits") - self.verticalLayout.addWidget(self.btn_apply_edits) - self.horizontalLayout_6 = QtWidgets.QHBoxLayout() - self.horizontalLayout_6.setObjectName("horizontalLayout_6") - spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_6.addItem(spacerItem2) - self.btn_undo = QtWidgets.QPushButton(self.widget) - font = QtGui.QFont() - font.setPointSize(8) - self.btn_undo.setFont(font) - self.btn_undo.setObjectName("btn_undo") - self.horizontalLayout_6.addWidget(self.btn_undo) - self.verticalLayout.addLayout(self.horizontalLayout_6) - self.btn_plot_from_file = QtWidgets.QPushButton(self.widget) - font = QtGui.QFont() - font.setPointSize(8) - self.btn_plot_from_file.setFont(font) - self.btn_plot_from_file.setObjectName("btn_plot_from_file") - self.verticalLayout.addWidget(self.btn_plot_from_file) - self.btn_run_profoil = QtWidgets.QPushButton(self.widget) - font = QtGui.QFont() - font.setPointSize(8) - self.btn_run_profoil.setFont(font) - self.btn_run_profoil.setStyleSheet("background-color:yellowgreen;") - self.btn_run_profoil.setObjectName("btn_run_profoil") - self.verticalLayout.addWidget(self.btn_run_profoil) - self.horizontalLayout_7 = QtWidgets.QHBoxLayout() - self.horizontalLayout_7.setObjectName("horizontalLayout_7") - spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_7.addItem(spacerItem3) - self.btn_revert = QtWidgets.QPushButton(self.widget) - font = QtGui.QFont() - font.setPointSize(8) - self.btn_revert.setFont(font) - self.btn_revert.setObjectName("btn_revert") - self.horizontalLayout_7.addWidget(self.btn_revert) - self.verticalLayout.addLayout(self.horizontalLayout_7) - self.lbl_summary = QtWidgets.QLabel(self.widget) - font = QtGui.QFont() - font.setPointSize(8) - font.setFamily("Courier") - self.lbl_summary.setFont(font) - self.lbl_summary.setWordWrap(True) - self.lbl_summary.setObjectName("lbl_summary") - self.verticalLayout.addWidget(self.lbl_summary) - spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem4) - self.horizontalLayout_10.addLayout(self.verticalLayout) - self.horizontalLayout_10.setStretch(0, 6) - self.horizontalLayout_10.setStretch(1, 1) - self.tabWidget.addTab(self.widget, "") - self.widget1 = QtWidgets.QWidget() - self.widget1.setObjectName("widget1") - self.gridLayout = QtWidgets.QGridLayout(self.widget1) - self.gridLayout.setObjectName("gridLayout") - self.plainTextEdit_profoil_log = QtWidgets.QPlainTextEdit(self.widget1) - font = QtGui.QFont() - font.setPointSize(8) - font.setFamily("Courier") - self.plainTextEdit_profoil_log.setFont(font) - self.plainTextEdit_profoil_log.setReadOnly(True) - self.plainTextEdit_profoil_log.setPlainText("") - self.plainTextEdit_profoil_log.setObjectName("plainTextEdit_profoil_log") - self.gridLayout.addWidget(self.plainTextEdit_profoil_log, 1, 1, 1, 1) - self.plainTextEdit_profoil_in = QtWidgets.QPlainTextEdit(self.widget1) - font = QtGui.QFont() - font.setPointSize(8) - font.setFamily("Courier") - self.plainTextEdit_profoil_in.setFont(font) - self.plainTextEdit_profoil_in.setPlainText("") - self.plainTextEdit_profoil_in.setObjectName("plainTextEdit_profoil_in") - self.gridLayout.addWidget(self.plainTextEdit_profoil_in, 1, 0, 1, 1) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem5) - self.btn_save_profoil_in = QtWidgets.QPushButton(self.widget1) - self.btn_save_profoil_in.setObjectName("btn_save_profoil_in") - self.horizontalLayout_2.addWidget(self.btn_save_profoil_in) - self.gridLayout.addLayout(self.horizontalLayout_2, 2, 0, 1, 1) - self.label = QtWidgets.QLabel(self.widget1) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 0, 0, 1, 1) - self.label_2 = QtWidgets.QLabel(self.widget1) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 0, 1, 1, 1) - self.tabWidget.addTab(self.widget1, "") - self.horizontalLayout.addWidget(self.tabWidget) - self.horizontalLayout.setStretch(0, 4) - MainWindow.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1313, 21)) - self.menubar.setObjectName("menubar") - self.menuFile = QtWidgets.QMenu(self.menubar) - self.menuFile.setObjectName("menuFile") - self.menuOverlay = QtWidgets.QMenu(self.menubar) - self.menuOverlay.setObjectName("menuOverlay") - MainWindow.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow) - self.statusbar.setObjectName("statusbar") - MainWindow.setStatusBar(self.statusbar) - self.actionOpen = QtWidgets.QAction(MainWindow) - self.actionOpen.setObjectName("actionOpen") - self.actionSave = QtWidgets.QAction(MainWindow) - self.actionSave.setObjectName("actionSave") - self.action_dat_File = QtWidgets.QAction(MainWindow) - self.action_dat_File.setObjectName("action_dat_File") - self.actionClear_Overlay = QtWidgets.QAction(MainWindow) - self.actionClear_Overlay.setObjectName("actionClear_Overlay") - self.menuFile.addAction(self.actionOpen) - self.menuFile.addAction(self.actionSave) - self.menuOverlay.addAction(self.action_dat_File) - self.menuOverlay.addAction(self.actionClear_Overlay) - self.menubar.addAction(self.menuFile.menuAction()) - self.menubar.addAction(self.menuOverlay.menuAction()) - - self.load_canvas() - self.connect_buttons() - - self.retranslateUi(MainWindow) - self.tabWidget.setCurrentIndex(0) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "PROFOIL-UI")) - self.checkBox_grid.setText(_translate("MainWindow", "Grid")) - self.checkBox_history.setText(_translate("MainWindow", "History")) - self.label_4.setText(_translate("MainWindow", "Surface")) - self.combo_switch_surface.setItemText(0, _translate("MainWindow", "Upper")) - self.combo_switch_surface.setItemText(1, _translate("MainWindow", "Lower")) - self.btn_start_edits.setText(_translate("MainWindow", "Start Edits")) - self.btn_cancel.setText(_translate("MainWindow", "Cancel")) - self.btn_apply_edits.setText(_translate("MainWindow", "Apply Edits")) - self.btn_undo.setText(_translate("MainWindow", "Undo")) - self.btn_plot_from_file.setText(_translate("MainWindow", "Plot From File")) - self.btn_run_profoil.setText(_translate("MainWindow", "Run Profoil")) - self.btn_revert.setText(_translate("MainWindow", "Revert")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.widget), _translate("MainWindow", "Design View")) - self.btn_save_profoil_in.setText(_translate("MainWindow", "Save")) - self.label.setText(_translate("MainWindow", "Profoil.in")) - self.label_2.setText(_translate("MainWindow", "Profoil.log")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.widget1), _translate("MainWindow", "File View")) - self.menuFile.setTitle(_translate("MainWindow", "File")) - self.menuOverlay.setTitle(_translate("MainWindow", "Overlay")) - self.actionOpen.setText(_translate("MainWindow", "Open")) - self.actionSave.setText(_translate("MainWindow", "Save")) - self.action_dat_File.setText(_translate("MainWindow", "*.dat File")) - self.actionClear_Overlay.setText(_translate("MainWindow", "Clear Overlay")) - - def load_canvas(self): - """ - creates FigureCanvas from matplotlib Figure and loads into PyQt Widget space - """ - self.canvas = FigureCanvas(self.gui_fig) - self.verticalLayout_canvas.addWidget(self.canvas) - - self.tool_bar = self.gen_toolbar() - self.verticalLayout_canvas.addWidget(self.tool_bar) - - def gen_toolbar(self): - """ - creates a custom tool bar without unnecessary buttons to minimize confusion - """ - tool_bar = NavigationToolbar(self.canvas, None) - selected_buttons = ['Home', 'Pan','Zoom','Save'] - for x in tool_bar.actions(): - if x.text() not in selected_buttons: - tool_bar.removeAction(x) - return tool_bar - - def connect_buttons(self): - """ - maps button click signals to functions - """ - self.btn_start_edits.clicked.connect(self.start_cursor_edits) - self.btn_cancel.clicked.connect(self.cancel_cursor_inputs) - self.btn_apply_edits.clicked.connect(self.apply_edits) - self.btn_undo.clicked.connect(self.undo_edits) - self.btn_plot_from_file.clicked.connect(self.plot_from_file) - self.btn_run_profoil.clicked.connect(self.run_profoil) - self.btn_revert.clicked.connect(self.revert) - - self.btn_save_profoil_in.clicked.connect(self.save_planTextEdit_to_profoil) - - self.actionOpen.triggered.connect(self.menu_file_open) - self.actionSave.triggered.connect(self.menu_file_save) - self.action_dat_File.triggered.connect(self.overlay_file_open) - self.actionClear_Overlay.triggered.connect(self.clear_overlay) - - self.checkBox_grid.stateChanged.connect(self.toggle_grid_lines) - self.checkBox_history.stateChanged.connect(self.toggle_previous_plots) - self.combo_switch_surface.currentIndexChanged.connect(self.switch_surface) - - self.combo_switch_surface.currentIndexChanged.connect(self.switch_surface) - - self.tabWidget.currentChanged.connect(self.load_file_view) - - def load_file_view(self, event): - """ - In file view, profoil.in is updated when the tab is selected so that the most up to date file - """ - if self.tabWidget.tabText(event) == "File View": - self.plainTextEdit_profoil_in.setPlainText(p_intf.catfile(WORKDIR/"profoil.in", tail=0)) - - def switch_surface(self, event): - """ - Switching the surface through the combo box. - """ - self.select_surface(self.combo_switch_surface.itemText(event)) - - def failure_error_dialog(self): - """ pops a Message box with convergence failure warning """ - QMessageBox.critical(self, "Error...", "Design Failed - Please check the .in File") - - def overlay_error_dialog(self): - """ pops a Message box with file loading error. """ - QMessageBox.information(self, "File loading Error...", "Please check the .dat File") - - def loading_warning_dialog(self): - """ pops a Message box with file loading error. """ - return QMessageBox.warning(self, - "Active Session", - "Current design session is active and all data will be lost - Please confirm", - QMessageBox.Yes | QMessageBox.Cancel) - - def save_planTextEdit_to_profoil(self): - """ - saves the profoil.in file view, in to the profoil.in file. - """ - p_intf.gen_buffer() - p_intf.save2profoil_in(self.plainTextEdit_profoil_in.toPlainText()) - - def menu_file_open(self): - """ - opens profoil.in file, if a session is current, warning will be shown. - """ - if self.ready_to_interact: - if self.loading_warning_dialog() == QMessageBox.Yes: - self.active_surface = "Upper" - self.setup_axes() - self.clear_axes() - else: - return - - filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file' ,'.', "Input File (*.in)")[0] - if filename: - self.load(filename) - - def menu_file_save(self): - """ - saves profoil.in file - """ - if not self.ready_to_interact: return - filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Save File' ,'.', "Input File (*.in)")[0] - if filename: - self.save_airfoil(filename) - - def overlay_file_open(self): - """ - overlays *.dat file - """ - filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file' ,'.', "All Files (*.*)")[0] - if filename: - self.overlay_dat(filename) +class ProfoilUI(QtWidgets.QMainWindow, Ui_MainWindow, ProfoilCanvas): + def __init__(self): + QtWidgets.QMainWindow.__init__(self) + ProfoilCanvas.__init__(self) + + def load_canvas(self): + """ + creates FigureCanvas from matplotlib Figure and loads into PyQt Widget space + """ + self.canvas = FigureCanvas(self.gui_fig) + self.verticalLayout_canvas.addWidget(self.canvas) + + self.tool_bar = self.gen_toolbar() + self.verticalLayout_canvas.addWidget(self.tool_bar) + + def gen_toolbar(self): + """ + creates a custom tool bar without unnecessary buttons to minimize confusion + """ + tool_bar = NavigationToolbar(self.canvas, None) + selected_buttons = ['Home', 'Pan','Zoom','Save'] + for x in tool_bar.actions(): + if x.text() not in selected_buttons: + tool_bar.removeAction(x) + return tool_bar + + def connect_widget_events(self): + """ + maps button/menu/combo_box and tab signals to functions + """ + # Button Events + self.btn_start_edits.clicked.connect(self.start_cursor_edits) + self.btn_cancel.clicked.connect(self.cancel_cursor_inputs) + self.btn_apply_edits.clicked.connect(self.apply_edits) + self.btn_undo.clicked.connect(self.undo_edits) + self.btn_plot_from_file.clicked.connect(self.plot_from_file) + self.btn_run_profoil.clicked.connect(self.run_profoil) + self.btn_revert.clicked.connect(self.revert) + self.btn_save_profoil_in.clicked.connect(self.save_planTextEdit_to_profoil) + + # Menu Events + self.actionOpen.triggered.connect(self.menu_file_open) + self.actionSave.triggered.connect(self.menu_file_save) + self.actionProfoil_dat_File.triggered.connect(lambda:self.overlay_file_open(skiprows=0)) + self.actionXFoil_dat_File.triggered.connect(lambda:self.overlay_file_open(skiprows=1)) + # self.actionMSES_dat_File.triggered.connect(lambda:self.overlay_file_open(skiprows=2)) + self.actionClear_Overlay.triggered.connect(self.clear_overlay) + + # CheckBox Events (History and Grid) + self.checkBox_grid.stateChanged.connect(self.toggle_grid_lines) + self.checkBox_history.stateChanged.connect(self.toggle_previous_plots) + + # Dropdown Events (Upper / Lower Surface Switch) + self.combo_switch_surface.currentIndexChanged.connect(self.switch_surface) + self.combo_switch_surface.currentIndexChanged.connect(self.switch_surface) + + def switch_surface(self, event): + """ + Switching the surface through the combo box. + """ + self.select_surface(self.combo_switch_surface.itemText(event)) + + def failure_error_dialog(self): + """ pops a Message box with convergence failure warning """ + QMessageBox.critical( + self, + "Error...", + "Design Failed - Please check the .in File") + + def overlay_error_dialog(self): + """ pops a Message box with file loading error. """ + QMessageBox.information( + self, + "File loading Error...", + "Please check the .dat File") + + def loading_warning_dialog(self): + """ pops a Message box with file loading error. """ + return QMessageBox.warning( + self, + "Active Session", + "Current design session is active and all data will be lost - Please confirm", + QMessageBox.Yes | QMessageBox.Cancel) + + def save_planTextEdit_to_profoil(self): + """ + saves the profoil.in file view, in to the profoil.in file. + """ + p_intf.gen_buffer() + p_intf.save2profoil_in(self.plainTextEdit_profoil_in.toPlainText()) + + def menu_file_open(self): + """ + opens profoil.in file, if a session is current, warning will be shown. + """ + if self.ready_to_interact: + if self.loading_warning_dialog() == QMessageBox.Yes: + self.active_surface = "Upper" + self.setup_axes() + self.clear_axes() + else: + return + + filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file' ,'../runs', "Input File (*.in)")[0] + if filename: + self.load_in_file(filename) + + def menu_file_save(self): + """ + saves profoil.in file + """ + if not self.ready_to_interact: return + filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Save File' ,'.', "Input File (*.in)")[0] + if filename: + self.save_airfoil(filename) + + def overlay_file_open(self, skiprows): + """ + overlays *.dat file + """ + filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file' ,'.', "All Files (*.*)")[0] + if filename: + self.overlay_dat(filename, skiprows) if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - MainWindow = QtWidgets.QMainWindow() - ui = Ui_MainWindow() - ui.setupUi(MainWindow) - MainWindow.show() - app.exec_() \ No newline at end of file + import sys + app = QtWidgets.QApplication(sys.argv) + MainWindow = QtWidgets.QMainWindow() + ui = ProfoilUI() + ui.setupUi(MainWindow) + ui.load_canvas() + ui.connect_widget_events() + MainWindow.show() + app.exec_() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 83eab32..2d16d7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -matplotlib==3.4.2 +matplotlib numpy scipy pyqt5 \ No newline at end of file