-
Notifications
You must be signed in to change notification settings - Fork 20
/
CMPLXFOIL.py
1111 lines (946 loc) · 44.3 KB
/
CMPLXFOIL.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
CMPLXFOIL
CMPLXFOIL is a wrapper for Mark Drela's XFOIL code. The purpose of this
class is to provide an easy to use wrapper for xfoil for intergration
into other projects. Both real and complex versions of xfoil can be used.
The version uses the MDO Lab's BaseSolver baseclass so it can be used
within the MACH-Aero optimization framework.
Developers:
-----------
- Eytan Adler (EA)
- Alasdair Gray (AG)
History
-------
v. 1.0 - Initial Class Creation (EA + AG, 2022)
"""
# =============================================================================
# Standard Python modules
# =============================================================================
import os
import time
from copy import copy, deepcopy
import pickle as pkl
from collections.abc import Iterable
import warnings
# =============================================================================
# Other Python modules
# =============================================================================
import numpy as np
from baseclasses import BaseSolver
from . import MExt
# Handle plotting imports
plt = None
try:
from matplotlib import pyplot
import matplotlib.lines as mpl_lines
plt = pyplot
except ImportError:
pass
# If the matplotlib import worked, try niceplots
if plt:
try:
import niceplots
plt.style.use(niceplots.get_style())
colors = niceplots.get_colors()
color = colors["Blue"]
cpUpColor = colors["Blue"]
cpLowColor = colors["Red"]
except ImportError:
color = "b"
cpUpColor = "b"
cpLowColor = "r"
class CMPLXFOIL(BaseSolver):
"""
CMPLXFOIL Class Initialization
Parameters
----------
fileName : str
Filename of DAT file to read in
options : dict of option-value pairs, optional
Options for the solver. Available options can be found
in the Options section of the documentation or the
options.yaml file in the docs directory.
debug : bool, optional
Set this flag to true when debugging with a symbolic
debugger. The MExt module deletes the copied .so file when not
required which causes issues debugging, by default False
"""
def __init__(self, fileName, options={}, debug=False):
# Load the compiled module using MExt, allowing multiple imports
curDir = os.path.basename(os.path.dirname(os.path.realpath(__file__)))
time.sleep(0.1) # BOTH of these sleeps are necessary for some reason!
self.xfoil = MExt.MExt("libcmplxfoil", curDir, debug=debug)._module
time.sleep(0.1) # BOTH of these sleeps are necessary for some reason!
self.xfoil_cs = MExt.MExt("libcmplxfoil_cs", curDir, debug=debug)._module
super().__init__("CMPLXFOIL", "Panel Method", defaultOptions=self._getDefaultOptions(), options=options)
if self.getOption("xTrip").shape != (2,):
raise ValueError("xTrip option shape must be (2,)")
# Read in DAT file and create coordinates. DVGeo needs 3D coordinates
# so keep the third dimension as dummy coordinates.
self.DATFileName = fileName
self.coords = self._readDat(fileName)
self.coords = np.hstack((self.coords, np.zeros((self.coords.shape[0], 1))))
self.coords0 = self.coords.copy() # initial coordinates (never changes)
self.setCoordinates(self.coords0) # set the initial coordinates
self.setCoordinatesComplex(self.coords0) # set the initial complex coordinates
self.pointSetKwargs = {}
self.curAP = None
self.DVGeo = None
# Dictionary with dictionary of functions for each aero problem
self.funcs = {}
self.funcsComplex = {}
self.functionList = ["cl", "cd", "cm"] # available functions
# When the XFOIL solver is called, slice data is saved (key is the current
# AeroProblem name). In the associated value is a dictionary containing
# "cp_visc_upper": viscous CP on the airfoil's upper surface
# "cp_invisc_upper": inviscid CP on the airfoil's upper surface
# "x_upper": x coordinates of the upper surface CP data
# "y_upper": y coordinates of the upper surface CP data
# "cp_visc_lower": viscous CP on the airfoil's lower surface
# "cp_invisc_lower": inviscid CP on the airfoil's lower surface
# "x_lower": x coordinates of the lower surface CP data
# "y_lower": y coordinates of the lower surface CP data
# "cf_upper": skin friction coefficient on the upper surface
# "x_cf_upper": x coordinates of upper surface skin friction coefficient
# "y_cf_upper": y coordinates of upper surface skin friction coefficient
# "cf_lower": skin friction coefficient on the lower surface
# "x_cf_lower": x coordinates of lower surface skin friction coefficient
# "y_cf_lower": y coordinates of lower surface skin friction coefficient
self.sliceData = {}
self.sliceDataComplex = {}
# Possible AeroProblem design variables (only alpha for CMPLXFOIL)
self.possibleAeroDVs = ["alpha"]
# Figure and axes used by self.plotAirfoil and self._updateAirfoilPlot
self.airfoilFig = None
self.airfoilAxs = None
self.CPlim = None # y limits on the CP plot
self.CFlim = None # y limits on the CF plot
def setDVGeo(self, DVGeo, pointSetKwargs=None):
"""
Set the DVGeometry object that will manipulate 'geometry' in
this object. Note that CMPLXFOIL does not **strictly** need a
DVGeometry object, but if optimization with geometric
changes is desired, then it is required.
Parameters
----------
DVGeo : A DVGeometry object.
Object responsible for manipulating the constraints that
this object is responsible for.
pointSetKwargs : dict
Keyword arguments to be passed to the DVGeo addPointSet call.
Useful for DVGeometryMulti, specifying FFD projection tolerances, etc
"""
self.DVGeo = DVGeo
# Get the number of geometry variables
self.numGeoDV = self.DVGeo.getNDV()
# Save kwargs for addPointSet
if pointSetKwargs is not None:
self.pointSetKwargs = pointSetKwargs
def setAeroProblem(self, aeroProblem):
"""
Sets the aeroProblem to by used by CMPLXFOIL.
Parameters
----------
aeroProblem : :class:`AeroProblem <baseclasses.problems.pyAero_problem.AeroProblem>` instance
Aero problem to set (gives flight conditions)
"""
ptSetName = "cmplxfoil_%s_coords" % aeroProblem.name
# Tell the user if we are switching aeroProblems
if self.curAP != aeroProblem:
self.pp("+" + "-" * 70 + "+")
self.pp("| Switching to Aero Problem: %-41s|" % aeroProblem.name)
self.pp("+" + "-" * 70 + "+")
# Now check if we have an DVGeo object to deal with:
if self.DVGeo is not None:
# DVGeo appeared and we have not embedded points!
if ptSetName not in self.DVGeo.points:
self.DVGeo.addPointSet(self.coords0, ptSetName, **self.pointSetKwargs)
aeroProblem.ptSetName = ptSetName
# Check if our point-set is up to date:
if not self.DVGeo.pointSetUpToDate(ptSetName):
coords = self.DVGeo.update(ptSetName, config=aeroProblem.name)
self.setCoordinates(coords)
# Initialize the callCounter if it's not already an attribute
try:
aeroProblem.callCounter
except AttributeError:
aeroProblem.callCounter = 0
self.curAP = aeroProblem
def setCoordinates(self, coords):
"""
Update the airfoil coordinates and associated point sets.
Parameters
----------
coords : ndarray
New airfoil coordinates (either 2 or 3 columns)
"""
self.coords[:, :2] = coords[:, :2].copy()
self._setCoordinates(self.coords[:, 0], self.coords[:, 1])
def getCoordinates(self):
"""
Return the current airfoil coordinates
Returns
-------
coords : ndarray
Airfoil coordinates with each column being (x, y, z)
where z is a dummy value.
"""
return self.coords.copy()
def setCoordinatesComplex(self, coords):
"""
Update the complex airfoil coordinates and associated point sets.
Parameters
----------
coords : ndarray
New airfoil coordinates (either 2 or 3 columns)
"""
self._setCoordinates(coords[:, 0].astype(complex), coords[:, 1].astype(complex), setComplex=True)
def __call__(self, aeroProblem, useComplex=False, deriv=False):
"""
Evaluate XFOIL with the current coordinates and flight conditions (from aeroProblem).
Parameters
----------
aeroProblem : :class:`AeroProblem <baseclasses.problems.pyAero_problem.AeroProblem>` instance
Aero problem to set (gives flight conditions)
useComplex : bool
Run XFOIL in complex mode
deriv : bool
Set to True if the call is associated with the derivative calculation. callCounter will be incremented and
writeSolution called only if this is False
"""
if useComplex:
dtype = complex
xfoil = self.xfoil_cs
funcs = self.funcsComplex
sliceData = self.sliceDataComplex
else:
dtype = float
xfoil = self.xfoil
funcs = self.funcs
sliceData = self.sliceData
self.setAeroProblem(aeroProblem)
# Set flight condition and options
xfoil.cr15.reinf1 = aeroProblem.re # Reynolds number
xfoil.cr09.minf1 = aeroProblem.mach # Mach Number set
xfoil.cr09.adeg = aeroProblem.alpha
xfoil.ci04.itmax = self.getOption("maxIters") # Iterations Limit Set
if not np.any(np.isnan(self.getOption("xTrip"))): # NaN is default to not set, otherwise set it
xfoil.cr15.xstrip = self.getOption("xTrip")
# Call XFOIL
xfoil.oper()
# Store results in dictionary for current aero problem
funcs[aeroProblem.name] = {
"cl": dtype(xfoil.cr09.cl),
"cd": dtype(xfoil.cr09.cd),
"cm": dtype(xfoil.cr09.cm),
}
# Pull out and process the pressure and skin friction coefficient data
cpv = xfoil.cr04.cpv # viscous
cpi = xfoil.cr04.cpi # inviscid
x = xfoil.cr05.x
y = xfoil.cr05.y
end_foil_idx = np.argmax(x > self.coords[0, 0]) + 1 # XFOIL includes wake panels, which we don't want
idx_lower_start = end_foil_idx // 2 # first half of data is upper surface
idxUpper = np.argwhere(xfoil.cr15.tau[:, 0] != 0).flatten()
idxLower = np.argwhere(xfoil.cr15.tau[:, 1] != 0).flatten()
iPanUpper = xfoil.ci05.ipan[idxUpper, 0] - 1 # FORTRAN uses 1-based indexing, need to adjust
iPanLower = xfoil.ci05.ipan[idxLower, 1] - 1 # FORTRAN uses 1-based indexing, need to adjust
tauUpper = xfoil.cr15.tau[idxUpper, 0].copy().astype(dtype)
tauLower = xfoil.cr15.tau[idxLower, 1].copy().astype(dtype)
uInf = xfoil.cr09.qinf
xCfUpper = x[iPanUpper].copy().astype(dtype)
yCfUpper = y[iPanUpper].copy().astype(dtype)
xCfLower = x[iPanLower].copy().astype(dtype)
yCfLower = y[iPanLower].copy().astype(dtype)
sliceData[aeroProblem.name] = {
"cp_visc_upper": cpv[:idx_lower_start].copy().astype(dtype),
"cp_invisc_upper": cpi[:idx_lower_start].copy().astype(dtype),
"x_upper": x[:idx_lower_start].copy().astype(dtype),
"y_upper": y[:idx_lower_start].copy().astype(dtype),
"cp_visc_lower": cpv[idx_lower_start:end_foil_idx].copy().astype(dtype),
"cp_invisc_lower": cpi[idx_lower_start:end_foil_idx].copy().astype(dtype),
"x_lower": x[idx_lower_start:end_foil_idx].copy().astype(dtype),
"y_lower": y[idx_lower_start:end_foil_idx].copy().astype(dtype),
"cf_upper": tauUpper / (0.5 * uInf**2),
"x_cf_upper": xCfUpper,
"y_cf_upper": yCfUpper,
"cf_lower": tauLower / (0.5 * uInf**2),
"x_cf_lower": xCfLower,
"y_cf_lower": yCfLower,
}
# Check for failure
self.curAP.solveFailed = self.curAP.fatalFail = xfoil.cl01.lexitflag != 0 or xfoil.cl01.lvconv == 0
# If not a derivative call, increment callCounter
if not deriv:
self.curAP.callCounter += 1
# Write solution files if desired
if not deriv and self.getOption("writeSolution"):
self.writeSolution()
def solveCL(
self,
aeroProblem,
CLStar,
alpha0=None,
alphaBound=None,
delta=0.5,
tol=1e-3,
CLalphaGuess=None,
maxIter=20,
useNewton=False,
):
"""Find the angle of attack that gives a target lift coefficient.
Parameters
----------
aeroProblem : pyAero_problem class
The aerodynamic problem to solve
CLStar : float
The desired CL
alpha0 : float, optional
Initial guess for secant search (deg). If None, use the value in the aeroProblem, by default None
alphaBound : float, tuple, list, optional
Bounds for angle of attack, if scalar then value is treated as a +- bound, by default None, in which case
limit is +/-15 deg
delta : float, optional
Initial step direction for secant search, by default 0.5
tol : float, optional
Desired tolerance for CL, by default 1e-3
CLalphaGuess : float, optional
The user can provide an estimate for the lift curve slope in order to accelerate convergence. If the user
supplies a value to this option, it will not use the delta value anymore to select the angle of attack of
the second run. The value should be in 1/deg., by default None
maxIter : int, optional
Maximum number of iterations, by default 20
useNewton : bool, optional
If True, Newton's method will be used where the dCL/dAlpha is computed using complex-step, otherwise the
secant method is used, by default False
Returns
-------
None, but the correct alpha is stored in the aeroProblem
"""
self.setAeroProblem(aeroProblem)
if alpha0 is not None:
aeroProblem.alpha = alpha0
else:
alpha0 = aeroProblem.alpha
if alphaBound is None:
alphaBound = (-15, 15)
elif isinstance(alphaBound, (int, float)):
alphaBound = (-alphaBound, alphaBound)
elif isinstance(alphaBound, Iterable):
alphaBound = (alphaBound[0], alphaBound[1])
else:
raise ValueError(
f'Supplied alphaBound value "{alphaBound}" is not the correct type, must be a scalar or array-like value.'
)
dCLdAlpha = CLalphaGuess
resPrev = None
alphaPrev = None
hasConverged = False
maxRes = 1e2
for _ in range(maxIter):
# Call the solver and compute the "residual"
self.__call__(aeroProblem, deriv=True)
failCheck = {}
self.checkSolutionFailure(aeroProblem, failCheck)
cl = float(self.funcs[aeroProblem.name]["cl"])
print(f"Alpha: {aeroProblem.alpha:>6.3f}, CL: {cl:>7.6f}")
res = cl - CLStar
# Check for convergence and divergence
hasConverged = abs(res) < tol and not failCheck["fail"]
if hasConverged:
print("Converged!")
break
elif abs(res > maxRes):
warnings.warn(f"solveCL diverged (CL = {cl:.2f})", stacklevel=2)
break
# If not converged or diverged, compute the next alpha
if not failCheck["fail"]:
if useNewton:
aeroProblem.alpha += 1e-200 * 1j
self.__call__(aeroProblem, useComplex=True, deriv=True)
dCLdAlpha = np.imag(self.funcsComplex[aeroProblem.name]["cl"]) * 1e200
aeroProblem.alpha = np.real(aeroProblem.alpha)
else:
# If using secant, we can only compute dCLdAlpha from the second iteration onwards
if resPrev is not None:
dCLdAlpha = (res - resPrev) / (aeroProblem.alpha - alphaPrev)
resPrev = res
alphaPrev = aeroProblem.alpha
# Update the alpha either using dCLdAlpha or delta, or backtracking if something went wrong
if dCLdAlpha == 0.0 or failCheck["fail"]:
aeroProblem.alpha *= 0.9
elif dCLdAlpha is not None:
aeroProblem.alpha = np.clip(aeroProblem.alpha - res / dCLdAlpha, alphaBound[0], alphaBound[1])
else:
aeroProblem.alpha = aeroProblem.alpha + delta
if not hasConverged:
print("Did not converge :(")
return hasConverged
def writeSolution(self, outputDir=None, baseName=None, number=None):
"""This is a generic shell function that potentially writes the various output files. The intent is that the
user or calling program can call this file and CMPLXFOIL write all the files that the user has defined. It is
recommended that this function is used along with the associated logical flags in the options to determine the
desired writing procedure.
Parameters
----------
outputDir : str
Use the supplied output directory
baseName : str
Use this supplied string for the base filename. Typically only used from an external solver.
number : int
Use the user supplied number to index solution. Again, only typically used from an external solver.
"""
if outputDir is None:
outputDir = self.getOption("outputDirectory")
if baseName is None:
baseName = self.curAP.name
# Add a number to the filename, either from the user or from the current callCounter
if number is not None:
baseName = baseName + "_%3.3d" % number
else:
if self.getOption("numberSolutions"):
baseName = baseName + "_%3.3d" % self.curAP.callCounter
# Join to get the actual filename root
base = os.path.join(outputDir, baseName)
if self.getOption("writeCoordinates"):
self.writeCoordinates(base)
if self.getOption("writeSliceFile"):
self.writeSlice(base)
if self.getOption("plotAirfoil"):
self.plotAirfoil()
def writeCoordinates(self, fileName):
"""Write dat file with the current coordinates.
Parameters
----------
fileName : str
File name for saved dat file (".dat" will be automatically appended).
"""
fileName += ".dat"
with open(fileName, "w") as f:
for i in range(self.coords.shape[0]):
f.write(str(round(self.coords[i, 0], 12)) + "\t\t" + str(round(self.coords[i, 1], 12)) + "\n")
def writeSlice(self, fileName):
"""Write pickle file containing the sliceData dictionary. The data can be
accessed using the AeroProblem name as the key. Within that is a dictionary containing
* Pressure coefficient data on the upper surface
* ``"cp_visc_upper"``: viscous CP on the airfoil's upper surface
* ``"cp_invisc_upper"``: inviscid CP on the airfoil's upper surface
* ``"x_upper"``: x coordinates of the upper surface CP data
* ``"y_upper"``: y coordinates of the upper surface CP data
* Pressure coefficient data on the lower surface
* ``"cp_visc_lower"``: viscous CP on the airfoil's lower surface
* ``"cp_invisc_lower"``: inviscid CP on the airfoil's lower surface
* ``"x_lower"``: x coordinates of the lower surface CP data
* ``"y_lower"``: y coordinates of the lower surface CP data
* Skin friction coefficient data on the upper surface
* ``"cf_upper"``: skin friction coefficient on the upper surface
* ``"x_cf_upper"``: x coordinates of upper surface skin friction coefficient
* ``"y_cf_upper"``: y coordinates of upper surface skin friction coefficient
* Skin friction coefficient data on the lower surface
* ``"cf_lower"``: skin friction coefficient on the lower surface
* ``"x_cf_lower"``: x coordinates of lower surface skin friction coefficient
* ``"y_cf_lower"``: y coordinates of lower surface skin friction coefficient
Parameters
----------
fileName : str
File name for saved pkl file (".pkl" will be automatically appended).
"""
fileName += ".pkl"
with open(fileName, "wb") as f:
pkl.dump(self.sliceData, f, protocol=pkl.HIGHEST_PROTOCOL)
def checkSolutionFailure(self, aeroProblem, funcs):
"""Take in a an aeroProblem and check for failure.
Then append the fail flag in funcs. Information regarding whether or not the last analysis with the aeroProblem
was sucessful is included. This information is included as "funcs['fail']". If the 'fail' entry already exits in
the dictionary the following operation is performed:
funcs['fail'] = funcs['fail'] or <did this problem fail>
In other words, if any one problem fails, the funcs['fail'] entry will be True. This information can then be
used directly in multiPointSparse. For direct interface with pyOptSparse the fail flag needs to be returned
separately from the funcs.
Parameters
----------
aeroProblem : pyAero_problem class
The aerodynamic problem to get the solution for
funcs : dict
Dictionary into which the functions are saved.
"""
self.setAeroProblem(aeroProblem)
# We also add the fail flag into the funcs dictionary. If fail is already there, we just logically 'or' what was
# there. Otherwise we add a new entry.
failFlag = self.curAP.solveFailed or self.curAP.fatalFail
if "fail" in funcs:
funcs["fail"] = funcs["fail"] or failFlag
else:
funcs["fail"] = failFlag
def checkAdjointFailure(self, aeroProblem, funcsSens):
"""
Pass through to checkSolutionFailure to maintain the same interface as ADflow.
This checks if the primal solve fails and can be called when the sensitivity
is being evaluated (either through FD or CS).
Parameters
----------
aeroProblem : pyAero_problem class
The aerodynamic problem to to get the solution for
funcsSens : dict
Dictionary into which the functions are saved.
"""
self.checkSolutionFailure(aeroProblem, funcsSens)
def evalFunctions(self, aeroProblem, funcs, evalFuncs=None, ignoreMissing=False):
"""
This is the main routine for returning useful information from CMPLXFOIL.
The functions corresponding to the strings in ``evalFuncs`` are evaluated
and updated into the provided dictionary.
Parameters
----------
aeroProblem : :class:`AeroProblem <baseclasses.problems.pyAero_problem.AeroProblem>` instance
Aero problem from which to pull evalFuncs and flight conditions.
funcs : dict
Dictionary into which the functions are saved.
evalFuncs : iterable object containing strings
If not none, use these functions to evaluate.
ignoreMissing : bool
Flag to supress checking for a valid function. Please use
this option with caution.
"""
self.setAeroProblem(aeroProblem)
if evalFuncs is None:
evalFuncs = sorted(list(self.curAP.evalFuncs))
else:
evalFuncs = sorted(list(evalFuncs))
# Make the functions lower case
evalFuncs = [s.lower() for s in evalFuncs]
returnFuncs = {}
for f in evalFuncs:
# If it's not in the list of available functions
if f not in self.functionList:
# Either throw an error (if requested) or skip it
if not ignoreMissing:
raise ValueError(f'Supplied function "{f}" is not in the available functions {self.functionList}.')
else:
returnFuncs[aeroProblem.name + "_" + f] = self.funcs[aeroProblem.name][f]
funcs.update(returnFuncs)
# Set the AeroProblem's function names
for name in evalFuncs:
self.curAP.funcNames[name] = self.curAP.name + "_" + name
def evalFunctionsSens(self, aeroProblem, funcsSens, evalFuncs=None, mode="CS", h=None):
"""
Evaluate the sensitivity of the desired functions given in
iterable object, 'evalFuncs' and add them to the dictionary
'funcSens'. The keys in the 'funcsSens' dictionary will be have an
``<ap.name>_`` prepended to them.
Parameters
----------
funcsSens : dict
Dictionary into which the function derivatives are saved
evalFuncs : iterable object containing strings
The additional functions the user wants returned that are
not already defined in the aeroProblem
mode : str ["FD" or "CS"]
Specifies how the jacobian vector products will be computed
h : float
Step sized used when the mode is "FD" or "CS" (must be complex
if mode = "CS")
"""
# This is the one and only gateway to the getting derivatives
# out of xfoil. If you want a derivative, it should come from
# here. Thank you.
self.setAeroProblem(aeroProblem)
if evalFuncs is None:
evalFuncs = sorted(list(self.curAP.evalFuncs))
else:
evalFuncs = sorted(list(evalFuncs))
# Make the functions lower case
evalFuncs = [s.lower() for s in evalFuncs]
# Get design variables
DVs = self.DVGeo.getValues()
for dv in self.curAP.DVs.values():
DVs[dv.key] = np.atleast_1d(dv.value)
# Preallocate the funcsSens dictionary with zeros for the desired sensitivities
for f in evalFuncs:
funcsSens[self.curAP.name + "_" + f] = {}
for dvName, dvVal in DVs.items():
if isinstance(dvVal, np.ndarray):
funcsSens[self.curAP.name + "_" + f][dvName] = np.zeros(dvVal.shape, dtype=float)
else:
funcsSens[self.curAP.name + "_" + f][dvName] = np.zeros(1)
# Loop over design variables and compute derivatives of each
for dvName, dvVal in DVs.items():
lenDV = 1 if not isinstance(dvVal, np.ndarray) else len(dvVal)
for i in range(lenDV):
# Compute the seed for the finite difference/complex step
seed = {dvName: np.zeros(dvVal.shape, dtype=float)}
seed[dvName][i] = 1.0
# Compute the sensitivity
sens = self.computeJacobianVectorProductFwd(xDvDot=seed, mode=mode, h=h)
for f in evalFuncs:
funcsSens[self.curAP.name + "_" + f][dvName][i] = sens[f]
# Check that the solution converged
self.checkSolutionFailure(self.curAP, funcsSens)
# Append "_" + (current aero problem name) to any design variables associated with the aero problem
for f in evalFuncs:
func = self.curAP.name + "_" + f
for dvName in DVs.keys():
if dvName in self.possibleAeroDVs and dvName in funcsSens[func]:
funcsSens[func][dvName + "_" + self.curAP.name] = funcsSens[func][dvName]
del funcsSens[func][dvName]
def computeJacobianVectorProductFwd(self, xDvDot=None, xSDot=None, mode="CS", h=None):
"""This the main Python gateway for producing forward mode jacobian
vector products. They are computed using either the complex step or finite
difference method. This function is not generally called by the user but
rather internally or from another solver. A DVGeo object must be set
for this routine.
Parameters
----------
xDvDot : dict
Perturbation on the design variables
xSDot : numpy array
Perturbation on the surface
mode : str ["FD" or "CS"]
Specifies how the jacobian vector products will be computed
h : float
Step sized used when the mode is "FD" or "CS" (must be complex
if mode = "CS"), by default 1e-6 for FD and 1e-200j for CS
Returns
-------
dict
Jacobian vector product of evalFuncs given perturbation
"""
if xDvDot is None and xSDot is None:
raise ValueError("xDvDot and xSDot cannot both be None")
if mode not in ["FD", "CS"]:
raise ValueError(f'Jacobian vector product mode "{mode}" invalid. Must be either "FD" or "CS"')
if self.DVGeo is None:
raise ValueError("DVGeo object must be added with setDVGeo before calling computeJacobianVectorProductFwd")
geoDVs = list(self.DVGeo.getValues().keys())
possibleDVs = self.possibleAeroDVs + geoDVs
for DV in xDvDot.keys():
if DV not in possibleDVs:
raise ValueError(f'Perturbed design variable "{DV}" is not valid')
if h is None:
if mode == "FD":
h = 1e-6
elif mode == "CS":
h = 1e-200j
if mode == "FD":
orig_funcs = deepcopy(self.funcs[self.curAP.name])
# Process the Xs perturbation
if xSDot is None:
xsdot = np.zeros_like(self.coords0)
else:
xsdot = xSDot
# For the geometric xDvDot perturbation we accumulate into the
# already existing (and possibly nonzero) xsdot and xvdot
geoxDvDot = {k: xDvDot[k] for k in geoDVs if k in xDvDot}
if xDvDot is not None and self.DVGeo is not None:
xsdot += self.DVGeo.totalSensitivityProd(geoxDvDot, self.curAP.ptSetName, config=self.curAP.name).reshape(
xsdot.shape
)
# Perturb the coordinates
orig_coords = self.getCoordinates()
if mode == "FD":
self.setCoordinates(orig_coords + xsdot * h)
else:
self.setCoordinatesComplex(orig_coords + xsdot * h)
orig_alpha = copy(self.curAP.alpha)
if "alpha" in xDvDot.keys():
self.curAP.alpha += xDvDot["alpha"] * h
self.__call__(self.curAP, useComplex=mode == "CS", deriv=True)
# Compute the Jacobian vector products
jacVecProd = {}
for f in self.functionList:
if mode == "FD":
jacVecProd[f] = (self.funcs[self.curAP.name][f] - orig_funcs[f]) / h
else:
jacVecProd[f] = np.imag(self.funcsComplex[self.curAP.name][f]) / np.imag(h)
# Reset the perturbed variables
self.curAP.alpha = orig_alpha
if mode == "FD":
self.setCoordinates(orig_coords)
self.funcs[self.curAP.name] = orig_funcs
else:
self.setCoordinatesComplex(orig_coords)
return jacVecProd
def getTriangulatedMeshSurface(self, offsetDist=1.0):
"""
This function returns a pyGeo surface. The intent is
to use this for DVConstraints.
.. note::
This method requires the pyGeo library
Parameters
----------
offsetDist : float
Distance to extrude airfoil (same units as airfoil coordinates)
Returns
-------
pyGeo surface
Extruded airfoil surface
"""
try:
from pygeo.pyGeo import pyGeo
except ImportError:
raise ImportError("pygeo is required to use getTriangulatedMeshSurface")
airfoil_list = [self.DATFileName] * 2
naf = len(airfoil_list)
# Airfoil leading edge positions
x = [0.0, 0.0]
y = [0.0, 0.0]
z = [0.0, offsetDist]
offset = np.zeros((naf, 2)) # x-y offset applied to airfoil position before scaling
# Airfoil rotations
rot_x = [0.0, 0.0]
rot_y = [0.0, 0.0]
rot_z = [0.0, 0.0]
# Airfoil scaling
scale = [1.0, 1.0] # scaling factor on chord lengths
# Find the trailing edge thickness
teThickness = self.coords[0, 1] - self.coords[-1, 1]
return pyGeo(
"liftingSurface",
xsections=airfoil_list,
scale=scale,
offset=offset,
x=x,
y=y,
z=z,
rotX=rot_x,
rotY=rot_y,
rotZ=rot_z,
bluntTe=True,
squareTeTip=True,
teHeight=teThickness,
)
def plotAirfoil(self, fileName=None, showPlot=True):
"""
Plots the current airfoil and returns the figure.
Parameters
----------
fileName : str, optional
FileName to save to, if none specified it will
show the plot with plt.show()
showPlot : bool, optional
Pop open the plot, by default True
Returns
-------
matplotlib figure
Figure with airfoil plotted to it
list of matplotlib axes
List of matplotlib axes for CP and airfoil plots (in that order)
"""
if plt is None:
raise ImportError("matplotlib is required to use the plotting tools")
if self.airfoilFig is None:
# Get data to plot
x = self.coords[:, 0]
y = self.coords[:, 1]
CPUpper = self.sliceData[self.curAP.name]["cp_visc_upper"]
CPUpperInvisc = self.sliceData[self.curAP.name]["cp_invisc_upper"]
xUpper = self.sliceData[self.curAP.name]["x_upper"]
CPLower = self.sliceData[self.curAP.name]["cp_visc_lower"]
CPLowerInvisc = self.sliceData[self.curAP.name]["cp_invisc_lower"]
xLower = self.sliceData[self.curAP.name]["x_lower"]
CFUpper = self.sliceData[self.curAP.name]["cf_upper"]
xCFUpper = self.sliceData[self.curAP.name]["x_cf_upper"]
CFLower = self.sliceData[self.curAP.name]["cf_lower"]
xCFLower = self.sliceData[self.curAP.name]["x_cf_lower"]
# Inverted CP axis
stackedCP = np.hstack((CPUpper, CPUpperInvisc, CPLower, CPLowerInvisc))
stackedCP = stackedCP[np.isfinite(stackedCP)]
stackedCF = np.hstack((CFUpper, CFLower))
stackedCF = stackedCF[np.isfinite(stackedCF)]
self.CPlim = [max(stackedCP) + 0.2, min(stackedCP) - 0.2]
self.CFlim = [min(stackedCF) - 0.002, max(stackedCF) + 0.002]
self.xlimFoil = [min(x) - 0.01, max(x) + 0.01]
self.ylimFoil = [min(y) - 0.01, max(y) + 0.01]
fig, axs = plt.subplots(nrows=3, ncols=1, sharex=True, figsize=[10, 13])
iAxsCP = 0
iAxsCF = 1
iAxsFoil = 2
plt.ion()
if showPlot:
plt.show()
# Plot the CP on the upper axis
axs[iAxsCP].plot(xUpper, CPUpper, color="k", zorder=-1, alpha=0.15)
axs[iAxsCP].plot(xLower, CPLower, color="k", zorder=-1, alpha=0.15)
axs[iAxsCP].plot(xUpper, CPUpper, color=cpUpColor)
axs[iAxsCP].plot(xLower, CPLower, color=cpLowColor)
axs[iAxsCP].plot(xUpper, CPUpperInvisc, "--", color=cpUpColor, linewidth=1.0)
axs[iAxsCP].plot(xLower, CPLowerInvisc, "--", color=cpLowColor, linewidth=1.0)
axs[iAxsCP].set_ylim(self.CPlim)
axs[iAxsCP].set_ylabel("$c_p$", rotation="horizontal", ha="right", va="center")
# Make legend for viscous vs. inviscid
customLines = [
mpl_lines.Line2D([0], [0], linestyle="-", color="k"),
mpl_lines.Line2D([0], [0], linestyle="--", color="k", linewidth=1.0),
]
axs[iAxsCP].legend(customLines, ["Viscous", "Inviscid"])
# Plot the skin friction coefficient
axs[iAxsCF].plot([min(x) - 1, max(x) + 1], [0.0, 0.0], zorder=-2, color="k", linewidth=0.7, alpha=0.3)
axs[iAxsCF].plot(xCFUpper, CFUpper, color="k", zorder=-1, alpha=0.15)
axs[iAxsCF].plot(xCFLower, CFLower, color="k", zorder=-1, alpha=0.15)
axs[iAxsCF].plot(xCFUpper, CFUpper, color=cpUpColor)
axs[iAxsCF].plot(xCFLower, CFLower, color=cpLowColor)
axs[iAxsCF].set_ylim(self.CFlim)
axs[iAxsCF].set_ylabel("$c_f$", rotation="horizontal", ha="right", va="center")
# Plot the airfoil on the lower axis
axs[iAxsFoil].plot(x, y, color="k", zorder=-1, alpha=0.15)
axs[iAxsFoil].plot(x, y, color=color)
axs[iAxsFoil].set_xlim(self.xlimFoil)
axs[iAxsFoil].set_ylim(self.ylimFoil)
axs[iAxsFoil].set_xlabel("x/c")
axs[iAxsFoil].set_ylabel("y/c", rotation="horizontal", ha="right", va="center")
axs[iAxsFoil].set_aspect("equal")
axs[iAxsFoil].spines["right"].set_visible(False)
axs[iAxsFoil].spines["top"].set_visible(False)
axs[iAxsFoil].yaxis.set_ticks_position("left")
axs[iAxsFoil].xaxis.set_ticks_position("bottom")
if fileName is None and showPlot:
plt.pause(0.5)
self.airfoilFig = fig
self.airfoilAxs = axs
else:
self._updateAirfoilPlot(pause=showPlot)
if fileName is not None:
self.airfoilFig.savefig(fileName)
return self.airfoilFig, self.airfoilAxs
def _updateAirfoilPlot(self, pause=True):
"""
Updates the airfoil plot with current airfoil shape.
Assumes that the current figure is the one with the
airfoil on it and it was the most recently plotted line.
This function should not be called directly by the user
unless you know what you're doing.
Parameters
----------
pause : bool
If true, pauses after updating plot for 0.5 sec. This is necessary
when updating a live plot, but breaks the animation in postprocess.
"""
if plt is None:
raise ImportError("matplotlib is required to use the plotting tools")
# Get data to plot
x = self.coords[:, 0]
y = self.coords[:, 1]
CPUpper = self.sliceData[self.curAP.name]["cp_visc_upper"]
CPUpperInvisc = self.sliceData[self.curAP.name]["cp_invisc_upper"]
xUpper = self.sliceData[self.curAP.name]["x_upper"]
CPLower = self.sliceData[self.curAP.name]["cp_visc_lower"]
CPLowerInvisc = self.sliceData[self.curAP.name]["cp_invisc_lower"]
xLower = self.sliceData[self.curAP.name]["x_lower"]
CFUpper = self.sliceData[self.curAP.name]["cf_upper"]
xCFUpper = self.sliceData[self.curAP.name]["x_cf_upper"]
CFLower = self.sliceData[self.curAP.name]["cf_lower"]
xCFLower = self.sliceData[self.curAP.name]["x_cf_lower"]
iAxsCP = 0
iAxsCF = 1
iAxsFoil = 2
# Compute new plot limits
stackedCP = np.hstack((CPUpper, CPUpperInvisc, CPLower, CPLowerInvisc))
stackedCP = stackedCP[np.isfinite(stackedCP)]
stackedCF = np.hstack((CFUpper, CFLower))
stackedCF = stackedCF[np.isfinite(stackedCF)]
CPlim = [max(stackedCP) + 0.2, min(stackedCP) - 0.2]
CPlim = [max(CPlim[0], self.CPlim[0]), min(CPlim[1], self.CPlim[1])] # inverted axis
CFlim = [min(stackedCF) - 0.002, max(stackedCF) + 0.002]
CFlim = [min(CFlim[0], self.CFlim[0]), max(CFlim[1], self.CFlim[1])]
xlimFoil = [min(x) - 0.01, max(x) + 0.01]
ylimFoil = [min(y) - 0.01, max(y) + 0.01]
xlimFoil = [min(xlimFoil[0], self.xlimFoil[0]), max(xlimFoil[1], self.xlimFoil[1])]
ylimFoil = [min(ylimFoil[0], self.ylimFoil[0]), max(ylimFoil[1], self.ylimFoil[1])]
# CP plot