├── .gitignore ├── .idea ├── BEM Python Development.iml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── UI ├── Airfoil.py ├── AirfoilManager.py ├── Analysis.py ├── Curve.py ├── CurveCollection.py ├── CurveControl.py ├── CurveEditor.py ├── CurveExtrapolationEditor.py ├── MainWindow.py ├── Optimization.py ├── Table.py ├── WindTurbineProperties.py ├── __init__.py └── helpers.py ├── bem.py ├── bending_inertia.py ├── calculation.py ├── calculation_runner.py ├── export_to_excel.py ├── icon_bem.ico ├── karlsen.bem ├── main.spec ├── montgomerie.py ├── optimization.py ├── popravki.py ├── requirements.txt ├── results.py ├── scraping.py ├── test_results.py ├── tests ├── test_just_calculate.py └── tests_moment.py ├── utils.py ├── visualization.py ├── xfoil.py └── xfoil_executables ├── xfoil └── xfoil.exe /.gitignore: -------------------------------------------------------------------------------- 1 | #OSX Files 2 | .DS_Store 3 | 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | desktop.ini 7 | 8 | #outputs, save files 9 | /out 10 | *.dat 11 | 12 | /foils 13 | /foils/* 14 | 15 | /export/* 16 | 17 | /.idea/ 18 | /.idea/* 19 | 20 | # User-specific stuff 21 | .idea/**/workspace.xml 22 | .idea/**/tasks.xml 23 | .idea/**/usage.statistics.xml 24 | .idea/**/dictionaries 25 | .idea/**/shelf 26 | 27 | # Generated files 28 | .idea/**/contentModel.xml 29 | 30 | main.build/ 31 | 32 | # Sensitive or high-churn files 33 | .idea/**/dataSources/ 34 | .idea/**/dataSources.ids 35 | .idea/**/dataSources.local.xml 36 | .idea/**/sqlDataSources.xml 37 | .idea/**/dynamic.xml 38 | .idea/**/uiDesigner.xml 39 | .idea/**/dbnavigator.xml 40 | 41 | # Gradle 42 | .idea/**/gradle.xml 43 | .idea/**/libraries 44 | 45 | # Gradle and Maven with auto-import 46 | # When using Gradle or Maven with auto-import, you should exclude module files, 47 | # since they will be recreated, and may cause churn. Uncomment if using 48 | # auto-import. 49 | # .idea/modules.xml 50 | # .idea/*.iml 51 | # .idea/modules 52 | 53 | # CMake 54 | cmake-build-*/ 55 | 56 | # Mongo Explorer plugin 57 | .idea/**/mongoSettings.xml 58 | 59 | # File-based project format 60 | *.iws 61 | 62 | # IntelliJ 63 | out/ 64 | 65 | # mpeltonen/sbt-idea plugin 66 | .idea_modules/ 67 | 68 | # JIRA plugin 69 | atlassian-ide-plugin.xml 70 | 71 | # Cursive Clojure plugin 72 | .idea/replstate.xml 73 | 74 | # Crashlytics plugin (for Android Studio and IntelliJ) 75 | com_crashlytics_export_strings.xml 76 | crashlytics.properties 77 | crashlytics-build.properties 78 | fabric.properties 79 | 80 | # Editor-based Rest Client 81 | .idea/httpRequests 82 | 83 | # Byte-compiled / optimized / DLL files 84 | __pycache__/ 85 | *.py[cod] 86 | *$py.class 87 | 88 | # C extensions 89 | *.so 90 | 91 | # Distribution / packaging 92 | .Python 93 | build/ 94 | develop-eggs/ 95 | dist/ 96 | downloads/ 97 | eggs/ 98 | .eggs/ 99 | lib/ 100 | lib64/ 101 | parts/ 102 | sdist/ 103 | var/ 104 | wheels/ 105 | *.egg-info/ 106 | .installed.cfg 107 | *.egg 108 | MANIFEST 109 | 110 | # PyInstaller 111 | # Usually these files are written by a python script from a template 112 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 113 | *.manifest 114 | #*.spec #this is needed to ensure correct build settings... 115 | 116 | # Installer logs 117 | pip-log.txt 118 | pip-delete-this-directory.txt 119 | 120 | # Unit test / coverage reports 121 | htmlcov/ 122 | .tox/ 123 | .nox/ 124 | .coverage 125 | .coverage.* 126 | .cache 127 | nosetests.xml 128 | coverage.xml 129 | *.cover 130 | .hypothesis/ 131 | .pytest_cache/ 132 | 133 | # Translations 134 | *.mo 135 | *.pot 136 | 137 | # Django stuff: 138 | *.log 139 | local_settings.py 140 | db.sqlite3 141 | 142 | # Flask stuff: 143 | instance/ 144 | .webassets-cache 145 | 146 | # Scrapy stuff: 147 | .scrapy 148 | 149 | # Sphinx documentation 150 | docs/_build/ 151 | 152 | # PyBuilder 153 | target/ 154 | 155 | # Jupyter Notebook 156 | .ipynb_checkpoints 157 | 158 | # IPython 159 | profile_default/ 160 | ipython_config.py 161 | 162 | # pyenv 163 | .python-version 164 | 165 | # celery beat schedule file 166 | celerybeat-schedule 167 | 168 | # SageMath parsed files 169 | *.sage.py 170 | 171 | # Environments 172 | .env 173 | .venv 174 | env/ 175 | venv/ 176 | ENV/ 177 | env.bak/ 178 | venv.bak/ 179 | 180 | # Spyder project settings 181 | .spyderproject 182 | .spyproject 183 | 184 | # Rope project settings 185 | .ropeproject 186 | 187 | # mkdocs documentation 188 | /site 189 | 190 | # mypy 191 | .mypy_cache/ 192 | .dmypy.json 193 | dmypy.json 194 | -------------------------------------------------------------------------------- /.idea/BEM Python Development.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About the software 2 | 3 | ## What is it for? 4 | Wind turbines and propellers involve relatively complex geometries, and due to the rotation of the turbine, movement of the air, and the fact that lift is a function of the shape of each blade, it is hard to calculate the forces, power and efficiency of such systems by hand. 5 | 6 | Methods for determining the parameters of such turbines were in the past mainly experimental, and nowadays relatively accurate calculations can be made using 3D computational fluid dynamics calculations. The main disadvantages to such methods are the cost, and time involved in preparation, calculation, and post-calculation analysis. 7 | 8 | In order for a turbine designer to quickly generate a geometry and determine the forces and other parameters of a turbine, a fast, cost-effective way is to use the blade element momentum theory. 9 | 10 | ## Blade element momentum theory 11 | 12 | Blade Element Momentum theory software (BEM) is a tool for calculating the local forces on a propeller or wind turbine using the blade element momentum theory. The calculation is based on the Rankine-Froude momentum theory and the blade element theory. 13 | 14 | The main disadvantage of BEM is that it does not account for complex 3-D hydrodynamic and aerodynamic effects. It is still relatively accurate, and the quality of the analysis is better with larger turbines and with higher-quality input data. 15 | 16 | In principle, the calculation is done on a single blade. Each blade is divided into sections, and forces on each section are calculated separately. The coefficients of lift and drag (cL and cD) of each section depends mainly on the Reynolds number of the medium and the angle of attack (but also depends on other factors, such as the boundary layer, etc.). An iterative system of equations is then set up to determine the needed parameters. 17 | 18 | The input to the calculation consists of the following data: \ 19 | a. basic turbine data (tip radius, hub radius, type of turbine, type of fluid) \ 20 | b. geometry of each section (chord length, twist angle, radius, type of airfoil) \ 21 | c. the lift/drag curves of each airfoil \ 22 | d. calculation parameters (main equation selection, selection of corrections, etc.) \ 23 | e. other parameters \ 24 | 25 | The output of the calculation consists of: \ 26 | a. forces acting on each blade \ 27 | b. the total power, torque and efficiency \ 28 | c. (optionally) statical analysis \ 29 | d. other parameters \ 30 | 31 | ## Similar software 32 | 33 | Software that fulfill a similar role, and were an inspiration to the development of this software, include: 34 | QBlade, Aerodyn, XFOIL, etc. 35 | 36 | There are many custom BEM-based applications that turbine and propeller manufacturers develop in-house and are not publicly available. 37 | 38 | ## How to use the software 39 | 40 | ### Main window 41 | 42 | The BEM software main window is divided into multiple tabs, which are divided by their corresponding function, these are: Airfoil management, Turbine info, Analysis and Optimization. The analysis is started on the Analyiss tab and after it is finished, an Results window is finally opened, from which results can be checked on and exported, as necessary. 43 | 44 | ### Airfoil management 45 | 46 | In the Airfoil management tab, a designer can: \ 47 | a. Add, rename, duplicate, remove, load and save airfoil data \ 48 | b. import airfoil data \ 49 | c. import or calculate the cL/cD curves of an airfoil \ 50 | d. create 360° cL/cD curves using Montgomerie methods \ 51 | 52 | ### Turbine info 53 | 54 | In the Turbine info tab, a designer can: \ 55 | a. Set up the basic turbine data (name, radii, number of blades, etc.) \ 56 | b. Set the turbine scaling and interpolation, if needed for calculation for different sizes and numbers of sections \ 57 | c. Generate a preliminary geometry of wind turbines or propellers using different methods (included are Betz, Schmitz, Larrabee, Adkins) \ 58 | d. View the generated geometry in 3D \ 59 | e. Export the geometry to Solidworks \ 60 | f. Create standard geometry graphs for distribution and documentation purposes \ 61 | 62 | ### Analysis 63 | 64 | In the Analysis tab, a designer can: \ 65 | a. Determine the basic calculation parameters \ 66 | b. Run the analysis, which will, after finishing, open the results window \ 67 | 68 | ### Optimization 69 | 70 | In the Optimization tab, the designer can optimize the geometry using an optimization algorithm in order to ensure that the geometry is optimized to a specific fluid speed/rotational velocity combination. The parameters that will be modified are called input parameters, and the parameters that will determine the success of the optimization (optimization function) are called the output variables. Target variables are also supported, in case one desires not the largest torque, power, or other parameter, but instead a specific torque, power or other parameter. 71 | 72 | The optimization GUI is still in developement. 73 | 74 | # Installation and usage 75 | 76 | ### Supported Python version 77 | 3.2+ 78 | 79 | ### Install 80 | pip install -r requirements.txt 81 | 82 | ### Usage 83 | python main.py 84 | 85 | ### Create Executable 86 | pyinstaller main.spec 87 | 88 | ### On Mac - to run xfoil 89 | brew install xquartz 90 | 91 | # License 92 | 93 | Python BEM - Blade Element Momentum Theory Software. 94 | 95 | Copyright (C) 2022 M. Smrekar 96 | 97 | This program is free software: you can redistribute it and/or modify 98 | it under the terms of the GNU General Public License as published by 99 | the Free Software Foundation, either version 3 of the License, or 100 | (at your option) any later version. 101 | 102 | This program is distributed in the hope that it will be useful, 103 | but WITHOUT ANY WARRANTY; without even the implied warranty of 104 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 105 | GNU General Public License for more details. 106 | 107 | You should have received a copy of the GNU General Public License 108 | along with this program. If not, see . -------------------------------------------------------------------------------- /UI/AirfoilManager.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from PyQt5.QtCore import pyqtSignal, QRect 19 | from PyQt5.QtWidgets import QWidget, QGridLayout, QPushButton, QFileDialog 20 | 21 | from UI.helpers import PopupText, TabWidget, PopupConfirmation 22 | from UI.Airfoil import Airfoil 23 | #from utils import fltr 24 | 25 | import numpy as np 26 | import json 27 | 28 | 29 | class AirfoilManager(QWidget): 30 | """ 31 | 32 | """ 33 | emitter = pyqtSignal(str) 34 | emitter_yes = pyqtSignal(str) 35 | 36 | def __init__(self, parent=None): 37 | super(AirfoilManager, self).__init__(parent) 38 | 39 | self.main = self.parent() 40 | 41 | self.grid = QGridLayout() 42 | self.setLayout(self.grid) 43 | 44 | self.tab_widget = TabWidget(self) 45 | self.grid.addWidget(self.tab_widget, 2, 0) 46 | 47 | self.upper_widget = QWidget() 48 | self.upper_layout = QGridLayout() 49 | self.upper_widget.setLayout(self.upper_layout) 50 | self.grid.addWidget(self.upper_widget, 1, 0) 51 | 52 | self.button_add_foil = QPushButton("Add foil") 53 | self.button_add_foil.clicked.connect(self.add_foil_popup) 54 | self.upper_layout.addWidget(self.button_add_foil, 0, 1) 55 | 56 | self.button_rename_foil = QPushButton("Rename foil") 57 | self.button_rename_foil.clicked.connect(self.rename_foil_popup) 58 | self.upper_layout.addWidget(self.button_rename_foil, 0, 2) 59 | 60 | self.button_duplicate_foil = QPushButton("Duplicate foil") 61 | self.button_duplicate_foil.clicked.connect(self.duplicate_foil) 62 | self.upper_layout.addWidget(self.button_duplicate_foil, 0, 3) 63 | 64 | self.button_remove_foil = QPushButton("Remove foil") 65 | self.button_remove_foil.clicked.connect(self.remove_foil_popup) 66 | self.upper_layout.addWidget(self.button_remove_foil, 0, 4) 67 | 68 | self.button_load_foil = QPushButton("Load foil") 69 | self.button_load_foil.clicked.connect(self.load_airfoil) 70 | self.upper_layout.addWidget(self.button_load_foil, 0, 5) 71 | 72 | self.button_save_foil = QPushButton("Save foil") 73 | self.button_save_foil.clicked.connect(self.save_airfoil) 74 | self.upper_layout.addWidget(self.button_save_foil, 0, 6) 75 | 76 | def add_foil_popup(self): 77 | """ 78 | 79 | """ 80 | self.emitter.connect(self.add_foil) 81 | self.p = PopupText("Add airfoil", "airfoil_name", self.emitter,"Airfoil name") 82 | self.p.show() 83 | 84 | def add_foil(self, string): 85 | """ 86 | 87 | :param string: 88 | """ 89 | c = Airfoil(string, self) 90 | self.tab_widget.add_tab(c, string) 91 | self.tab_widget.setCurrentIndex(len(self.tab_widget.tabs) - 1) 92 | 93 | def remove_foil_popup(self): 94 | cur_widget, cur_name = self.tab_widget.tabs[self.tab_widget.currentIndex()] 95 | self.p = PopupConfirmation("Do you really want to delete "+cur_name+"?","Remove foil confirmation",self.emitter_yes) 96 | self.emitter_yes.connect(self.tab_widget.remove_current_tab) 97 | self.p.show() 98 | 99 | def rename_foil_popup(self): 100 | """ 101 | 102 | """ 103 | self.emitter.connect(self.rename_foil) 104 | self.p = PopupText("Rename airfoil", self.tab_widget.current_tab_name(), self.emitter, "Rename airfoil") 105 | self.p.show() 106 | 107 | def rename_foil(self, string): 108 | """ 109 | 110 | :param string: 111 | """ 112 | self.tab_widget.rename_current_tab(string) # self.tab_widget.tabs 113 | 114 | def duplicate_foil(self): 115 | cur_widget, cur_name = self.tab_widget.tabs[self.tab_widget.currentIndex()] 116 | names = [name for _,name in self.tab_widget.tabs] 117 | i=0 118 | while i<100: 119 | new_name = cur_name+"_"+str(i) 120 | if not new_name in names: 121 | break 122 | i+=1 123 | old_settings = cur_widget.get_settings() 124 | new_airfoil = Airfoil(new_name, self) 125 | new_airfoil.set_settings(old_settings) 126 | self.tab_widget.add_tab(new_airfoil,new_name) 127 | 128 | def load_airfoil(self): 129 | file_path = QFileDialog.getOpenFileName(self, "Load File", "", "BEM airfoil (*.bemfoil)")[0] 130 | if file_path != "": 131 | with open(file_path, "r") as fp: 132 | data = json.load(fp) 133 | airfoil_name,airfoil_settings = list(data.items())[0] 134 | new_airfoil = Airfoil(airfoil_name, self) 135 | new_airfoil.set_settings(airfoil_settings) 136 | self.tab_widget.add_tab(new_airfoil,airfoil_name) 137 | 138 | def save_airfoil(self): 139 | name = QFileDialog.getSaveFileName(self, 'Save File', "", "BEM airfoil (*.bemfoil)")[0] 140 | if name != "": 141 | cur_widget, cur_name = self.tab_widget.tabs[self.tab_widget.currentIndex()] 142 | cur_airfoil_settings = cur_widget.get_settings() 143 | d = {cur_name:cur_airfoil_settings} 144 | d_to_save = fltr(d, (float, int, list, str, bool, np.ndarray)) 145 | json_d = json.dumps(d_to_save) 146 | file = open(name, 'w') 147 | file.write(json_d) 148 | file.close() 149 | 150 | 151 | def get_settings(self): 152 | """ 153 | 154 | :return: 155 | """ 156 | out = {} 157 | i = 0 158 | 159 | # TODO Dont rely on name being set correctly in n! 160 | for w, n in self.tab_widget.tabs: 161 | out[n] = w.get_settings() 162 | i += 1 163 | 164 | return {"airfoils": out} 165 | 166 | def set_settings(self, dict_settings): 167 | """ 168 | 169 | :param dict_settings: 170 | """ 171 | self.tab_widget.remove_all_tabs() 172 | if "airfoils" in dict_settings: 173 | if len(dict_settings["airfoils"]) > 0: 174 | for c_name, c_dict in dict_settings["airfoils"].items(): 175 | curve_widget = Airfoil(c_name, self) 176 | curve_widget.set_settings(c_dict) 177 | self.tab_widget.add_tab(curve_widget, c_name) -------------------------------------------------------------------------------- /UI/Curve.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from scipy.interpolate import interp1d 19 | 20 | from montgomerie import Montgomerie 21 | 22 | 23 | class Curve: 24 | """ 25 | 26 | """ 27 | def __init__(self): 28 | self.x = None 29 | self.y = None 30 | self.Re = None 31 | self.ncrit = None 32 | self.alpha = None 33 | self.cl = None 34 | self.cd = None 35 | self.A = None 36 | self.B = None 37 | self.Am = None 38 | self.Bm = None 39 | self.m_CD90 = None 40 | self.slope = None 41 | self.min_stable_aoa = None 42 | self.max_stable_aoa = None 43 | 44 | def create(self, x, y, Re, ncrit, alpha, cl, cd): 45 | """ 46 | 47 | :param x: 48 | :param y: 49 | :param Re: 50 | :param ncrit: 51 | :param alpha: 52 | :param cl: 53 | :param cd: 54 | """ 55 | self.x = x 56 | self.y = y 57 | self.Re = Re 58 | self.ncrit = ncrit 59 | self.alpha = alpha 60 | self.cl = cl 61 | self.cd = cd 62 | self.A = -5 63 | self.B = 5 64 | self.Am = 8 65 | self.Bm = 5 66 | self.m_CD90 = 1.5 67 | self.slope = 0.106 68 | self.max_stable_aoa = 10 69 | self.min_stable_aoa = -5 70 | 71 | def get_curves(self): 72 | """ 73 | 74 | :return: 75 | """ 76 | return self.alpha, self.cl, self.cd 77 | 78 | def get_extrapolated_curve(self): 79 | """ 80 | 81 | :return: 82 | """ 83 | M = Montgomerie(x=self.x, y=self.y, alpha=self.alpha, Cl=self.cl, Cd=self.cd, Re=self.Re, A=self.A, Am=self.Am, 84 | B=self.B, Bm=self.Bm, m_CD90=self.m_CD90, slope=self.slope) 85 | alpha, cl, cd = M.calculate_extrapolation() 86 | return alpha, cl, cd 87 | 88 | def get_combined_curve(self): 89 | """ 90 | 91 | :return: 92 | """ 93 | M = Montgomerie(x=self.x, y=self.y, alpha=self.alpha, Cl=self.cl, Cd=self.cd, Re=self.Re, A=self.A, Am=self.Am, 94 | B=self.B, Bm=self.Bm, m_CD90=self.m_CD90, slope=self.slope) 95 | _alpha, _cl, _cd = M.calculate_extrapolation() 96 | cl_out, cd_out = [], [] 97 | f_cl = interp1d(self.alpha, self.cl, bounds_error=True) 98 | f_cd = interp1d(self.alpha, self.cd, bounds_error=True) 99 | for i in range(len(_alpha)): 100 | a = _alpha[i] 101 | try: 102 | cl = f_cl(a) 103 | except ValueError: 104 | cl = _cl[i] 105 | try: 106 | cd = f_cd(a) 107 | except ValueError: 108 | cd = _cd[i] 109 | cl_out.append(cl) 110 | cd_out.append(cd) 111 | return _alpha, cl_out, cd_out 112 | 113 | def save_curve(self): 114 | """ 115 | 116 | :return: 117 | """ 118 | out = {"x": list(self.x), "y": list(self.y), "Re": self.Re, "ncrit": self.ncrit, "alpha": list(self.alpha), 119 | "cl": list(self.cl), "cd": list(self.cd), "A": self.A, "B": self.B, "Am": self.Am, "Bm": self.Bm, 120 | "m_CD90": self.m_CD90, "slope": self.slope, "min_stable_aoa": self.min_stable_aoa, 121 | "max_stable_aoa": self.max_stable_aoa} 122 | return out 123 | 124 | def load_curve(self, out): 125 | """ 126 | 127 | :param out: 128 | """ 129 | self.x = out["x"] 130 | self.y = out["y"] 131 | self.Re = out["Re"] 132 | self.ncrit = out["ncrit"] 133 | self.alpha = out["alpha"] 134 | self.cl = out["cl"] 135 | self.cd = out["cd"] 136 | self.A = out["A"] 137 | self.B = out["B"] 138 | self.Am = out["Am"] 139 | self.Bm = out["Bm"] 140 | self.m_CD90 = out["m_CD90"] 141 | self.slope = out["slope"] 142 | try: 143 | self.min_stable_aoa = out["min_stable_aoa"] 144 | self.max_stable_aoa = out["max_stable_aoa"] 145 | except: 146 | self.max_stable_aoa = 10 147 | self.min_stable_aoa = -5 148 | -------------------------------------------------------------------------------- /UI/CurveCollection.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from numpy.core._multiarray_umath import array 19 | 20 | from UI.Curve import Curve 21 | 22 | 23 | class CurveCollection: 24 | """ 25 | 26 | """ 27 | def __init__(self): 28 | self.curve_list = [] 29 | 30 | def add(self, curve): 31 | """ 32 | 33 | :param curve: 34 | """ 35 | self.curve_list.append(curve) 36 | 37 | def get_stall_angles(self): 38 | """ 39 | 40 | :return: 41 | """ 42 | out_re = [] 43 | out_min_aoa_list = [] 44 | out_max_aoa_list = [] 45 | 46 | for c in self.curve_list: 47 | Re = c.Re 48 | min_stable_aoa = c.min_stable_aoa 49 | max_stable_aoa = c.max_stable_aoa 50 | 51 | out_re.append(Re) 52 | out_min_aoa_list.append(min_stable_aoa) 53 | out_max_aoa_list.append(max_stable_aoa) 54 | 55 | return out_re, out_min_aoa_list, out_max_aoa_list 56 | 57 | def get_curves_sorted(self): 58 | """ 59 | 60 | :return: 61 | """ 62 | return sorted(self.curve_list, key=lambda c: (c.ncrit, c.Re)) 63 | 64 | def save_curves(self): 65 | """ 66 | 67 | :return: 68 | """ 69 | out_list = [] 70 | for c in self.curve_list: 71 | data_curve = c.save_curve() 72 | out_list.append(data_curve) 73 | return out_list 74 | 75 | def load_curves(self, out): 76 | """ 77 | 78 | :param out: 79 | """ 80 | self.curve_list = [] 81 | for data_curve in out: 82 | c = Curve() 83 | c.load_curve(data_curve) 84 | self.curve_list.append(c) 85 | 86 | def gather_curves(self, extrapolation=True): 87 | """ 88 | 89 | :param extrapolation: 90 | :return: 91 | """ 92 | out = [] 93 | for curve in self.get_curves_sorted(): 94 | if extrapolation: 95 | alpha, cl, cd = curve.get_combined_curve() 96 | else: 97 | alpha, cl, cd = curve.get_curves() 98 | for i in range(len(alpha)): 99 | Re = curve.Re 100 | ncrit = curve.ncrit 101 | _alpha = alpha[i] 102 | _cl = cl[i] 103 | _cd = cd[i] 104 | out.append([Re, ncrit, _alpha, _cl, _cd]) 105 | out = array(out) 106 | return out 107 | 108 | def get_curve(self, re_in, ncrit_in): 109 | """ 110 | 111 | :param re_in: 112 | :param ncrit_in: 113 | :return: 114 | """ 115 | out = [] 116 | for curve in self.curve_list: 117 | re = curve.Re 118 | ncrit = curve.ncrit 119 | if ncrit == ncrit_in and re == re_in: 120 | out.append(curve) 121 | 122 | if len(out) == 0: 123 | return None 124 | if len(out) == 1: 125 | return out[0] 126 | if len(out) > 1: 127 | for c in out: 128 | print(c.Re, c.ncrit) 129 | raise Exception("DataError: Multiple curves have same Reynolds and Ncrit...") 130 | 131 | def remove_curve(self, re_in, ncrit_in): 132 | """ 133 | 134 | :param re_in: 135 | :param ncrit_in: 136 | :return: 137 | """ 138 | out = [] 139 | i = 0 140 | for curve in self.curve_list: 141 | re = curve.Re 142 | ncrit = curve.ncrit 143 | if ncrit == ncrit_in and re == re_in: 144 | j = i 145 | out.append(curve) 146 | i += 1 147 | 148 | if len(out) == 0: 149 | return None 150 | if len(out) > 1: 151 | for c in out: 152 | print(c.Re, c.ncrit) 153 | raise Exception("DataError: Multiple curves have same Reynolds and Ncrit...") 154 | 155 | del self.curve_list[j] 156 | -------------------------------------------------------------------------------- /UI/CurveControl.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from PyQt5.QtCore import Qt 19 | from PyQt5.QtWidgets import QWidget, QGridLayout, QFormLayout, QLineEdit, QSlider, QPushButton 20 | from matplotlib import pyplot as plt 21 | from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar 22 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 23 | from UI.helpers import ErrorMessageBox 24 | 25 | class CurveControl(QWidget): 26 | """ 27 | 28 | """ 29 | 30 | def __init__(self, parent=None, curve=None): 31 | super(CurveControl, self).__init__(parent) 32 | # self.setMinimumSize(300,400) 33 | self.parent = parent 34 | 35 | self.layout = QGridLayout() 36 | self.setLayout(self.layout) 37 | 38 | self.curve = curve 39 | 40 | self.right = QWidget() 41 | self.right_layout = QFormLayout() 42 | self.right.setLayout(self.right_layout) 43 | 44 | self.A = QLineEdit(str(self.curve.A)) 45 | self.B = QLineEdit(str(self.curve.B)) 46 | self.Am = QLineEdit(str(self.curve.Am)) 47 | self.Bm = QLineEdit(str(self.curve.Bm)) 48 | 49 | self.A = QSlider(Qt.Horizontal) 50 | self.A.setMinimum(-10) 51 | self.A.setMaximum(30) 52 | self.A.setValue(self.curve.A) 53 | self.A.setTickPosition(QSlider.TicksBelow) 54 | self.A.setTickInterval(1) 55 | self.A.valueChanged.connect(self.update) 56 | 57 | self.B = QSlider(Qt.Horizontal) 58 | self.B.setMinimum(1) 59 | self.B.setMaximum(100) 60 | self.B.setValue(self.curve.B) 61 | self.B.setTickPosition(QSlider.TicksBelow) 62 | self.B.setTickInterval(1) 63 | self.B.valueChanged.connect(self.update) 64 | 65 | self.Am = QSlider(Qt.Horizontal) 66 | self.Am.setMinimum(1) 67 | self.Am.setMaximum(80) 68 | self.Am.setValue(self.curve.Am) 69 | self.Am.setTickPosition(QSlider.TicksBelow) 70 | self.Am.setTickInterval(1) 71 | self.Am.valueChanged.connect(self.update) 72 | 73 | self.Bm = QSlider(Qt.Horizontal) 74 | self.Bm.setMinimum(1) 75 | self.Bm.setMaximum(70) 76 | self.Bm.setValue(self.curve.Bm) 77 | self.Bm.setTickPosition(QSlider.TicksBelow) 78 | self.Bm.setTickInterval(1) 79 | self.Bm.valueChanged.connect(self.update) 80 | 81 | self.m_CD90 = QLineEdit(str(self.curve.m_CD90)) 82 | self.m_CD90.textChanged.connect(self.update) 83 | 84 | self.slope = QLineEdit(str(self.curve.slope)) 85 | self.slope.textChanged.connect(self.update) 86 | 87 | self.min_stable_aoa = QLineEdit(str(self.curve.min_stable_aoa)) 88 | self.min_stable_aoa.textChanged.connect(self.update) 89 | 90 | self.max_stable_aoa = QLineEdit(str(self.curve.max_stable_aoa)) 91 | self.max_stable_aoa.textChanged.connect(self.update) 92 | 93 | self.right_layout.addRow("A", self.A) 94 | self.right_layout.addRow("B", self.B) 95 | self.right_layout.addRow("A-", self.Am) 96 | self.right_layout.addRow("B-", self.Bm) 97 | self.right_layout.addRow("CD@90°", self.m_CD90) 98 | self.right_layout.addRow("Slope", self.slope) 99 | self.right_layout.addRow("aoa_min", self.min_stable_aoa) 100 | self.right_layout.addRow("aoa_max", self.max_stable_aoa) 101 | 102 | self.layout.addWidget(self.right, 1, 2) 103 | 104 | self.left = QWidget() 105 | self.left_layout = QGridLayout() 106 | self.left.setLayout(self.left_layout) 107 | 108 | self.figure = plt.figure(figsize=(5, 5)) 109 | self.canvas = FigureCanvas(self.figure) 110 | self.canvas.setMinimumSize(100, 100) 111 | self.toolbar = NavigationToolbar(self.canvas, self) 112 | self.ax = self.figure.add_subplot(111) 113 | self.left_layout.addWidget(self.canvas) 114 | self.left_layout.addWidget(self.toolbar) 115 | 116 | self.layout.addWidget(self.left, 1, 1) 117 | 118 | self.button_update = QPushButton("update") 119 | self.button_update.clicked.connect(self.draw_extrapolation) 120 | self.left_layout.addWidget(self.button_update) 121 | 122 | # self.draw_base() 123 | 124 | self.show() 125 | 126 | def clear(self): 127 | """ 128 | 129 | """ 130 | self.ax.cla() 131 | 132 | def draw_base(self): 133 | """ 134 | 135 | """ 136 | self.ax.plot(self.curve.alpha, self.curve.cl) 137 | self.ax.plot(self.curve.alpha, self.curve.cd, "o-") 138 | self.canvas.draw() 139 | 140 | def draw_extrapolation(self): 141 | """ 142 | 143 | """ 144 | self.clear() 145 | 146 | self.draw_base() 147 | 148 | try: 149 | alpha, cl, cd = self.curve.get_extrapolated_curve() 150 | self.ax.plot(alpha, cl, "g.") 151 | self.ax.plot(alpha, cd, "r.") 152 | try: 153 | self.ax.axvline(x=float(self.min_stable_aoa.text()), color="red") 154 | self.ax.axvline(x=float(self.max_stable_aoa.text()), color="red") 155 | except: 156 | pass 157 | except: 158 | msg = ErrorMessageBox() 159 | 160 | self.canvas.draw() 161 | 162 | def update(self): 163 | """ 164 | 165 | """ 166 | self.curve.A = int(self.A.value()) 167 | self.curve.B = int(self.B.value()) 168 | self.curve.Am = int(self.Am.value()) 169 | self.curve.Bm = int(self.Bm.value()) 170 | # print("A",self.curve.A,"B",self.curve.B,"A-",self.curve.Am,"B-",self.curve.Bm) 171 | try: 172 | self.curve.m_CD90 = float(self.m_CD90.text()) 173 | self.curve.slope = float(self.slope.text()) 174 | if self.curve.slope == 0: 175 | self.curve.slope = 1.0 176 | except: 177 | print("Error in slope or m_CD90") 178 | try: 179 | self.curve.min_stable_aoa = float(self.min_stable_aoa.text()) 180 | self.curve.max_stable_aoa = float(self.max_stable_aoa.text()) 181 | except: 182 | print("Error in min or max AoA") 183 | self.draw_extrapolation() 184 | -------------------------------------------------------------------------------- /UI/CurveEditor.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from PyQt5 import QtGui 19 | from PyQt5.QtWidgets import QWidget, QGridLayout, QPushButton, QLineEdit, QLabel, QComboBox, QMessageBox 20 | 21 | from UI.Curve import Curve 22 | from utils import transpose 23 | from UI.helpers import MyMessageBox 24 | from UI.Table import Table 25 | 26 | 27 | class CurveEditor(QWidget): 28 | """ 29 | 30 | """ 31 | def __init__(self, parent=None): 32 | super(CurveEditor, self).__init__(None) 33 | self.resize(1600, 768) 34 | self.parent = parent 35 | 36 | self.grid = QGridLayout() 37 | self.setLayout(self.grid) 38 | 39 | # self.tab_widget = TabWidget(self) 40 | # self.grid.addWidget(self.tab_widget, 2, 0) 41 | self.setWindowTitle("Curve Editor") 42 | 43 | self.validator = QtGui.QDoubleValidator() 44 | 45 | self.table = Table() 46 | self.table.createTable([[0, 0, 0]]) 47 | self.table.set_labels(["alpha [°]", "cL (lift coeff.)", "cD (drag coeff.)"]) 48 | self.grid.addWidget(self.table, 2, 0) 49 | 50 | self.upper_widget = QWidget() 51 | self.upper_layout = QGridLayout() 52 | self.upper_widget.setLayout(self.upper_layout) 53 | self.grid.addWidget(self.upper_widget, 1, 0) 54 | 55 | self.button_remove_curve = QPushButton("Remove curve") 56 | self.button_remove_curve.clicked.connect(self.remove_curve) 57 | self.upper_layout.addWidget(self.button_remove_curve, 2, 3) 58 | self.button_save_curve = QPushButton("Update curve") 59 | self.button_save_curve.clicked.connect(self.save_curve) 60 | self.upper_layout.addWidget(self.button_save_curve, 1, 3) 61 | self.button_add_curve = QPushButton("Add curve") 62 | self.button_add_curve.clicked.connect(self.add_curve) 63 | self.upper_layout.addWidget(self.button_add_curve, 4, 3) 64 | 65 | self.ncrit_edit = QLineEdit("Insert ncrit") 66 | self.upper_layout.addWidget(self.ncrit_edit, 4, 1) 67 | self.ncrit_edit.setValidator(self.validator) 68 | self.ncrit_edit.textChanged.connect(self.check_state) 69 | self.re_edit = QLineEdit("Insert reynolds") 70 | self.upper_layout.addWidget(self.re_edit, 4, 2) 71 | self.re_edit.setValidator(self.validator) 72 | self.re_edit.textChanged.connect(self.check_state) 73 | 74 | # self.picker_mach_label = QLabel("Mach number:") 75 | # self.picker_mach = QComboBox() 76 | # self.picker_mach.setEditable(True) 77 | # self.upper_layout.addWidget(self.picker_mach_label,2,1) 78 | # self.upper_layout.addWidget(self.picker_mach,3,1) 79 | 80 | self.picker_ncrit_label = QLabel("NCrit:") 81 | self.picker_ncrit = QComboBox() 82 | self.picker_ncrit.setEditable(False) 83 | # 84 | self.upper_layout.addWidget(self.picker_ncrit_label, 1, 1) 85 | self.upper_layout.addWidget(self.picker_ncrit, 2, 1) 86 | 87 | self.picker_reynolds_label = QLabel("Reynolds:") 88 | self.picker_reynolds = QComboBox() 89 | self.picker_reynolds.setEditable(False) 90 | # 91 | self.upper_layout.addWidget(self.picker_reynolds_label, 1, 2) 92 | self.upper_layout.addWidget(self.picker_reynolds, 2, 2) 93 | 94 | # self.picker_mach.lineEdit().returnPressed.connect(self.refresh_dropdowns) 95 | # self.sig1 = self.picker_reynolds.lineEdit().returnPressed.connect(self.save_curve) 96 | # self.sig2 = self.picker_ncrit.lineEdit().returnPressed.connect(self.save_curve) 97 | 98 | self.connect() 99 | 100 | # self.load_curves() 101 | 102 | def save_curve(self): 103 | """ 104 | 105 | """ 106 | out_chosen_values = self.get_chosen_values_from_dropdowns() 107 | re_chosen, ncrit_chosen = out_chosen_values 108 | 109 | data_from_table = self.table.get_values() 110 | alpha_table, cl_table, cd_table = transpose(data_from_table) 111 | alpha, cl, cd = [], [], [] 112 | 113 | for i in range(len(alpha_table)): 114 | if alpha_table[i] != "" and cl_table[i] != "" and cd_table[i] != "": 115 | alpha.append(float(alpha_table[i])) 116 | cl.append(float(cl_table[i])) 117 | cd.append(float(cd_table[i])) 118 | 119 | if self.parent.curves.get_curve(re_in=re_chosen, ncrit_in=ncrit_chosen) == None: 120 | msg = MyMessageBox() 121 | msg.setIcon(QMessageBox.Warning) 122 | msg.setText("Curve with these Re - ncrit values does not exist yet. Did you mean to add a new curve?") 123 | msg.setDetailedText( 124 | "Curve with Re %s and ncrit %s values does not exist yet. Did you mean to add a new curve?" % ( 125 | re_chosen, ncrit_chosen)) 126 | msg.exec_() 127 | else: 128 | self.current_curve = self.parent.curves.get_curve(re_in=re_chosen, ncrit_in=ncrit_chosen) 129 | self.current_curve.alpha = alpha 130 | self.current_curve.cl = cl 131 | self.current_curve.cd = cd 132 | self.load_curves() 133 | 134 | def add_curve(self): 135 | """ 136 | 137 | :return: 138 | """ 139 | self.disconnect() 140 | 141 | re_chosen, ncrit_chosen = self.re_edit.text(), self.ncrit_edit.text() 142 | try: 143 | re_chosen, ncrit_chosen = float(re_chosen), float(ncrit_chosen) 144 | except: 145 | msg = MyMessageBox() 146 | msg.setIcon(QMessageBox.Warning) 147 | msg.setText("Values of Re and ncrit for new curve do not seem to be valid.") 148 | msg.setDetailedText( 149 | "Values of Re '%s' and ncrit '%s' could not be converted to float. Please double check the numbers." % ( 150 | re_chosen, ncrit_chosen)) 151 | msg.exec_() 152 | return 153 | 154 | data_from_table = self.table.get_values() 155 | alpha_table, cl_table, cd_table = transpose(data_from_table) 156 | alpha, cl, cd = [], [], [] 157 | 158 | for i in range(len(alpha_table)): 159 | if alpha_table[i] != "" and cl_table[i] != "" and cd_table[i] != "": 160 | alpha.append(float(alpha_table[i])) 161 | cl.append(float(cl_table[i])) 162 | cd.append(float(cd_table[i])) 163 | 164 | if self.parent.curves.get_curve(re_in=re_chosen, ncrit_in=ncrit_chosen) == None: 165 | print("item does not exist,creating new...") 166 | x, y = self.parent.get_x_y() 167 | self.current_curve = Curve() 168 | self.current_curve.create(x, y, re_chosen, ncrit_chosen, alpha, cl, cd) 169 | self.parent.curves.add(self.current_curve) 170 | print("self.parent.curves.curve_list", self.parent.curves.curve_list) 171 | self.load_curves() 172 | else: 173 | msg = MyMessageBox() 174 | msg.setIcon(QMessageBox.Warning) 175 | msg.setText("Curve with this Re and ncrit already exists!") 176 | msg.setDetailedText( 177 | "Curve with Re %s and ncrit %s values already exists. Did you mean to update the existing curve?" % ( 178 | re_chosen, ncrit_chosen)) 179 | msg.exec_() 180 | 181 | self.connect() 182 | 183 | def remove_curve(self): 184 | """ 185 | 186 | :return: 187 | """ 188 | if len(self.parent.curves.curve_list) == 0: 189 | return 190 | out_chosen_values = self.get_chosen_values_from_dropdowns() 191 | re_chosen, ncrit_chosen = out_chosen_values 192 | self.parent.curves.remove_curve(re_chosen, ncrit_chosen) 193 | self.load_curves() 194 | 195 | def get_chosen_values_from_dropdowns(self): 196 | """ 197 | 198 | :return: 199 | """ 200 | re_chosen = self.picker_reynolds.itemText(self.picker_reynolds.currentIndex()) 201 | ncrit_chosen = self.picker_ncrit.itemText(self.picker_ncrit.currentIndex()) 202 | return float(re_chosen), float(ncrit_chosen) 203 | 204 | def load_curves(self): 205 | """ 206 | 207 | :return: 208 | """ 209 | try: 210 | re_last, ncrit_last = self.get_chosen_values_from_dropdowns() 211 | except ValueError: 212 | re_last, ncrit_last = None, None 213 | 214 | if len(self.parent.curves.curve_list) == 0: 215 | self.disconnect() 216 | 217 | self.picker_reynolds.clear() 218 | self.picker_ncrit.clear() 219 | 220 | self.connect() 221 | return 222 | 223 | self.disconnect() 224 | 225 | self.picker_reynolds.clear() 226 | self.picker_ncrit.clear() 227 | 228 | ncrit_list = [] 229 | for curve in self.parent.curves.curve_list: 230 | ncrit = curve.ncrit 231 | if not ncrit in ncrit_list: 232 | ncrit_list.append(ncrit) 233 | 234 | ncrit_list.sort() 235 | ncrit_list_str = [str(n) for n in ncrit_list] 236 | self.picker_ncrit.addItems(ncrit_list_str) 237 | 238 | if ncrit_last in ncrit_list: 239 | ncrit_index = ncrit_list.index(ncrit_last) 240 | else: 241 | ncrit_index = 0 242 | ncrit_last = ncrit_list[ncrit_index] 243 | 244 | self.picker_ncrit.setCurrentIndex(ncrit_index) 245 | 246 | re_list = [] 247 | for curve in self.parent.curves.curve_list: 248 | re = curve.Re 249 | ncrit = curve.ncrit 250 | if not re in re_list: 251 | if ncrit == ncrit_last: 252 | re_list.append(re) 253 | 254 | re_list.sort() 255 | re_list_str = [str(r) for r in re_list] 256 | self.picker_reynolds.addItems(re_list_str) 257 | 258 | if re_last in re_list: 259 | re_index = re_list.index(re_last) 260 | else: 261 | re_index = 0 262 | re_last = re_list[re_index] 263 | 264 | self.picker_reynolds.setCurrentIndex(re_index) 265 | 266 | self.connect() 267 | 268 | self.load_values_into_table() 269 | 270 | def disconnect(self): 271 | """ 272 | 273 | """ 274 | try: 275 | self.picker_reynolds.currentIndexChanged.disconnect() 276 | self.picker_ncrit.currentIndexChanged.disconnect() 277 | except TypeError: 278 | pass 279 | 280 | def connect(self): 281 | """ 282 | 283 | """ 284 | self.sig3 = self.picker_reynolds.currentIndexChanged.connect(self.load_curves) 285 | self.sig4 = self.picker_ncrit.currentIndexChanged.connect(self.load_curves) 286 | 287 | def load_values_into_table(self): 288 | """ 289 | 290 | :return: 291 | """ 292 | out_chosen_values = self.get_chosen_values_from_dropdowns() 293 | if out_chosen_values == None: 294 | return 295 | re_chosen, ncrit_chosen = out_chosen_values 296 | chosen_curve = self.parent.curves.get_curve(re_in=re_chosen, ncrit_in=ncrit_chosen) 297 | if chosen_curve != None: 298 | self.current_curve = chosen_curve 299 | array = [self.current_curve.alpha, self.current_curve.cl, self.current_curve.cd] 300 | array = transpose(array) 301 | self.table.clear_table() 302 | self.table.createTable(array) 303 | 304 | def check_forms_angles(self): 305 | """ 306 | 307 | :return: 308 | """ 309 | out = "" 310 | _needed_vars = [[self._target_speed, self.target_speed], [self._target_rpm, self.target_rpm], ] 311 | for n, f in _needed_vars: 312 | if isinstance(f, QLineEdit): 313 | state = self.validator.validate(f.text(), 0)[0] 314 | if state == QtGui.QValidator.Acceptable: 315 | pass 316 | elif state == QtGui.QValidator.Intermediate: 317 | out += "Form %s appears not to be valid.\n" % n.text() 318 | else: 319 | out += "Form %s is not of the valid type.\n" % n.text() 320 | if out == "": 321 | return True 322 | return out 323 | 324 | def check_state(self, *args, **kwargs): 325 | """ 326 | 327 | :param args: 328 | :param kwargs: 329 | """ 330 | sender = self.sender() 331 | validator = sender.validator() 332 | state = validator.validate(sender.text(), 0)[0] 333 | if state == QtGui.QValidator.Acceptable: 334 | color = "#edf5e1" # green 335 | elif state == QtGui.QValidator.Intermediate: 336 | color = "#fff79a" # yellow 337 | else: 338 | color = "#f6989d" # red 339 | -------------------------------------------------------------------------------- /UI/CurveExtrapolationEditor.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from PyQt5.QtWidgets import QWidget, QGridLayout, QPushButton, QScrollArea, QVBoxLayout, QLabel 19 | 20 | from UI.CurveControl import CurveControl 21 | 22 | 23 | class CurveExtrapolationEditor(QWidget): 24 | """ 25 | 26 | """ 27 | def __init__(self, parent=None): 28 | super(CurveExtrapolationEditor, self).__init__(None) 29 | self.resize(1600, 768) 30 | self.parent = parent 31 | 32 | self.setWindowTitle("Curve Extrapolation Editor") 33 | 34 | self.grid = QGridLayout() 35 | self.setLayout(self.grid) 36 | self.button = QPushButton("Close") 37 | self.grid.addWidget(self.button, 1, 1) 38 | self.button.clicked.connect(self.close) 39 | self.button_refresh = QPushButton("Refresh") 40 | self.grid.addWidget(self.button_refresh, 1, 2) 41 | self.button_refresh.clicked.connect(self.generate_views) 42 | 43 | self.bottom = QWidget() 44 | self.grid_curves = QGridLayout() 45 | self.bottom.setLayout(self.grid_curves) 46 | 47 | self.scroll_area = QScrollArea() 48 | self.scroll_widget = QWidget() 49 | self.scroll_widget_layout = QVBoxLayout() 50 | 51 | self.scroll_widget.setLayout(self.scroll_widget_layout) 52 | self.scroll_area.setWidget(self.bottom) 53 | self.scroll_area.setWidgetResizable(True) 54 | self.grid.addWidget(self.scroll_area, 2, 1, 2, 2) 55 | 56 | # self.generate_views() 57 | 58 | def generate_views(self): 59 | """ 60 | 61 | """ 62 | # delete stuff already here 63 | for i in reversed(range(self.grid_curves.count())): 64 | self.grid_curves.itemAt(i).widget().setParent(None) 65 | 66 | for curve in self.parent.curves.get_curves_sorted(): 67 | label = QLabel("Re = " + str(round(curve.Re, 2)) + ", Ncrit = " + str(round(curve.ncrit, 2))) 68 | control = CurveControl(self, curve) 69 | control.update() 70 | self.grid_curves.addWidget(label) 71 | self.grid_curves.addWidget(control) -------------------------------------------------------------------------------- /UI/MainWindow.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import json 19 | import os 20 | from multiprocessing import Manager 21 | 22 | import numpy as np 23 | from PyQt5 import QtCore, QtWidgets 24 | from PyQt5.QtCore import pyqtSignal 25 | from PyQt5.QtWidgets import QMainWindow, QAction, QFileDialog, QApplication 26 | 27 | from UI.AirfoilManager import AirfoilManager 28 | from UI.Analysis import Analysis 29 | from UI.Optimization import Optimization 30 | from UI.WindTurbineProperties import WindTurbineProperties 31 | from UI.helpers import ThreadGetter, TabWidget, ErrorMessageBox 32 | from bem import TITLE_STR, DEFAULT_SETTINGS_PATH, application_path 33 | from utils import create_folder 34 | 35 | class MainWindow(QMainWindow): 36 | """ 37 | Main class that sets up the UI layout. 38 | All QWidgets within the MainWindow class are displayed using the custom TabWidget. 39 | The MainWindow class also holds the functions for saving and loading data from files. 40 | 41 | The MainWindow class stores references to all other subclasses used in the program. 42 | There are four subclasses in MainWindow called AirfoilManager, WindTurbineProperties, Analysis, and ThreadGetter. 43 | """ 44 | emitter_add = pyqtSignal(str) 45 | emitter_done = pyqtSignal() 46 | 47 | def __init__(self, width, height): 48 | super().__init__() 49 | 50 | self.screen_width = width 51 | self.screen_height = height 52 | 53 | mainMenu = self.menuBar() 54 | fileMenu = mainMenu.addMenu('&File') 55 | saveFile = QAction("&Save File", self) 56 | saveFile.setShortcut("Ctrl+S") 57 | saveFile.setStatusTip('Save File') 58 | saveFile.triggered.connect(self.file_save) 59 | loadFile = QAction("&Load File", self) 60 | loadFile.setShortcut("Ctrl+L") 61 | loadFile.setStatusTip('Load File') 62 | loadFile.triggered.connect(self.file_load) 63 | getSettings = QAction("Get settings", self) 64 | getSettings.triggered.connect(self.get_all_settings) 65 | fileMenu.addAction(saveFile) 66 | fileMenu.addAction(loadFile) 67 | fileMenu.addAction(getSettings) 68 | 69 | # self.setGeometry(width * 0.125, height * 0.125, width * 0.75, height * 0.75) 70 | self.setWindowTitle(TITLE_STR) 71 | self.tab_widget = TabWidget(self) 72 | self.setCentralWidget(self.tab_widget) 73 | 74 | self.curve_manager = AirfoilManager(self) 75 | self.tab_widget.add_tab(self.curve_manager, "Airfoil management", "Izbira profilov, cL/cD krivulje") 76 | 77 | self.wind_turbine_properties = WindTurbineProperties(self) 78 | self.tab_widget.add_tab(self.wind_turbine_properties, "Turbine info", "Nastavitev geometrije turbine/lopatic") 79 | 80 | self.analysis = Analysis(self) 81 | self.tab_widget.add_tab(self.analysis, "Analysis", "Nastavitev parametrov in BEM analiza") 82 | 83 | self.getter = ThreadGetter(self) 84 | 85 | self.optimization = Optimization(self) 86 | self.tab_widget.add_tab(self.optimization, "Optimization", 87 | "Optimizacija lopatice s pomočjo algoritma diferencialne evolucije") 88 | 89 | self.running = False 90 | self.manager = Manager() 91 | 92 | create_folder(os.path.join(application_path, "foils")) # Used by XFoil 93 | 94 | self.set_process_stopped() 95 | 96 | self.load_default_settings() 97 | 98 | self.show() 99 | 100 | def set_title(self): 101 | """ 102 | Sets the title of the UI window, based on the name of the wind turbine. 103 | """ 104 | s = self.wind_turbine_properties.turbine_name.text() 105 | if s == "": 106 | self.setWindowTitle(TITLE_STR) 107 | else: 108 | self.setWindowTitle(TITLE_STR + " - " + s) 109 | 110 | def file_save(self): 111 | """ 112 | Saves the wind turbine data to a file. 113 | """ 114 | name = QFileDialog.getSaveFileName(self, 'Save File', "", "BEM (*.bem)")[0] 115 | if name != "": 116 | d = self.get_all_settings() 117 | json_d = json.dumps(d) 118 | file = open(name, 'w') 119 | file.write(json_d) 120 | file.close() 121 | 122 | def file_load(self): 123 | """ 124 | Loads the wind turbine data from a file. Also clears the calculation text areas and sets the appropriate title. 125 | """ 126 | file_path = QFileDialog.getOpenFileName(self, "Load File", "", "BEM (*.bem)")[0] 127 | if file_path != "": 128 | with open(file_path, "r") as fp: 129 | data = json.load(fp) 130 | self.set_all_settings(data) 131 | self.analysis.clear() 132 | self.optimization.clear() 133 | self.set_title() 134 | 135 | def load_default_settings(self): 136 | """ 137 | 138 | """ 139 | with open(DEFAULT_SETTINGS_PATH, "r") as fp: 140 | data = json.load(fp) 141 | self.set_all_settings(data) 142 | self.analysis.clear() 143 | self.optimization.clear() 144 | self.set_title() 145 | 146 | def get_all_settings(self): 147 | """ 148 | Used to save the input configuration for the BEM method calculation. 149 | 150 | It fetches the settings from the four subclasses and combines them into a single settings dictionary. 151 | 152 | After all the settings are combined, it calculates the interpolation of the wind turbine geometry as per 153 | user choice. (e.g. if you want to increase or decrease the number of sections with regard to the original 154 | geometry). 155 | 156 | :return: dict: Settings 157 | """ 158 | try: 159 | properties = self.wind_turbine_properties.get_settings() 160 | except: 161 | ErrorMessageBox() 162 | properties = {} 163 | 164 | try: 165 | settings = self.analysis.get_settings() 166 | except: 167 | ErrorMessageBox() 168 | settings = {} 169 | 170 | try: 171 | opt_settings = self.optimization.get_settings() 172 | except: 173 | ErrorMessageBox() 174 | opt_settings = {} 175 | 176 | try: 177 | curve_manager_settings = self.curve_manager.get_settings() 178 | except: 179 | ErrorMessageBox() 180 | curve_manager_settings = {} 181 | 182 | try: 183 | out = {**properties, **settings, **opt_settings, **curve_manager_settings} 184 | # pprint(out) 185 | return out 186 | except: 187 | ErrorMessageBox() 188 | return None 189 | 190 | # noinspection PyBroadException 191 | def set_all_settings(self, inp_dict): 192 | """ 193 | Sets the settings from the appropriate dictionary object by sending the input dictionary object to each of the 194 | subclasses. 195 | :param inp_dict: dict: Settings dictionary. 196 | """ 197 | try: 198 | self.analysis.set_settings(inp_dict) 199 | except: 200 | msg = ErrorMessageBox() 201 | 202 | try: 203 | self.wind_turbine_properties.set_settings(inp_dict) 204 | except: 205 | msg = ErrorMessageBox() 206 | 207 | try: 208 | self.optimization.set_settings(inp_dict) 209 | except: 210 | msg = ErrorMessageBox() 211 | 212 | try: 213 | self.curve_manager.set_settings(inp_dict) 214 | except: 215 | msg = ErrorMessageBox() 216 | 217 | def get_input_params(self): 218 | """ 219 | Used to fetch the BEM calculation settings, create (reset) the multiprocessing queues used for communication 220 | between processes and for creating the final dictionary object which is then sent to the calculation module. 221 | :return: dict: Settings object (with multiprocessing communication entries). 222 | """ 223 | settings = self.get_all_settings() 224 | if settings == None: 225 | return None 226 | self.return_print = self.manager.list([]) 227 | self.return_results = self.manager.list([]) 228 | self.queue_pyqtgraph = self.manager.list([]) 229 | self.end_of_file = self.manager.Value("EOF", False) 230 | inp_params = {**settings, "return_print": self.return_print, "return_results": self.return_results, 231 | "EOF": self.end_of_file} 232 | return inp_params 233 | 234 | def set_process_running(self): 235 | """ 236 | Enables/disables the main stop/start buttons and sets the main boolean self.running to True. 237 | 238 | Used at the beginning of the calculation process. 239 | """ 240 | self.analysis.buttonRun.setEnabled(False) 241 | self.optimization.buttonOptimization.setEnabled(False) 242 | self.analysis.buttonStop.setEnabled(True) 243 | self.optimization.buttonStop.setEnabled(True) 244 | self.running = True 245 | 246 | def set_process_stopped(self): 247 | """ 248 | Enables/disables the main start/stop buttons and sets the main boolean self.running to True. 249 | 250 | Used at the end of the calculation process. 251 | """ 252 | self.analysis.buttonRun.setEnabled(True) 253 | self.optimization.buttonOptimization.setEnabled(True) 254 | self.analysis.buttonStop.setEnabled(False) 255 | self.optimization.buttonStop.setEnabled(False) 256 | self.running = False 257 | 258 | def wheelEvent(self, event): 259 | """ 260 | 261 | :param event: 262 | """ 263 | modifiers = QtWidgets.QApplication.keyboardModifiers() 264 | if modifiers == QtCore.Qt.ControlModifier: 265 | font = QApplication.instance().font() 266 | size = font.pointSizeF() 267 | if event.angleDelta().y() > 0: 268 | size = size + 1 269 | else: 270 | size = size - 1 271 | font.setPointSize(int(size)) 272 | QApplication.instance().setFont(font) 273 | for w in QApplication.allWidgets(): 274 | w.setFont(font) 275 | -------------------------------------------------------------------------------- /UI/Optimization.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from multiprocessing import Manager 19 | from multiprocessing.context import Process 20 | 21 | import numpy as np 22 | from PyQt5 import QtGui 23 | from PyQt5.QtWidgets import QWidget, QGridLayout, QFormLayout, QScrollArea, QVBoxLayout, QTextEdit, QPushButton, \ 24 | QComboBox, QCheckBox, QLineEdit, QLabel, QMessageBox 25 | 26 | from UI.helpers import PyQtGraphWindow, MyMessageBox 27 | from optimization import optimize 28 | from utils import to_float 29 | 30 | 31 | class Optimization(QWidget): 32 | """ 33 | 34 | """ 35 | def __init__(self, parent=None): 36 | super(Optimization, self).__init__(parent) 37 | self.main = self.parent() 38 | 39 | self.grid = QGridLayout() 40 | self.setLayout(self.grid) 41 | 42 | self.left = QWidget() 43 | self.fbox = QFormLayout() 44 | self.left.setLayout(self.fbox) 45 | 46 | self.scroll_area = QScrollArea() 47 | self.scroll_widget = QWidget() 48 | self.scroll_widget_layout = QVBoxLayout() 49 | 50 | self.scroll_widget.setLayout(self.scroll_widget_layout) 51 | self.scroll_area.setWidget(self.left) 52 | self.scroll_area.setWidgetResizable(True) 53 | 54 | self.grid.addWidget(self.scroll_area, 1, 1) 55 | 56 | self.right_output_text_area = QTextEdit() 57 | self.right_output_text_area.setReadOnly(True) 58 | self.grid.addWidget(self.right_output_text_area, 1, 2) 59 | 60 | self.validator = QtGui.QDoubleValidator() 61 | 62 | self.form_list = [] 63 | 64 | self.target_speed = QLineEdit() 65 | self.target_speed.setValidator(self.validator) 66 | self.target_speed.textChanged.connect(self.check_state) 67 | self.target_speed.textChanged.emit(self.target_speed.text()) 68 | self._target_speed = QLabel("Target speed [m/s]") 69 | self.form_list.append([self._target_speed, self.target_speed]) 70 | self.target_speed.setToolTip("Ciljna hitrost vetra. Pri tej hitrosti se bo izvajala BEM analiza.") 71 | 72 | self.target_rpm = QLineEdit() 73 | self.target_rpm.setValidator(self.validator) 74 | self.target_rpm.textChanged.connect(self.check_state) 75 | self.target_rpm.textChanged.emit(self.target_rpm.text()) 76 | self._target_rpm = QLabel("Target rpm [RPM]") 77 | self.form_list.append([self._target_rpm, self.target_rpm]) 78 | self.target_rpm.setToolTip("Ciljni vrtljaji rotorja turbine. Pri teh vrtljajih se bo izvajala BEM analiza.") 79 | 80 | self.mut_coeff = QLineEdit() 81 | self.mut_coeff.setValidator(self.validator) 82 | self.mut_coeff.textChanged.connect(self.check_state) 83 | self.mut_coeff.textChanged.emit(self.mut_coeff.text()) 84 | self._mut_coeff = QLabel("Mutation coefficient") 85 | self.form_list.append([self._mut_coeff, self.mut_coeff]) 86 | self.mut_coeff.setToolTip( 87 | "Mutacijski koeficient nastavlja jakost naključnih mutacij, ki se zgodijo pri vsaki novi generaciji (iteraciji).") 88 | 89 | self.population = QLineEdit() 90 | self.population.setValidator(self.validator) 91 | self.population.textChanged.connect(self.check_state) 92 | self.population.textChanged.emit(self.population.text()) 93 | self._population = QLabel("Population") 94 | self.form_list.append([self._population, self.population]) 95 | self.population.setToolTip("Število posameznikov v algoritmu diferencialne evolucije.") 96 | 97 | self.num_iter = QLineEdit() 98 | self.num_iter.setValidator(self.validator) 99 | self.num_iter.textChanged.connect(self.check_state) 100 | self.num_iter.textChanged.emit(self.num_iter.text()) 101 | self._num_iter = QLabel("Number of iterations") 102 | self.form_list.append([self._num_iter, self.num_iter]) 103 | self.num_iter.setToolTip( 104 | "Število generacij (iteracij). Konvergenčni kriterij je pri tovrstnih algoritmih težko določljiv, zato izberemo fiksno vrednost.") 105 | 106 | self.input_variables_widget = QWidget() 107 | self.input_variables_layout = QGridLayout() 108 | self.input_variables_widget.setLayout(self.input_variables_layout) 109 | self.button_add_input_variable = QPushButton("Add input variable") 110 | self.button_add_input_variable.clicked.connect(self.add_input_variable) 111 | self.input_variable_selection = QComboBox() 112 | self.input_variable_selection.addItems(["_theta", "_c"]) 113 | self.list_input_variables = [] 114 | 115 | self.output_variables_widget = QWidget() 116 | self.output_variables_layout = QGridLayout() 117 | self.output_variables_widget.setLayout(self.output_variables_layout) 118 | self.button_add_output_variable = QPushButton("Add output variable") 119 | self.button_add_output_variable.clicked.connect(self.add_output_variable) 120 | self.output_variable_selection = QComboBox() 121 | self.output_variable_selection.addItems(["dQ", "dT", "a", "a'", "Cl", "Cd", "dFn", "dFt", "U4", "alpha", "phi"]) 122 | self.list_output_variables = [] 123 | 124 | self.target_variables_widget = QWidget() 125 | self.target_variables_layout = QGridLayout() 126 | self.target_variables_widget.setLayout(self.target_variables_layout) 127 | self.button_add_target_variable = QPushButton("Add target variable") 128 | self.button_add_target_variable.clicked.connect(self.add_target_variable) 129 | self.target_variable_selection = QComboBox() 130 | self.target_variable_selection.addItems(["dQ", "dT", "a", "a'", "Cl", "Cd", "dFn", "dFt", "U4", "alpha", "phi"]) 131 | self.list_target_variables = [] 132 | 133 | self.buttonOptimization = QPushButton("Run optimization") 134 | self.buttonOptimization.clicked.connect(self.run) 135 | 136 | self.buttonStop = QPushButton("Stop") 137 | self.buttonStop.clicked.connect(self.terminate) 138 | 139 | self.buttonClear = QPushButton("Clear screen") 140 | self.buttonClear.clicked.connect(self.clear) 141 | 142 | self.buttonEOF = QCheckBox() 143 | self.buttonEOF.setChecked(True) 144 | self.buttonEOFdescription = QLabel("Scroll to end of screen") 145 | 146 | for a, b in self.form_list: 147 | self.fbox.addRow(a, b) 148 | 149 | self.fbox.addRow(self.button_add_input_variable, self.input_variable_selection) 150 | self.fbox.addRow(self.input_variables_widget) 151 | 152 | self.fbox.addRow(self.button_add_output_variable, self.output_variable_selection) 153 | self.fbox.addRow(self.output_variables_widget) 154 | 155 | self.fbox.addRow(self.button_add_target_variable, self.target_variable_selection) 156 | self.fbox.addRow(self.target_variables_widget) 157 | 158 | self.fbox.addRow(self.buttonOptimization) 159 | self.fbox.addRow("", QLabel()) 160 | 161 | self.fbox.addRow(self.buttonClear, self.buttonStop) 162 | self.fbox.addRow(self.buttonEOFdescription, self.buttonEOF) 163 | 164 | self.win = PyQtGraphWindow(self) 165 | self.win.setWindowTitle("Live Optimization Visualizer") 166 | self.manager_pyqtgraph = Manager() 167 | self.queue_pyqtgraph = self.manager_pyqtgraph.list() 168 | self.queue_pyqtgraph.append([[0], [0], 0, 0]) 169 | 170 | self.tsr_string = QLabel("0") 171 | self.J_string = QLabel("0") 172 | self.fbox.addRow("TSR:", self.tsr_string) 173 | self.fbox.addRow("J:", self.J_string) 174 | 175 | def refresh_input_variables(self): 176 | """ 177 | 178 | """ 179 | for i in reversed(range(self.input_variables_layout.count())): 180 | self.input_variables_layout.itemAt(i).widget().setParent(None) 181 | 182 | i = 0 183 | for var, min_b, max_b in self.list_input_variables: 184 | name = QLabel(var) 185 | min_bound = QLineEdit(str(min_b)) 186 | min_bound.textChanged.connect(self.update_input_variables_list) 187 | max_bound = QLineEdit(str(max_b)) 188 | max_bound.textChanged.connect(self.update_input_variables_list) 189 | delete_button = QPushButton("X") 190 | delete_button.clicked.connect(self.delete_input_variable) 191 | self.input_variables_layout.addWidget(name, i, 0) 192 | self.input_variables_layout.addWidget(min_bound, i, 1) 193 | self.input_variables_layout.addWidget(max_bound, i, 2) 194 | self.input_variables_layout.addWidget(delete_button, i, 3) 195 | i += 1 196 | 197 | def refresh_output_variables(self): 198 | """ 199 | 200 | """ 201 | for i in reversed(range(self.output_variables_layout.count())): 202 | self.output_variables_layout.itemAt(i).widget().setParent(None) 203 | 204 | i = 0 205 | for var, coefficient in self.list_output_variables: 206 | name = QLabel(var) 207 | coefficient = QLineEdit(str(coefficient)) 208 | coefficient.textChanged.connect(self.update_output_variables_list) 209 | delete_button = QPushButton("X") 210 | delete_button.clicked.connect(self.delete_output_variable) 211 | self.output_variables_layout.addWidget(name, i, 0) 212 | self.output_variables_layout.addWidget(coefficient, i, 1) 213 | self.output_variables_layout.addWidget(delete_button, i, 2) 214 | i += 1 215 | 216 | def refresh_target_variables(self): 217 | """ 218 | 219 | """ 220 | for i in reversed(range(self.target_variables_layout.count())): 221 | self.target_variables_layout.itemAt(i).widget().setParent(None) 222 | 223 | i = 0 224 | for var, target_value, coefficient in self.list_target_variables: 225 | name = QLabel(var) 226 | 227 | target_value = QLineEdit(str(target_value)) 228 | target_value.textChanged.connect(self.update_target_variables_list) 229 | 230 | coefficient = QLineEdit(str(coefficient)) 231 | coefficient.textChanged.connect(self.update_target_variables_list) 232 | 233 | delete_button = QPushButton("X") 234 | delete_button.clicked.connect(self.delete_target_variable) 235 | 236 | self.target_variables_layout.addWidget(name, i, 0) 237 | self.target_variables_layout.addWidget(target_value, i, 1) 238 | self.target_variables_layout.addWidget(coefficient, i, 2) 239 | self.target_variables_layout.addWidget(delete_button, i, 3) 240 | i += 1 241 | 242 | def update_input_variables_list(self): 243 | """ 244 | 245 | """ 246 | for row in range(len(self.list_input_variables)): 247 | try: 248 | min_b = float(self.input_variables_layout.itemAtPosition(row, 1).widget().text()) 249 | self.list_input_variables[row][1] = min_b 250 | except ValueError: 251 | pass 252 | try: 253 | max_b = float(self.input_variables_layout.itemAtPosition(row, 2).widget().text()) 254 | self.list_input_variables[row][2] = max_b 255 | except ValueError: 256 | pass 257 | 258 | def update_output_variables_list(self): 259 | """ 260 | 261 | """ 262 | for row in range(len(self.list_output_variables)): 263 | try: 264 | coeff = float(self.output_variables_layout.itemAtPosition(row, 1).widget().text()) 265 | self.list_output_variables[row][1] = coeff 266 | except ValueError: 267 | pass 268 | 269 | def update_target_variables_list(self): 270 | """ 271 | 272 | """ 273 | for row in range(len(self.list_target_variables)): 274 | try: 275 | target_value = float(self.target_variables_layout.itemAtPosition(row, 1).widget().text()) 276 | self.list_target_variables[row][1] = target_value 277 | except ValueError: 278 | pass 279 | try: 280 | coeff = float(self.target_variables_layout.itemAtPosition(row, 2).widget().text()) 281 | self.list_target_variables[row][2] = coeff 282 | except ValueError: 283 | pass 284 | 285 | def delete_input_variable(self): 286 | """ 287 | 288 | """ 289 | button = self.sender() 290 | index = self.input_variables_layout.indexOf(button) 291 | row, column, _, _ = self.input_variables_layout.getItemPosition(index) 292 | del self.list_input_variables[row] 293 | self.refresh_input_variables() 294 | 295 | def delete_output_variable(self): 296 | """ 297 | 298 | """ 299 | button = self.sender() 300 | index = self.output_variables_layout.indexOf(button) 301 | row, column, _, _ = self.output_variables_layout.getItemPosition(index) 302 | del self.list_output_variables[row] 303 | self.refresh_output_variables() 304 | 305 | def delete_target_variable(self): 306 | """ 307 | 308 | """ 309 | button = self.sender() 310 | index = self.target_variables_layout.indexOf(button) 311 | row, column, _, _ = self.target_variables_layout.getItemPosition(index) 312 | del self.list_target_variables[row] 313 | self.refresh_target_variables() 314 | 315 | def add_input_variable(self): 316 | """ 317 | 318 | """ 319 | variable = str(self.input_variable_selection.currentText()) 320 | self.list_input_variables.append([variable, 0, 0]) 321 | self.refresh_input_variables() 322 | 323 | def add_output_variable(self): 324 | """ 325 | 326 | """ 327 | variable = str(self.output_variable_selection.currentText()) 328 | self.list_output_variables.append([variable, 0]) 329 | self.refresh_output_variables() 330 | 331 | def add_target_variable(self): 332 | """ 333 | 334 | """ 335 | variable = str(self.target_variable_selection.currentText()) 336 | self.list_target_variables.append([variable, 0, 1]) 337 | self.refresh_target_variables() 338 | 339 | def update_tsr_and_j(self): 340 | """ 341 | 342 | """ 343 | try: 344 | s = self.get_settings() 345 | R = float(self.main.wind_turbine_properties.R.text()) 346 | tsr = 2 * np.pi * float(s["target_rpm"]) * R / 60 / float(s["target_speed"]) 347 | self.tsr_string.setText("%.2f" % tsr) 348 | J = float(s["target_speed"]) / (float(s["target_rpm"]) / 60 * 2 * R) 349 | self.J_string.setText("%.2f" % J) 350 | except: 351 | pass 352 | 353 | def check_forms_angles(self): 354 | """ 355 | 356 | :return: 357 | """ 358 | out = "" 359 | _needed_vars = [[self._target_speed, self.target_speed], [self._target_rpm, self.target_rpm], ] 360 | for n, f in _needed_vars: 361 | if isinstance(f, QLineEdit): 362 | state = self.validator.validate(f.text(), 0)[0] 363 | if state == QtGui.QValidator.Acceptable: 364 | pass 365 | elif state == QtGui.QValidator.Intermediate: 366 | out += "Form %s appears not to be valid.\n" % n.text() 367 | else: 368 | out += "Form %s is not of the valid type.\n" % n.text() 369 | if out == "": 370 | return True 371 | return out 372 | 373 | def check_state(self, *args, **kwargs): 374 | """ 375 | 376 | :param args: 377 | :param kwargs: 378 | """ 379 | self.update_tsr_and_j() 380 | 381 | sender = self.sender() 382 | validator = sender.validator() 383 | state = validator.validate(sender.text(), 0)[0] 384 | if state == QtGui.QValidator.Acceptable: 385 | color = "#edf5e1" # green 386 | elif state == QtGui.QValidator.Intermediate: 387 | color = "#fff79a" # yellow 388 | else: 389 | color = "#f6989d" # red 390 | # sender.setStyleSheet("QLineEdit { background-color: %s; color: #000000 }" % color) 391 | 392 | def validate_inputs(self): 393 | """ 394 | 395 | :return: 396 | """ 397 | check = self.check_forms_angles() 398 | check_analysis = self.main.analysis.check_forms() 399 | if check != True or check_analysis != True: 400 | if check == True: 401 | check = "" 402 | if check_analysis == True: 403 | check_analysis = "" 404 | check = check + check_analysis 405 | msg = MyMessageBox() 406 | msg.setIcon(QMessageBox.Warning) 407 | msg.setText("Input validation error") 408 | msg.setDetailedText(check) 409 | msg.exec_() 410 | return False 411 | return True 412 | 413 | def clear(self): 414 | """ 415 | 416 | """ 417 | self.right_output_text_area.clear() 418 | 419 | def add_text(self, string): 420 | """ 421 | 422 | :param string: 423 | """ 424 | self.right_output_text_area.insertPlainText(string) 425 | if self.buttonEOF.checkState() == 2: 426 | self.right_output_text_area.moveCursor(QtGui.QTextCursor.End) 427 | 428 | def run(self): 429 | """ 430 | 431 | :return: 432 | """ 433 | self.clear() 434 | 435 | if not self.validate_inputs(): 436 | return 437 | 438 | self.main.emitter_add.connect(self.add_text) 439 | self.main.emitter_done.connect(self.terminate) 440 | 441 | if not self.main.running: 442 | self.main.set_process_running() 443 | self.runner_input = self.main.get_input_params() 444 | self.main.getter.start() 445 | self.p = Process(target=optimize, args=[self.runner_input, self.queue_pyqtgraph]) 446 | self.p.start() 447 | self.win.show() 448 | self.win.start_update() 449 | 450 | def terminate(self): 451 | """ 452 | 453 | """ 454 | self.main.set_process_stopped() 455 | try: 456 | self.p.terminate() 457 | except: 458 | pass 459 | self.main.getter.quit() 460 | 461 | self.main.emitter_add.disconnect() 462 | self.main.emitter_done.disconnect() 463 | 464 | self.win.stop_update() 465 | 466 | def get_settings(self): 467 | """ 468 | 469 | :return: 470 | """ 471 | out = {} 472 | out["target_rpm"] = self.target_rpm.text() 473 | out["target_speed"] = self.target_speed.text() 474 | out["mut_coeff"] = self.mut_coeff.text() 475 | out["population"] = self.population.text() 476 | out["num_iter"] = self.num_iter.text() 477 | 478 | for k, v in out.items(): 479 | if v == "": 480 | v = None 481 | elif v == None: 482 | pass 483 | else: 484 | v = to_float(v) 485 | out[k] = v 486 | 487 | self.update_input_variables_list() 488 | self.update_output_variables_list() 489 | self.update_target_variables_list() 490 | out["optimization_inputs"] = self.list_input_variables 491 | out["optimization_outputs"] = self.list_output_variables 492 | out["optimization_targets"] = self.list_target_variables 493 | return out 494 | 495 | def set_settings(self, inp_dict): 496 | """ 497 | 498 | :param inp_dict: 499 | """ 500 | if "target_rpm" in inp_dict.keys(): 501 | self.target_rpm.setText(str(inp_dict["target_rpm"])) 502 | if "target_speed" in inp_dict.keys(): 503 | self.target_speed.setText(str(inp_dict["target_speed"])) 504 | if "mut_coeff" in inp_dict.keys(): 505 | self.mut_coeff.setText(str(inp_dict["mut_coeff"])) 506 | if "population" in inp_dict.keys(): 507 | self.population.setText(str(inp_dict["population"])) 508 | if "num_iter" in inp_dict.keys(): 509 | self.num_iter.setText(str(inp_dict["num_iter"])) 510 | if "optimization_inputs" in inp_dict.keys(): 511 | self.list_input_variables = inp_dict["optimization_inputs"] 512 | if "optimization_outputs" in inp_dict.keys(): 513 | self.list_output_variables = inp_dict["optimization_outputs"] 514 | 515 | self.refresh_output_variables() 516 | self.refresh_input_variables() 517 | self.refresh_target_variables() 518 | -------------------------------------------------------------------------------- /UI/Table.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import csv 19 | 20 | from PyQt5 import QtCore 21 | from PyQt5.QtCore import pyqtSlot 22 | from PyQt5.QtWidgets import QWidget, QTableWidget, QVBoxLayout, QApplication, QTableWidgetItem, QMenu 23 | 24 | from utils import array_to_csv 25 | 26 | 27 | class Table(QWidget): 28 | """ 29 | 30 | """ 31 | def __init__(self, fixed_columns=False): 32 | super().__init__() 33 | self.selected_array = [] 34 | self.fixed_columns = fixed_columns 35 | self.tableWidget = QTableWidget() 36 | self.tableWidget.setTabKeyNavigation(False) 37 | self.layout = QVBoxLayout() 38 | self.initUI() 39 | self.clip = QApplication.clipboard() 40 | self.set_headers() 41 | 42 | def set_headers(self): 43 | """ 44 | 45 | """ 46 | self.horizontal_headers = self.tableWidget.horizontalHeader() 47 | self.horizontal_headers.setContextMenuPolicy( 48 | QtCore.Qt.CustomContextMenu) 49 | self.horizontal_headers.customContextMenuRequested.connect( 50 | self.horizontal_header_popup) 51 | self.vertical_headers = self.tableWidget.verticalHeader() 52 | self.vertical_headers.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 53 | self.vertical_headers.customContextMenuRequested.connect( 54 | self.vertical_header_popup) 55 | 56 | def set_labels(self, arr): 57 | """ 58 | 59 | :param arr: 60 | """ 61 | self.tableWidget.setHorizontalHeaderLabels(arr) 62 | 63 | def initUI(self): 64 | """ 65 | 66 | """ 67 | self.createEmpty(4, 4) 68 | 69 | # Add box layout, add table to box layout and add box layout to widget 70 | self.layout.addWidget(self.tableWidget) 71 | self.setLayout(self.layout) 72 | 73 | # Show widget 74 | self.show() 75 | 76 | def createTable(self, array): 77 | """ 78 | 79 | :param array: 80 | """ 81 | if len(array) > 0: 82 | # Create table 83 | self.tableWidget.setRowCount(len(array)) 84 | self.tableWidget.setColumnCount(len(array[0])) 85 | i = 0 86 | for r in array: 87 | j = 0 88 | for c in r: 89 | if not isinstance(c, str): 90 | c = str(c) 91 | self.tableWidget.setItem(i, j, QTableWidgetItem(c)) 92 | j += 1 93 | i += 1 94 | 95 | self.tableWidget.move(0, 0) 96 | 97 | # table selection change 98 | self.tableWidget.clicked.connect(self.on_click) 99 | 100 | def createEmpty(self, x, y): 101 | """ 102 | 103 | :param x: 104 | :param y: 105 | """ 106 | # Create table 107 | self.tableWidget.setRowCount(y) 108 | self.tableWidget.setColumnCount(x) 109 | 110 | self.tableWidget.move(0, 0) 111 | 112 | # table selection change 113 | self.tableWidget.clicked.connect(self.on_click) 114 | 115 | @pyqtSlot() 116 | def on_click(self): 117 | """ 118 | 119 | """ 120 | self.get_selected() 121 | 122 | def get_selected(self): 123 | """ 124 | 125 | :return: 126 | """ 127 | self.selected_array = [] 128 | 129 | rows_added = sorted(set(index.row() 130 | for index in self.tableWidget.selectedIndexes())) 131 | columns_added = sorted(set(index.column() 132 | for index in self.tableWidget.selectedIndexes())) 133 | 134 | delta_r = rows_added[0] 135 | delta_c = columns_added[0] 136 | 137 | for r in range(rows_added[-1] - rows_added[0] + 1): 138 | self.selected_array.append([]) 139 | for c in range(columns_added[-1] - columns_added[0] + 1): 140 | self.selected_array[r].append(None) 141 | for currentQTableWidgetItem in self.tableWidget.selectedItems(): 142 | row = currentQTableWidgetItem.row() - delta_r 143 | column = currentQTableWidgetItem.column() - delta_c 144 | text = currentQTableWidgetItem.text() 145 | self.selected_array[row][column] = text 146 | return self.selected_array 147 | 148 | def keyPressEvent(self, e): 149 | """ 150 | 151 | :param e: 152 | """ 153 | if e.modifiers() & QtCore.Qt.ControlModifier: 154 | if e.key() == QtCore.Qt.Key_C: # copy 155 | s = array_to_csv(self.get_selected()) 156 | self.clip.setText(s) 157 | if e.key() == QtCore.Qt.Key_Return or e.key() == QtCore.Qt.Key_Enter: 158 | self.select_next_row() 159 | if e.modifiers() & QtCore.Qt.ControlModifier: 160 | if e.key() == QtCore.Qt.Key_V: # paste 161 | self.paste() 162 | 163 | if e.modifiers() & QtCore.Qt.ControlModifier: 164 | if e.key() == QtCore.Qt.Key_S: # test 165 | self.get_values() 166 | 167 | if e.key() == QtCore.Qt.Key_Delete: 168 | self.delete_data() 169 | 170 | def paste(self): 171 | """ 172 | 173 | :return: 174 | """ 175 | results = [] 176 | text = self.clip.text() 177 | text = text.replace(" ", "\t") 178 | text = text.replace(" ", "\t") 179 | text = text.replace(",", ".") 180 | if len(text) > 0: 181 | # change contents to floats 182 | reader = csv.reader(text.splitlines(), delimiter="\t") 183 | for row in reader: # each row is a list 184 | results.append(row) 185 | numrows = len(results) 186 | numcolumns = len(results[0]) 187 | selected_row = sorted( 188 | set(index.row() for index in self.tableWidget.selectedIndexes()))[0] 189 | selected_column = sorted( 190 | set(index.column() for index in self.tableWidget.selectedIndexes()))[0] 191 | if selected_row + numrows >= self.tableWidget.rowCount(): 192 | self.tableWidget.setRowCount(selected_row + numrows) 193 | if not self.fixed_columns: 194 | if selected_column + numcolumns >= self.tableWidget.columnCount(): 195 | self.tableWidget.setColumnCount(selected_column + numcolumns) 196 | currow = selected_row 197 | for r in results: 198 | curcolumn = selected_column 199 | for c in r: 200 | if curcolumn < self.tableWidget.columnCount(): 201 | self.tableWidget.setItem( 202 | currow, curcolumn, QTableWidgetItem(c)) 203 | curcolumn += 1 204 | currow += 1 205 | return 206 | 207 | def delete_data(self): 208 | """ 209 | 210 | """ 211 | rows = sorted(set(index.row() for index in self.tableWidget.selectedIndexes())) 212 | columns = sorted(set(index.column() for index in self.tableWidget.selectedIndexes())) 213 | for r in rows: 214 | for c in columns: 215 | self.tableWidget.setItem(r, c, QTableWidgetItem("")) 216 | 217 | def clear_table(self): 218 | """ 219 | 220 | """ 221 | num_rows = self.tableWidget.rowCount() 222 | num_columns = self.tableWidget.columnCount() 223 | row_indexes = range(num_rows) 224 | column_indexes = range(num_columns) 225 | for r in row_indexes: 226 | for c in column_indexes: 227 | self.tableWidget.setItem(r, c, QTableWidgetItem("")) 228 | 229 | def select_next_row(self): 230 | """ 231 | 232 | """ 233 | rows = sorted(set(index.row() 234 | for index in self.tableWidget.selectedIndexes())) 235 | columns = sorted(set(index.column() 236 | for index in self.tableWidget.selectedIndexes())) 237 | last_selected_row = rows[-1] 238 | first_selected_column = columns[0] 239 | num_rows = self.tableWidget.rowCount() 240 | if last_selected_row + 1 >= num_rows: 241 | self.tableWidget.insertRow(num_rows) 242 | self.tableWidget.setCurrentCell( 243 | last_selected_row + 1, first_selected_column) 244 | 245 | def get_values(self): 246 | """ 247 | 248 | :return: 249 | """ 250 | data = [] 251 | for row in range(self.tableWidget.rowCount()): 252 | data.append([]) 253 | for column in range(self.tableWidget.columnCount()): 254 | item = self.tableWidget.item(row, column) 255 | if item == None: 256 | item = "" 257 | else: 258 | item = item.text() 259 | data[row].append(item) 260 | return data 261 | 262 | def contextMenuEvent(self, event): 263 | """ 264 | 265 | :param event: 266 | """ 267 | menu = QMenu(self) 268 | item = self.tableWidget.itemAt(event.pos()) 269 | 270 | if not self.fixed_columns: 271 | delete_column = menu.addAction("Delete column(s)") 272 | insert_column = menu.addAction("Insert column") 273 | 274 | delete_row = menu.addAction("Delete row(s)") 275 | insert_row = menu.addAction("Insert row") 276 | 277 | action = menu.exec_(self.mapToGlobal(event.pos())) 278 | 279 | rows = sorted(set(index.row() for index in self.tableWidget.selectedIndexes()), reverse=True) 280 | columns = sorted(set(index.column() for index in self.tableWidget.selectedIndexes()), reverse=True) 281 | 282 | if action == insert_row: 283 | if len(rows) == 0: 284 | rows.append(0) 285 | self.tableWidget.insertRow(rows[-1]) 286 | for c in range(self.tableWidget.columnCount()): 287 | self.tableWidget.setItem(rows[-1], c, QTableWidgetItem("")) 288 | if action == delete_row: 289 | for r in rows: 290 | self.tableWidget.removeRow(r) 291 | if not self.fixed_columns: 292 | if action == insert_column: 293 | if len(columns) == 0: 294 | columns.append(0) 295 | self.tableWidget.insertColumn(columns[-1]) 296 | for r in range(self.tableWidget.rowCount()): 297 | self.tableWidget.setItem(r, columns[-1], QTableWidgetItem("")) 298 | if action == delete_column: 299 | for c in columns: 300 | self.tableWidget.removeColumn(c) 301 | 302 | def horizontal_header_popup(self, position): 303 | """ 304 | 305 | :param position: 306 | """ 307 | pass 308 | 309 | def vertical_header_popup(self, position): 310 | """ 311 | 312 | :param position: 313 | """ 314 | pass 315 | -------------------------------------------------------------------------------- /UI/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihasm/Python-BEM/27801aa46112661f4e476f2996d2cfb6395899d6/UI/__init__.py -------------------------------------------------------------------------------- /UI/helpers.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import sys 19 | import traceback 20 | 21 | import pyqtgraph as pg 22 | import pyqtgraph.opengl as gl 23 | from PyQt5 import QtCore, QtGui 24 | from PyQt5.QtCore import QThread 25 | from PyQt5.QtGui import QTextCursor 26 | from PyQt5.QtWidgets import QMainWindow, QWidget, QGridLayout, QLabel, QLineEdit, QPushButton, QTabWidget, \ 27 | QTextEdit, QFormLayout, QMessageBox, QErrorMessage 28 | from matplotlib import pyplot as plt 29 | from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar 30 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 31 | import numpy as np 32 | 33 | from scraping import scrape_data, get_x_y_from_link 34 | from utils import generate_propeller_adkins 35 | from bem import ICON_PATH 36 | from xfoil import generate_polars_data 37 | 38 | 39 | 40 | class ThreadGetter(QThread): 41 | """ 42 | 43 | """ 44 | def __init__(self, parent): 45 | super(ThreadGetter, self).__init__(parent) 46 | self.dataCollectionTimer = QtCore.QTimer() 47 | self.dataCollectionTimer.moveToThread(self) 48 | self.dataCollectionTimer.timeout.connect(self.updateInProc) 49 | 50 | def run(self): 51 | """ 52 | 53 | """ 54 | self.dataCollectionTimer.start(2) # 0 causes freeze 55 | self.loop = QtCore.QEventLoop() 56 | self.loop.exec_() 57 | 58 | def updateInProc(self): 59 | """ 60 | 61 | """ 62 | if len(self.parent().return_print) > 0: 63 | t = self.parent().return_print.pop(0) 64 | self.parent().emitter_add.emit(str(t)) 65 | if self.parent().end_of_file.value == True and len(self.parent().return_print) == 0: 66 | self.parent().emitter_done.emit() 67 | 68 | 69 | class DataCaptureThread(QThread): 70 | """ 71 | 72 | """ 73 | def __init__(self, parent, *args, **kwargs): 74 | QThread.__init__(self, parent, *args, **kwargs) 75 | self.dataCollectionTimer = QtCore.QTimer() 76 | self.dataCollectionTimer.moveToThread(self) 77 | self.dataCollectionTimer.timeout.connect(self.updateInProc) 78 | 79 | def run(self): 80 | """ 81 | 82 | """ 83 | self.dataCollectionTimer.start(50) # 0 causes freeze 84 | self.loop = QtCore.QEventLoop() 85 | self.loop.exec_() 86 | 87 | def updateInProc(self): 88 | """ 89 | 90 | """ 91 | if len(self.parent().parent.queue_pyqtgraph) > 0: 92 | item = self.parent().parent.queue_pyqtgraph[0] 93 | x = item[0] 94 | y = item[1] 95 | best_x = [item[2]] 96 | best_y = [item[3]] 97 | self.parent().curve.setData(x, y) 98 | self.parent().curve_red.setData(best_x, best_y) 99 | 100 | 101 | class XFoilThread(QThread): 102 | """ 103 | 104 | """ 105 | startedSignal = QtCore.Signal(int) 106 | completeSignal = QtCore.Signal(str) 107 | 108 | def __init__(self, parent, *args, **kwargs): 109 | QThread.__init__(self, parent) 110 | self.parent = parent 111 | 112 | def set_params(self, dat_path, 113 | alpha_from, alpha_to, alpha_num, 114 | reynolds_from, reynolds_to, reynolds_num, 115 | ncrit): 116 | """ 117 | 118 | :param dat_path: 119 | :param alpha_from: 120 | :param alpha_to: 121 | :param alpha_num: 122 | :param reynolds_from: 123 | :param reynolds_to: 124 | :param reynolds_num: 125 | :param ncrit: 126 | """ 127 | self.dat_path = dat_path 128 | self.alpha_from = alpha_from 129 | self.alpha_to = alpha_to 130 | self.alpha_num = alpha_num 131 | self.reynolds_from = reynolds_from 132 | self.reynolds_to = reynolds_to 133 | self.reynolds_num = reynolds_num 134 | self.ncrit = ncrit 135 | 136 | def run(self): 137 | """ 138 | 139 | """ 140 | self.startedSignal.emit("Started") 141 | out = generate_polars_data( 142 | self.dat_path, 143 | self.alpha_from, 144 | self.alpha_to, 145 | self.alpha_num, 146 | self.reynolds_from, 147 | self.reynolds_to, 148 | self.reynolds_num, 149 | self.ncrit 150 | ) 151 | self.parent.xfoil_generated_data = out 152 | self.completeSignal.emit("Done") 153 | 154 | 155 | class XfoilOptionsWindow(QWidget): 156 | """ 157 | 158 | """ 159 | def __init__(self, parent): 160 | super(XfoilOptionsWindow, self).__init__(None) 161 | self.setWindowIcon(QtGui.QIcon(ICON_PATH)) 162 | 163 | self.parent = parent 164 | self.layout = QFormLayout() 165 | self.setLayout(self.layout) 166 | 167 | self.alpha_from_label = QLabel("Alpha from") 168 | self.alpha_from = QLineEdit("-10") 169 | self.layout.addRow(self.alpha_from_label, self.alpha_from) 170 | 171 | self.alpha_to_label = QLabel("Alpha to") 172 | self.alpha_to = QLineEdit("20") 173 | self.layout.addRow(self.alpha_to_label, self.alpha_to) 174 | 175 | self.alpha_num_label = QLabel("Alpha number") 176 | self.alpha_num = QLineEdit("31") 177 | self.layout.addRow(self.alpha_num_label, self.alpha_num) 178 | 179 | self.reynolds_from_label = QLabel("Reynolds from") 180 | self.reynolds_from = QLineEdit("50000") 181 | self.layout.addRow(self.reynolds_from_label, self.reynolds_from) 182 | 183 | self.reynolds_to_label = QLabel("Reynolds to") 184 | self.reynolds_to = QLineEdit("1000000") 185 | self.layout.addRow(self.reynolds_to_label, self.reynolds_to) 186 | 187 | self.reynolds_num_label = QLabel("Reynolds number") 188 | self.reynolds_num = QLineEdit("5") 189 | self.layout.addRow(self.reynolds_num_label, self.reynolds_num) 190 | 191 | self.ncrit_label = QLabel("Ncrit") 192 | self.ncrit = QLineEdit("9") 193 | self.layout.addRow(self.ncrit_label, self.ncrit) 194 | 195 | self.button_run_xfoil = QPushButton("Run") 196 | self.button_run_xfoil.clicked.connect(self.parent.generate_curves_xfoil) 197 | self.layout.addRow(self.button_run_xfoil) 198 | 199 | self.button_stop_xfoil = QPushButton("Stop") 200 | self.button_stop_xfoil.clicked.connect(self.parent.stop_xfoil) 201 | self.layout.addRow(self.button_stop_xfoil) 202 | self.button_stop_xfoil.setDisabled(True) 203 | 204 | self.show() 205 | 206 | def closeEvent(self, event): 207 | """ 208 | 209 | :param event: 210 | """ 211 | event.accept() # let the window close 212 | 213 | 214 | class AdkinsThread(QThread): 215 | """ 216 | 217 | """ 218 | progressSignal = QtCore.Signal(int) 219 | completeSignal = QtCore.Signal(str) 220 | 221 | def __init__(self, parent, *args, **kwargs): 222 | QThread.__init__(self, parent) 223 | self.parent = parent 224 | 225 | def set_params(self, inp): 226 | """ 227 | 228 | :param inp: 229 | """ 230 | self.inp = inp 231 | 232 | def run(self): 233 | """ 234 | 235 | """ 236 | out = generate_propeller_adkins(self.inp) 237 | self.parent.adkins_return = out 238 | self.completeSignal.emit("Done") 239 | 240 | 241 | class ScrapeThread(QThread): 242 | """ 243 | 244 | """ 245 | progressSignal = QtCore.Signal(int) 246 | completeSignal = QtCore.Signal(str) 247 | 248 | def __init__(self, parent, *args, **kwargs): 249 | QThread.__init__(self, parent) 250 | self.parent = parent 251 | 252 | def set_params(self, link): 253 | """ 254 | 255 | :param link: 256 | """ 257 | self.link = link 258 | 259 | def run(self): 260 | """ 261 | 262 | """ 263 | print("Running scrape") 264 | data = scrape_data(self.link.text()) 265 | if not isinstance(data,np.ndarray): 266 | if data == None: 267 | print("Scraping error, aborting!") 268 | self.completeSignal.emit("Done") 269 | return 270 | x, y = get_x_y_from_link(self.link.text()) 271 | out = [data, x, y] 272 | self.parent.scraping_generated_data = out 273 | self.completeSignal.emit("Done") 274 | print("Done scraping thread") 275 | 276 | 277 | class PyQtGraphWindow(QMainWindow): 278 | """ 279 | 280 | """ 281 | def __init__(self, parent): 282 | super(PyQtGraphWindow, self).__init__(parent) 283 | self.setWindowIcon(QtGui.QIcon(ICON_PATH)) 284 | self.obj = pg.PlotWidget() 285 | self.setCentralWidget(self.obj) 286 | self.curve = self.obj.plot(pen=None, symbol='o', symbolPen=None, symbolSize=4, symbolBrush='g') 287 | self.curve_red = self.obj.plot(pen=None, symbol='o', symbolPen=None, symbolSize=5, symbolBrush='r') 288 | self.obj.setLabel("left", "Optimization variable") 289 | self.obj.setLabel("bottom", "Theta [°]") 290 | self.parent = parent 291 | self.thread = DataCaptureThread(self) 292 | 293 | def start_update(self): 294 | """ 295 | 296 | """ 297 | self.thread.start() 298 | 299 | def stop_update(self): 300 | """ 301 | 302 | """ 303 | self.thread.quit() 304 | 305 | class PyQtGraph3DWindow(QMainWindow): 306 | """ 307 | 308 | """ 309 | def __init__(self, parent): 310 | super(PyQtGraph3DWindow, self).__init__(parent) 311 | self.setWindowIcon(QtGui.QIcon(ICON_PATH)) 312 | self.setWindowTitle("3D visualization") 313 | #self.layout = QGridLayout() 314 | #self.setLayout(self.layout) 315 | 316 | self.w = gl.GLViewWidget(self) 317 | self.w.opts['distance'] = 1 318 | #self.layout.addWidget(self.w,0,0) 319 | self.setCentralWidget(self.w) 320 | 321 | # create the background grids 322 | gx = gl.GLGridItem() 323 | gx.setSize(2,2,2) 324 | gx.setSpacing(0.1,0.1,0.1) 325 | gx.rotate(90, 0, 1, 0) 326 | gx.translate(-1, 0, 1) 327 | self.w.addItem(gx) 328 | gy = gl.GLGridItem() 329 | gy.setSize(2,2,2) 330 | gy.setSpacing(0.1,0.1,0.1) 331 | gy.rotate(90, 1, 0, 0) 332 | gy.translate(0, -1, 1) 333 | self.w.addItem(gy) 334 | gz = gl.GLGridItem() 335 | gz.setSize(2,2,2) 336 | gz.setSpacing(0.1,0.1,0.1) 337 | gz.translate(0, 0, 0) 338 | self.w.addItem(gz) 339 | 340 | self.show() 341 | 342 | def set_points(self,points_x,points_y,points_z): 343 | ar = np.array([points_x,points_y,points_z]).transpose() 344 | points = gl.GLLinePlotItem(pos=ar) 345 | self.w.addItem(points) 346 | 347 | 348 | class PopupText(QWidget): 349 | """ 350 | 351 | """ 352 | def __init__(self, message="message", default_str="", emitter=None, windowTitle=""): 353 | QWidget.__init__(self) 354 | 355 | self.emitter = emitter 356 | 357 | self.layout = QGridLayout() 358 | self.setLayout(self.layout) 359 | 360 | self.message = QLabel(message) 361 | 362 | self.layout.addWidget(self.message, 0, 0) 363 | 364 | self.inp = QLineEdit() 365 | self.inp.setText(default_str) 366 | self.inp.returnPressed.connect(self.send_signal) 367 | self.layout.addWidget(self.inp, 1, 0) 368 | 369 | self.button = QPushButton("OK") 370 | self.button.clicked.connect(self.send_signal) 371 | self.layout.addWidget(self.button, 2, 0) 372 | 373 | self.setWindowTitle(windowTitle) 374 | self.setWindowIcon(QtGui.QIcon(ICON_PATH)) 375 | 376 | def send_signal(self): 377 | """ 378 | 379 | """ 380 | if self.emitter != None: 381 | self.emitter.emit(self.inp.text()) 382 | self.close() 383 | 384 | def closeEvent(self, event): 385 | """ 386 | 387 | :param event: 388 | """ 389 | self.emitter.disconnect() 390 | event.accept() 391 | 392 | class PopupConfirmation(QWidget): 393 | """ 394 | 395 | """ 396 | def __init__(self, message="message", windowTitle="", emitter_yes=None, emitter_no=None): 397 | QWidget.__init__(self) 398 | 399 | self.emitter_yes = emitter_yes 400 | self.emitter_no = emitter_no 401 | 402 | self.layout = QGridLayout() 403 | self.setLayout(self.layout) 404 | 405 | self.message = QLabel(message) 406 | 407 | self.layout.addWidget(self.message, 0, 0) 408 | 409 | self.button_yes = QPushButton("Yes") 410 | self.button_yes.clicked.connect(self.send_signal_yes) 411 | self.layout.addWidget(self.button_yes, 1, 0) 412 | 413 | self.button_no = QPushButton("No") 414 | self.button_no.clicked.connect(self.send_signal_no) 415 | self.layout.addWidget(self.button_no, 2, 0) 416 | 417 | self.setWindowTitle(windowTitle) 418 | self.setWindowIcon(QtGui.QIcon(ICON_PATH)) 419 | 420 | def send_signal_yes(self): 421 | """ 422 | 423 | """ 424 | if self.emitter_yes != None: 425 | self.emitter_yes.emit("1") 426 | self.close() 427 | 428 | def send_signal_no(self): 429 | """ 430 | 431 | """ 432 | if self.emitter_no != None: 433 | self.emitter_no.emit("1") 434 | self.close() 435 | 436 | def closeEvent(self, event): 437 | """ 438 | 439 | :param event: 440 | """ 441 | if self.emitter_yes != None: 442 | self.emitter_yes.disconnect() 443 | if self.emitter_no != None: 444 | self.emitter_no.disconnect() 445 | event.accept() 446 | 447 | 448 | class MatplotlibWindow(QWidget): 449 | """ 450 | 451 | """ 452 | def __init__(self): 453 | super(MatplotlibWindow, self).__init__(None) 454 | self.setWindowIcon(QtGui.QIcon(ICON_PATH)) 455 | self.layout = QGridLayout() 456 | self.setLayout(self.layout) 457 | self.figure = plt.figure(figsize=(10, 5)) 458 | self.canvas = FigureCanvas(self.figure) 459 | self.canvas.setMinimumSize(500, 500) 460 | self.toolbar = NavigationToolbar(self.canvas, self) 461 | self.layout.addWidget(self.canvas) 462 | self.layout.addWidget(self.toolbar) 463 | self.show() 464 | 465 | def closeEvent(self, event): 466 | """ 467 | 468 | :param event: 469 | """ 470 | self.figure.clear() 471 | plt.close(self.figure) 472 | event.accept() # let the window close 473 | 474 | 475 | class PrintoutWindow(QMainWindow): 476 | """ 477 | 478 | """ 479 | def __init__(self, parent): 480 | super(PrintoutWindow, self).__init__(parent) 481 | self.setWindowIcon(QtGui.QIcon(ICON_PATH)) 482 | self.setWindowTitle("Progress") 483 | self.setGeometry(50, 50, 500, 300) 484 | self.parent = parent 485 | sys.stdout = Stream(newText=self.onUpdateText) 486 | sys.stderr = Stream(newText=self.onUpdateText) 487 | self.process = QTextEdit() 488 | self.setCentralWidget(self.process) 489 | self.show() 490 | 491 | def onUpdateText(self, text): 492 | """ 493 | 494 | :param text: 495 | """ 496 | cursor = self.process.textCursor() 497 | cursor.movePosition(QTextCursor.End) 498 | cursor.insertText(text) 499 | self.process.setTextCursor(cursor) 500 | self.process.ensureCursorVisible() 501 | 502 | def closeEvent(self, event): 503 | """ 504 | 505 | :param event: 506 | """ 507 | sys.stdout = sys.__stdout__ 508 | sys.stderr = sys.__stderr__ 509 | QMainWindow.closeEvent(self, event) 510 | 511 | 512 | class Stream(QtCore.QObject): 513 | newText = QtCore.pyqtSignal(str) 514 | 515 | def write(self, text): 516 | """ 517 | 518 | :param text: 519 | """ 520 | self.newText.emit(str(text)) 521 | 522 | 523 | class TabWidget(QTabWidget): 524 | """ 525 | 526 | """ 527 | def __init__(self, parent=None): 528 | super(TabWidget, self).__init__(parent) 529 | self.tabs = [] 530 | 531 | def add_tab(self, widget, tab_name, tooltip=None): 532 | """ 533 | 534 | :param widget: 535 | :param tab_name: 536 | :param tooltip: 537 | """ 538 | self.tabs.append([widget, tab_name]) 539 | self.addTab(widget, tab_name) 540 | if tooltip != None: 541 | self.setTabToolTip(len(self.tabs) - 1, tooltip) 542 | 543 | def remove_tab(self, index): 544 | """ 545 | 546 | :param index: 547 | """ 548 | self.removeTab(index) 549 | del self.tabs[index] 550 | 551 | def remove_all_tabs(self): 552 | """ 553 | 554 | """ 555 | while len(self.tabs) > 0: 556 | self.remove_tab(0) 557 | 558 | def remove_current_tab(self): 559 | """ 560 | 561 | """ 562 | self.remove_tab(self.currentIndex()) 563 | 564 | def rename_current_tab(self, string): 565 | """ 566 | 567 | :param string: 568 | """ 569 | self.setTabText(self.currentIndex(), string) 570 | self.tabs[self.currentIndex()][1] = string 571 | 572 | def current_tab_name(self): 573 | """ 574 | 575 | :return: 576 | """ 577 | return self.tabText(self.currentIndex()) 578 | 579 | 580 | class MyMessageBox(QMessageBox): 581 | """ 582 | 583 | """ 584 | 585 | def __init__(self): 586 | QMessageBox.__init__(self) 587 | super().__init__() 588 | self.setSizeGripEnabled(True) 589 | 590 | def event(self, e): 591 | """ 592 | 593 | :param e: 594 | :return: 595 | """ 596 | result = QMessageBox.event(self, e) 597 | 598 | self.setMinimumHeight(0) 599 | self.setMaximumHeight(16777215) 600 | self.setMinimumWidth(0) 601 | self.setMaximumWidth(16777215) 602 | textEdit = self.findChild(QTextEdit) 603 | if textEdit != None: 604 | textEdit.setMinimumHeight(0) 605 | textEdit.setMaximumHeight(16777215) 606 | textEdit.setMinimumWidth(0) 607 | textEdit.setMaximumWidth(16777215) 608 | return result 609 | 610 | 611 | def ErrorMessageBox(): 612 | """ 613 | 614 | """ 615 | var = traceback.format_exc() 616 | error_dialog = QErrorMessage() 617 | error_dialog.setWindowTitle("Error") 618 | #error_dialog.setIcon(QMessageBox.Warning) 619 | error_dialog.showMessage(str(var)) 620 | error_dialog.exec_() -------------------------------------------------------------------------------- /bem.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # coding=utf-8 19 | __author__ = "Miha Smrekar" 20 | __credits__ = ["Miha Smrekar"] 21 | __license__ = "GPL" 22 | __version__ = "0.4.8" 23 | __maintainer__ = "Miha Smrekar" 24 | __email__ = "xmiha.xsmrekar@gmail.com" 25 | __status__ = "Development" 26 | 27 | import ctypes 28 | import multiprocessing 29 | import os 30 | import sys 31 | 32 | import numpy as np 33 | from PyQt5 import QtCore 34 | from PyQt5 import QtGui 35 | from PyQt5 import QtWidgets 36 | from PyQt5.QtCore import QLocale 37 | from PyQt5.QtWidgets import (QApplication) 38 | 39 | from UI import MainWindow 40 | from utils import (QDarkPalette) 41 | 42 | np.set_printoptions(threshold=sys.maxsize) 43 | 44 | TITLE_STR = "BEM analiza v%s" % __version__ 45 | 46 | # determine if application is a script file or frozen exe 47 | if getattr(sys, 'frozen', False): 48 | application_path = os.path.dirname(sys.executable) 49 | elif __file__: 50 | application_path = os.path.dirname(__file__) 51 | 52 | # determine if application is a one-file exe 53 | if hasattr(sys, "_MEIPASS"): 54 | # yes, resources are stored in temporary folder C:\TEMP or wherever it is 55 | data_path = sys._MEIPASS 56 | else: 57 | # else, resources are stored in same folder as executable 58 | data_path = application_path 59 | 60 | ICON_PATH = os.path.join(data_path, "icon_bem.ico") 61 | DEFAULT_SETTINGS_PATH = os.path.join(data_path,"karlsen.bem") 62 | 63 | QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) 64 | 65 | def main(quick_results=False): 66 | """ 67 | :param quick_results: 68 | """ 69 | if sys.platform.startswith("win"): 70 | # On Windows calling this function is necessary for multiprocessing. 71 | multiprocessing.freeze_support() 72 | # To show icon in taskbar 73 | myappid = 'FEUM.BEM_Analiza.%s' % __version__ # arbitrary string 74 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) 75 | 76 | app = QApplication([]) 77 | QLocale.setDefault(QLocale(QLocale.English)) # da je pika decimalno mesto 78 | app.setWindowIcon(QtGui.QIcon(ICON_PATH)) 79 | app.setStyle("Fusion") 80 | if sys.platform.startswith("darwin") or True: 81 | # dark theme fix on OSX 82 | palette = QDarkPalette() 83 | palette.set_app(app) 84 | palette.set_stylesheet(app) 85 | screen = app.primaryScreen() 86 | size = screen.size() 87 | main_win = MainWindow.MainWindow(size.width(), size.height()) 88 | font = main_win.font() 89 | font.setPointSize(7) 90 | app.instance().setFont(font) 91 | if quick_results: 92 | main_win.analysis.run() 93 | sys.exit(app.exec_()) 94 | 95 | 96 | if __name__ == "__main__": 97 | main() 98 | -------------------------------------------------------------------------------- /bending_inertia.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import math 19 | 20 | import numpy as np 21 | from scipy.interpolate import interp1d 22 | 23 | pi = math.pi 24 | 25 | 26 | def PointsInCircum(r, n=1000): 27 | """ 28 | Generates circle x,y coordinates. 29 | :param r: float: radius 30 | :param n: int: number of points 31 | :return: tuple: (list of x points, list of y points) 32 | """ 33 | x_points = [math.cos(2 * pi / n * x) * r for x in range(0, n + 1)] 34 | y_points = [math.sin(2 * pi / n * x) * r for x in range(0, n + 1)] 35 | return x_points, y_points 36 | 37 | 38 | def interpolate_airfoil(x, y, num_interp=100): 39 | """ 40 | Interpolates airfoil x,y data. Airfoil has to be non-rotated. 41 | :param x: list of floats: x coordinates 42 | :param y: list of floats: y coordinates 43 | :param num_interp: int: number of interpolated points 44 | :return: tuple: (list of x points, list of y points) 45 | """ 46 | cross = np.where(np.diff(np.signbit(np.gradient(x))))[0][0] 47 | 48 | x_up = x[:cross + 2] 49 | y_up = y[:cross + 2] 50 | x_down = x[cross + 1:] 51 | y_down = y[cross + 1:] 52 | 53 | interp_up = interp1d(x_up, y_up, 'linear') 54 | interp_down = interp1d(x_down, y_down, 'linear') 55 | 56 | _x = np.linspace(np.min(x), np.max(x), num_interp) 57 | _y_up = interp_up(_x) 58 | _y_down = interp_down(_x) 59 | x_out = np.concatenate((np.flip(_x), _x)) 60 | y_out = np.concatenate((np.flip(_y_up), _y_down)) 61 | return x_out, y_out 62 | 63 | 64 | def calculate_bending_inertia_2(x, y): 65 | """ 66 | Calculates the bending intertia (second moment of area) for a given set of points. 67 | Any polygon equations: https://en.wikipedia.org/wiki/Second_moment_of_area 68 | Area equation: https://en.wikipedia.org/wiki/Polygon#Area 69 | :param x: list of floats: x coordinates 70 | :param y: list of floats: y coordinates 71 | :return: tuple: (Ix,Iy,Ixy,A) 72 | """ 73 | Iy = 0 74 | Ix = 0 75 | Ixy = 0 76 | A = 0 77 | for i in range(len(x) - 1): 78 | Iy += 1 / 12 * (x[i] * y[i + 1] - x[i + 1] * y[i]) * (x[i] ** 2 + x[i] * x[i + 1] + x[i + 1] ** 2) 79 | Ix += 1 / 12 * (x[i] * y[i + 1] - x[i + 1] * y[i]) * (y[i] ** 2 + y[i] * y[i + 1] + y[i + 1] ** 2) 80 | Ixy += 1 / 24 * (x[i] * y[i + 1] - x[i + 1] * y[i]) * ( 81 | x[i] * y[i + 1] + 2 * x[i] * y[i] + 2 * x[i + 1] * y[i + 1] + x[i + 1] * y[i]) 82 | A += 0.5 * (x[i] * y[i + 1] - x[i + 1] * y[i]) 83 | return Ix, Iy, Ixy, A 84 | 85 | 86 | def generate_hollow_foil(x, y, thickness): 87 | """ 88 | Generates hollow airfoil from x,y coordinates. 89 | 90 | thickness should be given in p.u. 91 | 92 | This operation must be done BEFORE ROTATION. 93 | (otherwise, criterion should be modified) 94 | 95 | :param x: 96 | :param y: 97 | :param thickness: 98 | :return: 99 | """ 100 | xout, yout = [], [] 101 | 102 | cross = np.where(np.diff(np.signbit(np.gradient(x))))[0][0] 103 | 104 | x_up = x[:cross + 2] 105 | y_up = y[:cross + 2] 106 | x_down = x[cross + 1:] 107 | y_down = y[cross + 1:] 108 | 109 | interp_up = interp1d(x_up, y_up, 'linear', fill_value="extrapolate") 110 | interp_down = interp1d(x_down, y_down, 'linear', fill_value="extrapolate") 111 | 112 | for i in range(1, len(x)): 113 | if interp_up(x[i]) - interp_down(x[i]) > 2 * thickness: 114 | dx = x[i] - x[i - 1] 115 | dy = y[i] - y[i - 1] 116 | x_90 = -dy 117 | y_90 = dx 118 | vec_len = np.sqrt(x_90 ** 2 + y_90 ** 2) 119 | a = thickness / vec_len 120 | pristeto_x = x[i] + x_90 * a 121 | pristeto_y = y[i] + y_90 * a 122 | xout.append(pristeto_x) 123 | yout.append(pristeto_y) 124 | 125 | return xout, yout 126 | -------------------------------------------------------------------------------- /calculation_runner.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import datetime 19 | import time 20 | import traceback 21 | from math import pi 22 | 23 | import numpy 24 | from mpl_toolkits.mplot3d import Axes3D 25 | 26 | from calculation import Calculator 27 | from utils import Printer, generate_v_and_rpm_from_tsr, generate_v_and_rpm_from_J 28 | 29 | a = Axes3D # only for passing code inspection -> Axes3D needs to be imported 30 | 31 | 32 | def calculate_power(inp_args): 33 | """ 34 | Returns calculated power using BEM analysis. 35 | 36 | Inputs are wind speed, rotational velocity, blade geometry, number of blades, and 37 | functions for calculating lift and drag coefficients. 38 | 39 | Output is a dictionary with all results. 40 | 41 | :return: dict with results 42 | """ 43 | p = Printer(inp_args["return_print"]) 44 | 45 | for f in inp_args["foils_in"]: 46 | if f not in inp_args["airfoils"].keys(): 47 | if f != "transition": 48 | p.print("Section foil %s does not exist in airfoil list." % f) 49 | raise Exception("Section foil not matching airfoil list error") 50 | 51 | try: 52 | c = Calculator(inp_args) 53 | results = c.run_array(**inp_args) 54 | return results 55 | except Exception as e: 56 | var = traceback.format_exc() 57 | p.print("Error in running analysis: %s \n %s" % (str(e), var)) 58 | inp_args["EOF"].value = True 59 | raise 60 | 61 | 62 | def calculate_power_3d(inp_args, print_eof=False, prepend="", print_progress=True): 63 | """ 64 | Calculates power for given geometry and data for every windspeed and rpm. 65 | 66 | Returns dictionary with arrays with data for every point. 67 | :return: dictionary with all results stored as numpy arrays 68 | """ 69 | if "return_print" not in inp_args: 70 | inp_args["return_print"] = [] 71 | if "return_results" not in inp_args: 72 | inp_args["return_results"] = [] 73 | p = Printer(inp_args["return_print"]) 74 | inp_args["print_progress"] = print_progress 75 | return_results = inp_args["return_results"] 76 | results_3d = {} 77 | 78 | try: 79 | # get parameters 80 | pitches = list( 81 | numpy.linspace(start=inp_args["pitch_min"], stop=inp_args["pitch_max"], num=int(inp_args["pitch_num"]))) 82 | tsr_list = list( 83 | numpy.linspace(start=inp_args["tsr_min"], stop=inp_args["tsr_max"], num=int(inp_args["tsr_num"]))) 84 | j_list = list(numpy.linspace(start=inp_args["J_min"], stop=inp_args["J_max"], num=int(inp_args["J_num"]))) 85 | 86 | constant_speed, constant_rpm, constant_pitch = inp_args["constant_speed"], inp_args["constant_rpm"], inp_args[ 87 | "pitch"] 88 | variable_selection = inp_args["variable_selection"] 89 | constant_selection = inp_args["constant_selection"] 90 | R = inp_args["R"] 91 | 92 | if variable_selection == 0: 93 | speeds = list(numpy.linspace(start=inp_args["v_min"], stop=inp_args["v_max"], num=int(inp_args["v_num"]))) 94 | rpms = list( 95 | numpy.linspace(start=inp_args["rpm_min"], stop=inp_args["rpm_max"], num=int(inp_args["rpm_num"]))) 96 | pitches = [constant_pitch] 97 | 98 | elif variable_selection == 1: 99 | # TSR mode 100 | if constant_selection == 0: 101 | # constant speed, so change constant rpm to None 102 | constant_rpm = None 103 | else: 104 | constant_speed = None 105 | speeds, rpms = generate_v_and_rpm_from_tsr(tsr_list=tsr_list, R=R, 106 | v=constant_speed, rpm=constant_rpm) 107 | pitches = [constant_pitch] 108 | 109 | elif variable_selection == 2: 110 | # J mode 111 | if constant_selection == 0: 112 | # constant speed, so change constant rpm to None 113 | constant_rpm = None 114 | else: 115 | constant_speed = None 116 | speeds, rpms = generate_v_and_rpm_from_J(J_list=j_list, R=R, 117 | v=constant_speed, rpm=constant_rpm, printer=p) 118 | pitches = [constant_pitch] 119 | 120 | elif variable_selection == 3: 121 | # pitches mode 122 | speeds, rpms = [constant_speed], [constant_rpm] 123 | 124 | elif variable_selection == 4: 125 | # pitch + TSR 126 | if constant_selection == 0: 127 | # constant speed, so change constant rpm to None 128 | constant_rpm = None 129 | else: 130 | constant_speed = None 131 | speeds, rpms = generate_v_and_rpm_from_tsr(tsr_list=tsr_list, R=R, 132 | v=constant_speed, rpm=constant_rpm) 133 | 134 | elif variable_selection == 5: 135 | # pitch + J 136 | if constant_selection == 0: 137 | # constant speed, so change constant rpm to None 138 | constant_rpm = None 139 | else: 140 | constant_speed = None 141 | speeds, rpms = generate_v_and_rpm_from_J(J_list=j_list, R=R, 142 | v=constant_speed, rpm=constant_rpm, printer=p) 143 | 144 | total_iterations = int(len(speeds) * len(rpms)) 145 | 146 | i = 0 147 | 148 | pitch_change_list = [] 149 | 150 | time_start = time.time() 151 | for pitch in pitches: 152 | p.print("Pitch:", pitch) 153 | pitch_change_list.append(i) 154 | for v in speeds: 155 | for rpm in rpms: 156 | print_progress_message(v, rpm, inp_args, p, prepend, print_progress) 157 | 158 | _inp_args = {**inp_args, "v": v, "rpm": rpm, "pitch": pitch} 159 | _results = calculate_power(_inp_args) 160 | 161 | # if results are valid, add them to results list 162 | if _results != None and _results["power"]: 163 | print_result_message(print_progress, p, prepend, _results) 164 | 165 | # append the value of the _results to the results_3d list 166 | for key, value in _results.items(): 167 | if key not in results_3d: 168 | results_3d[key] = [] 169 | results_3d[key].append(value) 170 | 171 | i += 1 172 | eta = process_time(time_start, i, total_iterations) 173 | # p.print(" ### Time left:", t_left_str, "ETA:", eta, "###") 174 | if print_progress: 175 | p.print("") 176 | 177 | results_3d["pitch_change_list"] = pitch_change_list 178 | return_results.append(results_3d) 179 | 180 | if print_eof: 181 | inp_args["EOF"].value = True 182 | 183 | return results_3d 184 | 185 | except Exception as e: 186 | var = traceback.format_exc() 187 | p.print("Error in running analysis: %s \n %s" % (str(e), var)) 188 | inp_args["EOF"].value = True 189 | raise 190 | 191 | 192 | def process_time(time_start, i, total_iterations): 193 | """ 194 | 195 | :param time_start: 196 | :param i: 197 | :param total_iterations: 198 | """ 199 | t_now = int(time.time() - time_start) 200 | t_left = int((total_iterations / i - 1) * t_now) 201 | t_left_str = str(datetime.timedelta(seconds=t_left)) 202 | eta_seconds = datetime.datetime.now() + datetime.timedelta(seconds=t_left) 203 | eta = str(eta_seconds).split(".")[0] 204 | 205 | 206 | def print_progress_message(v, rpm, inp_args, p, prepend, print_progress): 207 | """ 208 | 209 | :param v: 210 | :param rpm: 211 | :param inp_args: 212 | :param p: 213 | :param prepend: 214 | :param print_progress: 215 | """ 216 | if print_progress: 217 | if v > 0: 218 | _lambda = rpm / 60 * 2 * pi * inp_args["R"] / v 219 | else: 220 | _lambda = 0.0 221 | _advance_ratio = v / (rpm / 60 * 2 * inp_args["R"]) 222 | # pitch = inp_args["pitch"] 223 | p.print(prepend + "v=%.1f m/s, n=%.0f RPM, λ=%.2f, J=%.2f" % (v, rpm, _lambda, _advance_ratio)) 224 | 225 | 226 | def print_result_message(print_progress, p, prepend, _results): 227 | """ 228 | 229 | :param print_progress: 230 | :param p: 231 | :param prepend: 232 | :param _results: 233 | """ 234 | if print_progress: 235 | p.print(prepend + " cp:", _results["cp"], "ct:", _results["ct"], "eff:", _results["eff"]) 236 | 237 | 238 | def max_calculate(X, Y, Z): 239 | """ 240 | Calculates maximum power for every wind speed. 241 | 242 | Returns only points that provide maximum power for given wind speed. 243 | 244 | :param X: Wind speed 245 | :param Y: RPM 246 | :param Z: Power 247 | :return: X,Y,Z (filtered) 248 | """ 249 | X_un = numpy.unique(X) 250 | 251 | max_x = [] 252 | max_y = [] 253 | max_z = [] 254 | 255 | for i in range(len(X_un)): 256 | X_max = 0.0 257 | Y_max = 0.0 258 | Z_max = 0.0 259 | for j in numpy.where(X == X_un[i])[0]: 260 | if Z[j] > Z_max: 261 | Z_max = Z[j] 262 | X_max = X[j] 263 | Y_max = Y[j] 264 | max_x.append(X_max) 265 | max_y.append(Y_max) 266 | max_z.append(Z_max) 267 | 268 | max_x = numpy.array(max_x) 269 | max_y = numpy.array(max_y) 270 | max_z = numpy.array(max_z) 271 | return max_x, max_y, max_z 272 | -------------------------------------------------------------------------------- /export_to_excel.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from openpyxl import Workbook 19 | from openpyxl.drawing.image import Image 20 | 21 | 22 | from test_results import res 23 | from utils import transpose 24 | import json 25 | from matplotlib import pyplot as plt 26 | 27 | SET_INIT = json.loads(open("karlsen.bem").read()) 28 | 29 | def generate_excel(settings, results): 30 | """ 31 | 32 | :param settings: 33 | :param results: 34 | """ 35 | wb = Workbook() 36 | ws = wb.active 37 | ws.title = "Turbine info" 38 | 39 | ### ADD BASIC DATA 40 | ws.append(["Turbine name", settings['turbine_name']]) 41 | ws.append(["Tip radius", settings['R'], 'm']) 42 | ws.append(["Hub radius", settings['Rhub'], 'm']) 43 | ws.append(["Number of blades", settings['B']]) 44 | ws.append(["Fluid density", settings['rho'], 'kg/m3']) 45 | ws.append(["Kinematic viscosity", settings['kin_viscosity'], 'm2/s']) 46 | 47 | ### ADD GEOMETRY DATA 48 | data_start = 10 49 | num_sections = len(settings['r']) 50 | 51 | row = data_start 52 | column = 1 53 | data_turbine = transpose([settings['r'], settings['c'], settings['theta'], settings['foils']]) 54 | data_turbine = [ 55 | ["r [m]", 'c [m]', 'theta [m]', 'profil [m]'] 56 | ] + data_turbine 57 | for r in data_turbine: 58 | column = 1 59 | for v in r: 60 | ws.cell(row=row, column=column, value=v) 61 | column += 1 62 | row += 1 63 | 64 | ### ADD R,C,THETA CHART 65 | 66 | fig, ax1 = plt.subplots() 67 | color = 'tab:blue' 68 | 69 | ax1.set_xlabel('r [m]') 70 | ax1.set_ylabel('c [m]', color=color) 71 | ax1.plot(settings['r'],settings['c'], color=color) 72 | ax1.tick_params(axis='y', labelcolor=color) 73 | 74 | ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis 75 | 76 | color = 'tab:red' 77 | ax2.set_ylabel('θ [°]', color=color) # we already handled the x-label with ax1 78 | ax2.plot(settings['r'],settings['theta'], color=color) 79 | ax2.tick_params(axis='y', labelcolor=color) 80 | 81 | fig.tight_layout() # otherwise the right y-label is slightly clipped 82 | 83 | plt.savefig("graph.png") 84 | 85 | image = Image('graph.png') 86 | 87 | ws.add_image(image,'E2') 88 | 89 | 90 | wb.save('test.xlsx') 91 | 92 | 93 | generate_excel(SET_INIT, res) 94 | -------------------------------------------------------------------------------- /icon_bem.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihasm/Python-BEM/27801aa46112661f4e476f2996d2cfb6395899d6/icon_bem.ico -------------------------------------------------------------------------------- /main.spec: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # -*- mode: python -*- 19 | 20 | block_cipher = None 21 | 22 | 23 | a = Analysis(['bem.py'], 24 | pathex=[], 25 | binaries=[], 26 | datas=[("icon_bem.ico","."), 27 | ("xfoil_executables/xfoil.exe","xfoil_executables"), 28 | ("xfoil_executables/xfoil","xfoil_executables"), 29 | ("karlsen.bem",".")], 30 | hiddenimports=['scipy._lib.messagestream','scipy.special.cython_special'], 31 | hookspath=[], 32 | runtime_hooks=[], 33 | excludes=[], 34 | win_no_prefer_redirects=False, 35 | win_private_assemblies=False, 36 | cipher=block_cipher, 37 | noarchive=False) 38 | 39 | pyz = PYZ(a.pure, a.zipped_data, 40 | cipher=block_cipher) 41 | 42 | Key = ['mkl'] 43 | 44 | def remove_from_list(input, keys): 45 | outlist = [] 46 | for item in input: 47 | name, _, _ = item 48 | flag = 0 49 | for key_word in keys: 50 | if key_word in name: 51 | flag = 1 52 | break 53 | if flag != 1: 54 | outlist.append(item) 55 | return outlist 56 | 57 | print("List of binaries:") 58 | for _name,_path,_type in a.binaries: 59 | print(_name,_path,_type) 60 | 61 | a.binaries = remove_from_list(a.binaries, Key) 62 | 63 | exe = EXE(pyz, 64 | a.scripts, 65 | a.binaries, 66 | a.zipfiles, 67 | a.datas, 68 | [], 69 | name='main', 70 | debug=False, 71 | bootloader_ignore_signals=False, 72 | strip=False, 73 | upx=False, 74 | runtime_tmpdir=None, 75 | console=False, 76 | icon="icon_bem.ico") 77 | 78 | coll = COLLECT(exe, 79 | a.binaries, 80 | a.zipfiles, 81 | a.datas, 82 | strip=False, 83 | upx=False, 84 | name='main') -------------------------------------------------------------------------------- /montgomerie.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from numpy import * 19 | from numpy import pi as PI 20 | import numpy as np 21 | 22 | 23 | class POLAR_CLASS: 24 | """ 25 | 26 | """ 27 | def __init__(self, x, y, alpha, Cl, Cd): 28 | self.x = x 29 | self.y = y 30 | self.m_Alpha = alpha 31 | self.m_Cl = Cl 32 | self.m_Cd = Cd 33 | 34 | 35 | class Montgomerie: 36 | """ 37 | 38 | """ 39 | def __init__(self, x, y, alpha, Cl, Cd, Re=100000, A=-5, Am=8, B=5, Bm=5, m_CD90=2.0, slope=0.106): 40 | # coefficient of lift at AoA == 0 41 | self.CLzero = np.interp(0, alpha, Cl) 42 | self.CL180 = 0 # lift coefficient at AoA == 180 43 | # angle of attack where lift == 0 44 | self.alphazero = np.interp(0, Cl, alpha) 45 | 46 | self.deltaCD = 0 47 | self.deltaalpha = 1 48 | 49 | self.slope = slope # ocitno od 0 do 1 50 | self.m_CD90 = m_CD90 # Drag coefficientat AoA == 90 51 | 52 | self.m_pctrlA = A # -10, 30 53 | self.m_pctrlB = B # 1-100 54 | self.m_pctrlAm = Am # 1-80 55 | self.m_pctrlBm = Bm # 1-70 56 | 57 | if len(y) > 0: 58 | self.m_fThickness = np.max(y)-np.min(y) 59 | else: 60 | self.m_fThickness = 0.1 61 | 62 | self.m_fCamber = 0.043 63 | 64 | self.m_pCurPolar = POLAR_CLASS(x, y, alpha, Cl, Cd) 65 | self.posalphamax = np.argmax(self.m_pCurPolar.m_Cl) 66 | 67 | self.reynolds = Re 68 | 69 | def CD90(self, alpha): 70 | """ 71 | 72 | :param alpha: 73 | :return: 74 | """ 75 | res = self.m_CD90 - 1.46 * self.m_fThickness / 2 + \ 76 | 1.46 * self.m_fCamber * sin(alpha / 360 * 2 * PI) 77 | return res 78 | 79 | def PlateFlow(self, alphazero, CLzero, alpha): 80 | """ 81 | 82 | :param alphazero: 83 | :param CLzero: 84 | :param alpha: 85 | :return: 86 | """ 87 | # tukaj bi CD90 rala biti funkcija 88 | # alpha in degrees 89 | a = (1 + CLzero / sin(PI / 4) * sin(alpha / 360 * 2 * PI)) * self.CD90(alpha) * sin((alpha - 57.6 * 0.08 * sin( 90 | alpha / 360 * 2 * PI) - alphazero * cos(alpha / 360 * 2 * PI)) / 360 * 2 * PI) * cos( 91 | (alpha - 57.6 * 0.08 * sin(alpha / 360 * 2 * PI) - alphazero * cos(alpha / 360 * 2 * PI)) / 360 * 2 * PI) 92 | return a 93 | 94 | def PotFlow(self, CLzero, slope, alpha): 95 | """ 96 | 97 | :param CLzero: 98 | :param slope: 99 | :param alpha: 100 | :return: 101 | """ 102 | return CLzero + slope * alpha 103 | 104 | def CDPlate(self, alpha): 105 | """ 106 | 107 | :param alpha: 108 | :return: 109 | """ 110 | res = self.CD90(alpha) * sin(alpha / 360 * 2 * PI) ** 2 111 | return res 112 | 113 | def calculate_extrapolation(self): 114 | """ 115 | 116 | :return: 117 | """ 118 | m_Alpha = [] 119 | m_Cl = [] 120 | m_Cd = [] 121 | # print("A",self.m_pctrlA,"B",self.m_pctrlB) 122 | # positive extrapolation 123 | 124 | if len(self.m_pCurPolar.m_Alpha) > self.m_pctrlA + self.posalphamax >= 0: 125 | a1plus = self.m_pCurPolar.m_Alpha[int( 126 | self.posalphamax + self.m_pctrlA)] 127 | CL1plus = self.m_pCurPolar.m_Cl[int( 128 | self.posalphamax + self.m_pctrlA)] 129 | else: 130 | a1plus = (self.posalphamax + self.m_pctrlA) * self.deltaalpha 131 | CL1plus = self.PlateFlow( 132 | self.alphazero, self.CLzero, a1plus) + 0.03 133 | 134 | if (self.posalphamax + self.m_pctrlB + self.m_pctrlA) < len( 135 | self.m_pCurPolar.m_Alpha) and self.posalphamax + self.m_pctrlB + self.m_pctrlA >= 0: 136 | a2plus = self.m_pCurPolar.m_Alpha[int( 137 | self.posalphamax + self.m_pctrlB + self.m_pctrlA)] 138 | CL2plus = self.m_pCurPolar.m_Cl[int( 139 | self.posalphamax + self.m_pctrlB + self.m_pctrlA)] 140 | else: 141 | a2plus = (self.posalphamax + self.m_pctrlB + 142 | self.m_pctrlA) * self.deltaalpha 143 | CL2plus = self.PlateFlow( 144 | self.alphazero, self.CLzero, a2plus) + 0.03 145 | 146 | A = (self.PotFlow(self.CLzero, self.slope, a1plus) - 147 | self.PlateFlow(self.alphazero, self.CLzero, a1plus)) 148 | if A == 0.: 149 | A = 1e-5 150 | f1plus = ( 151 | (CL1plus - self.PlateFlow(self.alphazero, self.CLzero, a1plus)) / A) 152 | B = (self.PotFlow(self.CLzero, self.slope, a2plus) - 153 | self.PlateFlow(self.alphazero, self.CLzero, a2plus)) 154 | if B == 0.: 155 | B = 1e-5 156 | f2plus = ( 157 | (CL2plus - self.PlateFlow(self.alphazero, self.CLzero, a2plus)) / B) 158 | 159 | if f1plus == 1: 160 | f1plus += 10e-6 161 | print("yes") 162 | if f2plus == 1: 163 | print("yes2") 164 | f2plus += 10e-6 165 | 166 | G = pow((fabs((1 / f1plus - 1) / (1 / f2plus - 1))), 0.25) 167 | 168 | am = (a1plus - G * a2plus) / (1 - G) 169 | 170 | k_div = pow((a2plus - am), 4) 171 | if k_div == 0: 172 | k_div = 1e-5 173 | k = (1 / f2plus - 1) * 1 / k_div 174 | 175 | # rear end flying 176 | self.CL180 = self.PlateFlow(self.alphazero, self.CLzero, 180) 177 | 178 | self.slope2 = 0.8 * self.slope 179 | Re = self.reynolds # Reynolds 180 | deltaCL = 1.324 * pow((1 - exp(Re / 1000000 * (-0.2))), 0.7262) 181 | 182 | CL1plus = self.CL180 - deltaCL 183 | a1plus = 170 + self.CL180 / self.slope2 184 | a2plus = a1plus - 15 185 | CL2plus = self.PlateFlow(self.alphazero, self.CLzero, a2plus) - 0.01 186 | 187 | f1plus = (CL1plus - self.PlateFlow(self.alphazero, self.CLzero, a1plus)) / ( 188 | self.PotFlow(self.CL180, self.slope2, a1plus - 180) - self.PlateFlow(self.alphazero, self.CLzero, 189 | a1plus)) 190 | f2plus = (CL2plus - self.PlateFlow(self.alphazero, self.CLzero, a2plus)) / ( 191 | self.PotFlow(self.CL180, self.slope2, a2plus - 180) - self.PlateFlow(self.alphazero, self.CLzero, 192 | a2plus)) 193 | 194 | G2 = pow(fabs(((1 / f1plus - 1) / (1 / f2plus - 1))), 0.25) 195 | 196 | am2 = (a1plus - G2 * a2plus) / (1 - G2) 197 | 198 | k2 = (1 / f2plus - 1) * 1 / pow((a2plus - am2), 4) 199 | 200 | alpha = int(1) 201 | 202 | while alpha <= 180: 203 | if alpha < (am2 - 70): 204 | if alpha < am: 205 | delta = 0 206 | else: 207 | delta = am - alpha 208 | f = 1 / (1 + k * pow(delta, 4)) 209 | m_Alpha.append(alpha) 210 | m_Cl.append(f * self.PotFlow(self.CLzero, self.slope, alpha) + (1 - f) * self.PlateFlow(self.alphazero, 211 | self.CLzero, 212 | alpha)) 213 | 214 | elif alpha < am2: 215 | delta = am2 - alpha 216 | f = 1 / (1 + k2 * pow(delta, 4)) 217 | m_Alpha.append(alpha) 218 | m_Cl.append( 219 | f * self.PotFlow(self.CL180, self.slope2, alpha - 180) + (1 - f) * self.PlateFlow(self.alphazero, 220 | self.CLzero, 221 | alpha)) 222 | else: 223 | m_Alpha.append(alpha) 224 | m_Cl.append(self.PotFlow(self.CL180, self.slope2, alpha - 180)) 225 | 226 | if alpha < am: 227 | delta = 0 228 | else: 229 | delta = am - alpha 230 | 231 | f = 1 / (1 + k * delta ** 4) 232 | self.deltaCD = 0.13 * ((f - 1) * self.PotFlow(self.CLzero, self.slope, alpha) - (1 - f) * self.PlateFlow( 233 | self.alphazero, self.CLzero, alpha)) 234 | if self.deltaCD <= 0: 235 | self.deltaCD = 0 236 | # tukaj nisem preprican kaj pomeni self.m_fThickness, je to v procentih tetive ali kaj? 237 | m_Cd.append( 238 | f * (self.deltaCD + 0.006 + 1.25 * self.m_fThickness ** 2 / 180 * abs(alpha)) + (1 - f) * self.CDPlate( 239 | alpha) + 0.006) 240 | 241 | alpha += self.deltaalpha 242 | 243 | # negative extrapolation 244 | a1minus = (-float(self.m_pctrlAm) / 20 - self.CLzero) / self.slope - 4 245 | CL1minus = -float(self.m_pctrlAm) / 20 246 | 247 | a2minus = a1minus - self.m_pctrlBm * 2 248 | CL2minus = self.PlateFlow(self.alphazero, self.CLzero, a2minus) - 0.03 249 | 250 | f1minus = (CL1minus - self.PlateFlow(self.alphazero, self.CLzero, a1minus)) / ( 251 | self.PotFlow(self.CLzero, self.slope, a1minus) - self.PlateFlow(self.alphazero, self.CLzero, a1minus)) 252 | f2minus = (CL2minus - self.PlateFlow(self.alphazero, self.CLzero, a2minus)) / ( 253 | self.PotFlow(self.CLzero, self.slope, a2minus) - self.PlateFlow(self.alphazero, self.CLzero, a2minus)) 254 | 255 | G = abs((1 / f1minus - 1) / (1 / f2minus - 1)) ** 0.25 256 | 257 | am = (a1minus - G * a2minus) / (1 - G) 258 | 259 | k = (1 / f2minus - 1) * 1 / (a2minus - am) ** 4 260 | 261 | # rear end flying first 262 | 263 | CL1minus = self.CL180 + deltaCL 264 | a1minus = -170 + self.CL180 / self.slope2 265 | a2minus = a1minus + 15 266 | CL2minus = self.PlateFlow(self.alphazero, self.CLzero, a2minus) - 0.01 267 | 268 | f1minus = (CL1minus - self.PlateFlow(self.alphazero, self.CLzero, a1minus)) / ( 269 | self.PotFlow(self.CL180, self.slope2, a1minus + 180) - self.PlateFlow(self.alphazero, self.CLzero, 270 | a1minus)) 271 | f2minus = (CL2minus - self.PlateFlow(self.alphazero, self.CLzero, a2minus)) / ( 272 | self.PotFlow(self.CL180, self.slope2, a2minus + 180) - self.PlateFlow(self.alphazero, self.CLzero, 273 | a2minus)) 274 | 275 | G2 = abs(((1 / f1minus - 1) / (1 / f2minus - 1))) ** 0.25 276 | 277 | am2 = (a1minus - G2 * a2minus) / (1 - G2) 278 | 279 | k2 = (1 / f2minus - 1) * 1 / (a2minus - am2) ** 4 280 | 281 | alpha = 0 282 | 283 | while alpha >= -180: 284 | if alpha > am2 + 70: 285 | if alpha > am: 286 | delta = 0 287 | else: 288 | delta = am - alpha 289 | f = 1 / (1 + abs(k * delta ** 4)) 290 | m_Alpha.append(alpha) 291 | m_Cl.append(f * self.PotFlow(self.CLzero, self.slope, alpha) + (1 - f) * self.PlateFlow(self.alphazero, 292 | self.CLzero, 293 | alpha)) 294 | elif alpha > am2: 295 | delta = am2 - alpha 296 | f = 1 / (1 + abs(k2 * delta ** 4)) 297 | m_Alpha.append(alpha) 298 | m_Cl.append( 299 | f * self.PotFlow(self.CL180, self.slope2, alpha + 180) + (1 - f) * self.PlateFlow(self.alphazero, 300 | self.CLzero, 301 | alpha)) 302 | else: 303 | m_Alpha.append(alpha) 304 | m_Cl.append(self.PotFlow(self.CL180, self.slope2, alpha + 180)) 305 | 306 | if alpha > am: 307 | delta = 0 308 | else: 309 | delta = am - alpha 310 | f = 1 / (1 + k * delta ** 4) 311 | self.deltaCD = 0.13 * ( 312 | self.PotFlow(self.CLzero, self.slope, alpha) - f * self.PotFlow(self.CLzero, self.slope, alpha) - ( 313 | 1 - f) * self.PlateFlow(self.alphazero, self.CLzero, alpha)) 314 | 315 | if self.deltaCD <= 0: 316 | self.deltaCD = 0 317 | m_Cd.append( 318 | f * (self.deltaCD + 0.006 + 1.25 * self.m_fThickness ** 2 / 180 * abs(alpha)) + (1 - f) * self.CDPlate( 319 | alpha) + 0.006) 320 | alpha = alpha - self.deltaalpha 321 | return m_Alpha, m_Cl, m_Cd -------------------------------------------------------------------------------- /optimization.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import time 19 | import traceback 20 | from math import pi 21 | from calculation import Calculator 22 | from utils import Printer 23 | import scipy.optimize 24 | 25 | 26 | def optimize(inp_args, queue_pyqtgraph): 27 | """ 28 | 29 | :param inp_args: 30 | :param queue_pyqtgraph: 31 | :return: 32 | """ 33 | p = Printer(inp_args["return_print"]) 34 | try: 35 | inp_args["v"] = inp_args["target_speed"] 36 | inp_args["rpm"] = inp_args["target_rpm"] 37 | inp_args["omega"] = 2 * pi * inp_args["rpm"] / 60 38 | 39 | num_sections = len(inp_args["theta"]) 40 | 41 | input_variables = inp_args["optimization_inputs"] 42 | output_variables = inp_args["optimization_outputs"] 43 | target_variables = inp_args["optimization_targets"] 44 | 45 | mut_coeff = inp_args["mut_coeff"] 46 | population_size = int(inp_args["population"]) 47 | num_iter = int(inp_args["num_iter"]) 48 | 49 | p.print("Input variables:", input_variables) 50 | p.print("Output variables:", output_variables) 51 | p.print("Target_variables:", target_variables) 52 | 53 | if inp_args["turbine_type"] == 0: 54 | p.print("Turbine type: Wind turbine") 55 | elif inp_args["turbine_type"] == 1: 56 | p.print("Turbine type: Propeller") 57 | 58 | C = Calculator(inp_args) 59 | 60 | output_list = [] 61 | 62 | mode = 0 63 | 64 | if mode == 0: 65 | 66 | for n in range(len(inp_args["r"])): 67 | p.print(" Section_number is", n) 68 | 69 | list_queue_internal_x = [] 70 | list_queue_internal_y = [] 71 | 72 | section_inp_args = { 73 | "_r": inp_args["r"][n], 74 | "_c": inp_args["c"][n], 75 | "_theta": inp_args["theta"][n], 76 | "_dr": inp_args["dr"][n], 77 | "transition": C.transition_array[n], 78 | "_airfoil": C.airfoils_list[n], 79 | "_airfoil_prev": C.transition_foils[n][0], 80 | "_airfoil_next": C.transition_foils[n][1], 81 | "transition_coefficient": C.transition_foils[n][2], 82 | "max_thickness": C.max_thickness_array[n] 83 | } 84 | 85 | inputs_list = [i[0] for i in input_variables] 86 | bounds_list = [(b[1], b[2]) for b in input_variables] 87 | 88 | def fobj(input_numbers): 89 | """ 90 | 91 | :param input_numbers: 92 | :return: 93 | """ 94 | for i in range(len(input_numbers)): 95 | section_inp_args[inputs_list[i]] = input_numbers[i] 96 | 97 | args = {**inp_args, **section_inp_args} 98 | 99 | d = C.calculate_section(**args, printer=p) 100 | if d == None or d == False: 101 | return 1e10 102 | 103 | fitness = 0 104 | 105 | for var, coeff in output_variables: 106 | value = d[var] * coeff 107 | fitness -= value 108 | for var, target_value, coeff in target_variables: 109 | comparison = abs(d[var] - target_value) * coeff 110 | fitness += comparison 111 | 112 | list_queue_internal_x.append(args["_theta"]) 113 | list_queue_internal_y.append(fitness) 114 | 115 | queue_pyqtgraph[0] = [list_queue_internal_x, list_queue_internal_y, args["_theta"], fitness] 116 | 117 | return fitness 118 | 119 | decreasing = {"_theta"} 120 | 121 | if n > 0: 122 | # use previous iteration to set new bound 123 | for _vname in decreasing: 124 | index = inputs_list.index(_vname) 125 | bounds_list[index] = (bounds_list[index][0], output_list[n - 1][index]) # construct new tuple 126 | 127 | p.print("Bounds:", bounds_list) 128 | 129 | initial_guess = [top for bottom, top in bounds_list] # guess the top guess 130 | 131 | # it = list(de2(fobj, bounds=bounds_list, iterations=num_iter, M=mut_coeff, num_individuals=population_size, printer=p, queue=queue_pyqtgraph)) 132 | it = list(scipy.optimize.minimize(fobj, initial_guess, method="powell", bounds=bounds_list).x) 133 | 134 | p.print("best combination", it) 135 | output_list.append(it) 136 | 137 | p.print("Final output:") 138 | p.print([v[0] for v in output_variables]) 139 | for i in output_list: 140 | if len(i) > 1: 141 | p.print(i) 142 | else: 143 | p.print(i[0]) 144 | 145 | p.print("Done!") 146 | time.sleep(0.5) 147 | inp_args["EOF"].value = True 148 | 149 | elif mode == 1: 150 | inputs_list = [i[0] for i in input_variables] 151 | bounds_list = [] 152 | for b in input_variables: 153 | for i in range(num_sections): 154 | bounds_list.append((b[1], b[2])) 155 | 156 | p.print(bounds_list) 157 | 158 | inputs_list = [["theta", -45, 45]] 159 | output_variables = [["cp", 1.0]] 160 | 161 | def fobj(*input_numbers): 162 | """ 163 | 164 | :param input_numbers: 165 | :return: 166 | """ 167 | input_numbers = input_numbers[0] 168 | k = 0 169 | for i in range(len(inputs_list)): 170 | for j in range(num_sections): 171 | inp_args[inputs_list[i][0]][j] = input_numbers[k] 172 | k += 1 173 | 174 | d = C.run_array(**inp_args, printer=p) 175 | 176 | if d == None or d == False: 177 | p.print("d is None or False") 178 | return -1e50 179 | 180 | fitness = 0 181 | for var, coeff in output_variables: 182 | fitness += d[var] * coeff 183 | return fitness 184 | 185 | # it = list(de2(fobj, bounds=bounds_list, iterations=num_iter, M=mut_coeff, num_individuals=population_size, printer=p, queue=queue_pyqtgraph)) 186 | it = list(scipy.optimize.differential_evolution(fobj, bounds=bounds_list, maxiter=num_iter, 187 | popsize=population_size)) 188 | 189 | except Exception as e: 190 | var = traceback.format_exc() 191 | p.print("Error in running optimizer: %s \n %s" % (str(e), var)) 192 | inp_args["EOF"].value = True 193 | raise 194 | -------------------------------------------------------------------------------- /popravki.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from math import sin, cos, acos, pi, exp, sqrt, atan2, tan, degrees, tanh 19 | 20 | METHODS_STRINGS = {"0": "Classic BEM", 21 | "1": "Spera Correction", 22 | "2": "Buhl Correction (Aerodyn)", 23 | "3": "Buhl Correction (QBlade)", 24 | "4": "Wilson and Walker model", 25 | "5": "Modified ABS model"} 26 | 27 | 28 | def calculate_coefficients(method, input_arguments): 29 | """ 30 | 31 | :param method: 32 | :param input_arguments: 33 | :return: 34 | """ 35 | if method == 0: 36 | return fInductionCoefficients0(**input_arguments) 37 | if method == 1: 38 | return fInductionCoefficients1(**input_arguments) 39 | if method == 2: 40 | return fInductionCoefficients2(**input_arguments) 41 | if method == 3: 42 | return fInductionCoefficients3(**input_arguments) 43 | if method == 4: 44 | return fInductionCoefficients4(**input_arguments) 45 | if method == 5: 46 | return fInductionCoefficients5(**input_arguments) 47 | raise Exception("Method " + str(method) + " does not exist.") 48 | 49 | 50 | def machNumberCorrection(Cl, Cd, M): 51 | """ 52 | 53 | :param Cl: Lift coefficient 54 | :param M: Mach number 55 | :return: Lift coefficient 56 | """ 57 | Cl = Cl / sqrt(1 - M ** 2) 58 | Cd = Cd / sqrt(1 - M ** 2) 59 | return Cl, Cd 60 | 61 | 62 | def fTipLoss(B, r, R, phi): 63 | """ 64 | Prandtl tip loss. 65 | :param B: number of blades 66 | :param r: section radius [m] 67 | :param R: tip radius [m] 68 | :param phi: angle of relative wind [rad] 69 | :return: returns tip loss factor [float] 70 | """ 71 | # F = 1 72 | F = 2 / pi * acos(exp(-B / 2 * abs((R - r) / r / sin(phi)))) 73 | return F 74 | 75 | 76 | def fHubLoss(B, r, Rhub, phi): 77 | """ 78 | Prandtl hub loss. 79 | :param B: number of blades 80 | :param r: section radius [m] 81 | :param Rhub: hub radius [m] 82 | :param phi: angle of relative wind [rad] 83 | :return: returns hub loss factor [float] 84 | """ 85 | F = 1 86 | f = sin(phi / 360 * 2 * pi) 87 | g = (r - Rhub) / r 88 | Fr = 2 / pi * acos(exp(-B / 2 * abs(g / f))) 89 | F = F * Fr 90 | return F 91 | 92 | 93 | def newTipLoss(B, r, R, phi, lambda_r): 94 | """ 95 | Prandtl tip loss with correction. 96 | :param B: number of blades 97 | :param r: section radius [m] 98 | :param R: tip radius [m] 99 | :param phi: angle of relative wind [rad] 100 | :param lambda_r: local speed ratio [float] 101 | :return: returns tip loss factor [float] 102 | """ 103 | F = 1 104 | f = sin(phi) 105 | g = (R - r) / r 106 | Flt = ( 107 | 2 108 | / pi 109 | * acos(exp(-B / 2 * abs(g / f) * (exp(-0.15 * (B * lambda_r - 21)) + 0.1))) 110 | ) 111 | F = F * Flt 112 | return F 113 | 114 | 115 | def newHubLoss(B, r, Rhub, phi, lambda_r): 116 | """ 117 | Prandtl hub loss. 118 | :param lambda_r: local speed ratio [float] 119 | :param B: number of blades [int] 120 | :param r: section radius [m] 121 | :param Rhub: hub radius [m] 122 | :param phi: angle of relative wind [rad] 123 | :return: returns hub loss factor [float] 124 | """ 125 | F = 1 126 | f = sin(phi) 127 | g = (Rhub - r) / r 128 | Flt = ( 129 | 2 130 | / pi 131 | * acos(exp(-B / 2 * abs(g / f) * (exp(-0.15 * (B * lambda_r - 21)) + 0.1))) 132 | ) 133 | F = F * Flt 134 | return F 135 | 136 | 137 | def fAdkinsTipLoss(B, r, R, phi): 138 | """ 139 | :param B: number of blades [int] 140 | :param r: section radius [m] 141 | :param Rhub: hub radius [m] 142 | :param phi: angle of relative wind [rad] 143 | :return: returns hub loss factor [float] 144 | """ 145 | F = 2 / pi * acos(exp(-B / 2 * abs((R - r) / r / sin(phi)))) 146 | return F 147 | 148 | 149 | # noinspection PyUnusedLocal,PyUnusedLocal 150 | def fInductionCoefficients0(F, phi, sigma, C_norm, C_tang, prop_coeff, *args, **kwargs): 151 | """ 152 | Calculates induction coefficients using no corrections. 153 | 154 | NAME: Original 155 | SOURCE: http://orbit.dtu.dk/files/86307371/A_Detailed_Study_of_the_Rotational.pdf 156 | 157 | :param F: loss factors 158 | :param phi: relative wind [rad] 159 | :param sigma: solidity 160 | :param C_norm: normal coefficient 161 | :param C_tang: tangential coefficient 162 | :return: axial induction factor, tangential induction factor 163 | """ 164 | 165 | a = (sigma * C_norm) / (4 * F * sin(phi) ** 2 + sigma * C_norm * prop_coeff) 166 | aprime = (sigma * C_tang) / (4 * F * sin(phi) * cos(phi) - sigma * C_tang * prop_coeff) 167 | return a, aprime 168 | 169 | 170 | # noinspection PyUnusedLocal,PyUnusedLocal 171 | def fInductionCoefficients1(F, phi, sigma, C_norm, C_tang, *args, **kwargs): 172 | """ 173 | Calculates induction coefficients using method using Spera correction. 174 | 175 | NAME: Spera 176 | SOURCE: https://cmm2017.sciencesconf.org/129068/document 177 | AUTHOR: Spera 1994 178 | 179 | :param F: loss factors 180 | :param phi: relative wind [rad] 181 | :param sigma: solidity 182 | :param C_tang: tangential coefficient [float] 183 | :param C_norm: normal coefficient [float] 184 | :return: axial induction factor, tangential induction factor 185 | """ 186 | a = (sigma * C_norm) / (4 * F * sin(phi) ** 2 + sigma * C_norm) 187 | # Spera's correction 188 | if a >= 0.2: 189 | ac = 0.2 190 | K = (4 * F * sin(phi) ** 2) / (sigma * C_norm) 191 | to_sqrt = abs((K * (1 - 2 * ac) + 2) ** 2 + 4 * (K * ac ** 2 - 1)) 192 | a = 1 + 0.5 * K * (1 - 2 * ac) - 0.5 * sqrt(to_sqrt) 193 | 194 | aprime = (sigma * C_tang) / (4 * F * sin(phi) * cos(phi) - sigma * C_tang) 195 | return a, aprime 196 | 197 | 198 | # noinspection PyUnusedLocal,PyUnusedLocal,PyUnusedLocal 199 | def fInductionCoefficients2(a_last, F, phi, sigma, C_norm, C_tang, Cl, Ct_r, *args, **kwargs): 200 | """ 201 | Calculates induction coefficients using method used in Aerodyn software (Buhl method). 202 | 203 | NAME: AERODYN (BUHL) 204 | SOURCE: AeroDyn manual - theory. 205 | 206 | This method is equal to Advanced brake state model method. 207 | """ 208 | 209 | if Ct_r > 0.96 * F: 210 | # Modified Glauert correction 211 | a = ( 212 | 18 * F - 20 - 3 * sqrt(Ct_r * (50 - 36 * F) + 213 | 12 * F * (3 * F - 4)) 214 | ) / (36 * F - 50) 215 | else: 216 | a = (1 + 4 * F * sin(phi) ** 2 / (sigma * C_norm)) ** -1 217 | aprime = (-1 + 4 * F * sin(phi) * cos(phi) / (sigma * C_tang)) ** -1 218 | return a, aprime 219 | 220 | 221 | # noinspection PyUnusedLocal,PyUnusedLocal,PyUnusedLocal 222 | def fInductionCoefficients3(a_last, F, lambda_r, phi, sigma, C_norm, Ct_r, *args, **kwargs): 223 | """ 224 | Calculates induction coefficients using Buhl correction (QBlade implementation). 225 | 226 | NAME: QBLADE (Buhl) 227 | SOURCE: QBlade/src/XBEM/BData.cpp 228 | AUTHOR: Buhl 229 | """ 230 | 231 | if Ct_r <= 0.96 * F: 232 | a = 1 / (4 * F * sin(phi) ** 2 / (sigma * C_norm) + 1) 233 | else: 234 | a = ( 235 | 18 * F - 20 - 3 * abs(Ct_r * (50 - 36 * F) + 236 | 12 * F * (3 * F - 4)) ** 0.5 237 | ) / (36 * F - 50) 238 | 239 | aprime = 0.5 * (abs(1 + 4 / (lambda_r ** 2) * a * (1 - a)) ** 0.5 - 1) 240 | 241 | return a, aprime 242 | 243 | 244 | def fInductionCoefficients4(a_last, F, phi, Cl, C_tang, C_norm, sigma, Ct_r, *args, **kwargs): 245 | """ 246 | NAME: Wilson and walker 247 | 248 | Method from Pratumnopharat,2010 249 | 250 | Wilson and Walker method 251 | """ 252 | a = a_last 253 | ac = 0.2 254 | 255 | if F == 0: 256 | F = 1e-6 257 | if Ct_r <= 0.64 * F: 258 | a = (1 - sqrt(1 - Ct_r / F)) / 2 259 | else: 260 | a = (Ct_r - 4 * F * ac ** 2) / (4 * F * (1 - 2 * ac)) 261 | 262 | aprime = (4 * F * sin(phi) * cos(phi) / (sigma * C_tang) - 1) ** -1 263 | 264 | return a, aprime 265 | 266 | 267 | def fInductionCoefficients5(a_last, F, phi, Cl, C_norm, sigma, lambda_r, Ct_r, *args, **kwargs): 268 | """ 269 | NAME: Modified ABS model 270 | Method from Pratumnopharat,2010 271 | 272 | Modified advanced brake state model 273 | 274 | :param lambda_r: local speed ratio 275 | :param C_norm: normal coefficient 276 | :param a_last: axial induction factor 277 | :param F: loss factors 278 | :param Cl: lift coefficient 279 | :param phi: relative wind [rad] 280 | :param sigma: solidity 281 | :return: axial induction factor, tangential induction factor 282 | """ 283 | a = a_last 284 | if Ct_r < 0.96 * F: 285 | a = (1 - sqrt(1 - Ct_r / F)) / 2 286 | else: 287 | a = 0.1432 + sqrt(-0.55106 + 0.6427 * Ct_r / F) 288 | aprimeprime = (4 * a * F * (1 - a)) / (lambda_r ** 2) 289 | if (1 + aprimeprime) < 0: 290 | aprime = 0 291 | else: 292 | aprime = (sqrt(1 + aprimeprime) - 1) / 2 293 | 294 | return a, aprime 295 | 296 | 297 | def cascadeEffectsCorrection(alpha, v, omega, r, R, c, B, a, aprime, max_thickness): 298 | """ 299 | Calculates cascade effects and corresponding change in angle of attack. 300 | Method from PROPX: Definitions, Derivations and Data Flow, C. Harman, 1994 301 | :param max_thickness: maximum airfoil thickness [m] 302 | :param alpha: angle of attack [rad] 303 | :param v: wind speed [m/s] 304 | :param omega: rotational velocity [rad/s] 305 | :param r: section radius [m] 306 | :param R: tip radius [m] 307 | :param c: section chord length [m] 308 | :param B: number of blades [int] 309 | :param a: axial induction 310 | :param aprime: tangential induction 311 | :return: new angle of attack [rad] 312 | """ 313 | 314 | delta_alpha_1 = ( 315 | 1 / 4 316 | * ( 317 | atan2((1 - a) * v, ((1 + 2 * aprime) * r * omega)) 318 | - atan2(((1 - a) * v), (r * omega)) 319 | ) 320 | ) 321 | delta_alpha_2 = ( 322 | 0.109 323 | * (B * c * max_thickness * R * omega / v) 324 | / (R * c * sqrt((1 - a) ** 2 + (r * R * omega / v / R) ** 2)) 325 | ) 326 | 327 | out = alpha + delta_alpha_1 + delta_alpha_2 328 | 329 | return out 330 | 331 | 332 | def calc_rotational_augmentation_correction( 333 | alpha, alpha_zero, Cl, Cd, omega, r, R, c, theta, v, Vrel_norm, method, 334 | printer, print_all): 335 | """ 336 | METHODS FROM http://orbit.dtu.dk/files/86307371/A_Detailed_Study_of_the_Rotational.pdf 337 | """ 338 | p = printer 339 | 340 | fl = 0 341 | fd = 0 342 | 343 | if method == 0: 344 | # Snel et al. 345 | a_s = 3 346 | h = 2 347 | fl = a_s * (c / r) ** h 348 | fd = 0 349 | 350 | if method == 1: 351 | # Du & Selig 352 | gama = omega * R / sqrt(abs(v ** 2 - (omega * R) ** 2)) 353 | ad, dd, bd = 1, 1, 1 354 | fl = ( 355 | 1 356 | / (2 * pi) 357 | * ( 358 | 1.6 359 | * (c / r) 360 | / 0.1267 361 | * (ad - (c / r) ** (dd * R / gama / r)) 362 | / (bd + (c / r) ** (dd * R / gama / r)) 363 | - 1 364 | ) 365 | ) 366 | fd = ( 367 | -1 368 | / (2 * pi) 369 | * ( 370 | 1.6 371 | * (c / r) 372 | / 0.1267 373 | * (ad - (c / r) ** (dd * R / gama / r / 2)) 374 | / (bd + (c / r) ** (dd * R / gama / r / 2)) 375 | - 1 376 | ) 377 | ) 378 | 379 | if method == 2: 380 | # Chaviaropoulos and Hansen 381 | ah = 2.2 382 | h = 1.0 383 | n = 4 384 | fl = ah * (c / r) ** h * cos(theta) ** n 385 | fd = fl 386 | 387 | if method == 3: 388 | # Lindenburg 389 | al = 3.1 390 | h = 2 391 | fl = al * (omega * R / Vrel_norm) ** 2 * (c / r) ** h 392 | fd = 0 393 | 394 | if method == 4: 395 | # Dumitrescu and Cardos 396 | gd = 1.25 397 | fl = 1 - exp(-gd / (r / c - 1)) 398 | fd = 0 399 | 400 | if method == 5: 401 | # Snel et al. for propellers 402 | # method for propellers from http://acoustics.ae.illinois.edu/pdfs/AIAA-Paper-2015-3296.pdf 403 | fl = (omega * r / Vrel_norm) ** 2 * (c / r) ** 2 * 1.5 404 | 405 | if method == 6: 406 | # Gur & Rosen 407 | # method for propellers from Aviv (2005), Propeller Performance at Low Advance Ratio - JoA vol. 42 No. 2 408 | if degrees(alpha) >= 8: 409 | fl = tanh(10.73 * (c / r)) 410 | elif degrees(alpha) > degrees(alpha_zero): 411 | fl = 0 412 | else: 413 | fl = -tanh(10.73 * (c / r)) 414 | 415 | Cl_pot = 2 * pi * sin(alpha - alpha_zero) 416 | if print_all: 417 | p.print(" Rotational augmentation:") 418 | p.print(" Cl_pot", Cl_pot) 419 | p.print(" fl", fl) 420 | p.print(" c/r", c / r) 421 | Cl_3D = Cl + fl * (Cl_pot - Cl) 422 | Cd_3D = Cd 423 | return Cl_3D, Cd_3D 424 | 425 | 426 | def skewed_wake_correction_calculate(yaw_angle, a, r, R): 427 | """ 428 | 429 | :param yaw_angle: 430 | :param a: 431 | :param r: 432 | :param R: 433 | :return: 434 | """ 435 | chi = (0.6 * a + 1) * yaw_angle 436 | a_skew = a * (1 + 15 * pi / 64 * r / R * tan(chi / 2)) 437 | return a_skew 438 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyqtgraph 2 | beautifulsoup4 3 | cycler 4 | requests 5 | numpy 6 | scipy 7 | pyqt5 8 | matplotlib 9 | openpyxl 10 | pillow 11 | pyopengl -------------------------------------------------------------------------------- /results.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import json 19 | import warnings 20 | 21 | import matplotlib as mpl 22 | import matplotlib.cbook 23 | import matplotlib.pyplot as plt 24 | import numpy as np 25 | from PyQt5 import QtWidgets 26 | from PyQt5.QtGui import QIcon 27 | from PyQt5.QtWidgets import (QComboBox, QMainWindow, QWidget, QAction, QFileDialog) 28 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 29 | from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar 30 | from matplotlib.figure import Figure 31 | 32 | from UI.Table import Table 33 | from calculation import OUTPUT_VARIABLES_LIST 34 | from utils import dict_to_ar, greek_letters_to_string, transpose, filter_3d_results 35 | 36 | warnings.filterwarnings("ignore", category=matplotlib.cbook.MatplotlibDeprecationWarning) 37 | 38 | 39 | class ResultsWindow(QMainWindow): 40 | """ 41 | 42 | """ 43 | 44 | def __init__(self, parent, width, height, results_3d, input_data): 45 | super(ResultsWindow, self).__init__(parent) 46 | self.input_data = input_data 47 | self.results_3d = results_3d 48 | self.title = "Results" 49 | 50 | self.statusBar() 51 | mainMenu = self.menuBar() 52 | fileMenu = mainMenu.addMenu('&File') 53 | 54 | saveFile = QAction("&Export to JSON", self) 55 | saveFile.setShortcut("Ctrl+S") 56 | saveFile.setStatusTip('Export to JSON') 57 | saveFile.triggered.connect(self.export) 58 | fileMenu.addAction(saveFile) 59 | 60 | self.screen_width = width 61 | self.screen_height = height 62 | self.setWindowTitle(self.title) 63 | self.setGeometry(int(self.screen_width * 0.125), int(self.screen_height * 0.125), int(self.screen_width * 0.75), 64 | int(self.screen_width * 0.75 * 0.4)) 65 | self.tab_widget = TabWidget(self) 66 | self.setCentralWidget(self.tab_widget) 67 | 68 | # list_of_variable_parameter = [] 69 | if input_data["variable_selection"] == 0: 70 | list_of_variable_parameter = np.array(results_3d["TSR"]) 71 | variable_parameter_title = r"$\lambda$" 72 | elif input_data["variable_selection"] == 1: 73 | list_of_variable_parameter = np.array(results_3d["TSR"]) 74 | variable_parameter_title = r"$\lambda$" 75 | elif input_data["variable_selection"] == 2: 76 | list_of_variable_parameter = np.array(results_3d["J"]) 77 | variable_parameter_title = "J" 78 | elif input_data["variable_selection"] == 3: 79 | list_of_variable_parameter = np.array(results_3d["pitch"]) 80 | variable_parameter_title = "pitch" 81 | elif input_data["variable_selection"] == 4: 82 | list_of_variable_parameter = np.array(results_3d["pitch"]) 83 | variable_parameter_title = "TSR" 84 | 85 | ########### CP(lambda) CURVE ############### 86 | if input_data["turbine_type"] == 0: # wind turbine 87 | f2 = self.tab_widget.add_tab_figure("Cp curve") 88 | 89 | mat = np.array( 90 | [results_3d["TSR"], results_3d["cp"], results_3d["pitch"], results_3d["blade_stall_percentage"]]) 91 | # transpose 92 | mat = mat.transpose() 93 | # sort by multiple columns 94 | r = np.core.records.fromarrays([mat[:, 2], mat[:, 0]], names='a,b') 95 | mat = mat[r.argsort()] 96 | # split at different column values 97 | dif = np.diff(mat[:, 2]) 98 | ind_split = np.where(dif != 0)[0] 99 | splitted = np.split(mat, ind_split + 1) 100 | 101 | ax_cp = f2.add_subplot(111) 102 | ax_cp.set_title(r"$C_{P}-\lambda$") 103 | ax_cp.set_xlabel(r"$\lambda$") 104 | ax_cp.set_ylabel(r"$C_{P}$") 105 | ax_cp.grid() 106 | 107 | for a in splitted: 108 | x = a[:, 0] 109 | y = a[:, 1] 110 | label = str(round(a[0, 2], 2)) 111 | stall = (1 - a[:, 3]) 112 | ax_cp.plot(x, y, label=label) 113 | sc = ax_cp.scatter(x, y, c=stall, cmap="RdYlGn") 114 | 115 | norm = mpl.colors.Normalize(vmin=0, vmax=1) 116 | sm = plt.cm.ScalarMappable(cmap="RdYlGn", norm=norm) 117 | sm.set_array([]) 118 | 119 | cbar = plt.colorbar(sm, ticks=[0, 0.5, 1]) 120 | cbar.set_label("Stalled area") 121 | cbar.ax.set_yticklabels(["100 %", "50 %", "0 %"]) 122 | 123 | if len(splitted) > 1: 124 | ax_cp.legend(title="Pitch [°]") 125 | ############################################ 126 | 127 | ############ Propeller curves ############## 128 | if input_data["turbine_type"] == 1: # propeller 129 | f3 = self.tab_widget.add_tab_figure("Prop curves") 130 | J, eff, ct, cp, cq = results_3d["J"], results_3d["eff"], results_3d["ct"], results_3d["cp"], results_3d[ 131 | "cq"] 132 | 133 | ax3 = f3.add_subplot(111) 134 | ax3.set_title("Propeller curves") 135 | ax3.set_xlabel("J") 136 | ax3.set_ylabel(r"$\eta$") 137 | ax3.grid() 138 | ax3.plot(J, eff, "-g", label=r"$\eta$") 139 | 140 | ax3_2 = ax3.twinx() 141 | ax3_2.plot(J, ct, "-r", label=r"$C_T$") 142 | ax3_2.plot(J, cp, "-b", label=r"$C_P$") 143 | ax3_2.plot(J, cq, "-", color="orange", label=r"$C_Q$") 144 | ax3_2.set_ylabel(r"$C_T, C_P, C_Q$") 145 | 146 | f3.legend(loc="center right") 147 | 148 | if input_data["turbine_type"] == 1: # propeller 149 | f4 = self.tab_widget.add_tab_figure("Propeller efficency") 150 | mat = np.array([results_3d["J"], results_3d["eff"], results_3d["pitch"]]) 151 | # transpose 152 | mat = mat.transpose() 153 | # sort by multiple columns 154 | r = np.core.records.fromarrays([mat[:, 2], mat[:, 0]], names='a,b') 155 | mat = mat[r.argsort()] 156 | # split at different column values 157 | dif = np.diff(mat[:, 2]) 158 | ind_split = np.where(dif != 0)[0] 159 | splitted = np.split(mat, ind_split + 1) 160 | 161 | ax_cp = f4.add_subplot(111) 162 | ax_cp.set_title(r"Propeller efficiency") 163 | ax_cp.set_xlabel(r"J") 164 | ax_cp.set_ylabel(r"$\eta$") 165 | ax_cp.grid() 166 | 167 | for a in splitted: 168 | x = a[:, 0] 169 | y = a[:, 1] 170 | label = str(round(a[0, 2], 2)) # label by pitch 171 | ax_cp.plot(x, y, label=label) 172 | 173 | if len(splitted) > 1: 174 | ax_cp.legend(title="Pitch [°]") 175 | ############################################ 176 | 177 | ############ geometry check ################ 178 | array_geom = [] 179 | for r in range(len(input_data["r"])): 180 | _r = input_data["r"][r] 181 | _c = input_data["c"][r] 182 | _theta = input_data["theta"][r] 183 | _dr = input_data["dr"][r] 184 | array_geom.append([input_data["r"][r], input_data["c"] 185 | [r], input_data["theta"][r], input_data["dr"][r], ]) 186 | t_geom = Table() 187 | t_geom.createTable(array_geom) 188 | t_geom.set_labels(["r", "c", "theta", "dr"]) 189 | self.tab_widget.add_tab_widget(t_geom, "Geometry check") 190 | ############################################ 191 | 192 | ########### data ########################### 193 | data = dict_to_ar(results_3d) 194 | names = [] 195 | symbols_and_units = [] 196 | for i in data[0]: 197 | names.append(OUTPUT_VARIABLES_LIST[i]["name"]) 198 | symbol = OUTPUT_VARIABLES_LIST[i]["symbol"] 199 | symbol = greek_letters_to_string(symbol) 200 | symbol = symbol.replace("$", "") 201 | unit = OUTPUT_VARIABLES_LIST[i]["unit"] 202 | unit = unit.replace("$", "") 203 | if unit == "": 204 | symbols_and_units.append(symbol) 205 | else: 206 | symbols_and_units.append(symbol + " [" + unit + "]") 207 | del data[0] 208 | data.insert(0, names) 209 | data.insert(1, symbols_and_units) 210 | t = Table() 211 | t.createTable(data) 212 | self.tab_widget.add_tab_widget(t, "Data") 213 | ############################################ 214 | 215 | ########## CUSTOM GRAPH #################### 216 | self.custom_graph = CustomGraphWidget(self) 217 | self.custom_graph.set_data(results_3d) 218 | self.tab_widget.add_tab_widget(self.custom_graph, "Custom graph") 219 | ############################################ 220 | 221 | self.show() 222 | 223 | def closeEvent(self, event): 224 | """ 225 | 226 | :param event: 227 | """ 228 | for f in self.tab_widget.figures: 229 | f.clear() 230 | plt.close(f) 231 | event.accept() # let the window close 232 | 233 | def set_menubar(self): 234 | """ 235 | 236 | """ 237 | mainMenu = self.menuBar() 238 | fileMenu = mainMenu.addMenu("File") 239 | exitButton = QAction(QIcon("exit24.png"), "Exit", self) 240 | exitButton.setShortcut("Ctrl+Q") 241 | exitButton.setStatusTip("Exit application") 242 | exitButton.triggered.connect(exit) 243 | fileMenu.addAction(exitButton) 244 | 245 | def export(self): 246 | """ 247 | 248 | """ 249 | name = QFileDialog.getSaveFileName(self, 'Save File', "", "JSON (*.json)")[0] 250 | if name != "": 251 | d = self.results_3d 252 | d_out = filter_3d_results(d) 253 | print(d_out) 254 | json_d = json.dumps(d_out) 255 | file = open(name, 'w') 256 | file.write(json_d) 257 | file.close() 258 | 259 | 260 | class TabWidget(QtWidgets.QTabWidget): 261 | """ 262 | 263 | """ 264 | 265 | def __init__(self, parent=None): 266 | super(TabWidget, self).__init__(parent) 267 | self.tabs = [] 268 | self.figures = [] 269 | self.canvas = [] 270 | self.toolbars = [] 271 | 272 | def add_tab_figure(self, tab_name): 273 | """ 274 | 275 | :param tab_name: 276 | :return: 277 | """ 278 | self.tabs.append(QtWidgets.QWidget()) 279 | self.addTab(self.tabs[-1], tab_name) 280 | self.figures.append(plt.figure(figsize=(10, 5))) 281 | self.canvas.append(FigureCanvas(self.figures[-1])) 282 | toolbar = NavigationToolbar(self.canvas[-1], self) 283 | layout = QtWidgets.QVBoxLayout() 284 | layout.addWidget(self.canvas[-1]) 285 | layout.addWidget(toolbar) 286 | self.tabs[-1].setLayout(layout) 287 | return self.figures[-1] 288 | 289 | def add_tab_widget(self, widget, tab_name): 290 | """ 291 | 292 | :param widget: 293 | :param tab_name: 294 | """ 295 | self.tabs.append(widget) 296 | self.addTab(self.tabs[-1], tab_name) 297 | 298 | 299 | class CustomGraphWidget(QWidget): 300 | """ 301 | 302 | """ 303 | 304 | def __init__(self, parent=None): 305 | super(CustomGraphWidget, self).__init__(parent) 306 | self.parent = parent 307 | 308 | self.figure = Figure() 309 | self.canvas = FigureCanvas(self.figure) 310 | self.toolbar = NavigationToolbar(self.canvas, self) 311 | self.ax = self.figure.add_subplot(111) 312 | 313 | self.layout = QtWidgets.QGridLayout() 314 | self.layout.addWidget(self.canvas, 1, 1) 315 | self.layout.addWidget(self.toolbar, 2, 1) 316 | 317 | self.table = Table() 318 | self.layout.addWidget(self.table, 1, 2) 319 | 320 | self.comboBox_x = QComboBox(self) 321 | self.comboBox_y = QComboBox(self) 322 | 323 | self.layout.addWidget(self.comboBox_x, 3, 1) 324 | self.layout.addWidget(self.comboBox_y, 4, 1) 325 | 326 | self.inverse_list = {} 327 | list_of_options = [] 328 | 329 | for k, v in OUTPUT_VARIABLES_LIST.items(): 330 | if v["unit"] != "": 331 | var_name = v["name"] + " " + v["symbol"] + " [" + v["unit"] + "]" 332 | else: 333 | var_name = v["name"] + " " + v["symbol"] 334 | var_name = greek_letters_to_string(var_name) 335 | var_name = var_name.replace("$", "") 336 | list_of_options.append(var_name) 337 | self.inverse_list[var_name] = k 338 | 339 | list_of_options.sort() 340 | for l in list_of_options: 341 | self.comboBox_x.addItem(l) 342 | self.comboBox_y.addItem(l) 343 | 344 | # self.button_draw = QPushButton("Draw") 345 | # self.layout.addWidget(self.button_draw) 346 | # self.button_draw.clicked.connect(self.draw_graph) 347 | 348 | self.comboBox_x.currentTextChanged.connect(self.draw_graph) 349 | self.comboBox_y.currentTextChanged.connect(self.draw_graph) 350 | 351 | self.setLayout(self.layout) 352 | 353 | def draw_graph(self): 354 | """ 355 | 356 | """ 357 | self.ax.clear() 358 | 359 | # set variable 360 | 361 | print("Current variable parameter:", self.parent.input_data["variable_selection"]) 362 | 363 | if self.parent.input_data["variable_selection"] == 0: 364 | list_of_variable_parameter = np.array(self.results["TSR"]) 365 | variable_parameter_title = r"$\lambda$" 366 | elif self.parent.input_data["variable_selection"] == 1: 367 | list_of_variable_parameter = np.array(self.results["TSR"]) 368 | variable_parameter_title = r"$\lambda$" 369 | elif self.parent.input_data["variable_selection"] == 2: 370 | list_of_variable_parameter = np.array(self.results["J"]) 371 | variable_parameter_title = "J" 372 | elif self.parent.input_data["variable_selection"] == 3: 373 | list_of_variable_parameter = np.array(self.results["pitch"]) 374 | variable_parameter_title = "pitch" 375 | elif self.parent.input_data["variable_selection"] == 4: 376 | list_of_variable_parameter_1 = np.array(self.results["pitch"]) 377 | variable_parameter_title_1 = "pitch" 378 | list_of_variable_parameter_2 = np.array(self.results["TSR"]) 379 | variable_parameter_title_2 = "TSR" 380 | 381 | x_data = self.inverse_list[str(self.comboBox_x.currentText())] 382 | y_data = self.inverse_list[str(self.comboBox_y.currentText())] 383 | x_ar = np.array(self.results[x_data]) 384 | y_ar = np.array(self.results[y_data]) 385 | 386 | # set labels 387 | xlabel = OUTPUT_VARIABLES_LIST[x_data]["symbol"] 388 | if OUTPUT_VARIABLES_LIST[x_data]["unit"] != "": 389 | xlabel = xlabel + " [" + OUTPUT_VARIABLES_LIST[x_data]["unit"] + "]" 390 | 391 | ylabel = OUTPUT_VARIABLES_LIST[y_data]["symbol"] 392 | if OUTPUT_VARIABLES_LIST[y_data]["unit"] != "": 393 | ylabel = ylabel + " [" + OUTPUT_VARIABLES_LIST[y_data]["unit"] + "]" 394 | 395 | is_3d = False 396 | 397 | type_x = OUTPUT_VARIABLES_LIST[x_data]["type"] 398 | type_y = OUTPUT_VARIABLES_LIST[y_data]["type"] 399 | 400 | try: 401 | self.figure.delaxes(self.ax) 402 | except Exception as e: 403 | print(e) 404 | print("Did not find axes, not deleting...") 405 | pass 406 | 407 | if type_x == "array" and type_y == "array": 408 | print("case 1") 409 | self.ax = self.figure.add_subplot(111) 410 | x_ar = np.transpose(x_ar) 411 | y_ar = np.transpose(y_ar) 412 | self.ax.plot(x_ar, y_ar, label="2d plot") 413 | self.ax.legend(np.round(list_of_variable_parameter, 2), title=variable_parameter_title) 414 | # data_table = np.column_stack((x_ar,y_ar)) 415 | # data_table=transpose(data_table) 416 | self.table.createTable(y_ar) 417 | # self.table.clear_table() 418 | 419 | 420 | elif type_x == "array" and type_y == "float": 421 | print("case 2") 422 | self.ax = self.figure.add_subplot(111) 423 | # x_ar=np.transpose(x_ar) 424 | # y_ar=np.transpose(y_ar) 425 | 426 | self.ax.plot(x_ar, y_ar, label="2d plot") 427 | self.ax.legend(self.parent.input_data["r"], title="r [m]") 428 | data_table = np.column_stack((y_ar, x_ar)) 429 | self.table.createTable(data_table) 430 | 431 | 432 | elif type_x == "float" and type_y == "array": 433 | print("case 3") 434 | self.ax = self.figure.add_subplot(111) 435 | x_ar = np.transpose(x_ar) 436 | self.ax.plot(x_ar, y_ar, label="2d plot") 437 | self.ax.legend(self.parent.input_data["r"], title="r [m]") 438 | data_table = list(np.column_stack((x_ar, y_ar))) 439 | self.table.createTable(data_table) 440 | 441 | elif type_x == "float" and type_y == "float": 442 | print("case 4") 443 | self.ax = self.figure.add_subplot(111) 444 | self.ax.plot(x_ar, y_ar, label="2d plot") 445 | data_table = [x_ar, y_ar] 446 | data_table = transpose(data_table) 447 | self.table.createTable(data_table) 448 | 449 | self.ax.set_title(OUTPUT_VARIABLES_LIST[y_data]["name"] + " vs. " + OUTPUT_VARIABLES_LIST[x_data]["name"]) 450 | self.ax.set_xlabel(xlabel) 451 | self.ax.set_ylabel(ylabel) 452 | 453 | self.canvas.draw() 454 | 455 | def set_data(self, results): 456 | """ 457 | 458 | :param results: 459 | """ 460 | self.results = results 461 | self.draw_graph() 462 | -------------------------------------------------------------------------------- /scraping.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from urllib.parse import parse_qsl, urlparse 19 | 20 | import numpy as np 21 | import requests 22 | from bs4 import BeautifulSoup as bs 23 | 24 | 25 | def scrape_data(link): 26 | """ 27 | 28 | :param link: 29 | :return: 30 | """ 31 | out = [] 32 | try: 33 | data = get_polars(link) 34 | except Exception as e: 35 | print(e) 36 | return None 37 | for Re, value in data.items(): 38 | for ncrit, value2 in value.items(): 39 | for alpha, value3 in value2.items(): 40 | cl = value3["cl"] 41 | cd = value3["cd"] 42 | out.append([Re, ncrit, alpha, cl, cd]) 43 | out = np.array(out) 44 | return out 45 | 46 | 47 | def get_polars(link): 48 | """ 49 | 50 | :param link: 51 | :return: 52 | """ 53 | results = {} 54 | print("Getting data from", link) 55 | r = requests.get(link) 56 | txt = r.text 57 | soup = bs(txt, features='html.parser') 58 | table_of_polars = soup.find_all("table", {"class": "polar"})[0] 59 | rows = table_of_polars.find_all("tr")[1:-1] 60 | links = [] 61 | for r in rows: 62 | data = r.find_all("td") 63 | checkbox, name, reynolds, ncrit, maxclcd, description, source, details = [ 64 | d for d in data] 65 | details_link = "http://airfoiltools.com" + \ 66 | details.find_all("a")[0].get('href') 67 | links.append(details_link) 68 | 69 | print("List of links:", links) 70 | 71 | csv_links = [] 72 | 73 | for l in links: 74 | print("getting csv links from", l) 75 | r = requests.get(l) 76 | txt = r.text 77 | soup = bs(txt, features='html.parser') 78 | details_table = soup.find_all("table", {"class": "details"})[0] 79 | td1 = details_table.find_all("td", {"class": "cell1"}) 80 | _links = details_table.find_all("a") 81 | for _l in _links: 82 | if "csv?polar" in _l.get("href"): 83 | csv_links.append("http://airfoiltools.com" + _l.get("href")) 84 | break 85 | 86 | print("CSV links:", csv_links) 87 | 88 | for l in csv_links: 89 | print("getting data from csv link", l) 90 | lines = None 91 | r = requests.get(l) 92 | text = r.text 93 | lines = text.splitlines() 94 | start = lines.index("Alpha,Cl,Cd,Cdp,Cm,Top_Xtr,Bot_Xtr") 95 | reynolds_number = [float(i.split(",")[1]) for i in lines if "Reynolds number," in i][0] 96 | ncrit = [float(i.split(",")[1]) for i in lines if "Ncrit," in i][0] 97 | mach = [float(i.split(",")[1]) for i in lines if "Mach," in i][0] 98 | 99 | for line_num in range(start + 1, len(lines)): 100 | alpha, cl, cd = None, None, None 101 | alpha, cl, cd = lines[line_num].split(",")[:3] 102 | alpha, cl, cd = float(alpha), float(cl), float(cd) 103 | if not reynolds_number in results.keys(): 104 | results[reynolds_number] = {} 105 | if not ncrit in results[reynolds_number].keys(): 106 | results[reynolds_number][ncrit] = {} 107 | results[reynolds_number][ncrit][alpha] = {"cl": cl, "cd": cd} 108 | 109 | return results 110 | 111 | 112 | def get_x_y_from_link(link): 113 | """ 114 | 115 | :param link: 116 | :return: 117 | """ 118 | print("Getting x-y airfoil data from", link) 119 | 120 | params = parse_qsl(urlparse(link.strip()).query, keep_blank_values=True) 121 | selig_link = "http://airfoiltools.com/airfoil/seligdatfile?airfoil=" + params[0][1] 122 | print("Presumed Selig dat link:") 123 | print(selig_link) 124 | 125 | r = requests.get(selig_link) 126 | text = r.text 127 | print("Got text:") 128 | print(text) 129 | print("Parsing...") 130 | lines = text.splitlines() 131 | 132 | x, y = [], [] 133 | 134 | for l in lines[1:]: 135 | stripped_line = l.strip().replace(" ", " ") 136 | x_l, y_l = stripped_line.split(" ") 137 | x.append(float(x_l)) 138 | y.append(float(y_l)) 139 | 140 | return x, y 141 | 142 | # scrape_data("http://airfoiltools.com/airfoil/details?airfoil=clarky-il") 143 | -------------------------------------------------------------------------------- /tests/test_just_calculate.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import sys, os 19 | sys.path.append("..") 20 | from calculation_runner import calculate_power_3d 21 | import json 22 | 23 | results = calculate_power_3d(json.loads(open(os.path.join("..","karlsen.bem")).read())) -------------------------------------------------------------------------------- /tests/tests_moment.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import sys 19 | sys.path.append('..') 20 | from bending_inertia import PointsInCircum, pi, calculate_bending_inertia_2 21 | 22 | 23 | def test_circle(): 24 | r = 7777777.7777777777777 25 | x, y = PointsInCircum(r, 10000) 26 | I, _, _, A = calculate_bending_inertia_2(x, y) 27 | I_theor = pi / 4 * r ** 4 28 | error = abs(I - I_theor) / I_theor * 100 29 | print("I:", I, "m4", "A:", A, "m2") 30 | print("I_theor", I_theor) 31 | print("Napaka:", abs(I - I_theor) / I_theor * 100, " %") 32 | if error <= 1e-4: 33 | return True 34 | return False 35 | 36 | 37 | def test_hollow_circle(): 38 | r1 = 0.5 39 | r2 = 1 40 | x1, y1 = PointsInCircum(r1, 10000) 41 | x2, y2 = PointsInCircum(r2, 10000) 42 | I1, _, _, A1 = calculate_bending_inertia_2(x1, y1) 43 | I2, _, _, A2 = calculate_bending_inertia_2(x2, y2) 44 | I_theor = pi / 4 * (r2 ** 4 - r1 ** 4) 45 | Ix = I2 - I1 46 | error = abs(Ix - I_theor) / I_theor * 100 47 | print("Ix", Ix) 48 | print("Ix_theor", I_theor) 49 | print("Napaka:", abs(Ix - I_theor) / I_theor * 100, " %") 50 | if error <= 1e-4: 51 | return True 52 | return False 53 | 54 | test_hollow_circle() 55 | test_circle() -------------------------------------------------------------------------------- /visualization.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import math 19 | 20 | import matplotlib.pyplot as plt 21 | import mpl_toolkits.mplot3d as mp3d 22 | import numpy as np 23 | from mpl_toolkits.mplot3d import Axes3D 24 | from scipy import interpolate 25 | 26 | from utils import get_transition_foils 27 | 28 | 29 | def create_face(p1, p2, p3, p4, *args, **kwargs): 30 | """ 31 | 32 | :param p1: 33 | :param p2: 34 | :param p3: 35 | :param p4: 36 | :param args: 37 | :param kwargs: 38 | :return: 39 | """ 40 | coords = [p1, p2, p3, p4] 41 | face = mp3d.art3d.Poly3DCollection( 42 | [coords], facecolors=facecolors, alpha=.5, linewidth=0.0, *args, **kwargs) 43 | return face 44 | 45 | 46 | def rotate(origin, point, angle): 47 | """ 48 | Rotate a point counterclockwise by a given angle around a given origin. 49 | 50 | The angle must be given in radians. 51 | """ 52 | ox, oy = origin 53 | px, py = point 54 | 55 | qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy) 56 | qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy) 57 | return qx, qy 58 | 59 | 60 | def rotate_array(x, y, xy, angle): 61 | """ 62 | 63 | :param x: 64 | :param y: 65 | :param xy: 66 | :param angle: in degrees 67 | :return: 68 | """ 69 | angle = math.radians(angle) 70 | x_out, y_out = [], [] 71 | for _x, _y in zip(x, y): 72 | _xq, _yq = rotate(xy, (_x, _y), angle) 73 | x_out.append(_xq) 74 | y_out.append(_yq) 75 | return x_out, y_out 76 | 77 | 78 | def scale_and_normalize(foil_x, foil_y, scale, centroid): 79 | """ 80 | 81 | :param foil_x: 82 | :param foil_y: 83 | :param scale: 84 | :param centroid: 85 | :return: 86 | """ 87 | foil_x, foil_y = np.array(foil_x), np.array(foil_y) 88 | foil_x = foil_x - centroid[0] 89 | foil_x, foil_y = foil_x * scale, foil_y * scale 90 | return foil_x, foil_y 91 | 92 | 93 | def closest_point(x_point, y_point, x_list, y_list): 94 | """ 95 | Find the closest point in zip(x_list,y_list), to the point x_point,y_point. 96 | """ 97 | dist = [0] * len(x_list) 98 | 99 | min_index = None 100 | min_distance = None 101 | 102 | for i in range(len(x_list)): 103 | dist_v = np.sqrt((x_list[i] - x_point) ** 2 + (y_list[i] - y_point) ** 2) 104 | if min_distance == None or dist_v < min_distance: 105 | min_index = i 106 | min_distance = dist_v 107 | 108 | return min_index, x_list[min_index], y_list[min_index] 109 | 110 | 111 | def point_along_line(x1, y1, x2, y2, t): 112 | """ 113 | if t < 0.5, new point is closer to x1,y1 114 | if t = 1.0, new point is == x2,y2 115 | if t = 0.0, new point is == x1,y1 116 | """ 117 | x, y = (1 - t) * x1 + t * x2, (1 - t) * y1 + t * y2 118 | return x, y 119 | 120 | 121 | def interpolate_foils(foil_1_x, foil_1_y, foil_2_x, foil_2_y, t): 122 | """ 123 | 124 | :param foil_1_x: 125 | :param foil_1_y: 126 | :param foil_2_x: 127 | :param foil_2_y: 128 | :param t: 129 | :return: 130 | """ 131 | out_x, out_y = [], [] 132 | 133 | tck, u = interpolate.splprep([foil_1_x, foil_1_y], s=0) 134 | xspline, yspline = interpolate.splev(np.linspace(0, 1, 500), tck, der=0) 135 | 136 | tck2, u2 = interpolate.splprep([foil_2_x, foil_2_y], s=0) 137 | xspline2, yspline2 = interpolate.splev(np.linspace(0, 1, 500), tck2, der=0) 138 | 139 | # get list of zeros 140 | zeros_idx_1 = np.where(np.diff(np.sign(yspline)))[0] 141 | zeros_idx_2 = np.where(np.diff(np.sign(yspline2)))[0] 142 | 143 | #zero_1_index = zeros_idx_1[np.abs(zeros_idx_1 - len(zeros_idx_1) / 2).argmin()] 144 | #zero_2_index = zeros_idx_2[np.abs(zeros_idx_2 - len(zeros_idx_2) / 2).argmin()] 145 | 146 | for i in range(len(xspline2)): 147 | x_point, y_point = xspline2[i], yspline2[i] 148 | if i <= 250: 149 | xspline_in, yspline_in = xspline[:250], yspline[:250] 150 | else: 151 | xspline_in, yspline_in = xspline[250:], yspline[250:] 152 | index, x_found, y_found = closest_point(x_point, y_point, xspline_in, yspline_in) 153 | x_new, y_new = point_along_line(x_found, y_found, x_point, y_point, t) 154 | out_x.append(x_new) 155 | out_y.append(y_new) 156 | 157 | return out_x, out_y 158 | 159 | 160 | def create_3d_blade(input_data, flip_turning_direction=False, propeller_geom=False): 161 | """ 162 | 163 | :param input_data: 164 | :param flip_turning_direction: 165 | :param propeller_geom: 166 | :return: 167 | """ 168 | theta = input_data["theta"] 169 | r = input_data["r"] 170 | c = input_data["c"] 171 | foil = input_data["foils"] 172 | airfoils = input_data["airfoils"] 173 | R = input_data["R"] 174 | Rhub = input_data["Rhub"] 175 | 176 | r = [Rhub] + list(r) + [R] 177 | c = [c[0]] + list(c) + [c[-1]] 178 | theta = [theta[0]] + list(theta) + [theta[-1]] 179 | foil = [foil[0]] + list(foil) + [foil[-1]] 180 | transition_foils = get_transition_foils(foil) 181 | 182 | out_x, out_y, out_z = [], [], [] 183 | data = [] 184 | for i in range(len(r)): 185 | _r = r[i] 186 | _c = c[i] 187 | _airfoil = foil[i] 188 | _theta = theta[i] # - because of direction 189 | 190 | if _airfoil == "transition": 191 | _prev_foil = transition_foils[i][0] 192 | _next_foil = transition_foils[i][1] 193 | _transition_coefficient = transition_foils[i][2] 194 | _airfoil_x_prev, _airfoil_y_prev = airfoils[_prev_foil]["x"], airfoils[_prev_foil]["y"] 195 | _airfoil_x_next, _airfoil_y_next = airfoils[_next_foil]["x"], airfoils[_next_foil]["y"] 196 | _airfoil_x, _airfoil_y = interpolate_foils(_airfoil_x_prev, _airfoil_y_prev, _airfoil_x_next, 197 | _airfoil_y_next, _transition_coefficient) 198 | _centroid_x_prev, _centroid_y_prev = airfoils[_prev_foil]["centroid_x"], airfoils[_prev_foil]["centroid_y"] 199 | _centroid_x_next, _centroid_y_next = airfoils[_next_foil]["centroid_x"], airfoils[_next_foil]["centroid_y"] 200 | _centroid_x, _centroid_y = point_along_line(_centroid_x_prev, _centroid_y_prev, _centroid_x_next, 201 | _centroid_y_next, _transition_coefficient) 202 | else: 203 | _airfoil_x, _airfoil_y = airfoils[_airfoil]["x"], airfoils[_airfoil]["y"] 204 | _centroid_x, _centroid_y = airfoils[_airfoil]["centroid_x"], airfoils[_airfoil]["centroid_y"] 205 | 206 | if propeller_geom: 207 | _theta = -_theta 208 | 209 | if flip_turning_direction: 210 | _airfoil_x = -np.array(_airfoil_x) 211 | _theta = -_theta 212 | _centroid_x = -_centroid_x 213 | 214 | _centroid = (_centroid_x, _centroid_y) 215 | 216 | _airfoil_x, _airfoil_y = scale_and_normalize(_airfoil_x, _airfoil_y, _c, _centroid) 217 | _airfoil_x, _airfoil_y = rotate_array(_airfoil_x, _airfoil_y, (0, 0), _theta) 218 | 219 | list_x, list_y = [], [] 220 | for _x, _y in zip(_airfoil_x, _airfoil_y): 221 | out_x.append(_x) 222 | out_y.append(_y) 223 | out_z.append(_r) 224 | list_x.append(_x) 225 | list_y.append(_y) 226 | 227 | data.append([_r, np.array(list_x), np.array(list_y)]) 228 | 229 | # DRAW ######## 230 | fig = plt.figure() 231 | ax = fig.add_subplot(111, projection='3d') 232 | X, Y, Z = np.array(out_x), np.array(out_y), np.array(out_z) 233 | max_range = np.array( 234 | [X.max() - X.min(), Y.max() - Y.min(), Z.max() - Z.min()]).max() / 2.0 235 | mid_x = (X.max() + X.min()) * 0.5 236 | mid_y = (Y.max() + Y.min()) * 0.5 237 | mid_z = (Z.max() + Z.min()) * 0.5 238 | ax.set_xlim(mid_x - max_range, mid_x + max_range) 239 | ax.set_ylim(mid_y - max_range, mid_y + max_range) 240 | ax.set_zlim(mid_z - max_range, mid_z + max_range) 241 | Axes3D.mouse_init(ax, rotate_btn=1, zoom_btn=2) 242 | ax.scatter(X, Y, Z) 243 | # plt.show() 244 | ############### 245 | 246 | # data = np.array(data) 247 | return {"data": data, "X": X, "Y": Y, "Z": Z} 248 | -------------------------------------------------------------------------------- /xfoil.py: -------------------------------------------------------------------------------- 1 | # Python BEM - Blade Element Momentum Theory Software. 2 | 3 | # Copyright (C) 2022 M. Smrekar 4 | 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import os 19 | import re as regex 20 | import sys 21 | from math import isinf 22 | from subprocess import Popen, PIPE 23 | import threading 24 | import time 25 | 26 | import matplotlib.cm as cm 27 | import numpy as np 28 | import scipy.interpolate 29 | import scipy.linalg 30 | from matplotlib import pyplot as plt 31 | from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import 32 | 33 | from bem import data_path 34 | 35 | xfoil_path = os.path.join(data_path,"xfoil_executables", "xfoil.exe") 36 | if sys.platform.startswith("darwin"): 37 | xfoil_path = os.path.join(data_path,"xfoil_executables", "xfoil") 38 | 39 | 40 | def xfoil_runner(airfoil, reynolds, alpha, ncrit, printer=None, print_all=False): 41 | """ 42 | 43 | :param airfoil: 44 | :param reynolds: 45 | :param alpha: 46 | :param printer: 47 | :param print_all: 48 | :return: 49 | """ 50 | # alpha = round(degrees(alpha)) 51 | # alpha in degrees 52 | if print_all: 53 | if printer != None: 54 | printer.print(" xfoil runner, alpha=", 55 | alpha, "re=", reynolds) 56 | out = run_xfoil_analysis(airfoil, reynolds, alpha, ncrit) 57 | # if out == False: 58 | # return xfoil_runner(airfoil, reynolds * 0.5, radians(alpha), printer, print_all=print_all) 59 | return out 60 | 61 | 62 | def get_coefficients_from_output(output_str): 63 | """ 64 | 65 | :param output_str: 66 | :return: 67 | """ 68 | lines = output_str.splitlines() 69 | a, CL, Cm, CD, CDf, CDp = [None] * 6 70 | cL_last = None 71 | cD_last = None 72 | 73 | for l in lines: 74 | # print(l) 75 | cL_find = regex.match(r".+CL =([- \d][- \d][ \d]\.\d\d\d\d)", l) 76 | cD_find = regex.match(r".+CD =([- \d][- \d][ \d]\.\d\d\d\d)", l) 77 | if cL_find: 78 | cL = float(cL_find.groups()[0]) 79 | if not isinf(cL): 80 | cL_last = cL 81 | if cD_find: 82 | cD = float(cD_find.groups()[0]) 83 | if not isinf(cD): 84 | cD_last = cD 85 | if "VISCAL: Convergence failed" in l: 86 | return False 87 | 88 | # Weird cases 89 | out = {"CL": cL_last, 90 | "CD": cD_last, 91 | "out": output_str} 92 | return out 93 | 94 | 95 | def run_xfoil_analysis(airfoil, reynolds, alpha, ncrit, iterations=100, max_next_angle=2., print_output=False): 96 | """ 97 | 98 | :param airfoil: 99 | :param reynolds: 100 | :param alpha: 101 | :param iterations: 102 | :param max_next_angle: 103 | :param print_output: 104 | :return: 105 | """ 106 | # print("running xfoil for %s,Re=%s,alpha=%s" % (airfoil,reynolds,alpha)) 107 | # alpha in degrees 108 | 109 | with Popen(os.path.abspath(xfoil_path), stdin=PIPE, stdout=PIPE, universal_newlines=True, shell=False) as process: 110 | 111 | def call(_str, proc=process): 112 | """ 113 | 114 | :param _str: 115 | :param proc: 116 | """ 117 | # print(_str,file=process.stdin) 118 | proc.stdin.write(_str + "\n") 119 | 120 | def kill(): 121 | """ 122 | 123 | """ 124 | time.sleep(2) 125 | process.kill() 126 | 127 | thread = threading.Thread(target=kill) 128 | thread.start() 129 | 130 | # disables graphical preview 131 | call("plop") 132 | call("G F") 133 | 134 | # go back 135 | call("") 136 | 137 | if "naca" in airfoil.lower(): 138 | # treat as standard naca airfoil 139 | airfoil = airfoil.replace(".dat", "").strip() 140 | call(airfoil) 141 | else: 142 | # open .dat file 143 | call("load %s" % os.path.join("foils", airfoil)) 144 | 145 | # create back-design from dat file 146 | call("mdes") 147 | call("filt") 148 | call("exec") 149 | call("") 150 | 151 | # calculation mode 152 | call("pane") 153 | call("oper") 154 | 155 | # input reynolds 156 | call("re") 157 | call("%s" % reynolds) 158 | 159 | # viscid mode 160 | call("v") 161 | 162 | # set ncrit 163 | call("vpar") 164 | call("n") 165 | call("%s" % ncrit) 166 | call("") 167 | 168 | # num iterations 169 | call("iteration") 170 | call("%s" % iterations) 171 | 172 | # alfa 173 | call("alfa") 174 | call("%s" % alpha) 175 | 176 | # quit 177 | call("") 178 | call("") 179 | call("quit") 180 | 181 | output = process.communicate()[0] 182 | if print_output: 183 | for l in output.splitlines(): 184 | print(l) # print(output) 185 | return get_coefficients_from_output(output) 186 | 187 | 188 | def draw_to_matplotlib(x, y, z, shrani=False, unit='CL'): 189 | """ 190 | 191 | :param x: 192 | :param y: 193 | :param z: 194 | :param shrani: 195 | :param unit: 196 | """ 197 | # Ne vem ali je to potrebno. Menda je. 198 | x = np.array(x) 199 | y = np.array(y) 200 | z = np.array(z) 201 | 202 | # x/y tocke za risati ozadje 203 | xi, yi = np.linspace(x.min(), x.max(), 100), np.linspace( 204 | y.min(), y.max(), 100) 205 | xi, yi = np.meshgrid(xi, yi) 206 | 207 | # Linearna interpolacija ozadja, glede na x,y,z 208 | # interpolacija je lahko linear, cubic, itd. 209 | rbf = scipy.interpolate.Rbf(x, y, z, function='quintic') 210 | zi = rbf(xi, yi) 211 | 212 | # Barvno ozadje 213 | # plt.imshow(zi, vmin=z.min(), vmax=z.max(), origin='lower', 214 | # extent=[x.min(), x.max(), y.min(), y.max()], cmap=cm.jet) 215 | 216 | plt.scatter(xi, yi, c=zi) 217 | plt.scatter(x, y, c=z, cmap=cm.jet) 218 | 219 | # ODKOMENTIRAJ ZA ABSOLUTNO SKALO 220 | # plt.clim(0,10) 221 | 222 | # nastavitev min/max vrednosti na osi 223 | # plt.xlim(-1000,0) 224 | # plt.ylim(-1000,0) 225 | 226 | # X Label 227 | # plt.xlabel('alfa' % (z.min(),z.max(),np.mean(z))) 228 | 229 | # Y Label 230 | plt.ylabel('re') 231 | 232 | # Title 233 | # plt.title('Hitrost') 234 | 235 | # Color bar 236 | cbar = plt.colorbar() 237 | cbar.ax.set_xlabel(unit) 238 | 239 | # Za shranjevanje 240 | if shrani: 241 | filename = "hitrosti.png" 242 | path = os.path.join(os.pwd(), filename) 243 | print(filename, "saved to", path) 244 | plt.savefig(path) 245 | 246 | plt.show() 247 | 248 | 249 | def generate_polars(foil, alpha_from, alpha_to, alpha_num, reynolds_from, reynolds_to, reynolds_num, ncrit): 250 | """ 251 | 252 | :param foil: 253 | :return: 254 | """ 255 | all_ncrit = [] 256 | all_re = [] 257 | all_a = [] 258 | all_cl = [] 259 | all_cd = [] 260 | 261 | for Re in np.linspace(reynolds_from, reynolds_to, reynolds_num): 262 | for a in np.linspace(alpha_from, alpha_to, alpha_num): 263 | Re = int(Re) 264 | cl = None 265 | cd = None 266 | print("Re", Re, "Alpha:", a) 267 | o = None 268 | o = xfoil_runner(foil, Re, a, ncrit) 269 | if o != False: 270 | cl = o["CL"] 271 | cd = o["CD"] 272 | if cl != None and cd != None: 273 | print("CL:", o["CL"], "CD", o["CD"]) 274 | all_re.append(Re) 275 | all_a.append(a) 276 | all_cl.append(cl) 277 | all_cd.append(cd) 278 | all_ncrit.append(ncrit) 279 | return all_ncrit, all_a, all_re, all_cl, all_cd 280 | 281 | 282 | def generate_polars_data(foil, alpha_from, alpha_to, alpha_num, reynolds_from, reynolds_to, reynolds_num, ncrit): 283 | """ 284 | 285 | :param foil: 286 | :return: 287 | """ 288 | all_ncrit, all_a, all_re, all_cl, all_cd = generate_polars(foil, alpha_from, alpha_to, alpha_num, reynolds_from, 289 | reynolds_to, reynolds_num, ncrit) 290 | out = [] 291 | for i in range(len(all_a)): 292 | out.append([all_re[i], all_ncrit[i], all_a[i], all_cl[i], all_cd[i]]) 293 | out = np.array(out) 294 | return out 295 | -------------------------------------------------------------------------------- /xfoil_executables/xfoil: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihasm/Python-BEM/27801aa46112661f4e476f2996d2cfb6395899d6/xfoil_executables/xfoil -------------------------------------------------------------------------------- /xfoil_executables/xfoil.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihasm/Python-BEM/27801aa46112661f4e476f2996d2cfb6395899d6/xfoil_executables/xfoil.exe --------------------------------------------------------------------------------