diff --git a/.github/workflows/example-dash.yml b/.github/workflows/example-dash.yml new file mode 100644 index 0000000..4370bf9 --- /dev/null +++ b/.github/workflows/example-dash.yml @@ -0,0 +1,66 @@ +name: Dash example workflow + +on: + push: + paths: + - "examples/dash/**" + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + type: choice + options: + - info + - warning + - debug + tags: + description: 'Manual run' + required: false + type: boolean + + +jobs: + push: + if: | + github.ref == 'refs/heads/main' && + github.repository == 'scilifelabdatacentre/serve-images' + runs-on: ubuntu-latest + concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + permissions: + contents: read + packages: write + + steps: + - name: 'Checkout github action' + uses: actions/checkout@main + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/scilifelabdatacentre/example-dash + tags: | + type=raw,value={{date 'YYMMDD-HHmm' tz='Europe/Stockholm'}} + + - name: 'Login to GHCR' + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{github.actor}} + password: ${{secrets.GITHUB_TOKEN}} + + - name: Publish image to GHCR + uses: docker/build-push-action@v3 + with: + file: ./examples/dash/Dockerfile + context: ./examples/dash + push: true + build-args: version=${{ github.ref_name }} + tags: | + ${{ steps.meta.outputs.tags }} + ghcr.io/scilifelabdatacentre/example-dash:latest + labels: ${{ steps.meta.outputs.labels }} diff --git a/examples/dash/Dockerfile b/examples/dash/Dockerfile new file mode 100644 index 0000000..f1d8b28 --- /dev/null +++ b/examples/dash/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.8-slim + +ENV USER=serve +ENV HOME=/home/$USER + +RUN apt-get update -yq \ + && useradd -m $USER \ + && pip install --upgrade --no-cache-dir pip \ + && rm -rf /var/lib/apt/lists/* + +COPY . $HOME/ + +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r $HOME/requirements.txt + +USER $USER +EXPOSE 8000 +WORKDIR $HOME + +ENTRYPOINT ["gunicorn", "app:server", "-b", "0.0.0.0:8000"] diff --git a/examples/dash/Procfile b/examples/dash/Procfile new file mode 100644 index 0000000..2c3403a --- /dev/null +++ b/examples/dash/Procfile @@ -0,0 +1 @@ +web: gunicorn app:server --workers 4 diff --git a/examples/dash/README.md b/examples/dash/README.md new file mode 100644 index 0000000..7f5cf55 --- /dev/null +++ b/examples/dash/README.md @@ -0,0 +1,9 @@ +# AeroSandbox-Interactive-Demo +by Peter Sharpe + +## Description +An interactive demo of AeroSandbox, powered by Dash! Work in progress. + +## Installation and Usage +1. Install all dependencies listed in `requirements.txt` - all packages are pip-installable. In particular, be sure to get a recent version of AeroSandbox (`pip install --upgrade aerosandbox`). +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. diff --git a/examples/dash/airplane.py b/examples/dash/airplane.py new file mode 100644 index 0000000..fc4e89f --- /dev/null +++ b/examples/dash/airplane.py @@ -0,0 +1,261 @@ +import aerosandbox as asb +from aerosandbox.library.airfoils import e216 +import numpy as np +import casadi as cas +import copy + +naca0008 = asb.Airfoil("naca0008") + + +def make_airplane( + n_booms, + wing_span, +): + # n_booms = 3 + + # wing + # wing_span = 37.126 + wing_root_chord = 2.316 + wing_x_quarter_chord = -0.1 + + # hstab + hstab_span = 2.867 + hstab_chord = 1.085 + hstab_twist_angle = -7 + + # vstab + vstab_span = 2.397 + vstab_chord = 1.134 + + # fuselage + boom_length = 6.181 + nose_length = 1.5 + fuse_diameter = 0.6 + boom_diameter = 0.2 + + wing = asb.Wing( + name="Main Wing", + # x_le=-0.05 * wing_root_chord, # Coordinates of the wing's leading edge # TODO make this a free parameter? + x_le=wing_x_quarter_chord, # Coordinates of the wing's leading edge # TODO make this a free parameter? + y_le=0, # Coordinates of the wing's leading edge + z_le=0, # Coordinates of the wing's leading edge + symmetric=True, + xsecs=[ # The wing's cross ("X") sections + asb.WingXSec( # Root + x_le=-wing_root_chord / 4, + # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + y_le=0, # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + z_le=0, # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + chord=wing_root_chord, + twist=0, # degrees + airfoil=e216, # Airfoils are blended between a given XSec and the next one. + control_surface_type="symmetric", + # Flap # Control surfaces are applied between a given XSec and the next one. + control_surface_deflection=0, # degrees + spanwise_panels=30, + ), + asb.WingXSec( # Tip + x_le=-wing_root_chord * 0.5 / 4, + y_le=wing_span / 2, + z_le=0, # wing_span / 2 * cas.pi / 180 * 5, + chord=wing_root_chord * 0.5, + twist=0, + airfoil=e216, + ), + ], + ) + hstab = asb.Wing( + name="Horizontal Stabilizer", + x_le=boom_length + - vstab_chord * 0.75 + - hstab_chord, # Coordinates of the wing's leading edge + y_le=0, # Coordinates of the wing's leading edge + z_le=0.1, # Coordinates of the wing's leading edge + symmetric=True, + xsecs=[ # The wing's cross ("X") sections + asb.WingXSec( # Root + x_le=0, # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + y_le=0, # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + z_le=0, # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + chord=hstab_chord, + twist=-3, # degrees # TODO fix + airfoil=naca0008, # Airfoils are blended between a given XSec and the next one. + control_surface_type="symmetric", + # Flap # Control surfaces are applied between a given XSec and the next one. + control_surface_deflection=0, # degrees + spanwise_panels=8, + ), + asb.WingXSec( # Tip + x_le=0, + y_le=hstab_span / 2, + z_le=0, + chord=hstab_chord, + twist=-3, # TODO fix + airfoil=naca0008, + ), + ], + ) + vstab = asb.Wing( + name="Vertical Stabilizer", + x_le=boom_length - vstab_chord * 0.75, # Coordinates of the wing's leading edge + y_le=0, # Coordinates of the wing's leading edge + z_le=-vstab_span / 2 + + vstab_span * 0.15, # Coordinates of the wing's leading edge + symmetric=False, + xsecs=[ # The wing's cross ("X") sections + asb.WingXSec( # Root + x_le=0, # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + y_le=0, # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + z_le=0, # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + chord=vstab_chord, + twist=0, # degrees + airfoil=naca0008, # Airfoils are blended between a given XSec and the next one. + control_surface_type="symmetric", + # Flap # Control surfaces are applied between a given XSec and the next one. + control_surface_deflection=0, # degrees + spanwise_panels=8, + ), + asb.WingXSec( # Tip + x_le=0, + y_le=0, + z_le=vstab_span, + chord=vstab_chord, + twist=0, + airfoil=naca0008, + ), + ], + ) + ### Build the fuselage geometry + blend = lambda x: (1 - np.cos(np.pi * x)) / 2 + fuse_x_c = [] + fuse_z_c = [] + fuse_radius = [] + fuse_resolution = 10 + # Nose geometry + fuse_nose_theta = np.linspace(0, np.pi / 2, fuse_resolution) + fuse_x_c.extend( + [ + (wing_x_quarter_chord - wing_root_chord / 4) - nose_length * np.cos(theta) + for theta in fuse_nose_theta + ] + ) + fuse_z_c.extend([-fuse_diameter / 2] * fuse_resolution) + fuse_radius.extend([fuse_diameter / 2 * np.sin(theta) for theta in fuse_nose_theta]) + # Taper + fuse_taper_x_nondim = np.linspace(0, 1, fuse_resolution) + fuse_x_c.extend( + [ + 0.0 * boom_length + (0.6 - 0.0) * boom_length * x_nd + for x_nd in fuse_taper_x_nondim + ] + ) + fuse_z_c.extend( + [ + -fuse_diameter / 2 * blend(1 - x_nd) - boom_diameter / 2 * blend(x_nd) + for x_nd in fuse_taper_x_nondim + ] + ) + fuse_radius.extend( + [ + fuse_diameter / 2 * blend(1 - x_nd) + boom_diameter / 2 * blend(x_nd) + for x_nd in fuse_taper_x_nondim + ] + ) + # Tail + # fuse_tail_x_nondim = np.linspace(0, 1, fuse_resolution)[1:] + # fuse_x_c.extend([ + # 0.9 * boom_length + (1 - 0.9) * boom_length * x_nd for x_nd in fuse_taper_x_nondim + # ]) + # fuse_z_c.extend([ + # -boom_diameter / 2 * blend(1 - x_nd) for x_nd in fuse_taper_x_nondim + # ]) + # fuse_radius.extend([ + # boom_diameter / 2 * blend(1 - x_nd) for x_nd in fuse_taper_x_nondim + # ]) + fuse_straight_resolution = 4 + fuse_x_c.extend( + [ + 0.6 * boom_length + (1 - 0.6) * boom_length * x_nd + for x_nd in np.linspace(0, 1, fuse_straight_resolution)[1:] + ] + ) + fuse_z_c.extend([-boom_diameter / 2] * (fuse_straight_resolution - 1)) + fuse_radius.extend([boom_diameter / 2] * (fuse_straight_resolution - 1)) + + fuse = asb.Fuselage( + name="Fuselage", + x_le=0, + y_le=0, + z_le=0, + xsecs=[ + asb.FuselageXSec(x_c=fuse_x_c[i], z_c=fuse_z_c[i], radius=fuse_radius[i]) + for i in range(len(fuse_x_c)) + ], + ) + + # Assemble the airplane + fuses = [] + hstabs = [] + vstabs = [] + if n_booms == 1: + fuses.append(fuse) + hstabs.append(hstab) + vstabs.append(vstab) + elif n_booms == 2: + boom_location = 0.40 # as a fraction of the half-span + + left_fuse = copy.deepcopy(fuse) + right_fuse = copy.deepcopy(fuse) + left_fuse.xyz_le += cas.vertcat(0, -wing_span / 2 * boom_location, 0) + right_fuse.xyz_le += cas.vertcat(0, wing_span / 2 * boom_location, 0) + fuses.extend([left_fuse, right_fuse]) + + left_hstab = copy.deepcopy(hstab) + right_hstab = copy.deepcopy(hstab) + left_hstab.xyz_le += cas.vertcat(0, -wing_span / 2 * boom_location, 0) + right_hstab.xyz_le += cas.vertcat(0, wing_span / 2 * boom_location, 0) + hstabs.extend([left_hstab, right_hstab]) + + left_vstab = copy.deepcopy(vstab) + right_vstab = copy.deepcopy(vstab) + left_vstab.xyz_le += cas.vertcat(0, -wing_span / 2 * boom_location, 0) + right_vstab.xyz_le += cas.vertcat(0, wing_span / 2 * boom_location, 0) + vstabs.extend([left_vstab, right_vstab]) + + elif n_booms == 3: + boom_location = 0.57 # as a fraction of the half-span + + left_fuse = copy.deepcopy(fuse) + center_fuse = copy.deepcopy(fuse) + right_fuse = copy.deepcopy(fuse) + left_fuse.xyz_le += cas.vertcat(0, -wing_span / 2 * boom_location, 0) + right_fuse.xyz_le += cas.vertcat(0, wing_span / 2 * boom_location, 0) + fuses.extend([left_fuse, center_fuse, right_fuse]) + + left_hstab = copy.deepcopy(hstab) + center_hstab = copy.deepcopy(hstab) + right_hstab = copy.deepcopy(hstab) + left_hstab.xyz_le += cas.vertcat(0, -wing_span / 2 * boom_location, 0) + right_hstab.xyz_le += cas.vertcat(0, wing_span / 2 * boom_location, 0) + hstabs.extend([left_hstab, center_hstab, right_hstab]) + + left_vstab = copy.deepcopy(vstab) + center_vstab = copy.deepcopy(vstab) + right_vstab = copy.deepcopy(vstab) + left_vstab.xyz_le += cas.vertcat(0, -wing_span / 2 * boom_location, 0) + right_vstab.xyz_le += cas.vertcat(0, wing_span / 2 * boom_location, 0) + vstabs.extend([left_vstab, center_vstab, right_vstab]) + + else: + raise ValueError("Bad value of n_booms!") + + airplane = asb.Airplane( + name="Solar1", + x_ref=0, + y_ref=0, + z_ref=0, + wings=[wing] + hstabs + vstabs, + fuselages=fuses, + ) + + return airplane diff --git a/examples/dash/app.py b/examples/dash/app.py new file mode 100644 index 0000000..7b781cc --- /dev/null +++ b/examples/dash/app.py @@ -0,0 +1,286 @@ +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 +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 + +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", + ), + 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"), + ] + ), + 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", + ), + ] + ), + html.Hr(), + html.Div( + [ + html.H5("Aerodynamic Performance"), + dbc.Spinner( + html.P(id="output"), + color="primary", + ), + ] + ), + ], + width=3, + ), + dbc.Col( + [ + # html.Div(id='display') + dbc.Spinner( + dcc.Graph(id="display", style={"height": "80vh"}), + color="primary", + ) + ], + width=True, + ), + ] + ), + html.Hr(), + 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/"), + "!", + ] + ), + ], + fluid=True, +) + + +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"), + ], +) +def 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 (figure, output) + + +try: # wrapping this, since a forum post said it may be deprecated at some point. + app.title = "Aircraft Design with Dash" +except: + print("Could not set the page title!") + + +if __name__ == "__main__": + app.run_server(debug=False) diff --git a/examples/dash/requirements.txt b/examples/dash/requirements.txt new file mode 100644 index 0000000..65f79f5 --- /dev/null +++ b/examples/dash/requirements.txt @@ -0,0 +1,13 @@ +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 +importlib-resources + diff --git a/examples/dash/tests.py b/examples/dash/tests.py new file mode 100644 index 0000000..33e1f1c --- /dev/null +++ b/examples/dash/tests.py @@ -0,0 +1,37 @@ +import aerosandbox as asb +import casadi as cas +from airplane 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, +) + +### LL +# Run an analysis +opti = cas.Opti() # Initialize an analysis/optimization environment +# airplane.fuselages=[] +ap = asb.Casvlm1(airplane=airplane, op_point=op_point, opti=opti) +# 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!") +# Postprocess +# ap.substitute_solution(sol) +ap.draw(show=False).show() diff --git a/serve-filemanager/branding/custom.css b/serve-filemanager/branding/custom.css index 8cca3e1..677a9bd 100644 --- a/serve-filemanager/branding/custom.css +++ b/serve-filemanager/branding/custom.css @@ -11,4 +11,8 @@ .button:hover { background-color: var(--serve-lime); box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), .15) !important; -} \ No newline at end of file +} + +.credits { + display: none; +}