Skip to content

Commit 4461d08

Browse files
marcomanganoewu63
andauthored
Bug fix for non-aligned FFD blocks for rotType=0 and generalization of xFraction (#67)
* added new vars for rotType0 scaling handling * applying axis scaling to rot0 - Mads way * extended nonaligned scaling to all rotTypes * added WARNING - nonzero rotType have inaccurate sensitivities * bugfix on single axis scaling * generalized axis nodes xyz location * implemented Mads-Sandy fix for rotAxisnodes when ffds are not aligned to the main sys ref * added test for generalized xyz fraction * added functions to commonUtils * WIP: added test for non-aligned FFDs - FD-trained test is failing * updated test 24 ref * added tests explanation * renamed vars to singular * cleaner coordinates array reshaping - thx Josh * less intrusive xyz fraction behavior * bugfix on a check that did not make sense * updated ang checks * limited rotation to rotType=0 only Co-authored-by: Neil Wu <[email protected]>
1 parent 1166d71 commit 4461d08

File tree

5 files changed

+543
-20
lines changed

5 files changed

+543
-20
lines changed

pygeo/DVGeometry.py

Lines changed: 109 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,9 @@ def __init__(self, fileName, complex=False, child=False, faceFreeze=None, name=N
196196
tmp[ind] = True
197197
self.masks = tmp
198198

199-
def addRefAxis(self, name, curve=None, xFraction=None, volumes=None,
199+
def addRefAxis(self, name, curve=None, xFraction=None, yFraction=None, zFraction=None, volumes=None,
200200
rotType=5, axis='x', alignIndex=None, rotAxisVar=None,
201+
rot0ang=None, rot0axis=[1, 0, 0],
201202
xFractionOrder=2, includeVols=[], ignoreInd=[],
202203
raySize=1.5):
203204
"""
@@ -268,7 +269,20 @@ def addRefAxis(self, name, curve=None, xFraction=None, volumes=None,
268269
variable which should be used to compute the orientation of the theta
269270
rotation.
270271
271-
xFractionOrder : int
272+
rot0ang: float
273+
If rotType == 0, defines the offset angle of the (child) FFD with respect
274+
to the main system of reference. This is necessary to use the scaling functions
275+
`scale_x`, `scale_y`, and `scale_z` with rotType == 0. The axis of rotation is
276+
defined by `rot0axis`.
277+
278+
rot0axis: list
279+
If rotType == 0, defines the rotation axis for the rotation offset of the
280+
FFD grid given by `rot0ang`. The variable has to be a list of 3 floats
281+
defining the [x,y,z] components of the axis direction.
282+
This is necessary to use the scaling functions `scale_x`, `scale_y`,
283+
and `scale_z` with rotType == 0.
284+
285+
xFractionOrder : int (NOT USED?)
272286
Order of spline used for refaxis curve.
273287
274288
includeVols : list
@@ -325,7 +339,7 @@ def addRefAxis(self, name, curve=None, xFraction=None, volumes=None,
325339
if volumes is None:
326340
volumes = numpy.arange(self.FFD.nVol)
327341
self.axis[name] = {'curve':curve, 'volumes':volumes,
328-
'rotType':rotType, 'axis':axis}
342+
'rotType':rotType, 'axis':axis, 'rot0ang':rot0ang, 'rot0axis':rot0axis}
329343

330344
else:
331345
# get the direction of the symmetry plane
@@ -349,11 +363,11 @@ def addRefAxis(self, name, curve=None, xFraction=None, volumes=None,
349363
for coef in curveSymm.coef:
350364
curveSymm.coef[:,index]=-curveSymm.coef[:,index]
351365
self.axis[name] = {'curve':curve, 'volumes':volumes,
352-
'rotType':rotType, 'axis':axis}
366+
'rotType':rotType, 'axis':axis,'rot0ang':rot0ang, 'rot0axis':rot0axis}
353367
self.axis[name+'Symm'] = {'curve':curveSymm, 'volumes':volumesSymm,
354-
'rotType':rotType, 'axis':axis}
368+
'rotType':rotType, 'axis':axis, 'rot0ang':rot0ang, 'rot0axis':rot0axis}
355369
nAxis = len(curve.coef)
356-
elif xFraction is not None:
370+
elif xFraction or yFraction or zFraction:
357371
# Some assumptions
358372
# - FFD should be a close approximation of geometry surface so that
359373
# xFraction roughly corresponds to airfoil LE, TE, or 1/4 chord
@@ -363,6 +377,8 @@ def addRefAxis(self, name, curve=None, xFraction=None, volumes=None,
363377
# included
364378
# - 'x' is streamwise direction
365379

380+
# Default to "mean" ref axis location along non-user specified direction
381+
366382
# This is the block direction along which the reference axis will lie
367383
# alignIndex = 'k'
368384
if alignIndex is None:
@@ -416,16 +432,62 @@ def addRefAxis(self, name, curve=None, xFraction=None, volumes=None,
416432
# Loop through sections and compute node location
417433
place = 0
418434
for j, vol in enumerate(volOrd):
435+
# sectionArr: indices of FFD points grouped by section
419436
sectionArr = numpy.rollaxis(lIndex[vol], alignIndex, 0)
420437
skip = 0
421438
if j > 0:
422439
skip = 1
423440
for i in range(nSections[j]):
424-
LE = numpy.min(self.FFD.coef[sectionArr[i+skip,:,:],0])
425-
TE = numpy.max(self.FFD.coef[sectionArr[i+skip,:,:],0])
426-
refaxisNodes[place+i,0] = xFraction*(TE - LE) + LE
427-
refaxisNodes[place+i,1] = numpy.mean(self.FFD.coef[sectionArr[i+skip,:,:],1])
428-
refaxisNodes[place+i,2] = numpy.mean(self.FFD.coef[sectionArr[i+skip,:,:],2])
441+
# getting all the section control points coordinates
442+
pts_tens = self.FFD.coef[sectionArr[i + skip, :, :], :] # shape=(xAxisNodes,yAxisnodes,3)
443+
444+
# reshaping into vector to allow rotation (if needed) - leveraging on pts_tens.shape[2]=3 (FFD cp coordinates)
445+
pts_vec = numpy.copy(pts_tens.reshape(-1, 3)) # new shape=(xAxisNodes*yAxisnodes,3)
446+
447+
if rot0ang:
448+
# rotating the FFD to be aligned with main axes
449+
for ct_ in range(numpy.shape(pts_vec)[0]):
450+
# here we loop over the pts_vec, rotate them and insert them inplace in pts_vec again
451+
p_ = numpy.copy(pts_vec[ct_ , :])
452+
p_rot = geo_utils.rotVbyW(p_, rot0axis, numpy.pi / 180 * (rot0ang))
453+
pts_vec[ct_ , :] = p_rot
454+
455+
# Temporary ref axis node coordinates - aligned with main system of reference
456+
if xFraction:
457+
# getting the bounds of the FFD section
458+
x_min = numpy.min(pts_vec[:, 0])
459+
x_max = numpy.max(pts_vec[:, 0])
460+
x_node = xFraction * (x_max - x_min) + x_min # chordwise
461+
else:
462+
x_node = numpy.mean(pts_vec[:, 0])
463+
464+
if yFraction:
465+
y_min = numpy.min(pts_vec[:, 1])
466+
y_max = numpy.max(pts_vec[:, 1])
467+
y_node = y_max - yFraction * (y_max - y_min) # top-bottom
468+
else:
469+
y_node = numpy.mean(pts_vec[:, 1])
470+
471+
if zFraction:
472+
z_min = numpy.min(pts_vec[:, 2])
473+
z_max = numpy.max(pts_vec[:, 2])
474+
z_node = z_max - zFraction * (z_max - z_min) # top-bottom
475+
else:
476+
z_node = numpy.mean(pts_vec[:, 2])
477+
478+
# This is the FFD ref axis node - if the block has not been rotated
479+
nd = [x_node, y_node, z_node]
480+
nd_final = numpy.copy(nd)
481+
482+
if rot0ang:
483+
# rotating the non-aligned FFDs back in position
484+
nd_final[:] = geo_utils.rotVbyW(nd, rot0axis, numpy.pi / 180 * (-rot0ang))
485+
486+
# insert the final coordinates in the var to be passed to pySpline:
487+
refaxisNodes[place + i, 0] = nd_final[0]
488+
refaxisNodes[place + i, 1] = nd_final[1]
489+
refaxisNodes[place + i, 2] = nd_final[2]
490+
429491
place += i + 1
430492

431493
# Add additional volumes
@@ -437,7 +499,7 @@ def addRefAxis(self, name, curve=None, xFraction=None, volumes=None,
437499
curve = pySpline.Curve(X=refaxisNodes, k=2)
438500
nAxis = len(curve.coef)
439501
self.axis[name] = {'curve':curve, 'volumes':volumes,
440-
'rotType':rotType, 'axis':axis,
502+
'rotType':rotType, 'axis':axis, 'rot0ang':rot0ang, 'rot0axis':rot0axis,
441503
'rotAxisVar':rotAxisVar}
442504
else:
443505
raise Error("One of 'curve' or 'xFraction' must be "
@@ -1188,23 +1250,53 @@ def updateCalculations(self, new_pts, isComplex, config):
11881250

11891251
for ipt in range(self.nPtAttach):
11901252
base_pt = self.refAxis.curves[self.curveIDs[ipt]](self.links_s[ipt])
1253+
# Variables for rotType = 0 rotation + scaling
1254+
ang = self.axis[self.curveIDNames[ipt]]['rot0ang']
1255+
ax_dir = self.axis[self.curveIDNames[ipt]]['rot0axis']
1256+
11911257
scale = self.scale[self.curveIDNames[ipt]](self.links_s[ipt])
11921258
scale_x = self.scale_x[self.curveIDNames[ipt]](self.links_s[ipt])
11931259
scale_y = self.scale_y[self.curveIDNames[ipt]](self.links_s[ipt])
11941260
scale_z = self.scale_z[self.curveIDNames[ipt]](self.links_s[ipt])
11951261

11961262
rotType = self.axis[self.curveIDNames[ipt]]['rotType']
11971263
if rotType == 0:
1264+
bp_ = numpy.copy(base_pt) # copy of original pointset - will not be rotated
1265+
if isinstance(ang,(float, int)): # rotation active only if a non-default value is provided
1266+
ang *= numpy.pi/180 # conv to [rad]
1267+
# Rotating the FFD according to inputs
1268+
# The FFD points should now be aligned with the main system of reference
1269+
base_pt = geo_utils.rotVbyW(bp_, ax_dir, ang)
11981270
deriv = self.refAxis.curves[
11991271
self.curveIDs[ipt]].getDerivative(self.links_s[ipt])
12001272
deriv /= geo_utils.euclideanNorm(deriv) # Normalize
12011273
new_vec = -numpy.cross(deriv, self.links_n[ipt])
1202-
new_vec = geo_utils.rotVbyW(new_vec, deriv, self.rot_theta[
1203-
self.curveIDNames[ipt]](self.links_s[ipt])*numpy.pi/180)
12041274
if isComplex:
1205-
new_pts[ipt] = base_pt + new_vec*scale
1275+
new_pts[ipt] = bp_ + new_vec*scale # using "unrotated" bp_ vector
1276+
else:
1277+
new_pts[ipt] = numpy.real(bp_ + new_vec*scale)
1278+
1279+
if isinstance(ang,(float, int)):
1280+
# Rotating to be aligned with main sys ref
1281+
nv_ = numpy.copy(new_vec)
1282+
new_vec = geo_utils.rotVbyW(nv_, ax_dir, ang)
1283+
1284+
# Apply scaling
1285+
new_vec[0] *= scale_x
1286+
new_vec[1] *= scale_y
1287+
new_vec[2] *= scale_z
1288+
1289+
if isinstance(ang,(float, int)):
1290+
# Rotating back the scaled pointset to its original position
1291+
nv_rot = numpy.copy(new_vec) # nv_rot is scaled and rotated
1292+
new_vec = geo_utils.rotVbyW(nv_rot , ax_dir, -ang)
1293+
1294+
new_vec = geo_utils.rotVbyW(new_vec, deriv, self.rot_theta[self.curveIDNames[ipt]](self.links_s[ipt])*numpy.pi/180)
1295+
1296+
if isComplex:
1297+
new_pts[ipt] = bp_ + new_vec
12061298
else:
1207-
new_pts[ipt] = numpy.real(base_pt + new_vec*scale)
1299+
new_pts[ipt] = numpy.real(bp_ + new_vec)
12081300

12091301
else:
12101302
rotX = geo_utils.rotxM(self.rot_x[
@@ -1246,6 +1338,7 @@ def updateCalculations(self, new_pts, isComplex, config):
12461338
D[0] *= scale_x
12471339
D[1] *= scale_y
12481340
D[2] *= scale_z
1341+
12491342
if isComplex:
12501343
new_pts[ipt] = base_pt + D*scale
12511344
else:

tests/reg_tests/commonUtils.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,17 @@ def totalSensitivityCS(DVGeo,nPt,ptName):
176176

177177
return dIdxCS
178178

179-
def testSensitivities(DVGeo,refDeriv,handler):
179+
def testSensitivities(DVGeo,refDeriv,handler,pointset=1):
180180
#create test points
181181
points = numpy.zeros([2,3])
182-
points[0,:] = [0.25,0,0]
183-
points[1,:] = [-0.25,0,0]
182+
if pointset == 1:
183+
points[0,:] = [0.25,0,0]
184+
points[1,:] = [-0.25,0,0]
185+
elif pointset == 2:
186+
points[0, :] = [0.25, 0.4, 4]
187+
points[1, :] = [-0.8, 0.2, 7]
188+
else:
189+
raise Warning("Enter a valid pointset")
184190

185191
# add points to the geometry object
186192
ptName = 'testPoints'
@@ -235,3 +241,26 @@ def testSensitivitiesD8(DVGeo,refDeriv,handler):
235241
dIdx = DVGeo.totalSensitivity(dIdPt,ptName)
236242

237243
handler.root_add_dict("dIdx",dIdx,rtol=1e-7,atol=1e-7)
244+
245+
# --- Adding standard twist and single axis scaling functions ---
246+
# These functions are added for Test 24 but could be extended to other tests
247+
248+
fix_root_sect=1
249+
nRefAxPts = 4
250+
251+
def twist(val, geo):
252+
axis_key = list(geo.axis.keys())[0]
253+
for i in range(fix_root_sect, nRefAxPts):
254+
geo.rot_theta[axis_key].coef[i] = val[i - fix_root_sect]
255+
256+
def thickness(val, geo):
257+
axis_key = list(geo.axis.keys())[0]
258+
259+
for i in range(1, nRefAxPts):
260+
geo.scale_z[axis_key].coef[i] = val[i - fix_root_sect]
261+
262+
def chord(val, geo):
263+
axis_key = list(geo.axis.keys())[0]
264+
265+
for i in range(1, nRefAxPts):
266+
geo.scale_x[axis_key].coef[i] = val[i - fix_root_sect]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"RefAxis_nodes_coord": {
3+
"__ndarray__": [
4+
[
5+
-0.4,
6+
-0.09999999999999998,
7+
0.0
8+
],
9+
[
10+
-0.4,
11+
-0.09999999999999998,
12+
3.32194917
13+
],
14+
[
15+
-0.4,
16+
-0.09999999999999998,
17+
5.78384945
18+
],
19+
[
20+
-0.4,
21+
-0.09999999999999998,
22+
8.0
23+
]
24+
],
25+
"dtype": "float64",
26+
"shape": [
27+
4,
28+
3
29+
]
30+
}
31+
}

0 commit comments

Comments
 (0)