Skip to content

Commit 7302612

Browse files
annaivagnesndem0
authored andcommitted
fix parametrization in sections
1 parent de92e45 commit 7302612

File tree

5 files changed

+314
-144
lines changed

5 files changed

+314
-144
lines changed

bladex/blade.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -313,16 +313,17 @@ def apply_transformations(self, reflect=True):
313313

314314
self._planar_to_cylindrical()
315315

316-
def rotate(self, deg_angle=None, rad_angle=None):
316+
def rotate(self, deg_angle=None, rad_angle=None, axis='x'):
317317
"""
318-
3D counter clockwise rotation about the X-axis of the Cartesian
318+
3D counter clockwise rotation about the specified axis of the Cartesian
319319
coordinate system, which is the axis of rotation of the propeller hub.
320320
321321
The rotation matrix, :math:`R(\\theta)`, is used to perform rotation
322-
in the 3D Euclidean space about the X-axis, which is -- by default --
323-
the propeller axis of rotation.
322+
in the 3D Euclidean space about the specified axis, which is
323+
-- by default -- the x axis.
324324
325-
:math:`R(\\theta)` is defined by:
325+
:math: when the axis of rotation is the x-axis `R(\\theta)` is defined
326+
by:
326327
327328
.. math::
328329
\\left(\\begin{matrix} 1 & 0 & 0 \\\\
@@ -344,6 +345,7 @@ def rotate(self, deg_angle=None, rad_angle=None):
344345
345346
:param float deg_angle: angle in degrees. Default value is None
346347
:param float rad_angle: angle in radians. Default value is None
348+
:param string axis: cartesian axis of rotation. Default value is 'x'
347349
:raises ValueError: if both rad_angle and deg_angle are inserted,
348350
or if neither is inserted
349351
@@ -371,6 +373,14 @@ def rotate(self, deg_angle=None, rad_angle=None):
371373
rot_matrix = np.array([1, 0, 0, 0, cosine, -sine, 0, sine,
372374
cosine]).reshape((3, 3))
373375

376+
if axis=='y':
377+
rot_matrix = np.array([cosine, 0, -sine, 0, 1, 0, sine, 0,
378+
cosine]).reshape((3, 3))
379+
380+
if axis=='z':
381+
rot_matrix = np.array([cosine, -sine, 0, sine, cosine, 0,
382+
0, 0, 1]).reshape((3, 3))
383+
374384
for i in range(self.n_sections):
375385
coord_matrix_up = np.vstack((self.blade_coordinates_up[i][0],
376386
self.blade_coordinates_up[i][1],
@@ -390,6 +400,37 @@ def rotate(self, deg_angle=None, rad_angle=None):
390400
self.blade_coordinates_down[i][1] = new_coord_matrix_down[1]
391401
self.blade_coordinates_down[i][2] = new_coord_matrix_down[2]
392402

403+
def scale(self, factor):
404+
'''
405+
Scale the blade coordinates by a specified factor.
406+
407+
:param float factor: scaling factor
408+
409+
'''
410+
scaling_matrix = np.array([factor, 0, 0, 0, factor,
411+
0, 0, 0, factor]).reshape((3, 3))
412+
413+
for i in range(self.n_sections):
414+
coord_matrix_up = np.vstack((self.blade_coordinates_up[i][0],
415+
self.blade_coordinates_up[i][1],
416+
self.blade_coordinates_up[i][2]))
417+
coord_matrix_down = np.vstack((self.blade_coordinates_down[i][0],
418+
self.blade_coordinates_down[i][1],
419+
self.blade_coordinates_down[i][2]))
420+
421+
new_coord_matrix_up = np.dot(scaling_matrix, coord_matrix_up)
422+
new_coord_matrix_down = np.dot(scaling_matrix, coord_matrix_down)
423+
424+
self.blade_coordinates_up[i][0] = new_coord_matrix_up[0]
425+
self.blade_coordinates_up[i][1] = new_coord_matrix_up[1]
426+
self.blade_coordinates_up[i][2] = new_coord_matrix_up[2]
427+
428+
self.blade_coordinates_down[i][0] = new_coord_matrix_down[0]
429+
self.blade_coordinates_down[i][1] = new_coord_matrix_down[1]
430+
self.blade_coordinates_down[i][2] = new_coord_matrix_down[2]
431+
432+
433+
393434
def plot(self, elev=None, azim=None, ax=None, outfile=None):
394435
"""
395436
Plot the generated blade sections.
@@ -784,7 +825,7 @@ def generate_iges(self,
784825
:param string root: if string is passed then the method generates
785826
the blade root using the BRepOffsetAPI_ThruSections algorithm
786827
in order to close the blade at the root, then exports the generated
787-
CAD into .iges file holding the name <tip_string>.iges.
828+
CAD into .iges file holding the name <tip_string>.iges.
788829
Default value is None
789830
:param int max_deg: Define the maximal U degree of generated surface.
790831
Default value is 1
@@ -949,12 +990,12 @@ def generate_stl(self, upper_face=None,
949990
:param string tip: if string is passed then the method generates
950991
the blade tip using the BRepOffsetAPI_ThruSections algorithm
951992
in order to close the blade at the tip, then exports the generated
952-
CAD into .stl file holding the name <tip_string>.stl.
993+
CAD into .stl file holding the name <tip_string>.stl.
953994
Default value is None
954995
:param string root: if string is passed then the method generates
955996
the blade root using the BRepOffsetAPI_ThruSections algorithm
956-
in order to close the blade at the root, then exports the generated
957-
CAD into .stl file holding the name <tip_string>.stl.
997+
in order to close the blade at the root, then exports the generated
998+
CAD into .stl file holding the name <tip_string>.stl.
958999
Default value is None
9591000
:param int max_deg: Define the maximal U degree of generated surface.
9601001
Default value is 1

bladex/customprofile.py

Lines changed: 121 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
profile. Input data can be:
44
- the coordinates arrays;
55
- the chord percentages, the associated nondimensional camber and thickness,
6-
the real values of chord lengths, camber and thickness associated to the
6+
the real values of chord lengths, camber and thickness associated to the
77
single blade sections.
88
"""
99

@@ -14,9 +14,9 @@
1414
class CustomProfile(ProfileBase):
1515
"""
1616
Provide custom profile, given the airfoil coordinates or the airfoil parameters,
17-
i.e. , chord percentages and length, nondimensional and maximum camber,
17+
i.e. , chord percentages and length, nondimensional and maximum camber,
1818
nondimensional and maximum thickness.
19-
19+
2020
If coordinates are directly given as input:
2121
2222
:param numpy.ndarray xup: 1D array that contains the X-components of the
@@ -27,17 +27,16 @@ class CustomProfile(ProfileBase):
2727
airfoil's upper surface
2828
:param numpy.ndarray ydown: 1D array that contains the Y-components of the
2929
airfoil's lower surface
30-
:param float chord_len: the chord length of the airfoil section
31-
30+
3231
If section parameters are given as input:
33-
32+
3433
:param numpy.ndarray chord_perc: 1D array that contains the chord percentages
3534
of an airfoil section for which camber and thickness are measured
3635
:param numpy.ndarray camber_perc: 1D array that contains the camber percentage
3736
of an airfoil section at all considered chord percentages. The percentage is
3837
taken with respect to the section maximum camber
39-
:param numpy.ndarray thickness_perc: 1D array that contains the thickness
40-
percentage of an airfoil section at all considered chord percentages.
38+
:param numpy.ndarray thickness_perc: 1D array that contains the thickness
39+
percentage of an airfoil section at all considered chord percentages.
4140
The percentage is with respect to the section maximum thickness
4241
:param float chord_len: the length of the chord line of a certain airfoil section
4342
:param float camber_max: the maximum camber at a certain airfoil section
@@ -51,9 +50,7 @@ def __init__(self, **kwargs):
5150
self.yup_coordinates = kwargs['yup']
5251
self.xdown_coordinates = kwargs['xdown']
5352
self.ydown_coordinates = kwargs['ydown']
54-
self.chord_len = kwargs.get('chord_len', 1)
55-
56-
self._generate_parameters()
53+
self._check_coordinates()
5754

5855
elif set(kwargs.keys()) == set([
5956
'chord_perc', 'camber_perc', 'thickness_perc', 'chord_len',
@@ -66,24 +63,22 @@ def __init__(self, **kwargs):
6663
self.chord_len = kwargs['chord_len']
6764
self.camber_max = kwargs['camber_max']
6865
self.thickness_max = kwargs['thickness_max']
66+
self._check_parameters()
6967

70-
self._generate_coordinates()
7168
else:
7269
raise RuntimeError(
7370
"""Input arguments should be the section coordinates
7471
(xup, yup, xdown, ydown) and chord_len (optional)
7572
or the section parameters (camber_perc, thickness_perc,
7673
camber_max, thickness_max, chord_perc, chord_len).""")
7774

78-
self._check_args()
79-
self._check_coordinates()
8075

81-
def _check_args(self):
76+
def _check_parameters(self):
8277
"""
8378
Private method that checks whether the airfoil parameters defined
8479
are provided correctly.
85-
In particular, the chord, camber and thickness percentages are consistent and
86-
have the same length.
80+
In particular, the chord, camber and thickness percentages are
81+
consistent and have the same length.
8782
"""
8883

8984
if self.chord_percentage is None:
@@ -131,72 +126,124 @@ def _check_args(self):
131126
raise ValueError(
132127
'thickness_perc and chord_perc must have same shape.')
133128

134-
def _generate_coordinates(self):
129+
def _compute_orth_camber_coordinates(self):
130+
'''
131+
Compute the coordinates of points on upper and lower profile on the
132+
line orthogonal to the camber line.
133+
134+
:return: x and y coordinates of section points on line orthogonal to
135+
camber line
136+
'''
137+
# Compute the angular coefficient of the camber line
138+
n_pos = self.chord_percentage.shape[0]
139+
m = np.zeros(n_pos)
140+
for i in range(1, n_pos, 1):
141+
m[i] = (self.camber_percentage[i]-
142+
self.camber_percentage[i-1])/(self.chord_percentage[i]-
143+
self.chord_percentage[i-1])*self.camber_max/self.chord_len
144+
145+
m_angle = np.arctan(m)
146+
147+
xup_tmp = (self.chord_percentage*self.chord_len -
148+
self.thickness_percentage*np.sin(m_angle)*self.thickness_max/2)
149+
xdown_tmp = (self.chord_percentage*self.chord_len +
150+
self.thickness_percentage*np.sin(m_angle)*self.thickness_max/2)
151+
yup_tmp = (self.camber_percentage*self.camber_max +
152+
self.thickness_max/2*self.thickness_percentage*np.cos(m_angle))
153+
ydown_tmp = (self.camber_percentage*self.camber_max -
154+
self.thickness_max/2*self.thickness_percentage*np.cos(m_angle))
155+
156+
if xup_tmp[1]<0:
157+
xup_tmp[1], xdown_tmp[1] = xup_tmp[2]-1e-16, xdown_tmp[2]-1e-16
158+
yup_tmp[1], ydown_tmp[1] = yup_tmp[2]-1e-16, ydown_tmp[2]-1e-16
159+
160+
return xup_tmp, xdown_tmp, yup_tmp, ydown_tmp
161+
162+
163+
164+
def generate_coordinates(self):
135165
"""
136-
Private method that generates the coordinates of a general airfoil profile,
137-
starting from the chord percantages and the related nondimensional
138-
camber and thickness. input data should be integrated with the information
139-
of chord length, camber and thickness of specific sections.
166+
Method that generates the coordinates of a general airfoil
167+
profile, starting from the chord percentages and the related
168+
nondimensional camber and thickness, the maximum values of thickness
169+
and camber.
140170
"""
141171

142-
# compute the angular coefficient of the camber line at each chord
143-
# percentage and convert it from degrees to radiant
144-
n_pos = len(self.chord_percentage)
145-
m = np.zeros(n_pos)
146-
m[0] = 0
147-
for i in range(1, len(self.chord_percentage), 1):
148-
m[i] = (self.camber_percentage[i] -
149-
self.camber_percentage[i - 1]) / (
150-
self.chord_percentage[i] - self.chord_percentage[i - 1])
151-
152-
m_angle = m * np.pi / 180
153-
self.xup_coordinates = np.zeros(n_pos)
154-
self.xdown_coordinates = np.zeros(n_pos)
155-
self.yup_coordinates = np.zeros(n_pos)
156-
self.ydown_coordinates = np.zeros(n_pos)
157-
158-
#compute the coordinates starting from a single section data and parameters
159-
for j in range(0, n_pos, 1):
160-
self.xup_coordinates[j] = self.chord_percentage[j]
161-
self.xdown_coordinates[j] = self.chord_percentage[j]
162-
self.yup_coordinates[j] = (
163-
self.camber_percentage[j] * self.camber_max +
164-
self.thickness_percentage[j] * self.thickness_max *
165-
np.cos(m_angle[j])) * self.chord_len
166-
self.ydown_coordinates[j] = (
167-
self.camber_percentage[j] * self.camber_max -
168-
self.thickness_percentage[j] * self.thickness_max *
169-
np.cos(m_angle[j])) * self.chord_len
170-
171-
def _generate_parameters(self):
172+
self.xup_coordinates, self.xdown_coordinates, self.yup_coordinates, self.ydown_coordinates = self._compute_orth_camber_coordinates()
173+
174+
self.ydown_coordinates = self.ydown_curve(
175+
self.chord_len*(self.chord_percentage).reshape(-1,1)).reshape(
176+
self.chord_percentage.shape)
177+
self.xup_coordinates = self.chord_percentage * self.chord_len
178+
self.xdown_coordinates = self.xup_coordinates.copy()
179+
self.yup_coordinates = (2*self.camber_max*self.camber_percentage -
180+
self.ydown_coordinates)
181+
182+
self.yup_coordinates[0] = 0
183+
self.yup_coordinates[-1] = 0
184+
self.ydown_coordinates[0] = 0
185+
self.ydown_coordinates[-1] = 0
186+
187+
188+
def adimensionalize(self):
172189
'''
173-
Private method to find parameters related to each section
174-
(chord percentages, camber max, camber percentages,
175-
thickness max and thickness percentage) starting from the
176-
xup, yup, xdown, ydown coordinates of the section.
177-
Useful for parametrization and deformation.
190+
Rescale coordinates of upper and lower profiles of section such that
191+
coordinates on x axis are between 0 and 1.
178192
'''
179-
self.chord_percentage = self.xup_coordinates
193+
factor = abs(self.xup_coordinates[-1]-self.xup_coordinates[0])
194+
self.yup_coordinates *= 1/factor
195+
self.xdown_coordinates *= 1/factor
196+
self.ydown_coordinates *= 1/factor
197+
self.xup_coordinates *= 1/factor
198+
199+
def generate_parameters(self):
200+
'''
201+
Method that generates the parameters of a general airfoil profile
202+
(chord length, chord percentages, camber max, thickness max, camber and
203+
thickness percentages), starting from the upper and lower
204+
coordinates of the section profile.
205+
'''
206+
n_pos = self.xup_coordinates.shape[0]
207+
208+
self.chord_len = abs(np.max(self.xup_coordinates)-
209+
np.min(self.xup_coordinates))
210+
self.chord_percentage = self.xup_coordinates/self.chord_len
211+
camber = (self.yup_coordinates + self.ydown_coordinates)/2
212+
self.camber_max = abs(np.max(camber)-np.min(camber))
213+
self.camber_percentage = camber/self.camber_max
214+
215+
n_pos = self.chord_percentage.shape[0]
216+
m = np.zeros(n_pos)
217+
for i in range(1, n_pos, 1):
218+
m[i] = (self.camber_percentage[i]-
219+
self.camber_percentage[i-1])/(self.chord_percentage[i]-
220+
self.chord_percentage[i-1])*self.camber_max/self.chord_len
221+
m_angle = np.arctan(m)
180222

181-
camber = (self.yup_coordinates +
182-
self.ydown_coordinates) / (2 * self.chord_len)
183-
self.camber_max = np.max(camber)
184-
self.camber_percentage = camber / self.camber_max
223+
from scipy.optimize import newton
185224

186-
delta_camber = np.zeros(len(camber))
187-
delta_x = np.zeros(len(camber))
188-
m_rad = np.zeros(len(camber))
189-
for i in range(1, len(camber), 1):
190-
delta_camber[
191-
i] = self.camber_percentage[i] - self.camber_percentage[i - 1]
192-
delta_x[i] = self.chord_percentage[i] - self.chord_percentage[i - 1]
225+
ind_horizontal_camber = (np.sin(m_angle)==0)
226+
def eq_to_solve(x):
227+
spline_curve = self.ydown_curve(x.reshape(-1,1)).reshape(x.shape[0],)
228+
line_orth_camber = (camber[~ind_horizontal_camber] +
229+
np.cos(m_angle[~ind_horizontal_camber])/
230+
np.sin(m_angle[~ind_horizontal_camber])*(self.chord_len*
231+
self.chord_percentage[~ind_horizontal_camber]-x))
232+
return spline_curve - line_orth_camber
193233

194-
m_rad[1:] = delta_camber[1:] * np.pi / (delta_x[1:] * 180)
234+
xdown_tmp = self.xdown_coordinates.copy()
235+
xdown_tmp[~ind_horizontal_camber] = newton(eq_to_solve,
236+
xdown_tmp[~ind_horizontal_camber])
237+
xup_tmp = 2*self.chord_len*self.chord_percentage - xdown_tmp
238+
ydown_tmp = self.ydown_curve(xdown_tmp.reshape(-1,1)).reshape(xdown_tmp.shape[0],)
239+
yup_tmp = 2*self.camber_max*self.camber_percentage - ydown_tmp
240+
if xup_tmp[1]<self.xup_coordinates[0]:
241+
xup_tmp[1], xdown_tmp[1] = xup_tmp[2], xdown_tmp[2]
242+
yup_tmp[1], ydown_tmp[1] = yup_tmp[2], ydown_tmp[2]
195243

196-
thickness = (self.yup_coordinates - self.ydown_coordinates) / (
197-
2 * self.chord_len * np.cos(m_rad))
244+
thickness = np.sqrt((xup_tmp-xdown_tmp)**2 + (yup_tmp-ydown_tmp)**2)
198245
self.thickness_max = np.max(thickness)
199-
self.thickness_percentage = thickness / self.thickness_max
246+
self.thickness_percentage = thickness/self.thickness_max
200247

201248
def _check_coordinates(self):
202249
"""
@@ -255,7 +302,7 @@ def _check_coordinates(self):
255302

256303
if not self.xdown_coordinates[0] == self.xup_coordinates[0]:
257304
raise ValueError('(xdown[0]=xup[0]) not satisfied.')
258-
if not self.ydown_coordinates[0] == self.yup_coordinates[0]:
305+
if not np.allclose(self.ydown_coordinates[0], self.yup_coordinates[0]):
259306
raise ValueError('(ydown[0]=yup[0]) not satisfied.')
260307
if not self.xdown_coordinates[-1] == self.xup_coordinates[-1]:
261308
raise ValueError('(xdown[-1]=xup[-1]) not satisfied.')

0 commit comments

Comments
 (0)