Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Commit 3e4d84e

Browse files
authored
Merge pull request #55 from ebranlard/f/welib-updates
Polar: using polar_file for io Tools: adding pandalib, signal_analysis and update of eva IO: update from weio IO: adding graph and SubDyn Summary file mode example Converters: update of hawc2ToBeamDyn (template optional, interp with pandalib and curvilinear interp optional)" Converter: adding hawc2toElastoDyn (tower and blade only) Removing future dependency
2 parents 4bc73f7 + 46e7f86 commit 3e4d84e

28 files changed

+3759
-563
lines changed

.github/workflows/development-pipeline.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
runs-on: ubuntu-latest
1818
strategy:
1919
matrix:
20-
python-version: [3.6, 3.7, 3.8] #
20+
python-version: [3.7, 3.8, 3.9, 3.11] #
2121
steps:
2222
- name: Checkout
2323
uses: actions/checkout@main

data/example_files/FASTSum_5MW_OC3Mnpl.SD.sum.yaml

+387
Large diffs are not rendered by default.

pyFAST/airfoils/Polar.py

+281-187
Large diffs are not rendered by default.

pyFAST/airfoils/examples/correction3D.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def main_correction3D(test=False):
1515
chord_over_r = 3./5.
1616
tsr = 10
1717

18-
polar = Polar(polarFile_in, compute_params=True)
18+
polar = Polar(polarFile_in, compute_params=True, verbose=False)
1919
#ADpol = polar.toAeroDyn(polarFile_AD)
2020
polar3D= polar.correction3D(r_over_R, chord_over_r, tsr)
2121

pyFAST/airfoils/examples/createADPolarFile.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def main_WriteADPolarLowLevel():
6464
# --- Creating a Polar object from Cl-Cd data
6565
polarFile = os.path.join(scriptDir,'../data/DU21_A17.csv')
6666
p=CSVFile(polarFile).toDataFrame().values
67-
polar= Polar(np.nan, p[:,0],p[:,1],p[:,2],p[:,3])
67+
polar= Polar(alpha=p[:,0],cl=p[:,1],cd=p[:,2],cm=p[:,3])
6868
(alpha0,alpha1,alpha2,cnSlope,cn1,cn2,cd0,cm0)=polar.unsteadyParams()
6969

7070
# --- Updating the AD polar file
@@ -109,6 +109,9 @@ def main_WriteADPolarLowLevel():
109109
plt.show()
110110

111111
if __name__ == '__test__':
112+
ADpol,polar = main_ReWriteADFile()
113+
ADpol,polar = main_WriteADPolar()
114+
ADpol,polar = main_WriteADPolar()
112115
try:
113116
os.remove('_Polar_out.dat.ignore')
114117
except:

pyFAST/airfoils/polar_file.py

