|
| 1 | +"""Optimizes the section chord distribution of a two section symmetrical wing using the constraint-based approach for section |
| 2 | +joining. This example is referenced as part of the multi-section tutorial.""" |
| 3 | + |
| 4 | +# docs checkpoint 0 |
| 5 | +import numpy as np |
| 6 | +import openmdao.api as om |
| 7 | +from openaerostruct.geometry.geometry_group import MultiSecGeometry |
| 8 | +from openaerostruct.aerodynamics.aero_groups import AeroPoint |
| 9 | +from openaerostruct.geometry.geometry_group import build_sections |
| 10 | +from openaerostruct.geometry.geometry_unification import unify_mesh |
| 11 | +import matplotlib.pyplot as plt |
| 12 | + |
| 13 | + |
| 14 | +# docs checkpoint 1 |
| 15 | + |
| 16 | +# The multi-section geometry parameterization number section from left to right starting with section #0. A two-section symmetric wing parameterization appears as follows. |
| 17 | +# For a symmetrical wing the last section in the sequence will always be marked as the "root section" as it's adjacent to the geometric centerline of the wing. |
| 18 | +# Geometeric parameters must be specified for each section using lists with values corresponding in order of the surface numbering. Section section supports all the |
| 19 | +# standard OpenAeroStruct geometery transformations including B-splines. |
| 20 | + |
| 21 | + |
| 22 | +""" |
| 23 | +
|
| 24 | +----------------------------------------------- ^ |
| 25 | +| | | | |
| 26 | +| | | | |
| 27 | +| sec 0 | sec 1 | | root symmetrical BC |
| 28 | +| | "root section" | | chord |
| 29 | +|______________________|_______________________| | |
| 30 | + _ |
| 31 | + y = 0 ------------------> + y |
| 32 | +
|
| 33 | +""" |
| 34 | + |
| 35 | + |
| 36 | +# A multi-section surface dictionary is very similar to the standard one. However, it features some additional options and requires that the user specify |
| 37 | +# parameters for each desired section. The multi-section geometery group also features an built in mesh generator so the wing mesh parameters can be specified right |
| 38 | +# in the surface dictionary. Let's create a dictionary with info and options for a two-section aerodynamic lifting surface |
| 39 | + |
| 40 | +surface = { |
| 41 | + # Wing definition |
| 42 | + # Basic surface parameters |
| 43 | + "name": "surface", |
| 44 | + "is_multi_section": True, # This key must be present for the AeroPoint to correctly interpret this surface as multi-section |
| 45 | + "num_sections": 2, # The number of sections in the multi-section surface |
| 46 | + "sec_name": [ |
| 47 | + "sec0", |
| 48 | + "sec1", |
| 49 | + ], # names of the individual sections. Each section must be named and the list length must match the specified number of sections. |
| 50 | + "symmetry": True, # if true, model one half of wing. reflected across the midspan of the root section |
| 51 | + "S_ref_type": "wetted", # how we compute the wing area, can be 'wetted' or 'projected' |
| 52 | + # Geometry Parameters |
| 53 | + "taper": [1.0, 1.0], # Wing taper for each section. The list length must match the specified number of sections. |
| 54 | + "span": [2.0, 2.0], # Wing span for each section. The list length must match the specified number of sections. |
| 55 | + "sweep": [0.0, 0.0], # Wing sweep for each section. The list length must match the specified number of sections. |
| 56 | + "chord_cp": [ |
| 57 | + np.array([1, 1]), |
| 58 | + np.array([1, 1]), |
| 59 | + ], # The chord B-spline parameterization for EACH SECTION. The list length must match the specified number of sections. |
| 60 | + "twist_cp": [ |
| 61 | + np.zeros(2), |
| 62 | + np.zeros(2), |
| 63 | + ], # The twist B-spline parameterization for EACH SECTION. The list length must match the specified number of sections. |
| 64 | + "root_chord": 1.0, # Root chord length of the section indicated as "root section"(required if using the built-in mesh generator) |
| 65 | + # Mesh Parameters |
| 66 | + "meshes": "gen-meshes", # Supply a list of meshes for each section or "gen-meshes" for automatic mesh generation |
| 67 | + "nx": 2, # Number of chordwise points. Same for all sections.(required if using the built-in mesh generator) |
| 68 | + "ny": [ |
| 69 | + 21, |
| 70 | + 21, |
| 71 | + ], # Number of spanwise points for each section. The list length must match the specified number of sections. (required if using the built-in mesh generator) |
| 72 | + # Aerodynamic Parameters |
| 73 | + "CL0": 0.0, # CL of the surface at alpha=0 |
| 74 | + "CD0": 0.015, # CD of the surface at alpha=0 |
| 75 | + # Airfoil properties for viscous drag calculation |
| 76 | + "k_lam": 0.05, # percentage of chord with laminar |
| 77 | + # flow, used for viscous drag |
| 78 | + "c_max_t": 0.303, # chordwise location of maximum (NACA0015) |
| 79 | + # thickness |
| 80 | + "with_viscous": False, # if true, compute viscous drag |
| 81 | + "with_wave": False, # if true, compute wave drag |
| 82 | + "groundplane": False, |
| 83 | +} |
| 84 | + |
| 85 | +# docs checkpoint 2 |
| 86 | + |
| 87 | +# Create the OpenMDAO problem |
| 88 | +prob = om.Problem() |
| 89 | + |
| 90 | +# Create an independent variable component that will supply the flow |
| 91 | +# conditions to the problem. |
| 92 | +indep_var_comp = om.IndepVarComp() |
| 93 | +indep_var_comp.add_output("v", val=1.0, units="m/s") |
| 94 | +indep_var_comp.add_output("alpha", val=10.0, units="deg") |
| 95 | +indep_var_comp.add_output("Mach_number", val=0.3) |
| 96 | +indep_var_comp.add_output("re", val=1.0e5, units="1/m") |
| 97 | +indep_var_comp.add_output("rho", val=0.38, units="kg/m**3") |
| 98 | +indep_var_comp.add_output("cg", val=np.zeros((3)), units="m") |
| 99 | + |
| 100 | +# Add this IndepVarComp to the problem model |
| 101 | +prob.model.add_subsystem("prob_vars", indep_var_comp, promotes=["*"]) |
| 102 | + |
| 103 | +# docs checkpoint 3 |
| 104 | + |
| 105 | +# Instead of creating a standard geometery group, here we will create a multi-section group that will accept our multi-section surface |
| 106 | +# dictionary and allow us to specify any C0 continuity constraints between the sections. In this example we will constrain the sections |
| 107 | +# into a C0 continuous surface using a component that the optimizer can use as a constraint. The joining constraint component returns the |
| 108 | +# distance between the leading edge and trailing edge points at section interections. Any combination of the x,y, and z distances can be returned |
| 109 | +# to constrain the surface in a particular direction. |
| 110 | + |
| 111 | + |
| 112 | +""" |
| 113 | + LE1 LE2 cLE = [LE2x-LE1x,LE2y-LE1y,LE2z-LE1z] |
| 114 | +------------------------- <-------------> ------------------------- |
| 115 | +| | | | |
| 116 | +| | | | |
| 117 | +| sec 0 | | sec 1 | |
| 118 | +| | | "root section" | |
| 119 | +|______________________ | <-------------> |_______________________| |
| 120 | + TE1 TE2 cTE = [TE2x-TE1x,TE2y-TE1y,TE2z-TE1z] |
| 121 | +
|
| 122 | +
|
| 123 | +
|
| 124 | +""" |
| 125 | + |
| 126 | +# We pass in the multi-section surface dictionary to the MultiSecGeometry geometery group. We also enabled joining_comp and pass two array to dim_contr |
| 127 | +# These two arrays should only consists of 1 and 0 and tell the joining component which of the x,y, and z distance constraints we wish to enforce at the LE and TE |
| 128 | +# In this example, we only wish to constraint the x-distance between the sections at both the leading and trailing edge. |
| 129 | + |
| 130 | +multi_geom_group = MultiSecGeometry( |
| 131 | + surface=surface, joining_comp=True, dim_constr=[np.array([1, 0, 0]), np.array([1, 0, 0])] |
| 132 | +) |
| 133 | +prob.model.add_subsystem(surface["name"], multi_geom_group) |
| 134 | + |
| 135 | +# docs checkpoint 4 |
| 136 | + |
| 137 | +# In this next part, we will setup the aerodynamics group. First we use a utility function called build_sections which takes our multi-section surface dictionary and outputs a |
| 138 | +# surface dictionary for each individual section. We then inputs these dictionaries into the mesh unification function unify_mesh to produce a single mesh array for the the entire surface. |
| 139 | +# We then add this mesh to the multi-section surface dictionary |
| 140 | +section_surfaces = build_sections(surface) |
| 141 | +uniMesh = unify_mesh(section_surfaces) |
| 142 | +surface["mesh"] = uniMesh |
| 143 | + |
| 144 | +# Create the aero point group, which contains the actual aerodynamic |
| 145 | +# analyses. This step is exactly as it's normally done except the surface dictionary we pass in is the multi-surface one |
| 146 | +aero_group = AeroPoint(surfaces=[surface]) |
| 147 | +point_name = "aero_point_0" |
| 148 | +prob.model.add_subsystem(point_name, aero_group, promotes_inputs=["v", "alpha", "Mach_number", "re", "rho", "cg"]) |
| 149 | + |
| 150 | +# docs checkpoint 5 |
| 151 | + |
| 152 | +# The following steps are similar to a normal OAS surface script but note the differences in surface naming. Note that |
| 153 | +# unified surface created by the multi-section geometry group needs to be connected to AeroPoint(be careful with the naming) |
| 154 | + |
| 155 | +# Get name of surface and construct the name of the unified surface mesh |
| 156 | +name = surface["name"] |
| 157 | +unification_name = "{}_unification".format(surface["name"]) |
| 158 | + |
| 159 | +# Connect the mesh from the mesh unification component to the analysis point. |
| 160 | +prob.model.connect(name + "." + unification_name + "." + name + "_uni_mesh", point_name + "." + "surface" + ".def_mesh") |
| 161 | + |
| 162 | +# Perform the connections with the modified names within the |
| 163 | +# 'aero_states' group. |
| 164 | +prob.model.connect( |
| 165 | + name + "." + unification_name + "." + name + "_uni_mesh", point_name + ".aero_states." + "surface" + "_def_mesh" |
| 166 | +) |
| 167 | + |
| 168 | +# docs checkpoint 6 |
| 169 | + |
| 170 | +# Next, we add the DVs to the OpenMDAO problem. Note that each surface's geometeric parameters are under the given section names specified in the multi-surface dictionary earlier. |
| 171 | +# Here we use the chord B-spline that we specified earlier for each section and the angle-of-attack as DVs. |
| 172 | +prob.model.add_design_var("surface.sec0.chord_cp", lower=0.1, upper=10.0, units=None) |
| 173 | +prob.model.add_design_var("surface.sec1.chord_cp", lower=0.1, upper=10.0, units=None) |
| 174 | +prob.model.add_design_var("alpha", lower=0.0, upper=10.0, units="deg") |
| 175 | + |
| 176 | + |
| 177 | +# Next, we add the C0 continuity constraint for this problem by constraining the x-distance between sections to 0. |
| 178 | +# NOTE: SLSQP optimizer does not handle the joining equality constraint properly so the constraint needs to be specified as an inequality constraint |
| 179 | +# All other optimizers like SNOPT can handle the equality constraint as is. |
| 180 | + |
| 181 | +prob.model.add_constraint("surface.surface_joining.section_separation", upper=0, lower=0) # FOR SLSQP |
| 182 | +# prob.model.add_constraint('surface.surface_joining.section_separation',equals=0.0,scaler=1e-4) #FOR OTHER OPTIMIZERS |
| 183 | + |
| 184 | +# Add CL constraint |
| 185 | +prob.model.add_constraint(point_name + ".CL", equals=0.3) |
| 186 | + |
| 187 | +# Add Wing total area constraint |
| 188 | +prob.model.add_constraint(point_name + ".total_perf.S_ref_total", equals=2.0) |
| 189 | + |
| 190 | +# Add objective |
| 191 | +prob.model.add_objective(point_name + ".CD", scaler=1e4) |
| 192 | + |
| 193 | + |
| 194 | +prob.driver = om.ScipyOptimizeDriver() |
| 195 | +prob.driver.options["optimizer"] = "SLSQP" |
| 196 | +prob.driver.options["tol"] = 1e-3 |
| 197 | +prob.driver.options["disp"] = True |
| 198 | +prob.driver.options["maxiter"] = 1000 |
| 199 | +prob.driver.options["debug_print"] = ["nl_cons", "objs", "desvars"] |
| 200 | + |
| 201 | +# Set up and run the optimization problem |
| 202 | +prob.setup() |
| 203 | + |
| 204 | +# prob.run_model() |
| 205 | +prob.run_driver() |
| 206 | +# om.n2(prob) |
| 207 | + |
| 208 | +# docs checkpoint 7 |
| 209 | + |
| 210 | +# Get each section mesh |
| 211 | +mesh1 = prob.get_val("surface.sec0.mesh", units="m") |
| 212 | +mesh2 = prob.get_val("surface.sec1.mesh", units="m") |
| 213 | + |
| 214 | +# Get the unified mesh |
| 215 | +meshUni = prob.get_val(name + "." + unification_name + "." + name + "_uni_mesh") |
| 216 | + |
| 217 | + |
| 218 | +# Plot the results |
| 219 | +def plot_meshes(meshes): |
| 220 | + """this function plots to plot the mesh""" |
| 221 | + plt.figure(figsize=(8, 4)) |
| 222 | + for i, mesh in enumerate(meshes): |
| 223 | + mesh_x = mesh[:, :, 0] |
| 224 | + mesh_y = mesh[:, :, 1] |
| 225 | + color = "k" |
| 226 | + for i in range(mesh_x.shape[0]): |
| 227 | + plt.plot(mesh_y[i, :], 1 - mesh_x[i, :], color, lw=1) |
| 228 | + plt.plot(-mesh_y[i, :], 1 - mesh_x[i, :], color, lw=1) # plots the other side of symmetric wing |
| 229 | + for j in range(mesh_x.shape[1]): |
| 230 | + plt.plot(mesh_y[:, j], 1 - mesh_x[:, j], color, lw=1) |
| 231 | + plt.plot(-mesh_y[:, j], 1 - mesh_x[:, j], color, lw=1) # plots the other side of symmetric wing |
| 232 | + plt.axis("equal") |
| 233 | + plt.xlabel("y (m)") |
| 234 | + plt.ylabel("x (m)") |
| 235 | + plt.savefig("opt_planform_constraint.png") |
| 236 | + |
| 237 | + |
| 238 | +# plot_meshes([mesh1,mesh2]) |
| 239 | +plot_meshes([meshUni]) |
| 240 | +# plt.show() |
| 241 | +# docs checkpoint 8 |
0 commit comments