Skip to content

Commit f79f9cc

Browse files
sabakhshikanekoshSafa Bakhshi
authored
Fixed incorrect surface moment derivatives and added a moment output to the component (#449)
* Moment deriv comments refactor * Refactor Moment derivatives. Add comments to explain process. Fix bugs related derivatives wrt reference area, chords, and panel width. Add output for moment with derivatives * Fixed a derivative. Black and Flake8 * Revert test changes. Fix comment errors. Minor derivative fix. * Restore complex step tests. Fix incorrect S_ref derivative accumulation for non wing surfaces * Fix area derivative one more time * Add new test to properly check moment derivatives * Removed debugging lines * Removed comments indicating that there may be problem with the moment derivativesas they have now been resolved. Don't want to confuse people in the future. * merged tests * Update multiple_surfaces.rst * Fix comment grammar and bump version --------- Co-authored-by: Shugo Kaneko <49300827+kanekosh@users.noreply.github.com> Co-authored-by: Safa Bakhshi <safa@Safas-MacBook-Pro.local>
1 parent 2802be2 commit f79f9cc

4 files changed

Lines changed: 103 additions & 21 deletions

File tree

openaerostruct/__init__.py

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

openaerostruct/docs/advanced_features/multiple_surfaces.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Multiple Lifting Surfaces
55

66
It's easily possible to simulate multiple lifting surfaces simultaneously in OpenAeroStruct.
77
The most straightforward example is a wing and a tail for a conventional airplane, as shown below, though OpenAeroStruct can handle any arbitrary collection of lifting surfaces.
8+
Note that the moment coefficient is always normalized with respect to the MAC of the first surface in the list; therefore, the main lifting surface (i.e., wing) should always be the first entry in the list.
89

910
.. literalinclude:: /../../tests/integration_tests/test_multiple_aero_analysis.py
1011
:start-after: docs checkpoint 0

openaerostruct/functionals/moment_coefficient.py

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
class MomentCoefficient(om.ExplicitComponent):
77
"""
8-
Compute the coefficient of moment (CM) for the entire aircraft.
8+
Compute the coefficient of moment (CM) and moment (M) for the entire aircraft.
99
1010
Parameters
1111
----------
@@ -34,6 +34,8 @@ class MomentCoefficient(om.ExplicitComponent):
3434
3535
Returns
3636
-------
37+
M[3] : numpy array
38+
The moment around the x-, y-, and z-axes at the cg point.
3739
CM[3] : numpy array
3840
The coefficient of moment around the x-, y-, and z-axes at the cg point.
3941
"""
@@ -59,6 +61,7 @@ def setup(self):
5961
self.add_input("S_ref_total", val=1.0, units="m**2", tags=["mphys_input"])
6062

6163
self.add_output("CM", val=np.ones((3)), tags=["mphys_result"])
64+
self.add_output("M", val=np.ones((3)), units="N*m", tags=["mphys_result"])
6265

6366
self.declare_partials(of="*", wrt="*")
6467

@@ -112,12 +115,15 @@ def compute(self, inputs, outputs):
112115
# normalize CM
113116
if j == 0:
114117
self.MAC_wing = MAC
118+
self.S_ref_wing = S_ref
115119

116120
self.M = M
117121

122+
# Output the moment vector
123+
outputs["M"] = M
124+
118125
# Compute the normalized CM
119-
rho = inputs["rho"]
120-
outputs["CM"] = M / (0.5 * rho * inputs["v"] ** 2 * inputs["S_ref_total"] * self.MAC_wing)
126+
outputs["CM"] = M / (0.5 * inputs["rho"] * inputs["v"] ** 2 * inputs["S_ref_total"] * self.MAC_wing)
121127

122128
def compute_partials(self, inputs, partials):
123129
cg = inputs["cg"]
@@ -128,12 +134,14 @@ def compute_partials(self, inputs, partials):
128134
# Cached values
129135
M = self.M
130136
MAC_wing = self.MAC_wing
137+
S_ref_wing = self.S_ref_wing
131138

139+
# Scaling factor of one over the dynamic pressure times sum of reference areas times the wing MAC
132140
fact = 1.0 / (0.5 * rho * v**2 * S_ref_total * MAC_wing)
133141

134-
partials["CM", "rho"] = -M * fact**2 * 0.5 * v**2 * S_ref_total * MAC_wing
135-
partials["CM", "v"] = -M * fact**2 * rho * v * S_ref_total * MAC_wing
136-
partials["CM", "S_ref_total"] = -M * fact**2 * 0.5 * rho * v**2 * MAC_wing
142+
partials["CM", "rho"] = -M * fact / rho
143+
partials["CM", "v"] = -2 * M * fact / v
144+
partials["CM", "S_ref_total"] = -M * fact / S_ref_total
137145

138146
partials["CM", "cg"][:] = 0.0
139147

@@ -156,7 +164,8 @@ def compute_partials(self, inputs, partials):
156164
panel_chords = (chords[1:] + chords[:-1]) * 0.5
157165
MAC = 1.0 / S_ref * np.sum(panel_chords**2 * widths)
158166

159-
# This transformation is used for multiple derivatives
167+
# This produces a bi-diagonal matrix for the derivative of panel_chords with respect to chords
168+
# This transformation matrix is further used for multiple derivatives later
160169
dpc_dc = np.zeros((ny - 1, ny))
161170
idx = np.arange(ny - 1)
162171
dpc_dc[idx, idx] = 0.5
@@ -174,34 +183,72 @@ def compute_partials(self, inputs, partials):
174183
dMAC_dw *= 2.0
175184
dMAC_dS *= 2.0
176185

177-
# diff derivs
186+
# Compute the bound vortex(quarter chord) points at mid-panel
178187
pts = (b_pts[:, 1:, :] + b_pts[:, :-1, :]) * 0.5
188+
189+
# Compute the vectors between the cg and the mid-panel bound vortex points
179190
diff = pts - cg
180191

192+
# Compute the cross product of the panel bound vortex vectors from cg and the panel forces
181193
c = np.cross(diff, sec_forces, axis=2)
194+
195+
# Compute the spanwise moment vector distribution by summing over each resulting column
182196
moment = np.sum(c, axis=0)
183197

198+
# Compute the derviative of the moment vectors(c) with respect to the diff vectors(a) multiplied by -1
184199
dcda = np.zeros((3, nx - 1, ny - 1, 3))
200+
201+
# Compute the derivative wrt to the first element of diff
185202
dcda[0, :, :, 1] = sec_forces[:, :, 2]
186203
dcda[0, :, :, 2] = -sec_forces[:, :, 1]
204+
205+
# Compute the derivative wrt to the second element of diff
187206
dcda[1, :, :, 0] = -sec_forces[:, :, 2]
188207
dcda[1, :, :, 2] = sec_forces[:, :, 0]
208+
209+
# Compute the derivative wrt to the third element of diff
189210
dcda[2, :, :, 0] = sec_forces[:, :, 1]
190211
dcda[2, :, :, 1] = -sec_forces[:, :, 0]
191212

213+
# Compute the derviative of the moment vectors(c) with respect to the sec_forces vectors(b) multiplied by -1
192214
dcdb = np.zeros((3, nx - 1, ny - 1, 3))
215+
216+
# Compute the derivative wrt to the first element of sec_forces
193217
dcdb[0, :, :, 1] = -diff[:, :, 2]
194218
dcdb[0, :, :, 2] = diff[:, :, 1]
219+
220+
# Compute the derivative wrt to the second element of sec_forces
195221
dcdb[1, :, :, 0] = diff[:, :, 2]
196222
dcdb[1, :, :, 2] = -diff[:, :, 0]
223+
224+
# Compute the derivative wrt to the third element of sec_forces
197225
dcdb[2, :, :, 0] = -diff[:, :, 1]
198226
dcdb[2, :, :, 1] = diff[:, :, 0]
199227

228+
# Compute derivative of CM wrt to the sec_forces of the section by reshaping to 3 rows and multiplying by fact.
200229
partials["CM", name + "_sec_forces"] += dcdb.reshape((3, 3 * (nx - 1) * (ny - 1))) * fact
201230

231+
# Compute derivative of M wrt to the sec_forces of the section by reshaping to 3 rows
232+
partials["M", name + "_sec_forces"] += dcdb.reshape((3, 3 * (nx - 1) * (ny - 1)))
233+
234+
# Project the derviative of the moment vectors(c) with respect to the diff vectors(a)
235+
# onto the derivative of mid-panel chord distribution wrt to the chord distribution giving the derivative of
236+
# the moment vectors(c) with respect to the chord distribution(dc_dchord). This works because the diff
237+
# vectors are difference between the mid-panel bound vortex(quarter chord) points and cg which is static in this derivative.
238+
# The spanwise component of the mid-panel bound vortex(quarter chord) points have the same derivatrive wrt to the chord
239+
# distribution as the mid-panel chord distribution does wrt the the chord distribution.
202240
dc_dchord = np.einsum("ijkl,km->ijml", dcda, dpc_dc)
241+
242+
# Compute the derivative of CM wrt to the bound vortex points(b_pts) by reshaping dc_dchord to three rows
243+
# and multiplying by fact.
203244
partials["CM", name + "_b_pts"] += dc_dchord.reshape((3, 3 * (nx - 1) * ny)) * fact
204245

246+
# Compute the derivative of M wrt to the bound vortex points(b_pts) by reshaping dc_dchord to three rows
247+
partials["M", name + "_b_pts"] += dc_dchord.reshape((3, 3 * (nx - 1) * ny))
248+
249+
# Reduce the derivative of the moment vectors(c) with respect to the diff vectors(a) by summing over all
250+
# chordwise and spanwise panels(j and k). Reduces to a 3x3 matrix for the whole surface by summing over all
251+
# panels.
205252
dcda = np.einsum("ijkl->il", dcda)
206253

207254
# If the surface is symmetric, set the x- and z-direction moments
@@ -216,28 +263,62 @@ def compute_partials(self, inputs, partials):
216263
partials["CM", name + "_b_pts"][0, :] = 0.0
217264
partials["CM", name + "_b_pts"][1, :] *= 2.0
218265
partials["CM", name + "_b_pts"][2, :] = 0.0
266+
partials["M", name + "_sec_forces"][0, :] = 0.0
267+
partials["M", name + "_sec_forces"][1, :] *= 2.0
268+
partials["M", name + "_sec_forces"][2, :] = 0.0
269+
partials["M", name + "_b_pts"][0, :] = 0.0
270+
partials["M", name + "_b_pts"][1, :] *= 2.0
271+
partials["M", name + "_b_pts"][2, :] = 0.0
219272
dcda[0, :] = 0.0
220273
dcda[1, :] *= 2.0
221274
dcda[2, :] = 0.0
222275

276+
# Compute the derivative of CM wrt to the cg position which is negative dcda since diff = pts - cg times fact
277+
# Accumlate the derivative over each surface as the total moment vector is sum over all surfaces.
223278
partials["CM", "cg"] -= dcda * fact
224279

280+
# Compute the derivative of M wrt to the cg position which is negative dcda since diff = pts - cg
281+
# Accumlate the derivative over each surface as the total moment vector is sum over all surfaces.
282+
partials["M", "cg"] -= dcda
283+
284+
# Compute the total surface moment vector by summing spanwise
225285
M_j = np.sum(moment, axis=0)
226-
term = fact / MAC
227-
partials["CM", name + "_chords"] = -np.outer(M_j * term, dMAC_dc)
228-
partials["CM", name + "_widths"] = -np.outer(M_j * term, dMAC_dw)
229-
partials["CM", name + "_S_ref"] = -np.outer(M_j, dMAC_dS * term)
230286

231287
# For first surface, we need to save the deriv results
232288
if j == 0:
289+
# Compute a term by dividing fact by MAC. Note that MAC is the mean aerodynamic chord for the surface and
290+
# the MAC_wing terms already factored into fact is of the main wing surface
291+
term = fact / MAC
292+
293+
# Compute the derivative of CM wrt to the chord distribution by taking the negative outer product of the
294+
# moment vector(M_j) time the term with the derivative of MAC wrt to the chord distribution. We only do
295+
# this for the main wing since CM only depends on the MAC of the main wing and the chord distribution of
296+
# the main wing is the only chord distribution of all the surfaces that can impact the MAC of the main wing.
297+
partials["CM", name + "_chords"] = -np.outer(M_j * term, dMAC_dc)
298+
299+
# Compute the derivative of CM wrt to the width distribution by taking the negative outer product of the
300+
# moment vector(M_j) time the term with the derivative of MAC wrt to the width distribution. We only do
301+
# this for the main wing since CM only depends on the MAC of the main wing and the panel width distribution of
302+
# the main wing is the only panel width distribution of all the surfaces that can impact the MAC of the main wing.
303+
partials["CM", name + "_widths"] = -np.outer(M_j * term, dMAC_dw)
304+
305+
# Compute the derivative of CM wrt to the surface S_ref by taking the negative outer product of the
306+
# moment vector(M_j) time the term with the derivative of MAC wrt to the surface S_ref. The CM depends on
307+
# the total references area of all surfaces including the main wing and the MAC of them main wing itself
308+
# As result, this derivative has two parts only for the main wing.
309+
# partials["CM", name + "_S_ref"] = -np.outer(M_j, dMAC_dS * term)
310+
partials["CM", name + "_S_ref"] = np.outer(M_j * fact, (1 / S_ref))
311+
312+
# Cache the main wing's MAC derivatives
233313
base_name = name
234314
base_dMAC_dc = dMAC_dc
235315
base_dMAC_dw = dMAC_dw
236-
base_dMAC_dS = dMAC_dS
237-
316+
# base_dMAC_dS = dMAC_dS
238317
else:
239318
# Apply this surface's portion of the moment to the MAC_wing term.
240-
term = fact / (MAC_wing * MAC)
319+
# We need to do this because CM is normalized by the MAC of the main wing
320+
term = fact / MAC_wing
241321
partials["CM", base_name + "_chords"] -= np.outer(M_j * term, base_dMAC_dc)
242322
partials["CM", base_name + "_widths"] -= np.outer(M_j * term, base_dMAC_dw)
243-
partials["CM", base_name + "_S_ref"] -= np.outer(M_j, base_dMAC_dS * term)
323+
# partials["CM", base_name + "_S_ref"] -= np.outer(M_j, base_dMAC_dS * term)
324+
partials["CM", base_name + "_S_ref"] += np.outer(M_j * fact, (1 / S_ref_wing))

tests/functionals_tests/test_moment_coefficient.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ def test(self):
1616

1717
comp = MomentCoefficient(surfaces=surfaces)
1818

19-
run_test(self, comp)
19+
run_test(self, comp, complex_flag=True, method="cs")
2020

21-
# This is known to have some issues for sufficiently small values of S_ref_total
22-
# There is probably a derivative bug somewhere in the moment_coefficient.py calcs
2321
def test2(self):
2422
surfaces = get_default_surfaces()
2523

@@ -30,13 +28,15 @@ def test2(self):
3028
indep_var_comp = om.IndepVarComp()
3129

3230
indep_var_comp.add_output("S_ref_total", val=1e4, units="m**2")
31+
indep_var_comp.add_output("cg", val=np.array([-10.0, 10.0, -10.0]), units="m")
3332

3433
group.add_subsystem("moment_calc", comp)
3534
group.add_subsystem("indep_var_comp", indep_var_comp)
3635

3736
group.connect("indep_var_comp.S_ref_total", "moment_calc.S_ref_total")
37+
group.connect("indep_var_comp.cg", "moment_calc.cg")
3838

39-
run_test(self, group)
39+
run_test(self, group, complex_flag=True, method="cs")
4040

4141

4242
if __name__ == "__main__":

0 commit comments

Comments
 (0)