Skip to content

Commit 4332e6e

Browse files
authored
Implemented multi-section surface parameterization (#446)
* Feature: Start port of geomEngine3 from Matlab to OSA.WIP * Implemented mesh generator for multisection wings * Added test code for plotting mesh * Fixed bugs related to chordwise panelling not working properly at wing root. Started proper multi-section surface implementation. * Complete support for multi-section wings * Clean-up plotting functions * Removed stray files * Fixed plotting bugs * Completed mesh plotting code * Add OAS mesh output * Start separating code into helper functions * Continue work on helper functions. * Code clean-up * Main mesh generator functions implemented and commented * Complete the initial implementation of the user custom mesh utility * Code cleanup * Fix flake8 and black versions used * Fix remaining formatting issues * Start new Geometry group implementation * Add support for sectional mesh generation * Enabled basic section adding functionality * Implemnented multi section geom group * First set of mesh bug fixes * Fixed implementation of mesh generation and multi section mesh geometry group. * Implemented basic continuity constraint * Implemented support for Aero Point on multisection. Requires a vortex mesh bug fix to function properly with symmetry. * Introduced the multi-section geometry group and associated components. * Refactored code, asymmetry support, and C0 by constuction support * Clean-up and added comments * Fixed t_over_c b-splint for multi section * Added tests in temp location * Cleaned up section dictionary handling * Improved dictionary handling. Added support for multi-surface + multi-section * Added multi-section geometry unit tests * Regression tests implemented * aero group updates * Remove rouge print statements and remove accidental use of finite difference partials for mesh unification * Added docs * remove personal test files * Corrected tests location * Formatting updates and reorganization of tests * Fix one more formatting issue * Added single and multisection comparison test * More black and flake8 fixes * Should fix the black and flake8 * Flake8 fixes * Fixed doc navigation * Formatting changes for change request * Update test initial design * Reworked the constraint vs construction test * Cleaned up leftover stuff * Fix flake8 * Disable SNOPT test for now * Fix failing test * SNOPT ON again * Go back to SLSQP * Reorganized test to help it pass * Try to fix test again * Fix formatting * Removed SNOPT test * Update test util names * Start moving test surfaces to testing utils functions * Reorganized and reworked all tests * Update multi-section docs and examples * Flake8 fixes * Merge check surface keys into one function * reformat function and variable names for PEP8 * Update surface keys user reference * Cleaned up test in response to feedback * Fix unit tests * Bump minor version
1 parent 87edebd commit 4332e6e

21 files changed

Lines changed: 2780 additions & 1 deletion

openaerostruct/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.8.0"
1+
__version__ = "2.9.0"

openaerostruct/aerodynamics/aero_groups.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,37 @@ def setup(self):
3030
surfaces = self.options["surfaces"]
3131
rotational = self.options["rotational"]
3232

33+
# Check for multi-section surfaces and create suitable surface dictionaries for them
34+
for i, surface in enumerate(surfaces):
35+
# If multisection mesh then build a single surface with the unified mesh data
36+
if "is_multi_section" in surface.keys():
37+
import copy
38+
39+
target_keys = [
40+
# Essential Info
41+
"name",
42+
"symmetry",
43+
"S_ref_type",
44+
"ref_axis_pos",
45+
"mesh",
46+
# aerodynamics
47+
"CL0",
48+
"CD0",
49+
"with_viscous",
50+
"with_wave",
51+
"groundplane",
52+
"k_lam",
53+
"t_over_c_cp",
54+
"c_max_t",
55+
]
56+
57+
# Constructs a surface dictionary and adds the specified supported keys and values from the mult-section surface dictionary.
58+
aeroSurface = {}
59+
for k in set(surface).intersection(target_keys):
60+
aeroSurface[k] = surface[k]
61+
# print(aeroSurface["name"])
62+
surfaces[i] = copy.deepcopy(aeroSurface)
63+
3364
# Loop through each surface and connect relevant parameters
3465
for surface in surfaces:
3566
name = surface["name"]

openaerostruct/docs/advanced_features.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ These examples show some advanced features in OpenAeroStruct.
1212
advanced_features/geometry_manipulation.rst
1313
advanced_features/custom_mesh_example.rst
1414
advanced_features/openvsp_mesh_example.rst
15+
advanced_features/multi_section_surfaces.rst
1516
advanced_features/multiple_surfaces.rst
1617
advanced_features/multipoint.rst
1718
advanced_features/multipoint_parallel.rst
98.1 KB
Loading
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
.. _Custom_Mesh:
2+
3+
Multi-section Surfaces
4+
==========================
5+
6+
OpenAeroStruct features the ability to specify surfaces as a series of sequentially connected sections.
7+
Rather than controling the geometry of the surface as a whole, the optimizer can control the geometric parameters of each section individually using any of the geometric transformations available in OpenAeroStruct.
8+
This feature was developed for the modular wing morphing applications but can be useful in situations where the user wishes to optimize a particular wing section while leaving the others fixed.
9+
10+
*Please note that aerostructural optimization with multi-section wings is currently not supported*
11+
12+
This example script demonstrate the usage of the multi-section wing geometry features in OpenAeroStruct.
13+
We first start with the induced drag minimization of a simple two-section symmetrical wing.
14+
15+
Let's start with the necessary imports.
16+
17+
.. literalinclude:: /advanced_features/scripts/basic_2_sec.py
18+
:start-after: checkpoint 0
19+
:end-before: checkpoint 1
20+
21+
22+
Then we are ready to discuss the multi-section parameterization and multi-section surface dictionary.
23+
24+
.. literalinclude:: /advanced_features/scripts/basic_2_sec.py
25+
:start-after: checkpoint 1
26+
:end-before: checkpoint 2
27+
28+
Next, we setup the flow variables.
29+
30+
.. literalinclude:: /advanced_features/scripts/basic_2_sec.py
31+
:start-after: checkpoint 2
32+
:end-before: checkpoint 3
33+
34+
35+
Giving the optimizer control of each wing section without any measure to constrain its motion can lead to undesirable geometries, such as wing sections separating from each other or large kinks appearing along the span, that cause numerical deficiencies in the optimization.
36+
This concern is addressed by enforcing C0 continuity along the span-wise junctions between the sections.
37+
C0 continuity can be enforced for any geometric parameter OAS controls for a given section.
38+
For example, the tip chord or twist of a given section should match the root chord or twist of the subsequent section.
39+
There are two approaches for enforcing C0 continuity between sections: a constraint-based approach and a construction-based approach.
40+
41+
The constaint-based approach involves explicitly setting a position constraint that joins the surface points at section junctions to a specified tolerance.
42+
This constraint is useful when varying section parameters like span or dihedral, where entire sections could collide or separate if C0 continuity is not strictly enforced.
43+
A fully differentiated implementation that facilitates setting this constraint is incorporated into OAS. This approach is robust but introduces at least two linear constraints per segment junction.
44+
The number of linear constraints can quickly grow for problems with many sections that this study anticipates for morphing applications.
45+
In cases where geometric parameters can be specified as a function of span, however, it is possible to eliminate these additional constraints and maintain C0 continuity using the construction-based approach.
46+
47+
.. literalinclude:: /advanced_features/scripts/basic_2_sec.py
48+
:start-after: checkpoint 3
49+
:end-before: checkpoint 4
50+
51+
52+
The construction-based approach forgoes the constraint entirely.
53+
Continuity by construction is enforced by assigning the B-spline control point located at section edges to the same independent variable controlled by the optimizer.
54+
Enforcing C0 continuity by construction solely applies to geometric parameters that employ B-spline parametrization in OAS.
55+
These include chord distribution, twist distribution, shear distribution in all three directions, and thickness and radius distributions for structural spars.
56+
Geometric transformations parameterized by a single value, such as sweep, taper, and dihedral, cannot be used with C0 continuity enforcement by construction.
57+
58+
59+
.. literalinclude:: /advanced_features/scripts/basic_2_sec_construction.py
60+
:start-after: checkpoint 0
61+
:end-before: checkpoint 1
62+
63+
.. literalinclude:: /advanced_features/scripts/basic_2_sec_construction.py
64+
:start-after: checkpoint 2
65+
:end-before: checkpoint 3
66+
67+
We can now create the aerodynamic analysis group.
68+
69+
.. literalinclude:: /advanced_features/scripts/basic_2_sec.py
70+
:start-after: checkpoint 4
71+
:end-before: checkpoint 5
72+
73+
Connecting the geometry and analysis groups requires care when using the multi-section parameterization.
74+
While the multi-section geometry group is effectively a drop-in replacement for the standard geometery group for multi-section wings, it does require that the unified mesh component be connected to the AeroPoint analysis.
75+
76+
.. literalinclude:: /advanced_features/scripts/basic_2_sec.py
77+
:start-after: checkpoint 5
78+
:end-before: checkpoint 6
79+
80+
*The following section applies to user considering cases with thickess to chord B-splines and viscous effects.*
81+
82+
When using a thickness to chord ratio B-spline to account for viscous effects the user should be careful to connect the unified thickess to chord ratio B-spline automatically generated by the multi-section geometery group.
83+
84+
.. literalinclude:: /advanced_features/scripts/basic_2_sec_visc.py
85+
:start-after: checkpoint 0
86+
:end-before: checkpoint 1
87+
88+
We can now setup our optimization problem and run it.
89+
90+
.. literalinclude:: /advanced_features/scripts/basic_2_sec.py
91+
:start-after: checkpoint 6
92+
:end-before: checkpoint 7
93+
94+
We then finish by plotting the result.
95+
96+
.. literalinclude:: /advanced_features/scripts/basic_2_sec.py
97+
:start-after: checkpoint 7
98+
:end-before: checkpoint 8
99+
100+
101+
102+
103+
104+
The following shows the resulting optimized mesh.
105+
The result is identical regardless of if the constraint-based or construction-based joining approahces are used.
106+
107+
.. image:: /advanced_features/figs/multi_section_2_sym.png
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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

Comments
 (0)