From 4ec5dcea337d5c8ae3d403e1070f12e583a2622c Mon Sep 17 00:00:00 2001 From: Anna Ivagnes Date: Thu, 14 Sep 2023 14:55:11 +0200 Subject: [PATCH] minor fixes --- bladex/__init__.py | 6 +-- bladex/blade.py | 4 +- bladex/profile/customprofile.py | 52 +++++++++++-------- bladex/profile/nacaprofile.py | 19 +++---- bladex/profile/profileinterface.py | 80 ++++++++++++++++++++++-------- tests/test_blade.py | 4 +- 6 files changed, 105 insertions(+), 60 deletions(-) diff --git a/bladex/__init__.py b/bladex/__init__.py index 2605d9a..e527a59 100644 --- a/bladex/__init__.py +++ b/bladex/__init__.py @@ -1,9 +1,9 @@ """ BladeX init """ -__all__ = ['ProfileInterface', 'NacaProfile', 'CustomProfile', 'ReversePropeller', - 'Blade', 'Shaft', 'Propeller', 'Deformation', 'ParamFile', - 'RBF', 'reconstruct_f', 'scipy_bspline'] +__all__ = ['ProfileInterface', 'NacaProfile', 'CustomProfile', + 'ReversePropeller', 'Blade', 'Shaft', 'Propeller', 'Deformation', + 'ParamFile', 'RBF', 'reconstruct_f', 'scipy_bspline'] from .meta import * from .profile import ProfileInterface diff --git a/bladex/blade.py b/bladex/blade.py index 488f144..e516cb5 100644 --- a/bladex/blade.py +++ b/bladex/blade.py @@ -479,7 +479,7 @@ def plot(self, elev=None, azim=None, ax=None, outfile=None): >>> blade_2.rotate(rot_angle_deg=72) >>> fig = plt.figure() - >>> ax = fig.gca(projection=Axes3D.name) + >>> ax = fig.add_subplot(projection='3d') >>> blade_1.plot(ax=ax) >>> blade_2.plot(ax=ax) @@ -504,7 +504,7 @@ def plot(self, elev=None, azim=None, ax=None, outfile=None): ax = ax else: fig = plt.figure() - ax = fig.gca(projection=Axes3D.name) + ax = fig.add_subplot(projection='3d') ax.set_aspect('auto') for i in range(self.n_sections): diff --git a/bladex/profile/customprofile.py b/bladex/profile/customprofile.py index 8377cdc..301d3ce 100644 --- a/bladex/profile/customprofile.py +++ b/bladex/profile/customprofile.py @@ -1,6 +1,6 @@ """ -Derived module from profilebase.py to provide the airfoil coordinates for a general -profile. Input data can be: +Derived module from profilebase.py to provide the airfoil coordinates for a +general profile. Input data can be: - the coordinates arrays; - the chord percentages, the associated nondimensional camber and thickness, the real values of chord lengths, camber and thickness associated to the @@ -9,14 +9,13 @@ import numpy as np from .profileinterface import ProfileInterface -from scipy.optimize import newton class CustomProfile(ProfileInterface): """ - Provide custom profile, given the airfoil coordinates or the airfoil parameters, - i.e. , chord percentages and length, nondimensional and maximum camber, - nondimensional and maximum thickness. + Provide custom profile, given the airfoil coordinates or the airfoil + parameters, i.e. , chord percentages and length, nondimensional and + maximum camber, nondimensional and maximum thickness. If coordinates are directly given as input: @@ -31,16 +30,19 @@ class CustomProfile(ProfileInterface): If section parameters are given as input: - :param numpy.ndarray chord_perc: 1D array that contains the chord percentages - of an airfoil section for which camber and thickness are measured - :param numpy.ndarray camber_perc: 1D array that contains the camber percentage - of an airfoil section at all considered chord percentages. The percentage is - taken with respect to the section maximum camber + :param numpy.ndarray chord_perc: 1D array that contains the chord + percentages of an airfoil section for which camber and thickness are + measured + :param numpy.ndarray camber_perc: 1D array that contains the camber + percentage of an airfoil section at all considered chord percentages. + The percentage is taken with respect to the section maximum camber :param numpy.ndarray thickness_perc: 1D array that contains the thickness percentage of an airfoil section at all considered chord percentages. The percentage is with respect to the section maximum thickness :param float camber_max: the maximum camber at a certain airfoil section - :param float thickness_max: the maximum thickness at a certain airfoil section + :param float chord_len: the chord length at a certain airfoil section + :param float thickness_max: the maximum thickness at a certain airfoil + section """ def __init__(self, **kwargs): super(CustomProfile, self).__init__() @@ -71,12 +73,13 @@ def __init__(self, **kwargs): else: raise RuntimeError( """Input arguments should be the section coordinates - (xup, yup, xdown, ydown) or the section parameters (camber_perc, thickness_perc, + (xup, yup, xdown, ydown) or the section parameters + (camber_perc, thickness_perc, camber_max, thickness_max, chord_perc).""") def generate_parameters(self, convention='british'): return super().generate_parameters(convention) - + def generate_coordinates(self, convention='british'): if convention == 'british': self._compute_coordinates_british_convention() @@ -94,7 +97,7 @@ def _compute_coordinates_british_convention(self): self.thickness_max/2*self.thickness_percentage) self._ydown_coordinates = (self.camber_percentage*self.camber_max - self.thickness_max/2*self.thickness_percentage) - + def _compute_orth_camber_coordinates(self): """ Compute the coordinates of points on upper and lower profile on the @@ -108,8 +111,9 @@ def _compute_orth_camber_coordinates(self): m = np.zeros(n_pos) for i in range(1, n_pos, 1): m[i] = (self.camber_percentage[i]- - self.camber_percentage[i-1])/(self.chord_percentage[i]- - self.chord_percentage[i-1])*self.camber_max/self.chord_length + self.camber_percentage[i-1])*self.camber_max/( + self.chord_percentage[i]- + self.chord_percentage[i-1])/self.chord_length m_angle = np.arctan(m) @@ -127,7 +131,7 @@ def _compute_orth_camber_coordinates(self): yup_tmp[1], ydown_tmp[1] = yup_tmp[2]-1e-16, ydown_tmp[2]-1e-16 return [xup_tmp, xdown_tmp, yup_tmp, ydown_tmp] - + def _compute_coordinates_american_convention(self): """ Compute the coordinates of points on upper and lower profile according @@ -137,8 +141,8 @@ def _compute_coordinates_american_convention(self): self._ydown_coordinates] = self._compute_orth_camber_coordinates() self._ydown_coordinates = self.ydown_curve( - (self.chord_percentage*self.chord_length).reshape(-1,1)).reshape( - self.chord_percentage.shape) + (self.chord_percentage*self.chord_length).reshape(-1,1) + ).reshape(self.chord_percentage.shape) self._yup_coordinates = (2*self.camber_max*self.camber_percentage - self.ydown_coordinates) @@ -161,6 +165,8 @@ def _check_parameters(self): raise ValueError('object "camber_max" refers to an empty array.') if self._thickness_max is None: raise ValueError('object "thickness_max" refers to an empty array.') + if self._chord_length is None: + raise ValueError('object "chord_length" refers to an empty array.') if not isinstance(self._chord_percentage, np.ndarray): self._chord_percentage = np.asarray(self._chord_percentage, @@ -241,7 +247,8 @@ def _check_coordinates(self): # The condition yup_coordinates >= ydown_coordinates must be satisfied # element-wise to the whole elements in the mentioned arrays. if not all( - np.greater_equal(self.yup_coordinates[1:-1], self.ydown_coordinates[1:-1])): + np.greater_equal(self.yup_coordinates[1:-1], + self.ydown_coordinates[1:-1])): raise ValueError('yup is not >= ydown elementwise.') if not np.isclose(self.xdown_coordinates[0], self.xup_coordinates[0], @@ -258,4 +265,5 @@ def _check_coordinates(self): if not np.isclose(self.ydown_coordinates[-1], self.yup_coordinates[-1], atol=1e-6): - raise ValueError('(ydown[0]=yup[0]) not satisfied.') \ No newline at end of file + raise ValueError('(ydown[-1]=yup[-1]) not satisfied.') + diff --git a/bladex/profile/nacaprofile.py b/bladex/profile/nacaprofile.py index b7d0bb1..726d4db 100644 --- a/bladex/profile/nacaprofile.py +++ b/bladex/profile/nacaprofile.py @@ -1,6 +1,6 @@ """ -Derived module from profilebase.py to provide the airfoil coordinates for standard -Naca profiles. +Derived module from profilebase.py to provide the airfoil coordinates for +standard Naca profiles. """ from scipy.interpolate import splev, splrep import numpy as np @@ -24,29 +24,29 @@ class NacaProfile(ProfileInterface): - P/10: indicates the location of the maximum camber measured from the leading edge. The location is normalized by the chord length. - + - TT/100: the maximum thickness as fraction of the chord length. The profile 00TT refers to a symmetrical NACA airfoil. The NACA five-digit series describes more complex airfoil shapes. Its format is: LPSTT, where: - + - L: the theoretical optimum lift coefficient at ideal angle-of-attack = 0.15*L - + - P: the x-coordinate of the point of maximum camber (max camber at x = 0.05*P) - + - S: indicates whether the camber is simple (S=0) or reflex (S=1) TT/100: the maximum thickness in percent of chord, as in a four-digit NACA airfoil code References: - + - Moran, Jack (2003). An introduction to theoretical and computational aerodynamics. Dover. p. 7. ISBN 0-486-42879-6. - + - Abbott, Ira (1959). Theory of Wing Sections: Including a Summary of Airfoil Data. New York: Dover Publications. p. 115. ISBN 978-0486605869. @@ -218,4 +218,5 @@ def generate_coordinates(self): self.ydown_coordinates = yc - yt else: - raise Exception \ No newline at end of file + raise Exception + diff --git a/bladex/profile/profileinterface.py b/bladex/profile/profileinterface.py index 0fbf337..fcd9bf3 100644 --- a/bladex/profile/profileinterface.py +++ b/bladex/profile/profileinterface.py @@ -2,12 +2,12 @@ Base module that provides essential tools and transformations on airfoils. """ +from abc import ABC, abstractmethod import numpy as np import matplotlib.pyplot as plt +from scipy.optimize import newton from ..ndinterpolator import reconstruct_f from scipy.interpolate import RBFInterpolator -from abc import ABC, abstractmethod -from scipy.optimize import newton class ProfileInterface(ABC): """ @@ -45,15 +45,18 @@ def generate_parameters(self, convention='british'): given coordinates. The method generates the airfoil's chord length, chord percentages, - maximum camber, camber percentages, maximum thickness, thickness percentages. - + maximum camber, camber percentages, maximum thickness, thickness + percentages. + :param str convention: convention of the airfoil coordinates. Default value is 'british' """ self._update_edges() # compute chord parameters - self._chord_length = np.linalg.norm(self.leading_edge - self.trailing_edge) - self._chord_percentage = (self.xup_coordinates - np.min(self.xup_coordinates))/self._chord_length + self._chord_length = np.linalg.norm(self.leading_edge - + self.trailing_edge) + self._chord_percentage = (self.xup_coordinates - np.min( + self.xup_coordinates))/self._chord_length # compute camber parameters _camber = (self.yup_coordinates + self.ydown_coordinates)/2 self._camber_max = abs(np.max(_camber)) @@ -89,78 +92,108 @@ def generate_coordinates(self): @property def xup_coordinates(self): + """ + X-coordinates of the upper surface of the airfoil. + """ return self._xup_coordinates - + @xup_coordinates.setter def xup_coordinates(self, xup_coordinates): self._xup_coordinates = xup_coordinates @property def xdown_coordinates(self): + """ + X-coordinates of the lower surface of the airfoil. + """ return self._xdown_coordinates - + @xdown_coordinates.setter def xdown_coordinates(self, xdown_coordinates): self._xdown_coordinates = xdown_coordinates @property def yup_coordinates(self): - return self._yup_coordinates + """ + Y-coordinates of the upper surface of the airfoil. + """ + return self._yup_coordinates @yup_coordinates.setter def yup_coordinates(self, yup_coordinates): self._yup_coordinates = yup_coordinates - + @property def ydown_coordinates(self): + """ + Y-coordinates of the lower surface of the airfoil. + """ return self._ydown_coordinates - + @ydown_coordinates.setter def ydown_coordinates(self, ydown_coordinates): self._ydown_coordinates = ydown_coordinates @property def chord_length(self): + """ + Chord length of the airfoil. + """ return self._chord_length - + @chord_length.setter def chord_length(self, chord_length): self._chord_length = chord_length @property def chord_percentage(self): + """ + Chord percentages of the airfoil. + """ return self._chord_percentage - + @chord_percentage.setter def chord_percentage(self, chord_percentage): self._chord_percentage = chord_percentage @property def camber_max(self): + """ + Maximum camber of the airfoil. + """ return self._camber_max - + @camber_max.setter def camber_max(self, camber_max): self._camber_max = camber_max @property def camber_percentage(self): + """ + Camber percentages of the airfoil. + """ return self._camber_percentage - + @camber_percentage.setter def camber_percentage(self, camber_percentage): self._camber_percentage = camber_percentage @property def thickness_max(self): + """ + Maximum thickness of the airfoil. + """ return self._thickness_max - + @thickness_max.setter def thickness_max(self, thickness_max): self._thickness_max = thickness_max @property def thickness_percentage(self): + """ + Thickness percentages of the airfoil. + """ return self._thickness_percentage @thickness_percentage.setter @@ -345,7 +378,8 @@ def deform_camber_line(self, percent_change, n_interpolated_points=None): The percentage of change is defined as follows: .. math:: - \\frac{\\text{new magnitude of max camber - old magnitude of maximum \ + \\frac{\\text{new magnitude of max camber - old magnitude of + maximum \ camber}}{\\text{old magnitude of maximum camber}} * 100 A positive percentage means the new camber is larger than the max @@ -459,20 +493,21 @@ def _compute_thickness_american(self): # generating temporary profile coordinates orthogonal to the camber # line camber = self._camber_max*self._camber_percentage - ind_horizontal_camber = (np.sin(m_angle)==0) + ind_horizontal_camber = np.sin(m_angle)==0 def eq_to_solve(x): spline_curve = self.ydown_curve(x.reshape(-1,1)).reshape( x.shape[0],) line_orth_camber = (camber[~ind_horizontal_camber] + np.cos(m_angle[~ind_horizontal_camber])/ np.sin(m_angle[~ind_horizontal_camber])*( - self._chord_percentage[~ind_horizontal_camber]*self._chord_length-x)) + self._chord_percentage[~ind_horizontal_camber] + *self._chord_length-x)) return spline_curve - line_orth_camber xdown_tmp = self.xdown_coordinates.copy() xdown_tmp[~ind_horizontal_camber] = newton(eq_to_solve, xdown_tmp[~ind_horizontal_camber]) - xup_tmp = (2*self._chord_percentage*self._chord_length - xdown_tmp) + xup_tmp = 2*self._chord_percentage*self._chord_length - xdown_tmp ydown_tmp = self.ydown_curve(xdown_tmp.reshape(-1,1)).reshape( xdown_tmp.shape[0],) yup_tmp = 2*self._camber_max*self._camber_percentage - ydown_tmp @@ -480,7 +515,7 @@ def eq_to_solve(x): xup_tmp[1], xdown_tmp[1] = xup_tmp[2], xdown_tmp[2] yup_tmp[1], ydown_tmp[1] = yup_tmp[2], ydown_tmp[2] return np.sqrt((xup_tmp-xdown_tmp)**2 + (yup_tmp-ydown_tmp)**2) - + def max_thickness(self, n_interpolated_points=None): """ Return the airfoil's maximum thickness. @@ -502,7 +537,7 @@ def max_thickness(self, n_interpolated_points=None): Phillips, Warren F. (2010). Mechanics of Flight (2nd ed.). \ Wiley & Sons. p. 27. ISBN 978-0-470-53975-0. - Bertin, John J.; Cummings, Russel M. (2009). Pearson Prentice Hall, \ + Bertin, John J.; Cummings, Russel M. (2009). Pearson Prentice Hall,\ ed. Aerodynamics for Engineers (5th ed.). \ p. 199. ISBN 978-0-13-227268-1. @@ -765,3 +800,4 @@ def plot(self, plt.savefig(outfile) else: plt.show() + diff --git a/tests/test_blade.py b/tests/test_blade.py index eaed000..f24f8a2 100644 --- a/tests/test_blade.py +++ b/tests/test_blade.py @@ -450,7 +450,7 @@ def test_plot_ax_multi(self): blade_2 = create_sample_blade_custom() blade_2.apply_transformations() fig = plt.figure() - ax = fig.gca(projection=Axes3D.name) + ax = fig.add_subplot(projection='3d') blade_1.plot(ax=ax) blade_2.plot(ax=ax) plt.close() @@ -755,7 +755,7 @@ def test_solid_errors_exception(self): def test_generate_solid(self): blade = create_sample_blade_NACA() blade.apply_transformations() - blade_solid = blade.generate_solid(max_deg=2, display=False, + blade_solid = blade.generate_solid(max_deg=2, display=False, errors=None) self.assertIsInstance(blade_solid, TopoDS_Solid)