@@ -96,8 +96,8 @@ class Blade(object):
9696 we generate 12 sectional profiles using NACA airfoils and we need to use
9797 them in two different blade classes, then we should instantiate two class
9898 objects for the profiles, as well as the blade. The following example
99- explains the fault and the correct implementations (assuming assuming we
100- already have the arrays radii, chord, pitch, rake, skew):
99+ explains the fault and the correct implementations (assuming we already
100+ have the arrays radii, chord, pitch, rake, skew):
101101
102102 INCORRECT IMPLEMENTATION:
103103
@@ -226,7 +226,8 @@ def _planar_to_cylindrical(self):
226226 \\ forall y_i \\ in Y`
227227
228228 After transformation, the method also fills the numpy.ndarray
229- "blade_coordinates" with the new :math:`(X, Y, Z)` coordinates.
229+ "blade_coordinates_up" and "blade_coordinates_down" with the new
230+ :math:`(X, Y, Z)` coordinates.
230231 """
231232 for section , radius in zip (self .sections , self .radii ):
232233 theta_up = section .yup_coordinates / radius
@@ -235,8 +236,8 @@ def _planar_to_cylindrical(self):
235236 y_section_up = radius * np .sin (theta_up )
236237 y_section_down = radius * np .sin (theta_down )
237238
238- z_section_up = radius * np .cos (theta_up )
239- z_section_down = radius * np .cos (theta_down )
239+ z_section_up = - radius * np .cos (theta_up )
240+ z_section_down = - radius * np .cos (theta_down )
240241
241242 self .blade_coordinates_up .append (
242243 np .array ([section .xup_coordinates , y_section_up , z_section_up ]))
@@ -269,8 +270,16 @@ def apply_transformations(self, reflect=True):
269270 each foil on a cylinder of radius equals to the section radius,
270271 and the cylinder axis is the propeller axis of rotation.
271272
272- :param bool reflect: if true, then reflect the coordinates of all the airfoils
273- about both X-axis and Y-axis. Default value is True.
273+ :param bool reflect: if true, then reflect the coordinates of all the
274+ airfoils about both X-axis and Y-axis. Default value is True.
275+
276+ We note that the implemented transformation operations with the current
277+ Cartesian coordinate system shown in :ref:`mytransformation_operations`
278+ assumes a right-handed propeller. In case of a desired left-handed
279+ propeller the user can either change the code for the negative
280+ Z-coordinates in the cylindrical transformation (i.e.
281+ `_planar_to_cylindrical` private method), or manipulating the
282+ orientation of the generated CAD with respect to the hub.
274283 """
275284 for i in range (self .n_sections ):
276285 # Translate reference point into origin
@@ -300,28 +309,157 @@ def apply_transformations(self, reflect=True):
300309
301310 self ._planar_to_cylindrical ()
302311
303- def plot (self , elev = None , azim = None , outfile = None ):
312+ def rotate (self , deg_angle = None , rad_angle = None ):
313+ """
314+ 3D counter clockwise rotation about the X-axis of the Cartesian
315+ coordinate system, which is the axis of rotation of the propeller hub.
316+
317+ The rotation matrix, :math:`R(\\ theta)`, is used to perform rotation
318+ in the 3D Euclidean space about the X-axis, which is -- by default --
319+ the propeller axis of rotation.
320+
321+ :math:`R(\\ theta)` is defined by:
322+
323+ .. math::
324+ \\ left(\\ begin{matrix} 1 & 0 & 0 \\ \\
325+ 0 & cos (\\ theta) & - sin (\\ theta) \\ \\
326+ 0 & sin (\\ theta) & cos (\\ theta) \\ end{matrix}\\ right)
327+
328+ Given the coordinates of point :math:`P` such that
329+
330+ .. math::
331+ P = \\ left(\\ begin{matrix} x \\ \\
332+ y \\ \\ z \\ end{matrix}\\ right),
333+
334+ Then, the rotated coordinates will be:
335+
336+ .. math::
337+ P^{'} = \\ left(\\ begin{matrix} x^{'} \\ \\
338+ y^{'} \\ \\ z^{'} \\ end{matrix}\\ right)
339+ = R (\\ theta) \\ cdot P
340+
341+ :param float deg_angle: angle in degrees. Default value is None
342+ :param float rad_angle: angle in radians. Default value is None
343+ :raises ValueError: if both rad_angle and deg_angle are inserted,
344+ or if neither is inserted
345+
346+ """
347+ if not self .blade_coordinates_up :
348+ raise ValueError ('You must apply transformations before rotation.' )
349+
350+ # Check rotation angle
351+ if deg_angle is not None and rad_angle is not None :
352+ raise ValueError (
353+ 'You have to pass either the angle in radians or in degrees,' \
354+ ' not both.' )
355+ if rad_angle is not None :
356+ cosine = np .cos (rad_angle )
357+ sine = np .sin (rad_angle )
358+ elif deg_angle is not None :
359+ cosine = np .cos (np .radians (deg_angle ))
360+ sine = np .sin (np .radians (deg_angle ))
361+ else :
362+ raise ValueError (
363+ 'You have to pass either the angle in radians or in degrees.' )
364+
365+ # Rotation is always about the X-axis, which is the center if the hub
366+ # according to the implemented transformation procedure
367+ rot_matrix = np .array ([1 , 0 , 0 , 0 , cosine , - sine , 0 , sine ,
368+ cosine ]).reshape ((3 , 3 ))
369+
370+ for i in range (self .n_sections ):
371+ coord_matrix_up = np .vstack ((self .blade_coordinates_up [i ][0 ],
372+ self .blade_coordinates_up [i ][1 ],
373+ self .blade_coordinates_up [i ][2 ]))
374+ coord_matrix_down = np .vstack ((self .blade_coordinates_down [i ][0 ],
375+ self .blade_coordinates_down [i ][1 ],
376+ self .blade_coordinates_down [i ][2 ]))
377+
378+ new_coord_matrix_up = np .dot (rot_matrix , coord_matrix_up )
379+ new_coord_matrix_down = np .dot (rot_matrix , coord_matrix_down )
380+
381+ self .blade_coordinates_up [i ][0 ] = new_coord_matrix_up [0 ]
382+ self .blade_coordinates_up [i ][1 ] = new_coord_matrix_up [1 ]
383+ self .blade_coordinates_up [i ][2 ] = new_coord_matrix_up [2 ]
384+
385+ self .blade_coordinates_down [i ][0 ] = new_coord_matrix_down [0 ]
386+ self .blade_coordinates_down [i ][1 ] = new_coord_matrix_down [1 ]
387+ self .blade_coordinates_down [i ][2 ] = new_coord_matrix_down [2 ]
388+
389+ def plot (self , elev = None , azim = None , ax = None , outfile = None ):
304390 """
305391 Plot the generated blade sections.
306392
307- :param int elev: Set the view elevation of the axes. This can be used
393+ :param int elev: set the view elevation of the axes. This can be used
308394 to rotate the axes programatically. 'elev' stores the elevation
309395 angle in the z plane. If elev is None, then the initial value is
310396 used which was specified in the mplot3d.Axes3D constructor. Default
311397 value is None
312- :param int azim: Set the view azimuth angle of the axes. This can be
398+ :param int azim: set the view azimuth angle of the axes. This can be
313399 used to rotate the axes programatically. 'azim' stores the azimuth
314400 angle in the x,y plane. If azim is None, then the initial value is
315401 used which was specified in the mplot3d.Axes3D constructor. Default
316402 value is None
403+ :param matplotlib.axes ax: allows to pass the instance of figure axes
404+ to the current plot. This is useful when the user needs to plot the
405+ coordinates of several blade objects on the same figure (see the
406+ example below). If nothing is passed then the method plots on a new
407+ figure axes. Default value is None
317408 :param string outfile: save the plot if a filename string is provided.
318- Default value is None.
409+ Default value is None
410+
411+ EXAMPLE:
412+ Assume we already have the arrays radii, chord, pitch, rake, skew for
413+ 10 blade sections.
414+
415+ >>> sections_1 = np.asarray([blade.NacaProfile(digits='0012')
416+ for i in range(10)])
417+ >>> blade_1 = blade.Blade(sections=sections,
418+ radii=radii,
419+ chord_lengths=chord,
420+ pitch=pitch,
421+ rake=rake,
422+ skew_angles=skew)
423+ >>> blade_1.apply_transformations()
424+
425+ >>> sections_2 = np.asarray([blade.NacaProfile(digits='0012')
426+ for i in range(10)])
427+ >>> blade_2 = blade.Blade(sections=sections,
428+ radii=radii,
429+ chord_lengths=chord,
430+ pitch=pitch,
431+ rake=rake,
432+ skew_angles=skew)
433+ >>> blade_2.apply_transformations()
434+ >>> blade_2.rotate(rot_angle_deg=72)
435+
436+ >>> fig = plt.figure()
437+ >>> ax = fig.gca(projection=Axes3D.name)
438+ >>> blade_1.plot(ax=ax)
439+ >>> blade_2.plot(ax=ax)
440+
441+ On the other hand, if we need to plot for a single blade object,
442+ we can just ignore such parameter, and the method will internally
443+ create a new instance for the figure axes, i.e.
444+
445+ >>> sections = np.asarray([blade.NacaProfile(digits='0012')
446+ for i in range(10)])
447+ >>> blade = blade.Blade(sections=sections,
448+ radii=radii,
449+ chord_lengths=chord,
450+ pitch=pitch,
451+ rake=rake,
452+ skew_angles=skew)
453+ >>> blade.apply_transformations()
454+ >>> blade.plot()
319455 """
320456 if not self .blade_coordinates_up :
321457 raise ValueError ('You must apply transformations before plotting.' )
322-
323- fig = plt .figure ()
324- ax = fig .gca (projection = Axes3D .name )
458+ if ax :
459+ ax = ax
460+ else :
461+ fig = plt .figure ()
462+ ax = fig .gca (projection = Axes3D .name )
325463 ax .set_aspect ('equal' )
326464
327465 for i in range (self .n_sections ):
@@ -606,7 +744,8 @@ def _check_errors(upper_face, lower_face):
606744 :param string lower_face: blade lower face.
607745 """
608746 if not (upper_face or lower_face ):
609- raise ValueError ('Either upper_face or lower_face must not be None.' )
747+ raise ValueError (
748+ 'Either upper_face or lower_face must not be None.' )
610749
611750 def _abs_to_norm (self , D_prop ):
612751 """
0 commit comments