+291
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
"""
2+
This module contains:
3+
- PolarFile: class to read different kind of polar formats
4+
5+
-
6+
"""
7+
8+
import os
9+
import numpy as np
10+
import pandas as pd
11+
12+
# --- Welib polar readers
13+
try:
14+
from pyFAST.input_output.csv_file import CSVFile
15+
except:
16+
CSVFile=None
17+
try:
18+
from pyFAST.input_output.fast_input_file import ADPolarFile
19+
except:
20+
ADPolarFile=None
21+
try:
22+
from pyFAST.input_output.hawc2_ae_file import HAWC2AEFile
23+
except:
24+
HAWC2AEFile=None
25+
26+
class WrongPolarFormatError(Exception): pass
27+
class BrokenPolarFormatError(Exception): pass
28+
29+
30+
# List of columns used to "unify" the dataframes coming out of "PolarFile"
31+
DEFAULT_COLUMNS={'alpha':'Alpha', 'cl':'Cl', 'cd':'Cd', 'cm':'Cm'}
32+
DEFAULT_COLUMNS_EXT={
33+
'clinv':'Cl_inv', 'clfs':'Cl_fs', 'fs':'fs',
34+
'cn':'Cn', 'cnpot':'Cn_pot', 'cnz12':'Cn_012', 'cnf':'Cn_f', 'cncd0off':'Cn_Cd0off'
35+
}
36+
37+
38+
# --------------------------------------------------------------------------------
39+
# --- Small Helper functions
40+
# --------------------------------------------------------------------------------
41+
def _load_txt(filename, commentChars, skiprows=0, **kwargs):
42+
"""
43+
Similar to np.loadtxt but also works if comments are present anywhere in the file (e.g. end of file)
44+
"""
45+
with open(filename) as f:
46+
lines = (line for iline, line in enumerate(f) if not line.startswith(commentChars) and iline>=skiprows)
47+
Lines = list(lines)
48+
if len(Lines)==0:
49+
raise Exception('Zero lines')
50+
else:
51+
return np.loadtxt(Lines, **kwargs)
52+
53+
54+
# --------------------------------------------------------------------------------}
55+
# --- Simple classes
56+
# --------------------------------------------------------------------------------{
57+
class BasePolarFile(dict):
58+
def __init__(self, filename=None):
59+
super().__init__()
60+
self.COMMENT_CHARS=('#','!','%')
61+
self['header'] = ''
62+
self['columns'] = []
63+
self['data'] = np.array([[]])
64+
self['nPolars'] = 0
65+
if filename is not None:
66+
self.read(filename)
67+
68+
def __repr__(self):
69+
s='<{} object>:\n'.format(type(self).__name__)
70+
s+='- header: {}\n'.format(self['header'])
71+
s+='- columns: {}\n'.format(self['columns'])
72+
s+='- nPolars:{}\n'.format(self['nPolars'])
73+
s+='- data: shape {}\n'.format(self['data'].shape)
74+
s+=' first: {}\n'.format(self['data'][0,:])
75+
s+=' last: {}\n'.format(self['data'][-1,:])
76+
return s
77+
78+
@staticmethod
79+
def formatName(): raise NotImplementedError()
80+
81+
def toDataFrame(self):
82+
if self['nPolars']==1:
83+
return pd.DataFrame(data=self['data'], columns=self['columns'])
84+
else:
85+
raise NotImplementedError()
86+
87+
class PolarFile_OneLineHeader(BasePolarFile):
88+
""" Polar file with exatcly one line of header.
89+
Column names in header can be separated by spaces or commas.
90+
Header may start with the following comment characters: ['#','!','%']
91+
Data may be space or column separated
92+
"""
93+
@staticmethod
94+
def formatName(): return 'Polar file one header line'
95+
96+
def read(self, filename):
97+
super().__init__()
98+
with open(filename) as f:
99+
header = f.readline().strip()
100+
second = f.readline()
101+
self['header'] = header
102+
for c in self.COMMENT_CHARS:
103+
header = header.lstrip(c)
104+
sep=','
105+
try:
106+
self['data'] = np.loadtxt(filename, delimiter=sep, skiprows=1)
107+
except:
108+
sep=None
109+
self['data'] = np.loadtxt(filename, delimiter=sep, skiprows=1)
110+
self['nPolars']=1
111+
112+
# --- Detect columns
113+
nCols = self['data'].shape[1]
114+
# First, if all values are numeric, abort
115+
onestring = header.replace(',',' ')
116+
try:
117+
vals = np.array(onestring.split()).astype(float) # This should fail
118+
except:
119+
pass # Great, it actually failed, the first line is not made of floats
120+
else:
121+
raise WrongPolarFormatError('The first line is all numeric, it should contain column names')
122+
# Then, try to split by commas or space
123+
colsComma = header.split(',')
124+
colsSpace = header.split()
125+
if len(colsComma)==nCols:
126+
cols = [c.strip() for c in colsComma]
127+
elif len(colsSpace)==nCols:
128+
cols = colsSpace
129+
else:
130+
raise BrokenPolarFormatError('The number of header columns ({}) does not match the number of columns in the data ({})'.format(len(cols),nCols))
131+
self['columns'] = cols
132+
133+
class PolarFile_NoHeader(BasePolarFile):
134+
"""
135+
Polar file with no header, or some "meaningless" comments that starts with ['#','!','%']
136+
Data may be space or column separated
137+
"""
138+
@staticmethod
139+
def formatName(): return 'Polar file no header'
140+
141+
def read(self, filename):
142+
self['data'] = _load_txt(filename, self.COMMENT_CHARS)
143+
self['nPolars'] = 1
144+
# --- Detect columns
145+
nCols = self['data'].shape[1]
146+
d = [DEFAULT_COLUMNS['alpha'], DEFAULT_COLUMNS['cl'], DEFAULT_COLUMNS['cd'], DEFAULT_COLUMNS['cm']]
147+
n2col = {2:d[0:2], 3:d[0:3], 4:d[0:4] }
148+
if nCols in n2col.keys():
149+
self['columns'] = n2col[nCols]
150+
else:
151+
raise BrokenPolarFormatError('The number of columns in the data ({}) is not amongst the supported ones ({}).'.format(nCols, n2col.keys()))
152+
153+
class PolarFile_AD_Basic(BasePolarFile):
154+
"""
155+
Reads a basic AeroDyn file
156+
"""
157+
@staticmethod
158+
def formatName(): return 'Polar AeroDyn file basic'
159+
160+
def read(self, filename):
161+
self['data'] = _load_txt(filename, self.COMMENT_CHARS, skiprows = 53)
162+
self['nPolars'] = 1
163+
# import pandas as pd
164+
# df=pd.read_csv(filename, skiprows = 53, header=None, delim_whitespace=True, names=['Alpha','Cl','Cd','Cm']).values
165+
# --- Detect columns
166+
nCols = self['data'].shape[1]
167+
n2col = {2:['Alpha','Cl'], 3:['Alpha','Cl', 'Cm'], 4:['Alpha','Cl', 'Cm', 'Cd'] }
168+
if nCols in n2col.keys():
169+
self['columns'] = n2col[nCols]
170+
else:
171+
raise BrokenPolarFormatError('The number of columns in the data ({}) is not amongst the supported ones ({}).'.format(nCols, n2col.keys()))
172+
173+
174+
class PolarFile(BasePolarFile):
175+
""" """
176+
@staticmethod
177+
def formatName(): return 'Polar file'
178+
179+
180+
def loadPolarFile(filename, fformat='auto', to_radians=False, standardizeCols=True, verbose=False):
181+
"""
182+
Loads a PolarFile, return a dataFrame
183+
"""
184+
if not os.path.exists(filename):
185+
raise Exception('File not found:',filename)
186+
print('[WARN] Not all file formats supported ')
187+
188+
allReaders = [ADPolarFile, PolarFile_OneLineHeader, PolarFile_NoHeader, PolarFile_AD_Basic, CSVFile]
189+
delimReaders = [PolarFile_OneLineHeader, PolarFile_AD_Basic, CSVFile]
190+
191+
def tryReading(f, reader):
192+
if f is not None:
193+
return f
194+
if reader is None:
195+
return None
196+
try:
197+
if verbose:
198+
print('')
199+
print('PolarFile: trying to read with format: {}'.format(reader.formatName()))
200+
return reader(filename)
201+
except:
202+
if verbose:
203+
print('>>> PolarFile: Failed to read with format: {}'.format(reader.formatName()))
204+
pass
205+
f = None
206+
Re = np.nan # TODO
207+
208+
if fformat==None:
209+
fformat = 'auto'
210+
211+
if fformat=='ADPolar':
212+
f = ADPolarFile(filename)
213+
214+
elif fformat=='delimited':
215+
216+
for reader in delimReaders:
217+
f = tryReading(f, reader)
218+
if f is not None:
219+
break
220+
221+
elif fformat=='auto':
222+
223+
for reader in allReaders:
224+
f = tryReading(f, reader)
225+
if f is not None:
226+
break
227+
228+
if f is None:
229+
raise Exception('Unable to read the polar {} using the fileformat {}. Use a supported fileformat'.format(filename, fformat))
230+
231+
# --- Store in DataFrame
232+
df = f.toDataFrame()
233+
if verbose:
234+
print('PolarFile: Columns before: ',df.columns.values)
235+
236+
# --- Rename columns - Standardize column names
237+
if standardizeCols:
238+
COLS_TODO= {**DEFAULT_COLUMNS,**DEFAULT_COLUMNS_EXT}
239+
for ic, col in enumerate(df.columns):
240+
c = col.strip().lower().replace('_','')
241+
c = c.replace('aoa','alpha')
242+
c = c.replace('fst','fs')
243+
c = c.replace('012','z12')
244+
c = c.replace('cllin','clinv')
245+
c = c.replace('clpot','clinv')
246+
known_keys = reversed(sorted(list(COLS_TODO.keys())))
247+
found=False
248+
for kk in known_keys:
249+
if c.startswith(kk):
250+
cnew = COLS_TODO.pop(kk)
251+
#print('changing {} to {}'.format(c, cnew))
252+
df.columns.values[ic] = cnew # rename column
253+
found=True
254+
break
255+
if not found:
256+
print('[WARN] PolarFile: The following column was not understood: {}'.format(col))
257+
258+
# --- Standardize data
259+
for k,v in DEFAULT_COLUMNS.items():
260+
if v not in df.columns:
261+
df[v] = np.nan
262+
if verbose:
263+
print('PolarFile: Columns after: ',df.columns.values)
264+
265+
if standardizeCols:
266+
cAlpha = DEFAULT_COLUMNS['alpha']
267+
if cAlpha not in df.columns:
268+
raise Exception('Angle of attack was not detected as part of the columns')
269+
else:
270+
cAlpha = df.columns.values[0]
271+
272+
if to_radians:
273+
# First, check the data, if the max alpha is above pi, most likely we are in degrees
274+
_radians = np.mean(np.abs(df[cAlpha])) <= np.pi / 2
275+
if _radians:
276+
raise Exception('PolarFile: Asked to convert input to radian, but the data is likely already in radians.')
277+
df[cAlpha]*=np.pi/180
278+
279+
Re = np.nan
280+
return df, Re
281+
282+
if __name__ == "__main__":
283+
from welib.tools.clean_exceptions import *
284+
# PolarFile_OneLineHeader('data/63-235.csv')
285+
#f = PolarFile_NoHeader('data/63-235.csv')
286+
# f = loadPolarFile('data/63-235.csv')
287+
f = loadPolarFile('data/FFA-W3-241-Re12M.dat', verbose=True)
288+
#f = loadPolarFile('data/Cylinder.dat')
289+
#f = loadPolarFile('../../data/NREL5MW/5MW_Baseline/Airfoils/DU21_A17.dat')
290+
print(f)
291+
pass

