Source code for RCAIDE.Library.Plots.Geometry.plot_3d_rotor

## @ingroup Library-Plots-Geometry  
# RCAIDE/Library/Plots/Geometry/plot_3d_rotor.py
# 
# 
# Created:  Jul 2023, M. Clarke

# ----------------------------------------------------------------------------------------------------------------------
#  IMPORT
# ----------------------------------------------------------------------------------------------------------------------  
from RCAIDE.Framework.Core import Data
from RCAIDE.Library.Plots.Geometry.Common.contour_surface_slice import contour_surface_slice
from RCAIDE.Library.Methods.Geometry.Airfoil import import_airfoil_geometry
from RCAIDE.Library.Methods.Geometry.Airfoil import compute_naca_4series

# python imports 
import numpy as np 
import plotly.graph_objects as go     

# ----------------------------------------------------------------------------------------------------------------------
#  PLOTS
# ----------------------------------------------------------------------------------------------------------------------    
[docs] def plot_3d_rotor(rotor, save_filename = "Rotor", save_figure = False, plot_data = None, show_figure = True, plot_axis = False, cpt = 0, number_of_airfoil_points = 21, color_map = 'turbid', alpha = 1): """ Creates a 3D visualization of a rotor with multiple blades. Parameters ---------- rotor : Rotor RCAIDE rotor data structure containing geometry and blade information save_filename : str, optional Name of file for saved figure (default: "Rotor") save_figure : bool, optional Flag for saving the figure (default: False) plot_data : list, optional Existing plot data to append to (default: None) show_figure : bool, optional Flag to display the figure (default: True) plot_axis : bool, optional Flag to show coordinate axes (default: False) cpt : int, optional Control point at which to plot the rotor (default: 0) number_of_airfoil_points : int, optional Number of points used to discretize airfoil sections (default: 21) color_map : str, optional Color specification for the rotor surface (default: 'turbid') alpha : float, optional Transparency value between 0 and 1 (default: 1) Returns ------- None or plot_data : list If plot_data provided, returns updated list of plot vertices Notes ----- Creates an interactive 3D visualization with: - Multiple blades at specified angular positions - Airfoil sections properly twisted and scaled - Optional coordinate axes - Adjustable view angles """ plot_propeller_only = False if plot_data == None: print("\nPlotting rotor") plot_propeller_only = True camera = dict(up=dict(x=0.5, y=0.5, z=1), center=dict(x=0, y=0, z=-0.5), eye=dict(x=1.5, y=1.5, z=.8)) plot_data = [] num_B = rotor.number_of_blades af_pts = number_of_airfoil_points-1 dim = len(rotor.radius_distribution) for i in range(num_B): G = generate_3d_blade_points(rotor,number_of_airfoil_points,dim,i) # ------------------------------------------------------------------------ # Plot Rotor Blade # ------------------------------------------------------------------------ for sec in range(dim-1): for loc in range(af_pts): X = np.array([[G.XA1[cpt,sec,loc],G.XA2[cpt,sec,loc]], [G.XB1[cpt,sec,loc],G.XB2[cpt,sec,loc]]]) Y = np.array([[G.YA1[cpt,sec,loc],G.YA2[cpt,sec,loc]], [G.YB1[cpt,sec,loc],G.YB2[cpt,sec,loc]]]) Z = np.array([[G.ZA1[cpt,sec,loc],G.ZA2[cpt,sec,loc]], [G.ZB1[cpt,sec,loc],G.ZB2[cpt,sec,loc]]]) values = np.ones_like(X) verts = contour_surface_slice(X, Y, Z ,values,color_map) plot_data.append(verts) axis_limits = np.maximum(np.max(G.XA1), np.maximum(np.max(G.YA1),np.max(G.ZA1)))*2 if plot_propeller_only: fig = go.Figure(data=plot_data) fig.update_scenes(aspectmode = 'auto', xaxis_visible=plot_axis, yaxis_visible=plot_axis, zaxis_visible=plot_axis ) fig.update_layout( width = 1500, height = 1500, scene = dict( xaxis = dict(backgroundcolor="lightgrey", gridcolor="white", showbackground=plot_axis, zerolinecolor="white", range=[-axis_limits,axis_limits]), yaxis = dict(backgroundcolor="lightgrey", gridcolor="white", showbackground=plot_axis, zerolinecolor="white", range=[-axis_limits,axis_limits]), zaxis = dict(backgroundcolor="lightgrey",gridcolor="white",showbackground=plot_axis, zerolinecolor="white", range=[-axis_limits,axis_limits])), scene_camera=camera) fig.update_coloraxes(showscale=False) fig.update_traces(opacity = alpha) if save_figure: fig.write_image(save_filename + ".png") if show_figure: fig.write_html( save_filename + '.html', auto_open=True) return else: return plot_data
[docs] def generate_3d_blade_points(rotor, n_points, dim, i, aircraftRefFrame = True): """ Generates 3D coordinate points for a single rotor blade. Parameters ---------- rotor : Rotor RCAIDE rotor data structure containing blade geometry information n_points : int Number of points around airfoil sections dim : int Number of radial blade sections i : int Blade number (0 to number_of_blades-1) aircraftRefFrame : bool, optional Convert coordinates to aircraft frame if True (default: True) Returns ------- G : Data Data structure containing generated points with attributes: - X, Y, Z : ndarray Raw coordinate points - PTS : ndarray Combined coordinate array - XA1, YA1, ZA1, XA2, YA2, ZA2 : ndarray Leading edge surface points - XB1, YB1, ZB1, XB2, YB2, ZB2 : ndarray Trailing edge surface points Notes ----- Generates blade geometry by: 1. Creating airfoil sections at specified radial positions 2. Applying twist, chord, and thickness distributions 3. Rotating to proper azimuthal position 4. Converting to aircraft frame if requested **Definitions** 'Mid-chord Alignment' Reference point for blade section positioning and twist 'Aircraft Frame' Coordinate system with x-back, z-up orientation """ # unpack num_B = rotor.number_of_blades airfoils = rotor.airfoils beta = rotor.twist_distribution a_o = rotor.start_angle b = rotor.chord_distribution r = rotor.radius_distribution MCA = rotor.mid_chord_alignment t = rotor.max_thickness_distribution a_loc = rotor.airfoil_polar_stations origin = rotor.origin if rotor.clockwise_rotation: # negative chord and twist to give opposite rotation direction b = -b beta = -beta theta = np.linspace(0,2*np.pi,num_B+1)[:-1] flip_2 = (np.pi/2) MCA_2d = np.repeat(np.atleast_2d(MCA).T,n_points,axis=1) b_2d = np.repeat(np.atleast_2d(b).T ,n_points,axis=1) t_2d = np.repeat(np.atleast_2d(t).T ,n_points,axis=1) r_2d = np.repeat(np.atleast_2d(r).T ,n_points,axis=1) airfoil_le_offset = np.repeat(b[:,None], n_points, axis=1)/2 # get airfoil coordinate geometry a_loc = np.array(a_loc) if len(airfoils.keys())>0: xpts = np.zeros((dim,n_points)) zpts = np.zeros((dim,n_points)) max_t = np.zeros(dim) for af_idx,airfoil in enumerate(airfoils): geometry = import_airfoil_geometry(airfoil.coordinate_file,n_points) locs = np.where(a_loc == af_idx) xpts[locs] = geometry.x_coordinates zpts[locs] = geometry.y_coordinates max_t[locs] = geometry.thickness_to_chord else: airfoil_data = compute_naca_4series('2410',n_points) xpts = np.repeat(np.atleast_2d(airfoil_data.x_coordinates) ,dim,axis=0) zpts = np.repeat(np.atleast_2d(airfoil_data.y_coordinates) ,dim,axis=0) max_t = np.repeat(airfoil_data.thickness_to_chord,dim,axis=0) # store points of airfoil in similar format as Vortex Points (i.e. in vertices) max_t2d = np.repeat(np.atleast_2d(max_t).T ,n_points,axis=1) xp = (- MCA_2d + xpts*b_2d - airfoil_le_offset) # x-coord of airfoil yp = r_2d*np.ones_like(xp) # radial location zp = zpts*(t_2d/max_t2d) # former airfoil y coord commanded_thrust_vector = np.zeros((1,1)) rotor_vel_to_body,orientaion = rotor.prop_vel_to_body(commanded_thrust_vector) cpts = len(rotor_vel_to_body[:,0,0]) matrix = np.zeros((len(zp),n_points,3)) # radial location, airfoil pts (same y) matrix[:,:,0] = xp matrix[:,:,1] = yp matrix[:,:,2] = zp matrix = np.repeat(matrix[None,:,:,:], cpts, axis=0) # ROTATION MATRICES FOR INNER SECTION # rotation about y axis to create twist and position blade upright trans_1 = np.zeros((dim,3,3)) trans_1[:,0,0] = np.cos(- beta) trans_1[:,0,2] = -np.sin(- beta) trans_1[:,1,1] = 1 trans_1[:,2,0] = np.sin(- beta) trans_1[:,2,2] = np.cos(- beta) trans_1 = np.repeat(trans_1[None,:,:,:], cpts, axis=0) # rotation about x axis to create azimuth locations trans_2 = np.array([[1 , 0 , 0], [0 , np.cos(theta[i] + a_o + flip_2 ), -np.sin(theta[i] +a_o + flip_2)], [0,np.sin(theta[i] + a_o + flip_2), np.cos(theta[i] + a_o + flip_2)]]) trans_2 = np.repeat(trans_2[None,:,:], dim, axis=0) trans_2 = np.repeat(trans_2[None,:,:,:], cpts, axis=0) # rotation about y to orient propeller/rotor to thrust angle (from propeller frame to aircraft frame) trans_3 = rotor_vel_to_body trans_3 = np.repeat(trans_3[:, None,:,: ],dim,axis=1) trans = np.matmul(trans_2,trans_1) rot_mat = np.repeat(trans[:,:, None,:,:],n_points,axis=2) # --------------------------------------------------------------------------------------------- # ROTATE POINTS if aircraftRefFrame: # rotate all points to the thrust angle with trans_3 mat = np.matmul(np.matmul(rot_mat,matrix[...,None]).squeeze(axis=-1), trans_3) else: # use the rotor frame mat = np.matmul(rot_mat,matrix[...,None]).squeeze(axis=-1) # --------------------------------------------------------------------------------------------- # create empty data structure for storing geometry G = Data() # store node points G.X = mat[:,:,:,0] + origin[0][0] G.Y = mat[:,:,:,1] + origin[0][1] G.Z = mat[:,:,:,2] + origin[0][2] G.PTS = np.zeros((cpts,len(zp),n_points,3)) G.PTS[:,:,:,0] = mat[:,:,:,0] + origin[0][0] G.PTS[:,:,:,1] = mat[:,:,:,1] + origin[0][1] G.PTS[:,:,:,2] = mat[:,:,:,2] + origin[0][2] # store points G.XA1 = mat[:,:-1,:-1,0] + origin[0][0] G.YA1 = mat[:,:-1,:-1,1] + origin[0][1] G.ZA1 = mat[:,:-1,:-1,2] + origin[0][2] G.XA2 = mat[:,:-1,1:,0] + origin[0][0] G.YA2 = mat[:,:-1,1:,1] + origin[0][1] G.ZA2 = mat[:,:-1,1:,2] + origin[0][2] G.XB1 = mat[:,1:,:-1,0] + origin[0][0] G.YB1 = mat[:,1:,:-1,1] + origin[0][1] G.ZB1 = mat[:,1:,:-1,2] + origin[0][2] G.XB2 = mat[:,1:,1:,0] + origin[0][0] G.YB2 = mat[:,1:,1:,1] + origin[0][1] G.ZB2 = mat[:,1:,1:,2] + origin[0][2] return G