├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
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
--------------------------------------------------------------------------------