├── runtime.txt ├── Procfile ├── assets ├── favicon.ico └── MIT-logo-red-gray-72x38.svg ├── miscellaneous └── mockup.pptx ├── requirements.txt ├── README.md ├── app_components.py ├── .gitignore └── app.py /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.8 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:server -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/Automotive-Airfoil-Design/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /miscellaneous/mockup.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/Automotive-Airfoil-Design/HEAD/miscellaneous/mockup.pptx -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aerosandbox == 3.0.0 # TODO fix a version 2 | plotly>= 4.14.3 3 | dash>= 1.19.0 4 | dash_core_components>=1.15.0 5 | dash_html_components>=1.1.2 6 | dash_bootstrap_components>=0.11.3 7 | gunicorn 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airfoil Analysis 2 | 3 | By Peter Sharpe 4 | 5 | Tools for airfoil analysis in your browser. 6 | 7 | This repository contains the code for the Dash web app itself. All aerodynamics math is contained in [AeroSandbox](https://github.com/peterdsharpe/AeroSandbox); specifically, [here](https://github.com/peterdsharpe/AeroSandbox/blob/master/aerosandbox/aerodynamics/aero_2D/airfoil_inviscid.py). -------------------------------------------------------------------------------- /assets/MIT-logo-red-gray-72x38.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | MIT large red and gray logo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app_components.py: -------------------------------------------------------------------------------- 1 | import dash_core_components as dcc 2 | import dash_html_components as html 3 | 4 | ### Operating things 5 | operating_slider_components = [ 6 | html.Div(id="alpha_slider_output"), 7 | dcc.Slider( 8 | id="alpha_slider_input", 9 | min=-20, 10 | max=20, 11 | step=1e-6, 12 | value=8.755, 13 | ), 14 | html.Div(id="height_slider_output"), 15 | dcc.Slider( 16 | id="height_slider_input", 17 | min=0, 18 | max=1, 19 | step=1e-6, 20 | value=0 21 | ), 22 | dcc.Checklist( 23 | id="operating_checklist", 24 | options=[ 25 | { 26 | 'label': " Ground Effect", 27 | 'value': "ground_effect" 28 | } 29 | ], 30 | value=[] 31 | ), 32 | html.Div(id="streamline_density_slider_output"), 33 | dcc.Slider( 34 | id="streamline_density_slider_input", 35 | min=0.1, 36 | max=5, 37 | step=0.1, 38 | value=1 39 | ), 40 | ] 41 | 42 | ### Shape things 43 | n_kulfan_inputs_per_side = 3 44 | sides = ["Upper", "Lower"] 45 | 46 | kulfan_slider_components = [] 47 | for side in sides: 48 | kulfan_slider_components.append(dcc.Markdown(f"##### {side} Surface")) 49 | for i in range(n_kulfan_inputs_per_side): 50 | 51 | slider_id = f"kulfan_{side.lower()}_{i}" 52 | slider_min = None 53 | slider_max = None 54 | slider_init = None 55 | 56 | if side == "Upper": 57 | slider_min = -0.3 58 | slider_max = 0.7 59 | slider_init = 0.2 60 | if i == 0: 61 | slider_min = 0.06 62 | if n_kulfan_inputs_per_side == 3: 63 | slider_init = [0.254094, 0.47475, 0.023816][i] 64 | else: 65 | slider_min = -0.7 66 | slider_max = 0.3 67 | slider_init = -0.2 68 | if i == 0: 69 | slider_max = -0.06 70 | if n_kulfan_inputs_per_side == 3: 71 | slider_init = [-0.107972, 0.061474, -0.055904][i] 72 | 73 | kulfan_slider_components.extend([ 74 | html.Div(id=slider_id + "_output"), 75 | dcc.Slider( 76 | id=slider_id + "_input", 77 | min=slider_min, 78 | max=slider_max, 79 | step=1e-6, # continuous 80 | value=slider_init 81 | ) 82 | ]) 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Peter's Python Template template 3 | # Created by .ignore support plugin (hsz.mobi) 4 | 5 | ### Project-Specific 6 | # Add things here! 7 | 8 | 9 | ### IntelliJ project files 10 | .idea 11 | *.iml 12 | out 13 | gen 14 | 15 | ### Python template 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | .Python 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | pip-wheel-metadata/ 39 | share/python-wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | MANIFEST 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .nox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | *.py,cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | cover/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | db.sqlite3 78 | db.sqlite3-journal 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | .pybuilder/ 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | # For a library or package, you might want to ignore these files since the code is 103 | # intended to run in multiple environments; otherwise, check them in: 104 | # .python-version 105 | 106 | # pipenv 107 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 108 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 109 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 110 | # install all needed dependencies. 111 | #Pipfile.lock 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | import plotly.graph_objects as go 3 | import dash 4 | import dash_core_components as dcc 5 | import dash_html_components as html 6 | import dash_bootstrap_components as dbc 7 | from dash.dependencies import Input, Output, State 8 | import aerosandbox as asb 9 | import aerosandbox.numpy as np 10 | import copy 11 | import plotly.figure_factory as ff 12 | import pandas as pd 13 | 14 | from app_components import * 15 | 16 | ### Build the app 17 | app = dash.Dash(__name__, external_stylesheets=[dbc.themes.MINTY], title="Airfoil Analysis") 18 | server = app.server 19 | 20 | app.layout = dbc.Container( 21 | [ 22 | dbc.Row([ 23 | dbc.Col([ 24 | dcc.Markdown(""" 25 | # Airfoil Analysis with [AeroSandbox](https://github.com/peterdsharpe/AeroSandbox) and [Dash](https://plotly.com/dash/) 26 | 27 | By [Peter Sharpe](https://peterdsharpe.github.io/). Uses potential flow theory (viscous effects neglected, for now). [Source code here](https://github.com/peterdsharpe/Automotive-Airfoil-Design). 28 | """) 29 | ], width=True), 30 | dbc.Col([ 31 | html.Img(src="assets/MIT-logo-red-gray-72x38.svg", alt="MIT Logo", height="30px"), 32 | ], width=1) 33 | ], align="end"), 34 | html.Hr(), 35 | dbc.Row([ 36 | dbc.Col([ 37 | dbc.Button( 38 | "Modify Operating Conditions", 39 | id="operating_button" 40 | ), 41 | dbc.Collapse( 42 | dbc.Card( 43 | dbc.CardBody( 44 | operating_slider_components, 45 | ) 46 | ), 47 | id="operating_collapse", 48 | is_open=False 49 | ), 50 | html.Hr(), 51 | dbc.Button( 52 | "Modify Shape Parameters (Kulfan)", 53 | id="shape_button" 54 | ), 55 | dbc.Collapse( 56 | dbc.Card( 57 | dbc.CardBody( 58 | kulfan_slider_components, 59 | ) 60 | ), 61 | id="shape_collapse", 62 | is_open=False 63 | ), 64 | html.Hr(), 65 | dbc.Button( 66 | "Show Raw Coordinates (*.dat format)", 67 | id="coordinates_button" 68 | ), 69 | dbc.Collapse( 70 | dbc.Card( 71 | dbc.CardBody( 72 | dcc.Markdown(id="coordinates_output") 73 | ) 74 | ), 75 | id="coordinates_collapse", 76 | is_open=False 77 | ), 78 | html.Hr(), 79 | dcc.Markdown("##### Commands"), 80 | dbc.Button( 81 | "Analyze", 82 | id="analyze", color="primary", style={"margin": "5px"}), 83 | html.Hr(), 84 | dcc.Markdown("##### Aerodynamic Performance"), 85 | dbc.Spinner( 86 | html.P(id='text_output'), 87 | color="primary", 88 | ) 89 | 90 | ], width=3), 91 | dbc.Col([ 92 | dcc.Graph(id='display', style={'height': '90vh'}), 93 | ], width=9, align="start") 94 | ]), 95 | html.Hr(), 96 | dcc.Markdown(""" 97 | Aircraft design tools powered by [AeroSandbox](https://github.com/peterdsharpe/AeroSandbox). Build beautiful UIs for your scientific computing apps with [Plot.ly](https://plotly.com/) and [Dash](https://plotly.com/dash/)! 98 | """), 99 | ], 100 | fluid=True 101 | ) 102 | 103 | 104 | ### Callback to make shape parameters menu expand 105 | @app.callback( 106 | Output("shape_collapse", "is_open"), 107 | [Input("shape_button", "n_clicks")], 108 | [State("shape_collapse", "is_open")] 109 | ) 110 | def toggle_shape_collapse(n_clicks, is_open): 111 | if n_clicks: 112 | return not is_open 113 | return is_open 114 | 115 | 116 | ### Callback to make operating parameters menu expand 117 | @app.callback( 118 | Output("operating_collapse", "is_open"), 119 | [Input("operating_button", "n_clicks")], 120 | [State("operating_collapse", "is_open")] 121 | ) 122 | def toggle_shape_collapse(n_clicks, is_open): 123 | if n_clicks: 124 | return not is_open 125 | return is_open 126 | 127 | 128 | ### Callback to make coordinates menu expand 129 | @app.callback( 130 | Output("coordinates_collapse", "is_open"), 131 | [Input("coordinates_button", "n_clicks")], 132 | [State("coordinates_collapse", "is_open")] 133 | ) 134 | def toggle_shape_collapse(n_clicks, is_open): 135 | if n_clicks: 136 | return not is_open 137 | return is_open 138 | 139 | 140 | ### Callback to make operating sliders display proper values 141 | @app.callback( 142 | Output("alpha_slider_output", "children"), 143 | [Input("alpha_slider_input", "drag_value")] 144 | ) 145 | def display_alpha_slider(drag_value): 146 | return f"Angle of Attack: {drag_value}" 147 | 148 | 149 | @app.callback( 150 | Output("height_slider_output", "children"), 151 | [Input("height_slider_input", "drag_value")] 152 | ) 153 | def display_alpha_slider(drag_value): 154 | return f"Height: {drag_value}" 155 | 156 | 157 | @app.callback( 158 | Output("streamline_density_slider_output", "children"), 159 | [Input("streamline_density_slider_input", "drag_value")] 160 | ) 161 | def display_streamline_density_slider(drag_value): 162 | return f"Streamline Density: {drag_value}" 163 | 164 | 165 | ### The callback to make the kulfan sliders display proper values 166 | for side in sides: 167 | for i in range(n_kulfan_inputs_per_side): 168 | @app.callback( 169 | Output(f"kulfan_{side.lower()}_{i}_output", "children"), 170 | [Input(f"kulfan_{side.lower()}_{i}_input", "drag_value")] 171 | ) 172 | def display_slider_value(drag_value): 173 | return f"Parameter: {drag_value}" 174 | 175 | 176 | def make_table(dataframe): 177 | return dbc.Table.from_dataframe( 178 | dataframe, 179 | bordered=True, 180 | hover=True, 181 | responsive=True, 182 | striped=True, 183 | style={ 184 | 185 | } 186 | ) 187 | 188 | 189 | last_analyze_timestamp = None 190 | 191 | n_clicks_last = 0 192 | 193 | ### The callback to draw the airfoil on the graph 194 | @app.callback( 195 | Output("display", "figure"), 196 | Output("text_output", "children"), 197 | Output("coordinates_output", "children"), 198 | [ 199 | Input('analyze', 'n_clicks'), 200 | Input('alpha_slider_input', "value"), 201 | Input("height_slider_input", "value"), 202 | Input("streamline_density_slider_input", "value"), 203 | Input("operating_checklist", "value"), 204 | ] + [ 205 | Input(f"kulfan_{side.lower()}_{i}_input", "value") 206 | for side in sides 207 | for i in range(n_kulfan_inputs_per_side) 208 | ] 209 | ) 210 | def display_graph(n_clicks, alpha, height, streamline_density, operating_checklist, *kulfan_inputs): 211 | ### Figure out if a button was pressed 212 | global n_clicks_last 213 | if n_clicks is None: 214 | n_clicks = 0 215 | 216 | analyze_button_pressed = n_clicks > n_clicks_last 217 | n_clicks_last = n_clicks 218 | 219 | ### Parse the checklist 220 | ground_effect = 'ground_effect' in operating_checklist 221 | 222 | ### Start constructing the figure 223 | airfoil = asb.Airfoil( 224 | coordinates=asb.get_kulfan_coordinates( 225 | lower_weights=np.array(kulfan_inputs[n_kulfan_inputs_per_side:]), 226 | upper_weights=np.array(kulfan_inputs[:n_kulfan_inputs_per_side]), 227 | TE_thickness=0, 228 | enforce_continuous_LE_radius=False, 229 | n_points_per_side=200 230 | ) 231 | ) 232 | 233 | ### Do coordinates output 234 | coordinates_output = "\n".join( 235 | ["```"] + 236 | ["AeroSandbox Airfoil"] + 237 | ["\t%f\t%f" % tuple(coordinate) for coordinate in airfoil.coordinates] + 238 | ["```"] 239 | ) 240 | 241 | ### Continue doing the airfoil things 242 | airfoil = airfoil.rotate(angle=-np.radians(alpha)) 243 | airfoil = airfoil.translate( 244 | 0, 245 | height + 0.5 * np.sind(alpha) 246 | ) 247 | fig = go.Figure() 248 | fig.add_trace( 249 | go.Scatter( 250 | x=airfoil.x(), 251 | y=airfoil.y(), 252 | mode="lines", 253 | name="Airfoil", 254 | fill="toself", 255 | line=dict( 256 | color="blue" 257 | ) 258 | ) 259 | ) 260 | 261 | ### Default text output 262 | text_output = 'Click "Analyze" to compute aerodynamics!' 263 | 264 | xrng = (-0.5, 1.5) 265 | yrng = (-0.6, 0.6) if not ground_effect else (0, 1.2) 266 | 267 | if analyze_button_pressed: 268 | 269 | analysis = asb.AirfoilInviscid( 270 | airfoil=airfoil.repanel(50), 271 | op_point=asb.OperatingPoint( 272 | velocity=1, 273 | alpha=0, 274 | ), 275 | ground_effect=ground_effect 276 | ) 277 | 278 | x = np.linspace(*xrng, 100) 279 | y = np.linspace(*yrng, 100) 280 | X, Y = np.meshgrid(x, y) 281 | u, v = analysis.calculate_velocity( 282 | x_field=X.flatten(), 283 | y_field=Y.flatten() 284 | ) 285 | U = u.reshape(X.shape) 286 | V = v.reshape(Y.shape) 287 | 288 | streamline_fig = ff.create_streamline( 289 | x, y, U, V, 290 | arrow_scale=1e-16, 291 | density=streamline_density, 292 | line=dict( 293 | color="#ff82a3" 294 | ), 295 | name="Streamlines" 296 | ) 297 | 298 | fig = go.Figure( 299 | data=streamline_fig.data + fig.data 300 | ) 301 | 302 | text_output = make_table(pd.DataFrame( 303 | { 304 | "Engineering Quantity": [ 305 | "C_L" 306 | ], 307 | "Value" : [ 308 | f"{analysis.Cl:.3f}" 309 | ] 310 | } 311 | )) 312 | 313 | fig.update_layout( 314 | xaxis_title="x/c", 315 | yaxis_title="y/c", 316 | showlegend=False, 317 | yaxis=dict(scaleanchor="x", scaleratio=1), 318 | margin={'t': 0}, 319 | title=None, 320 | ) 321 | 322 | fig.update_xaxes(range=xrng) 323 | fig.update_yaxes(range=yrng) 324 | 325 | return fig, text_output, [coordinates_output] 326 | 327 | 328 | if __name__ == '__main__': 329 | app.run_server(debug=False) 330 | --------------------------------------------------------------------------------