From 8e2f01f0cb58c289671ed5bb898b0195f4a79046 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Mon, 30 May 2022 16:24:45 -0400 Subject: [PATCH] refactoring --- apps/dash-aerosandbox/.gitignore | 126 --------- apps/dash-aerosandbox/README.md | 2 +- apps/dash-aerosandbox/app.py | 239 +++--------------- apps/dash-aerosandbox/assets/css/app.css | 63 +++++ .../assets/{ => github}/screenshot.png | Bin .../{ => images}/MIT-logo-red-gray-72x38.svg | 0 .../assets/{ => images}/favicon.ico | Bin .../assets/images/plotly-logo-light-theme.png | Bin 0 -> 30763 bytes apps/dash-aerosandbox/gitignore | 191 ++++++++++++++ apps/dash-aerosandbox/requirements.txt | 16 +- apps/dash-aerosandbox/runtime.txt | 3 +- apps/dash-aerosandbox/utils/components.py | 127 ++++++++++ apps/dash-aerosandbox/utils/figures.py | 132 ++++++++++ .../helper_functions.py} | 11 +- apps/dash-aerosandbox/{ => utils}/tests.py | 13 +- 15 files changed, 583 insertions(+), 340 deletions(-) delete mode 100644 apps/dash-aerosandbox/.gitignore create mode 100644 apps/dash-aerosandbox/assets/css/app.css rename apps/dash-aerosandbox/assets/{ => github}/screenshot.png (100%) rename apps/dash-aerosandbox/assets/{ => images}/MIT-logo-red-gray-72x38.svg (100%) rename apps/dash-aerosandbox/assets/{ => images}/favicon.ico (100%) create mode 100644 apps/dash-aerosandbox/assets/images/plotly-logo-light-theme.png create mode 100644 apps/dash-aerosandbox/gitignore create mode 100644 apps/dash-aerosandbox/utils/components.py create mode 100644 apps/dash-aerosandbox/utils/figures.py rename apps/dash-aerosandbox/{airplane.py => utils/helper_functions.py} (97%) rename apps/dash-aerosandbox/{ => utils}/tests.py (73%) diff --git a/apps/dash-aerosandbox/.gitignore b/apps/dash-aerosandbox/.gitignore deleted file mode 100644 index 3e884ed9e..000000000 --- a/apps/dash-aerosandbox/.gitignore +++ /dev/null @@ -1,126 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ -/.idea/ diff --git a/apps/dash-aerosandbox/README.md b/apps/dash-aerosandbox/README.md index 6e0082825..11b16d860 100644 --- a/apps/dash-aerosandbox/README.md +++ b/apps/dash-aerosandbox/README.md @@ -9,4 +9,4 @@ An interactive demo of AeroSandbox, powered by Dash! Work in progress. 2. Run `app.py` to launch a local Dash server to host the Dash app. A link will appear in your console; click this to use the Dash app. ## Illustration -![Screenshot of Demo](assets/screenshot.png) +![Screenshot of Demo](assets/github/screenshot.png) diff --git a/apps/dash-aerosandbox/app.py b/apps/dash-aerosandbox/app.py index 8583714c2..da3bd40f7 100644 --- a/apps/dash-aerosandbox/app.py +++ b/apps/dash-aerosandbox/app.py @@ -1,108 +1,48 @@ -import plotly.express as px -import plotly.graph_objects as go -import plotly.subplots as sub import dash -import dash_core_components as dcc -import dash_html_components as html +from dash import Dash, html, dcc, Input, Output, State, callback, callback_context import dash_bootstrap_components as dbc -from dash.dependencies import Input, Output, State -import aerosandbox as asb -import casadi as cas -from airplane import make_airplane -import numpy as np -import pandas as pd + + +from utils.components import ( + header, + key_parameters_card, + commands_card, + aerodynamic_performance_card, + figure_card, +) +from utils.figures import analysis_and_display app = dash.Dash(external_stylesheets=[dbc.themes.MINTY]) app.title = "Aircraft CFD" server = app.server + app.layout = dbc.Container( [ dbc.Row( - [ - dbc.Col( - [ - html.H2("Solar Aircraft Design with AeroSandbox and Dash"), - html.H5("Peter Sharpe"), - ], - width=True, - ), - # dbc.Col([ - # html.Img(src="assets/MIT-logo-red-gray-72x38.svg", alt="MIT Logo", height="30px"), - # ], width=1) - ], - align="end", + header( + app, + "white", + "Solar Aircraft Design with AeroSandbox and Dash", + "Peter Sharpe", + ), ), html.Hr(), dbc.Row( [ dbc.Col( [ - html.Div( - [ - html.H5("Key Parameters"), - html.P("Number of booms:"), - dcc.Slider( - id="n_booms", - min=1, - max=3, - step=1, - value=3, - marks={1: "1", 2: "2", 3: "3",}, - ), - html.P("Wing Span [m]:"), - dcc.Input(id="wing_span", value=43, type="number"), - html.P("Angle of Attack [deg]:"), - dcc.Input(id="alpha", value=7.0, type="number"), - ] - ), + key_parameters_card("n_booms", "wing_span", "alpha"), html.Hr(), - html.Div( - [ - html.H5("Commands"), - dbc.Button( - "Display (1s)", - id="display_geometry", - color="primary", - style={"margin": "5px"}, - n_clicks_timestamp="0", - ), - dbc.Button( - "LL Analysis (3s)", - id="run_ll_analysis", - color="secondary", - style={"margin": "5px"}, - n_clicks_timestamp="0", - ), - dbc.Button( - "VLM Analysis (15s)", - id="run_vlm_analysis", - color="secondary", - style={"margin": "5px"}, - n_clicks_timestamp="0", - ), - ] + commands_card( + "display_geometry", "run_ll_analysis", "run_vlm_analysis" ), html.Hr(), - html.Div( - [ - html.H5("Aerodynamic Performance"), - dbc.Spinner(html.P(id="output"), color="primary",), - ] - ), + aerodynamic_performance_card("output"), ], width=3, ), - dbc.Col( - [ - # html.Div(id='display') - dbc.Spinner( - dcc.Graph(id="display", style={"height": "80vh"}), - color="primary", - ) - ], - width=True, - ), + figure_card("display"), ] ), html.Hr(), @@ -128,125 +68,28 @@ ) -def make_table(dataframe): - return dbc.Table.from_dataframe( - dataframe, bordered=True, hover=True, responsive=True, striped=True, style={} - ) - - -@app.callback( - [Output("display", "figure"), Output("output", "children")], - [ - Input("display_geometry", "n_clicks_timestamp"), - Input("run_ll_analysis", "n_clicks_timestamp"), - Input("run_vlm_analysis", "n_clicks_timestamp"), - ], - [State("n_booms", "value"), State("wing_span", "value"), State("alpha", "value"),], +@callback( + Output("display", "figure"), + Output("output", "children"), + Input("display_geometry", "n_clicks_timestamp"), + Input("run_ll_analysis", "n_clicks_timestamp"), + Input("run_vlm_analysis", "n_clicks_timestamp"), + State("n_booms", "value"), + State("wing_span", "value"), + State("alpha", "value"), ) -def display_geometry( - display_geometry, run_ll_analysis, run_vlm_analysis, n_booms, wing_span, alpha, +def run_display_geometry( + display_geometry, + run_ll_analysis, + run_vlm_analysis, + n_booms, + wing_span, + alpha, ): - ### Figure out which button was clicked - try: - button_pressed = np.argmax( - np.array( - [ - float(display_geometry), - float(run_ll_analysis), - float(run_vlm_analysis), - ] - ) - ) - assert button_pressed is not None - except: - button_pressed = 0 - - ### Make the airplane - airplane = make_airplane(n_booms=n_booms, wing_span=wing_span,) - op_point = asb.OperatingPoint(density=0.10, velocity=20, alpha=alpha,) - if button_pressed == 0: - # Display the geometry - figure = airplane.draw(show=False, colorbar_title=None) - output = "Please run an analysis to display the data." - elif button_pressed == 1: - # Run an analysis - opti = cas.Opti() # Initialize an analysis/optimization environment - ap = asb.Casll1( - airplane=airplane, op_point=op_point, opti=opti, run_setup=False - ) - ap.setup(verbose=False) - # Solver options - p_opts = {} - s_opts = {} - # s_opts["mu_strategy"] = "adaptive" - opti.solver("ipopt", p_opts, s_opts) - # Solve - try: - sol = opti.solve() - except RuntimeError: - sol = opti.debug - raise Exception("An error occurred!") - - figure = ap.draw(show=False) # Generates figure - - output = make_table( - pd.DataFrame( - { - "Figure": ["CL", "CD", "CDi", "CDp", "L/D"], - "Value": [ - sol.value(ap.CL), - sol.value(ap.CD), - sol.value(ap.CDi), - sol.value(ap.CDp), - sol.value(ap.CL / ap.CD), - ], - } - ) - ) - - elif button_pressed == 2: - # Run an analysis - opti = cas.Opti() # Initialize an analysis/optimization environment - ap = asb.Casvlm1( - airplane=airplane, op_point=op_point, opti=opti, run_setup=False - ) - ap.setup(verbose=False) - # Solver options - p_opts = {} - s_opts = {} - # s_opts["mu_strategy"] = "adaptive" - opti.solver("ipopt", p_opts, s_opts) - # Solve - try: - sol = opti.solve() - except RuntimeError: - sol = opti.debug - raise Exception("An error occurred!") - - figure = ap.draw(show=False) # Generates figure - - output = make_table( - pd.DataFrame( - { - "Figure": ["CL", "CDi", "L/Di"], - "Value": [ - sol.value(ap.CL), - sol.value(ap.CDi), - sol.value(ap.CL / ap.CDi), - ], - } - ) - ) - - figure.update_layout( - autosize=True, - # width=1000, - # height=700, - margin=dict(l=0, r=0, b=0, t=0,), + return analysis_and_display( + display_geometry, run_ll_analysis, run_vlm_analysis, n_booms, wing_span, alpha ) - return (figure, output) - try: # wrapping this, since a forum post said it may be deprecated at some point. app.title = "Aircraft Design with Dash" diff --git a/apps/dash-aerosandbox/assets/css/app.css b/apps/dash-aerosandbox/assets/css/app.css new file mode 100644 index 000000000..63fddf0bc --- /dev/null +++ b/apps/dash-aerosandbox/assets/css/app.css @@ -0,0 +1,63 @@ +/* Header */ +.header { + height: 10vh; + display: flex; + padding-left: 2%; + padding-right: 2%; + font-family: playfair display, sans-serif; + font-weight: bold; +} + +.header .header-title { + font-size: 5vh; +} + +.subheader-title { + font-size: 1.5vh; +} + +.header-logos { + margin-left: auto; +} + +.header-logos img { + margin-left: 3vh !important; + max-height: 5vh; +} + + +/* Demo button css */ +.demo-button { + font-size: 1.5vh; + font-family: Open Sans, sans-serif; + text-decoration: none; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 8px; + font-weight: 700; + -webkit-padding-start: 1rem; + padding-inline-start: 1rem; + -webkit-padding-end: 1rem; + padding-inline-end: 1rem; + color: #ffffff; + letter-spacing: 1.5px; + border: solid 1.5px transparent; + box-shadow: 2px 1000px 1px #0c0c0c inset; + background-image: linear-gradient(135deg, #7A76FF, #7A76FF, #7FE4FF); + -webkit-background-size: 200% 100%; + background-size: 200% 100%; + -webkit-background-position: 99%; + background-position: 99%; + background-origin: border-box; + transition: all .4s ease-in-out; + padding-top: 1vh; + padding-bottom: 1vh; + vertical-align: super; +} + +.demo-button:hover { + color: #7A76FF; + background-position: 0%; +} \ No newline at end of file diff --git a/apps/dash-aerosandbox/assets/screenshot.png b/apps/dash-aerosandbox/assets/github/screenshot.png similarity index 100% rename from apps/dash-aerosandbox/assets/screenshot.png rename to apps/dash-aerosandbox/assets/github/screenshot.png diff --git a/apps/dash-aerosandbox/assets/MIT-logo-red-gray-72x38.svg b/apps/dash-aerosandbox/assets/images/MIT-logo-red-gray-72x38.svg similarity index 100% rename from apps/dash-aerosandbox/assets/MIT-logo-red-gray-72x38.svg rename to apps/dash-aerosandbox/assets/images/MIT-logo-red-gray-72x38.svg diff --git a/apps/dash-aerosandbox/assets/favicon.ico b/apps/dash-aerosandbox/assets/images/favicon.ico similarity index 100% rename from apps/dash-aerosandbox/assets/favicon.ico rename to apps/dash-aerosandbox/assets/images/favicon.ico diff --git a/apps/dash-aerosandbox/assets/images/plotly-logo-light-theme.png b/apps/dash-aerosandbox/assets/images/plotly-logo-light-theme.png new file mode 100644 index 0000000000000000000000000000000000000000..4920c6e340b86d3b6ca4b5603e899020768ad352 GIT binary patch literal 30763 zcmZs?1yr2Pnl%cI2Z!M9?gR@O+}(m(aCZo9!CeyE-QC^Y8+W(h4!1dTzM22b+_kEq zo7L1?^2o0J>Tv?6+4j_sfg@T*pq$+J-9&zJf!jZAzqR%5A9dhKah#CXsZemF$~i0K2)ne;$reH z+I5_tUG6JQufQeei0y1dKzWG~dJj3qM-jy}(~49AUW0+=Up zpL=Dc*I85CTs@NvY8ERIv0;}-l`$v*kv_e)R(GkD#Xov@@2+^`kY!;v7tglgHY@dK zQ#+#K^4h4X+TYcmTow>ea3VX!lf=YC7mC#n9!xGH1&-}r_vHHr7B+~OgRI$qo{HF~ z%HLt}-P~TrWelDywle3)Ik68^A$jL8%a1baN#mvQzABGCp=LT+x^!DI{8@OP6Jsm; z1|S#w&$h`5C_unB1U}e|=q0hQBzb=2=w3Ja`>WQ6iPKKxm!Gh=9dDGtIOPSj~W{W-J4^Z<;3lT+T?iNzx7--D)%y!Kbu{N;>I zouctiM6Jy}bCLhCxgUF-2N`m0Slh3XqVwQ`c?X`MBB-T~-TmFxV^?;NiV zr@Q&IyN`4bsqcr*w&%dlUi&ieJ|;M~CxkpsPIEAs-fViYyq*0ouh(SP0s;gq1GYlJ zzGN(sHXdi!J+j-kIZ~8+F<@~2t8ocuEk|m1>+ut={OoNIDp74RUQ~De`FQpIDp{{x zdL{SrGiPbCvh5xVn-f>7TuaLxn|s+N^ZNc)wZ^r`swaVdDeL{>>Z~xFn}=K zWv7Kt)?&ZHYxzAM4%IjGN*lH6rQf%MI?Ds?b3>5`jx)L3�vNUu%(;ewduU+-%ln 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 literal 0 HcmV?d00001 diff --git a/apps/dash-aerosandbox/gitignore b/apps/dash-aerosandbox/gitignore new file mode 100644 index 000000000..90ecc9b06 --- /dev/null +++ b/apps/dash-aerosandbox/gitignore @@ -0,0 +1,191 @@ +# .gitignore specifies the files that shouldn't be included +# in version control and therefore shouldn't be included when +# deploying an application to Dash Enterprise +# This is a very exhaustive list! +# This list was based off of https://github.com/github/gitignore + +# Ignore data that is generated during the runtime of an application +# This folder is used by the "Large Data" sample applications +runtime_data/ +data/ + +# Omit SQLite databases that may be produced by dash-snapshots in development +*.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + + +# Jupyter Notebook + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + + +# macOS General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md + +# R Environment Variables +.Renviron + +# Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# SublineText +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings \ No newline at end of file diff --git a/apps/dash-aerosandbox/requirements.txt b/apps/dash-aerosandbox/requirements.txt index 17877d736..77500b120 100644 --- a/apps/dash-aerosandbox/requirements.txt +++ b/apps/dash-aerosandbox/requirements.txt @@ -1,12 +1,8 @@ aerosandbox==1.1.20 -plotly>= 4.6.0 -dash>= 1.9 -dash_core_components>=1.8.0 -dash_html_components>=1.0.2 -dash_bootstrap_components>=0.8.0 -numpy>=1.18 -casadi>=3.5 -gunicorn -pandas -seaborn +dash_bootstrap_components==1.1.0 +dash==2.4.1 +pandas==1.4.2 +gunicorn==20.1.0 +casadi==3.5.5 +seaborn==0.11.2 importlib-resources diff --git a/apps/dash-aerosandbox/runtime.txt b/apps/dash-aerosandbox/runtime.txt index 6919bf9ed..5fc4376c1 100644 --- a/apps/dash-aerosandbox/runtime.txt +++ b/apps/dash-aerosandbox/runtime.txt @@ -1 +1,2 @@ -python-3.7.6 +python-3.8.0 + diff --git a/apps/dash-aerosandbox/utils/components.py b/apps/dash-aerosandbox/utils/components.py new file mode 100644 index 000000000..c3654f40c --- /dev/null +++ b/apps/dash-aerosandbox/utils/components.py @@ -0,0 +1,127 @@ +from dash import html, dcc +import dash_bootstrap_components as dbc + + +def header( + app, header_color, header, subheader=None, header_background_color="transparent" +): + left_headers = html.Div( + [ + html.Div(header, className="header-title"), + html.Div(subheader, className="subheader-title"), + ], + style={"color": header_color}, + ) + + logo = html.Img(src=app.get_asset_url("assets/images/plotly-logo-light-theme.png")) + logo_link = html.A(logo, href="https://plotly.com/get-demo/", target="_blank") + demo_link = html.A( + "LEARN MORE", + href="https://plotly.com/dash/", + target="_blank", + className="demo-button", + ) + right_logos = html.Div([demo_link, logo_link], className="header-logos") + + return html.Div( + [left_headers, right_logos], + className="header", + style={"background-color": header_background_color}, + ) + + +def key_parameters_card(booms_id, wing_span_id, alpha_id): + return html.Div( + [ + html.H5("Key Parameters"), + html.P("Number of booms:"), + dcc.Slider( + id=booms_id, + min=1, + max=3, + step=1, + value=3, + marks={ + 1: "1", + 2: "2", + 3: "3", + }, + ), + html.P("Wing Span [m]:"), + dcc.Input(id=wing_span_id, value=43, type="number"), + html.P("Angle of Attack [deg]:"), + dcc.Input(id=alpha_id, value=7.0, type="number"), + ] + ) + + +def commands_card(display_geometry_id, run_ll_analysis_id, run_vlm_analysis_id): + return html.Div( + [ + html.H5("Commands"), + dbc.Button( + "Display (1s)", + id="display_geometry", + color="primary", + style={"margin": "5px"}, + n_clicks_timestamp="0", + ), + dbc.Button( + "LL Analysis (3s)", + id="run_ll_analysis", + color="secondary", + style={"margin": "5px"}, + n_clicks_timestamp="0", + ), + dbc.Button( + "VLM Analysis (15s)", + id="run_vlm_analysis", + color="secondary", + style={"margin": "5px"}, + n_clicks_timestamp="0", + ), + ] + ) + + +def aerodynamic_performance_card(output_id): + return html.Div( + [ + html.H5("Aerodynamic Performance"), + dbc.Spinner( + html.P(id=output_id), + color="primary", + ), + ] + ) + + +def figure_card(display_id): + return dbc.Col( + [ + # html.Div(id='display') + dbc.Spinner( + dcc.Graph(id=display_id, style={"height": "80vh"}), + color="primary", + ) + ], + width=True, + ) + + +# def footer_card(): +# return html.P( +# [ +# html.A( +# "Source code", +# href="https://github.com/peterdsharpe/AeroSandbox-Interactive-Demo", +# ), +# ". Aircraft design tools powered by ", +# html.A("AeroSandbox", href="https://peterdsharpe.github.com/AeroSandbox"), +# ". Build beautiful UIs for your scientific computing apps with ", +# html.A("Plot.ly ", href="https://plotly.com/"), +# "and ", +# html.A("Dash", href="https://plotly.com/dash/"), +# "!", +# ] +# ) diff --git a/apps/dash-aerosandbox/utils/figures.py b/apps/dash-aerosandbox/utils/figures.py new file mode 100644 index 000000000..8dd101ffc --- /dev/null +++ b/apps/dash-aerosandbox/utils/figures.py @@ -0,0 +1,132 @@ +import aerosandbox as asb +import dash_bootstrap_components as dbc +import numpy as np +import casadi as cas +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +import plotly.subplots as sub + +from utils.helper_functions import make_table, make_airplane + + +def analysis_and_display( + display_geometry, + run_ll_analysis, + run_vlm_analysis, + n_booms, + wing_span, + alpha, +): + ### Figure out which button was clicked + try: + button_pressed = np.argmax( + np.array( + [ + float(display_geometry), + float(run_ll_analysis), + float(run_vlm_analysis), + ] + ) + ) + assert button_pressed is not None + except: + button_pressed = 0 + + ### Make the airplane + airplane = make_airplane( + n_booms=n_booms, + wing_span=wing_span, + ) + op_point = asb.OperatingPoint( + density=0.10, + velocity=20, + alpha=alpha, + ) + if button_pressed == 0: + # Display the geometry + figure = airplane.draw(show=False, colorbar_title=None) + output = "Please run an analysis to display the data." + elif button_pressed == 1: + # Run an analysis + opti = cas.Opti() # Initialize an analysis/optimization environment + ap = asb.Casll1( + airplane=airplane, op_point=op_point, opti=opti, run_setup=False + ) + ap.setup(verbose=False) + # Solver options + p_opts = {} + s_opts = {} + # s_opts["mu_strategy"] = "adaptive" + opti.solver("ipopt", p_opts, s_opts) + # Solve + try: + sol = opti.solve() + except RuntimeError: + sol = opti.debug + raise Exception("An error occurred!") + + figure = ap.draw(show=False) # Generates figure + + output = make_table( + pd.DataFrame( + { + "Figure": ["CL", "CD", "CDi", "CDp", "L/D"], + "Value": [ + sol.value(ap.CL), + sol.value(ap.CD), + sol.value(ap.CDi), + sol.value(ap.CDp), + sol.value(ap.CL / ap.CD), + ], + } + ) + ) + + elif button_pressed == 2: + # Run an analysis + opti = cas.Opti() # Initialize an analysis/optimization environment + ap = asb.Casvlm1( + airplane=airplane, op_point=op_point, opti=opti, run_setup=False + ) + ap.setup(verbose=False) + # Solver options + p_opts = {} + s_opts = {} + # s_opts["mu_strategy"] = "adaptive" + opti.solver("ipopt", p_opts, s_opts) + # Solve + try: + sol = opti.solve() + except RuntimeError: + sol = opti.debug + raise Exception("An error occurred!") + + figure = ap.draw(show=False) # Generates figure + + output = make_table( + pd.DataFrame( + { + "Figure": ["CL", "CDi", "L/Di"], + "Value": [ + sol.value(ap.CL), + sol.value(ap.CDi), + sol.value(ap.CL / ap.CDi), + ], + } + ) + ) + + figure.update_layout( + autosize=True, + # width=1000, + # height=700, + margin=dict( + l=0, + r=0, + b=0, + t=0, + ), + ) + + return (figure, output) diff --git a/apps/dash-aerosandbox/airplane.py b/apps/dash-aerosandbox/utils/helper_functions.py similarity index 97% rename from apps/dash-aerosandbox/airplane.py rename to apps/dash-aerosandbox/utils/helper_functions.py index d3a82293d..26a60bb2c 100644 --- a/apps/dash-aerosandbox/airplane.py +++ b/apps/dash-aerosandbox/utils/helper_functions.py @@ -1,3 +1,5 @@ +import dash_bootstrap_components as dbc + import aerosandbox as asb from aerosandbox.library.airfoils import e216 import numpy as np @@ -7,8 +9,15 @@ naca0008 = asb.Airfoil("naca0008") +def make_table(dataframe): + return dbc.Table.from_dataframe( + dataframe, bordered=True, hover=True, responsive=True, striped=True, style={} + ) + + def make_airplane( - n_booms, wing_span, + n_booms, + wing_span, ): # n_booms = 3 diff --git a/apps/dash-aerosandbox/tests.py b/apps/dash-aerosandbox/utils/tests.py similarity index 73% rename from apps/dash-aerosandbox/tests.py rename to apps/dash-aerosandbox/utils/tests.py index 95ee4b41f..355576d9b 100644 --- a/apps/dash-aerosandbox/tests.py +++ b/apps/dash-aerosandbox/utils/tests.py @@ -1,13 +1,20 @@ import aerosandbox as asb import casadi as cas -from airplane import make_airplane +from utils.helper_functions import make_airplane n_booms = 1 wing_span = 40 alpha = 5 -airplane = make_airplane(n_booms=n_booms, wing_span=wing_span,) -op_point = asb.OperatingPoint(density=0.10, velocity=20, alpha=alpha,) +airplane = make_airplane( + n_booms=n_booms, + wing_span=wing_span, +) +op_point = asb.OperatingPoint( + density=0.10, + velocity=20, + alpha=alpha, +) ### LL # Run an analysis