pyFAST/airfoils/tests/test_polar_interp.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ class TestPolarInterp(unittest.TestCase):
1111

1212
def test_interp(self):
1313
# --- Interpolation of self is self
14-
P1=Polar.fromfile(os.path.join(MyDir,'../data/FFA-W3-241-Re12M.dat'))
15-
P2=Polar.fromfile(os.path.join(MyDir,'../data/FFA-W3-241-Re12M.dat'))
14+
P1=Polar(os.path.join(MyDir,'../data/FFA-W3-241-Re12M.dat'))
15+
P2=Polar(os.path.join(MyDir,'../data/FFA-W3-241-Re12M.dat'))
1616
P3= blend(P1,P2,0.5)
1717
np.testing.assert_equal(P3.alpha,P1.alpha)
1818
np.testing.assert_equal(P3.cl,P1.cl)

pyFAST/airfoils/tests/test_polar_manip.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ def assertNaN(self,x):
1212
self.assertTrue(np.isnan(x))
1313

1414
def test_read(self):
15-
P=Polar.fromfile(os.path.join(MyDir,'../data/FFA-W3-241-Re12M.dat'))
15+
P=Polar(os.path.join(MyDir,'../data/FFA-W3-241-Re12M.dat'))
1616
self.assertEqual(P.alpha[-1],180)
1717
self.assertEqual(P.cl[-1],0)
1818

19-
P=Polar.fromfile(os.path.join(MyDir,'../data/Cylinder.dat'))
19+
P=Polar(os.path.join(MyDir,'../data/Cylinder.dat'))
2020
self.assertEqual(P.cl.size,3)
2121

2222
if __name__ == '__main__':

0 commit comments

Comments
 (0)