Skip to content

Commit

Permalink
Precise thickness and volume constraints for non-trapezoidal wings (#127
Browse files Browse the repository at this point in the history
)

* added more precise thickness and volume constraints

* added tests

* clarify nSpan variable
  • Loading branch information
sseraj authored Mar 24, 2022
1 parent 2fb4fcf commit 5057ede
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 27 deletions.
113 changes: 86 additions & 27 deletions pygeo/constraints/DVCon.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,13 +460,14 @@ def addThicknessConstraints2D(
* The leading and trailing edges are approximated using
2-order splines (line segments) and nSpan points are
interpolated in a linear fashion. Note that the thickness
constraint may not correspond **EXACT** to intermediate
locations in leList and teList. For example, in the example
above, with leList=3 and nSpan=3, the three thickness
interpolated in a linear fashion. For integer nSpan, the thickness
constraint may not correspond **EXACTLY** to intermediate
locations in leList and teList. In the example above,
with len(leList)=3 and nSpan=3, the three thickness
constraints on the leading edge of the 2D domain would be at
the left and right boundaries, and at the point denoted by
'o' which is equidistance between the root and tip.
'o' which is equidistant between the root and tip.
To match intermediate locations exactly, pass a list for nSpan.
* If a curved leading or trailing edge domain is desired,
simply pass in lists for leList and teList with a sufficient
Expand All @@ -490,18 +491,21 @@ def addThicknessConstraints2D(
leList : list or array
A list or array of points (size should be (Nx3) where N is
at least 2) defining the 'leading edge' or the start of the
domain
domain.
teList : list or array
Same as leList but for the trailing edge.
nSpan : int
nSpan : int or list of int
The number of thickness constraints to be (linear)
interpolated *along* the leading and trailing edges
interpolated *along* the leading and trailing edges.
A list of length N-1 can be used to specify the number
for each segment defined by leList and teList and
precisely match intermediate locations.
nChord : int
The number of thickness constraints to be (linearly)
interpolated between the leading and trailing edges
interpolated between the leading and trailing edges.
lower : float or array of size (nSpan x nChord)
The lower bound for the constraint. A single float will
Expand Down Expand Up @@ -575,19 +579,23 @@ def addThicknessConstraints2D(
"""

self._checkDVGeo(DVGeoName)
upper = convertTo2D(upper, nSpan, nChord).flatten()
lower = convertTo2D(lower, nSpan, nChord).flatten()
scale = convertTo2D(scale, nSpan, nChord).flatten()

coords = self._generateIntersections(leList, teList, nSpan, nChord, surfaceName)

# Get the total number of spanwise sections
nSpanTotal = np.sum(nSpan)

# Create the thickness constraint object:
coords = coords.reshape((nSpan * nChord * 2, 3))
coords = coords.reshape((nSpanTotal * nChord * 2, 3))

typeName = "thickCon"
if typeName not in self.constraints:
self.constraints[typeName] = OrderedDict()

upper = convertTo2D(upper, nSpanTotal, nChord).flatten()
lower = convertTo2D(lower, nSpanTotal, nChord).flatten()
scale = convertTo2D(scale, nSpanTotal, nChord).flatten()

# Create a name
if name is None:
conName = "%s_thickness_constraints_%d" % (self.name, len(self.constraints[typeName]))
Expand Down Expand Up @@ -1610,18 +1618,21 @@ def addVolumeConstraint(
leList : list or array
A list or array of points (size should be (Nx3) where N is
at least 2) defining the 'leading edge' or the start of the
domain
domain.
teList : list or array
Same as leList but for the trailing edge.
nSpan : int
The number of thickness constraints to be (linear)
interpolated *along* the leading and trailing edges
nSpan : int or list of int
The number of projected points to be (linear)
interpolated *along* the leading and trailing edges.
A list of length N-1 can be used to specify the number
for each segment defined by leList and teList and
precisely match intermediate locations.
nChord : int
The number of thickness constraints to be (linearly)
interpolated between the leading and trailing edges
The number of projected points to be (linearly)
interpolated between the leading and trailing edges.
lower : float
The lower bound for the volume constraint.
Expand Down Expand Up @@ -1698,12 +1709,16 @@ def addVolumeConstraint(
conName = name

coords = self._generateIntersections(leList, teList, nSpan, nChord, surfaceName)
coords = coords.reshape((nSpan * nChord * 2, 3))

# Get the total number of spanwise sections
nSpanTotal = np.sum(nSpan)

coords = coords.reshape((nSpanTotal * nChord * 2, 3))

# Finally add the volume constraint object
self.constraints[typeName][conName] = VolumeConstraint(
conName,
nSpan,
nSpanTotal,
nChord,
coords,
lower,
Expand Down Expand Up @@ -3204,20 +3219,64 @@ def _generateIntersections(self, leList, teList, nSpan, nChord, surfaceName):
root_s = Curve(X=[leList[0], teList[0]], k=2)
tip_s = Curve(X=[leList[-1], teList[-1]], k=2)

# Generate parametric distances
span_s = np.linspace(0.0, 1.0, nSpan)
# Generate spanwise parametric distances
if isinstance(nSpan, int):
# Use equal spacing along the curve
le_span_s = te_span_s = np.linspace(0.0, 1.0, nSpan)
elif isinstance(nSpan, list):
# Use equal spacing within each segment defined by leList and teList

# We use the same nSpan for the leading and trailing edges, so check that the lists are the same size
if len(leList) != len(teList):
raise ValueError("leList and teList must be the same length if nSpan is provided as a list.")

# Also check that nSpan is the correct length
numSegments = len(leList) - 1
if len(nSpan) != numSegments:
raise ValueError(f"nSpan must be of length {numSegments}.")

# Find the parametric distances of the break points that define each segment
le_breakPoints = le_s.projectPoint(leList)[0]
te_breakPoints = te_s.projectPoint(teList)[0]

# Initialize empty arrays for the full spanwise parameteric distances
le_span_s = np.array([])
te_span_s = np.array([])

for i in range(numSegments):

# Only include the endpoint if this is the last segment to avoid double counting points
if i == numSegments - 1:
endpoint = True
else:
endpoint = False

# Interpolate over this segment and append to the parametric distance array
le_span_s = np.append(
le_span_s, np.linspace(le_breakPoints[i], le_breakPoints[i + 1], nSpan[i], endpoint=endpoint)
)
te_span_s = np.append(
te_span_s, np.linspace(te_breakPoints[i], te_breakPoints[i + 1], nSpan[i], endpoint=endpoint)
)
else:
raise TypeError("nSpan must be either an int or a list.")

# Generate chordwise parametric distances
chord_s = np.linspace(0.0, 1.0, nChord)

# Get the total number of spanwise sections
nSpanTotal = np.sum(nSpan)

# Generate a 2D region of intersections
X = geo_utils.tfi_2d(le_s(span_s), te_s(span_s), root_s(chord_s), tip_s(chord_s))
coords = np.zeros((nSpan, nChord, 2, 3))
for i in range(nSpan):
X = geo_utils.tfi_2d(le_s(le_span_s), te_s(te_span_s), root_s(chord_s), tip_s(chord_s))
coords = np.zeros((nSpanTotal, nChord, 2, 3))
for i in range(nSpanTotal):
for j in range(nChord):
# Generate the 'up_vec' from taking the cross product
# across a quad
if i == 0:
uVec = X[i + 1, j] - X[i, j]
elif i == nSpan - 1:
elif i == nSpanTotal - 1:
uVec = X[i, j] - X[i - 1, j]
else:
uVec = X[i + 1, j] - X[i - 1, j]
Expand Down
50 changes: 50 additions & 0 deletions tests/reg_tests/test_DVConstraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,31 @@ def test_thickness2D(self, train=False, refDeriv=False):

funcs, funcsSens = self.wing_test_deformed(DVGeo, DVCon, handler)

def test_thickness2D_nSpanList(self, train=False, refDeriv=False):
refFile = os.path.join(self.base_path, "ref/test_DVConstraints_thickness2D.ref")
with BaseRegTest(refFile, train=train) as handler:
DVGeo, DVCon = self.generate_dvgeo_dvcon("c172")

leList = [[0.7, 0.0, 0.1], [0.7, 0.0, 1.325], [0.7, 0.0, 5.0]]
teList = [[0.9, 0.0, 0.1], [0.9, 0.0, 1.325], [0.9, 0.0, 5.0]]

# Use a list for nSpan instead of an integer
DVCon.addThicknessConstraints2D(leList, teList, [1, 4], 5)

funcs, funcsSens = generic_test_base(DVGeo, DVCon, handler)
# 2D thickness should be all ones at the start
handler.assert_allclose(
funcs["DVCon1_thickness_constraints_0"], np.ones(25), name="thickness_base", rtol=1e-7, atol=1e-7
)

funcs, funcsSens = self.wing_test_twist(DVGeo, DVCon, handler)
# 2D thickness shouldn't change much under only twist
handler.assert_allclose(
funcs["DVCon1_thickness_constraints_0"], np.ones(25), name="thickness_twisted", rtol=1e-2, atol=1e-2
)

funcs, funcsSens = self.wing_test_deformed(DVGeo, DVCon, handler)

def test_thickness2D_box(self, train=False, refDeriv=False):
refFile = os.path.join(self.base_path, "ref/test_DVConstraints_thickness2D_box.ref")
with BaseRegTest(refFile, train=train) as handler:
Expand Down Expand Up @@ -384,6 +409,31 @@ def test_volume(self, train=False, refDeriv=False):

funcs, funcsSens = self.wing_test_deformed(DVGeo, DVCon, handler)

def test_volume_nSpanList(self, train=False, refDeriv=False):
refFile = os.path.join(self.base_path, "ref/test_DVConstraints_volume.ref")
with BaseRegTest(refFile, train=train) as handler:
DVGeo, DVCon = self.generate_dvgeo_dvcon("c172")

leList = [[0.7, 0.0, 0.1], [0.7, 0.0, 1.325], [0.7, 0.0, 5.0]]
teList = [[0.9, 0.0, 0.1], [0.9, 0.0, 1.325], [0.9, 0.0, 5.0]]

# Use a list for nSpan instead of an integer
DVCon.addVolumeConstraint(leList, teList, [1, 4], 5)

funcs, funcsSens = generic_test_base(DVGeo, DVCon, handler)
# Volume should be normalized to 1 at the start
handler.assert_allclose(
funcs["DVCon1_volume_constraint_0"], np.ones(1), name="volume_base", rtol=1e-7, atol=1e-7
)

funcs, funcsSens = self.wing_test_twist(DVGeo, DVCon, handler)
# Volume shouldn't change much with twist only
handler.assert_allclose(
funcs["DVCon1_volume_constraint_0"], np.ones(1), name="volume_twisted", rtol=1e-2, atol=1e-2
)

funcs, funcsSens = self.wing_test_deformed(DVGeo, DVCon, handler)

def test_volume_box(self, train=False, refDeriv=False):
refFile = os.path.join(self.base_path, "ref/test_DVConstraints_volume_box.ref")
with BaseRegTest(refFile, train=train) as handler:
Expand Down

0 comments on commit 5057ede

Please sign in to comment.