def enforce_length(x, y, z, L0): r0 = np.sum(L0) xn = x.copy() yn = y.copy() zn = z.copy() rn = util.arc_length(np.c_[xn, yn, zn]) while np.abs(rn[-1] - r0) > 1e-5: L = np.diff(rn) xn[1:] = (L0/L) * (xn[1:]-xn[:-1]) + xn[:-1] zn[1:] = (L0/L) * (zn[1:]-zn[:-1]) + zn[:-1] rn = util.arc_length(np.c_[xn, yn, zn]) return xn, zn
def _compute_s(self): """ compute normalized curve length """ s = arc_length(self.points) self.length = s[-1] self.ds = np.diff(s) self.s = s / s[-1]
def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): ############################## def region_stacking(i, idx, start_nd_arc, end_nd_arc, layer_name, layer_thickness, fiber_orientation, layer_mat, material_dict, materials, region_loc): # Recieve start and end of composite sections chordwise, find which composites layers are in each # chordwise regions, generate the precomp composite class instance # error handling to makes sure there were no numeric errors causing values very close too, but not exactly, 0 or 1 start_nd_arc = [ 0. if start_nd_arci != 0. and np.isclose(start_nd_arci, 0.) else start_nd_arci for start_nd_arci in start_nd_arc ] end_nd_arc = [ 0. if end_nd_arci != 0. and np.isclose(end_nd_arci, 0.) else end_nd_arci for end_nd_arci in end_nd_arc ] start_nd_arc = [ 1. if start_nd_arci != 1. and np.isclose(start_nd_arci, 1.) else start_nd_arci for start_nd_arci in start_nd_arc ] end_nd_arc = [ 1. if end_nd_arci != 1. and np.isclose(end_nd_arci, 1.) else end_nd_arci for end_nd_arci in end_nd_arc ] # region end points dp = sorted(list(set(start_nd_arc + end_nd_arc))) #initialize n_plies = [] thk = [] theta = [] mat_idx = [] # loop through division points, find what layers make up the stack between those bounds for i_reg, (dp0, dp1) in enumerate(zip(dp[0:-1], dp[1:])): n_pliesi = [] thki = [] thetai = [] mati = [] for i_sec, start_nd_arci, end_nd_arci in zip( idx, start_nd_arc, end_nd_arc): name = layer_name[i_sec] if start_nd_arci <= dp0 and end_nd_arci >= dp1: if name in region_loc.keys(): if region_loc[name][i] == None: region_loc[name][i] = [i_reg] else: region_loc[name][i].append(i_reg) n_pliesi.append(1.) thki.append(layer_thickness[i_sec]) if fiber_orientation[i_sec] == None: thetai.append(0.) else: thetai.append(fiber_orientation[i_sec]) mati.append(material_dict[layer_mat[i_sec]]) n_plies.append(np.array(n_pliesi)) thk.append(np.array(thki)) theta.append(np.array(thetai)) mat_idx.append(np.array(mati)) # print('----------------------') # print('dp', dp) # print('n_plies', n_plies) # print('thk', thk) # print('theta', theta) # print('mat_idx', mat_idx) # print('materials', materials) sec = CompositeSection(dp, n_plies, thk, theta, mat_idx, materials) return sec, region_loc ############################## def web_stacking(i, web_idx, web_start_nd_arc, web_end_nd_arc, layer_thickness, fiber_orientation, layer_mat, material_dict, materials, flatback, upperCSi): dp = [] n_plies = [] thk = [] theta = [] mat_idx = [] if len(web_idx) > 0: dp = np.mean( (np.abs(web_start_nd_arc), np.abs(web_start_nd_arc)), axis=0).tolist() dp_all = [[-1. * start_nd_arci, -1. * end_nd_arci] for start_nd_arci, end_nd_arci in zip( web_start_nd_arc, web_end_nd_arc)] web_dp, web_ids = np.unique(dp_all, axis=0, return_inverse=True) for webi in np.unique(web_ids): # store variable values (thickness, orientation, material) for layers that make up each web, based on the mapping array web_ids n_pliesi = [ 1. for i_reg, web_idi in zip(web_idx, web_ids) if web_idi == webi ] thki = [ layer_thickness[i_reg] for i_reg, web_idi in zip(web_idx, web_ids) if web_idi == webi ] thetai = [ fiber_orientation[i_reg] for i_reg, web_idi in zip(web_idx, web_ids) if web_idi == webi ] thetai = [ 0. if theta_ij == None else theta_ij for theta_ij in thetai ] mati = [ material_dict[layer_mat[i_reg]] for i_reg, web_idi in zip(web_idx, web_ids) if web_idi == webi ] n_plies.append(np.array(n_pliesi)) thk.append(np.array(thki)) theta.append(np.array(thetai)) mat_idx.append(np.array(mati)) if flatback: dp.append(1.) n_plies.append(upperCSi.n_plies[-1]) thk.append(upperCSi.t[-1]) theta.append(upperCSi.theta[-1]) mat_idx.append(upperCSi.mat_idx[-1]) dp_out = sorted(list(set(dp))) sec = CompositeSection(dp_out, n_plies, thk, theta, mat_idx, materials) return sec ############################## layer_name = self.options['modeling_options']['blade']['layer_name'] layer_mat = self.options['modeling_options']['blade']['layer_mat'] upperCS = [None] * self.n_span lowerCS = [None] * self.n_span websCS = [None] * self.n_span profile = [None] * self.n_span # Check that the layer to be optimized actually exist te_ss_var_ok = False te_ps_var_ok = False spar_cap_ss_var_ok = False spar_cap_ps_var_ok = False for i_layer in range(self.n_layers): if layer_name[i_layer] == self.te_ss_var: te_ss_var_ok = True if layer_name[i_layer] == self.te_ps_var: te_ps_var_ok = True if layer_name[i_layer] == self.spar_cap_ss_var: spar_cap_ss_var_ok = True if layer_name[i_layer] == self.spar_cap_ps_var: spar_cap_ps_var_ok = True if te_ss_var_ok == False: print( 'The layer at the trailing edge suction side is set to be optimized, but does not exist in the input yaml. Please check.' ) if te_ps_var_ok == False: print( 'The layer at the trailing edge pressure side is set to be optimized, but does not exist in the input yaml. Please check.' ) if spar_cap_ss_var_ok == False: print( 'The layer at the spar cap suction side is set to be optimized, but does not exist in the input yaml. Please check.' ) if spar_cap_ps_var_ok == False: print( 'The layer at the spar cap pressure side is set to be optimized, but does not exist in the input yaml. Please check.' ) region_loc_vars = [ self.te_ss_var, self.te_ps_var, self.spar_cap_ss_var, self.spar_cap_ps_var ] region_loc_ss = { } # track precomp regions for user selected composite layers region_loc_ps = {} for var in region_loc_vars: region_loc_ss[var] = [None] * self.n_span region_loc_ps[var] = [None] * self.n_span ## Materials material_dict = {} materials = [] for i_mat in range(self.n_mat): materials.append( Orthotropic2DMaterial(inputs['E'][i_mat, 0], inputs['E'][i_mat, 1], inputs['G'][i_mat, 0], inputs['nu'][i_mat, 0], inputs['rho'][i_mat], discrete_inputs['mat_name'][i_mat])) material_dict[discrete_inputs['mat_name'][i_mat]] = i_mat ## Spanwise for i in range(self.n_span): # time0 = time.time() ## Profiles # rotate profile_i = inputs['coord_xy_interp'][i, :, :] profile_i_rot = np.column_stack( rotate(inputs['pitch_axis'][i], 0., profile_i[:, 0], profile_i[:, 1], np.radians(inputs['theta'][i]))) # import matplotlib.pyplot as plt # plt.plot(profile_i[:,0], profile_i[:,1]) # plt.plot(profile_i_rot[:,0], profile_i_rot[:,1]) # plt.axis('equal') # plt.title(i) # plt.show() # normalize profile_i_rot[:, 0] -= min(profile_i_rot[:, 0]) profile_i_rot = profile_i_rot / max(profile_i_rot[:, 0]) profile_i_rot_precomp = copy.copy(profile_i_rot) idx_s = 0 idx_le_precomp = np.argmax(profile_i_rot_precomp[:, 0]) if idx_le_precomp != 0: if profile_i_rot_precomp[0, 0] == profile_i_rot_precomp[-1, 0]: idx_s = 1 profile_i_rot_precomp = np.row_stack( (profile_i_rot_precomp[idx_le_precomp:], profile_i_rot_precomp[idx_s:idx_le_precomp, :])) profile_i_rot_precomp[:, 1] -= profile_i_rot_precomp[ np.argmin(profile_i_rot_precomp[:, 0]), 1] # # renormalize profile_i_rot_precomp[:, 0] -= min(profile_i_rot_precomp[:, 0]) profile_i_rot_precomp = profile_i_rot_precomp / max( profile_i_rot_precomp[:, 0]) if profile_i_rot_precomp[-1, 0] != 1.: profile_i_rot_precomp = np.row_stack( (profile_i_rot_precomp, profile_i_rot_precomp[0, :])) # 'web' at trailing edge needed for flatback airfoils if profile_i_rot_precomp[0, 1] != profile_i_rot_precomp[ -1, 1] and profile_i_rot_precomp[ 0, 0] == profile_i_rot_precomp[-1, 0]: flatback = True else: flatback = False profile[i] = Profile.initWithTEtoTEdata( profile_i_rot_precomp[:, 0], profile_i_rot_precomp[:, 1]) # import matplotlib.pyplot as plt # plt.plot(profile_i_rot_precomp[:,0], profile_i_rot_precomp[:,1]) # plt.axis('equal') # plt.title(i) # plt.show() idx_le = np.argmin(profile_i_rot[:, 0]) profile_i_arc = arc_length(profile_i_rot) arc_L = profile_i_arc[-1] profile_i_arc /= arc_L loc_LE = profile_i_arc[idx_le] len_PS = 1. - loc_LE ## Composites ss_idx = [] ss_start_nd_arc = [] ss_end_nd_arc = [] ps_idx = [] ps_start_nd_arc = [] ps_end_nd_arc = [] web_start_nd_arc = [] web_end_nd_arc = [] web_idx = [] # Determine spanwise composite layer elements that are non-zero at this spanwise location, # determine their chord-wise start and end location on the pressure and suctions side spline_arc2xnd = PchipInterpolator(profile_i_arc, profile_i_rot[:, 0]) # time1 = time.time() for idx_sec in range(self.n_layers): if discrete_inputs['definition_layer'][idx_sec] != 10: if inputs['layer_thickness'][idx_sec, i] != 0.: if inputs['layer_start_nd'][ idx_sec, i] < loc_LE or inputs['layer_end_nd'][ idx_sec, i] < loc_LE: ss_idx.append(idx_sec) if inputs['layer_start_nd'][idx_sec, i] < loc_LE: # ss_start_nd_arc.append(sec['start_nd_arc']['values'][i]) ss_end_nd_arc_temp = float( spline_arc2xnd( inputs['layer_start_nd'][idx_sec, i])) if ss_end_nd_arc_temp > 1 or ss_end_nd_arc_temp < 0: exit( 'Error in the definition of material ' + layer_name[idx_sec] + '. It cannot fit in the section number ' + str(i) + ' at span location ' + str(inputs['r'][i] / inputs['r'][-1] * 100.) + ' %.') if ss_end_nd_arc_temp == profile_i_rot[ 0, 0] and profile_i_rot[0, 0] != 1.: ss_end_nd_arc_temp = 1. ss_end_nd_arc.append(ss_end_nd_arc_temp) else: ss_end_nd_arc.append(1.) # ss_end_nd_arc.append(min(sec['end_nd_arc']['values'][i], loc_LE)/loc_LE) if inputs['layer_end_nd'][idx_sec, i] < loc_LE: ss_start_nd_arc.append( float( spline_arc2xnd( inputs['layer_end_nd'][idx_sec, i]))) else: ss_start_nd_arc.append(0.) if inputs['layer_start_nd'][ idx_sec, i] > loc_LE or inputs['layer_end_nd'][ idx_sec, i] > loc_LE: ps_idx.append(idx_sec) # ps_start_nd_arc.append((max(sec['start_nd_arc']['values'][i], loc_LE)-loc_LE)/len_PS) # ps_end_nd_arc.append((min(sec['end_nd_arc']['values'][i], 1.)-loc_LE)/len_PS) if inputs['layer_start_nd'][ idx_sec, i] > loc_LE and inputs['layer_end_nd'][ idx_sec, i] < loc_LE: # ps_start_nd_arc.append(float(remap2grid(profile_i_arc, profile_i_rot[:,0], sec['start_nd_arc']['values'][i]))) ps_end_nd_arc.append(1.) else: ps_end_nd_arc_temp = float( spline_arc2xnd( inputs['layer_end_nd'][idx_sec, i])) if np.isclose(ps_end_nd_arc_temp, profile_i_rot[-1, 0] ) and profile_i_rot[-1, 0] != 1.: ps_end_nd_arc_temp = 1. ps_end_nd_arc.append(ps_end_nd_arc_temp) if inputs['layer_start_nd'][idx_sec, i] < loc_LE: ps_start_nd_arc.append(0.) else: ps_start_nd_arc.append( float( spline_arc2xnd( inputs['layer_start_nd'][idx_sec, i]))) else: target_idx = inputs['layer_web'][idx_sec] - 1 if inputs['layer_thickness'][idx_sec, i] != 0.: web_idx.append(idx_sec) start_nd_arc = float( spline_arc2xnd( inputs['web_start_nd'][int(target_idx), i])) end_nd_arc = float( spline_arc2xnd( inputs['web_end_nd'][int(target_idx), i])) web_start_nd_arc.append(start_nd_arc) web_end_nd_arc.append(end_nd_arc) # time1 = time.time() - time1 # print(time1) # generate the Precomp composite stacks for chordwise regions if np.min([ss_start_nd_arc, ss_end_nd_arc]) < 0 or np.max( [ss_start_nd_arc, ss_end_nd_arc]) > 1: print('Error in the layer definition at station number ' + str(i)) exit() upperCS[i], region_loc_ss = region_stacking( i, ss_idx, ss_start_nd_arc, ss_end_nd_arc, layer_name, inputs['layer_thickness'][:, i], inputs['fiber_orientation'][:, i], layer_mat, material_dict, materials, region_loc_ss) lowerCS[i], region_loc_ps = region_stacking( i, ps_idx, ps_start_nd_arc, ps_end_nd_arc, layer_name, inputs['layer_thickness'][:, i], inputs['fiber_orientation'][:, i], layer_mat, material_dict, materials, region_loc_ps) if len(web_idx) > 0 or flatback: websCS[i] = web_stacking(i, web_idx, web_start_nd_arc, web_end_nd_arc, inputs['layer_thickness'][:, i], inputs['fiber_orientation'][:, i], layer_mat, material_dict, materials, flatback, upperCS[i]) else: websCS[i] = CompositeSection([], [], [], [], [], []) sector_idx_strain_spar_cap_ss = [ None if regs == None else regs[int(len(regs) / 2)] for regs in region_loc_ss[self.spar_cap_ss_var] ] sector_idx_strain_spar_cap_ps = [ None if regs == None else regs[int(len(regs) / 2)] for regs in region_loc_ps[self.spar_cap_ps_var] ] sector_idx_strain_te_ss = [ None if regs == None else regs[int(len(regs) / 2)] for regs in region_loc_ss[self.te_ss_var] ] sector_idx_strain_te_ps = [ None if regs == None else regs[int(len(regs) / 2)] for regs in region_loc_ps[self.te_ps_var] ] # Get Beam Properties beam = PreComp(inputs['r'], inputs['chord'], np.zeros_like( inputs['r']), inputs['pitch_axis'], inputs['precurve'], inputs['presweep'], profile, materials, upperCS, lowerCS, websCS, sector_idx_strain_spar_cap_ps, sector_idx_strain_spar_cap_ss, sector_idx_strain_te_ps, sector_idx_strain_te_ss) EIxx, EIyy, GJ, EA, EIxy, x_ec, y_ec, rhoA, area, rhoJ, Tw_iner, flap_iner, edge_iner, x_tc, y_tc, x_sc, y_sc, x_cg, y_cg = beam.sectionProperties( ) # outputs['eps_crit_spar'] = beam.panelBucklingStrain(sector_idx_strain_spar_cap_ss) # outputs['eps_crit_te'] = beam.panelBucklingStrain(sector_idx_strain_te_ss) xu_strain_spar, xl_strain_spar, yu_strain_spar, yl_strain_spar = beam.criticalStrainLocations( sector_idx_strain_spar_cap_ss, sector_idx_strain_spar_cap_ps) xu_strain_te, xl_strain_te, yu_strain_te, yl_strain_te = beam.criticalStrainLocations( sector_idx_strain_te_ss, sector_idx_strain_te_ps) # Store what materials make up the composites for SC/TE for i in range(self.n_span): for j in range(self.n_mat): if sector_idx_strain_spar_cap_ss[i]: if j in upperCS[i].mat_idx[ sector_idx_strain_spar_cap_ss[i]]: outputs['sc_ss_mats'][i, j] = 1. if sector_idx_strain_spar_cap_ps[i]: if j in lowerCS[i].mat_idx[ sector_idx_strain_spar_cap_ps[i]]: outputs['sc_ps_mats'][i, j] = 1. if sector_idx_strain_te_ss[i]: if j in upperCS[i].mat_idx[sector_idx_strain_te_ss[i]]: outputs['te_ss_mats'][i, j] = 1. if sector_idx_strain_te_ps[i]: if j in lowerCS[i].mat_idx[sector_idx_strain_te_ps[i]]: outputs['te_ps_mats'][i, j] = 1. outputs['z'] = inputs['r'] outputs['EIxx'] = EIxx outputs['EIyy'] = EIyy outputs['GJ'] = GJ outputs['EA'] = EA outputs['EIxy'] = EIxy outputs['x_ec'] = x_ec outputs['y_ec'] = y_ec outputs['rhoA'] = rhoA outputs['A'] = area outputs['rhoJ'] = rhoJ outputs['Tw_iner'] = Tw_iner outputs['flap_iner'] = flap_iner outputs['edge_iner'] = edge_iner outputs["x_tc"] = x_tc outputs["y_tc"] = y_tc outputs["x_sc"] = x_sc outputs["y_sc"] = y_sc outputs["x_cg"] = x_cg outputs["y_cg"] = y_cg outputs['xu_strain_spar'] = xu_strain_spar outputs['xl_strain_spar'] = xl_strain_spar outputs['yu_strain_spar'] = yu_strain_spar outputs['yl_strain_spar'] = yl_strain_spar outputs['xu_strain_te'] = xu_strain_te outputs['xl_strain_te'] = xl_strain_te outputs['yu_strain_te'] = yu_strain_te outputs['yl_strain_te'] = yl_strain_te # Compute mass and inertia of blade and rotor blade_mass = np.trapz(rhoA, inputs['r']) blade_moment_of_inertia = np.trapz(rhoA * inputs['r']**2., inputs['r']) tilt = inputs['uptilt'] n_blades = discrete_inputs['n_blades'] mass_all_blades = n_blades * blade_mass Ibeam = n_blades * blade_moment_of_inertia Ixx = Ibeam Iyy = Ibeam / 2.0 # azimuthal average for 2 blades, exact for 3+ Izz = Ibeam / 2.0 Ixy = 0.0 Ixz = 0.0 Iyz = 0.0 # azimuthal average for 2 blades, exact for 3+ # rotate to yaw c.s. I = DirectionVector(Ixx, Iyy, Izz).hubToYaw( tilt) # because off-diagonal components are all zero I_all_blades = np.r_[I.x, I.y, I.z, Ixy, Ixz, Iyz] outputs['blade_mass'] = blade_mass outputs['blade_moment_of_inertia'] = blade_moment_of_inertia outputs['mass_all_blades'] = mass_all_blades outputs['I_all_blades'] = I_all_blades # Placeholder - rotor cost bcm = blade_cost_model(verbosity=self.verbosity) bcm.name = '' bcm.materials = {} bcm.mat_options = {} bcm.mat_options['core_mat_id'] = np.zeros(self.n_mat) bcm.mat_options['coating_mat_id'] = -1 bcm.mat_options['le_reinf_mat_id'] = -1 bcm.mat_options['te_reinf_mat_id'] = -1 for i_mat in range(self.n_mat): name = discrete_inputs['mat_name'][i_mat] # if name != 'resin': bcm.materials[name] = {} bcm.materials[name]['id'] = i_mat + 1 bcm.materials[name]['name'] = discrete_inputs['mat_name'][i_mat] bcm.materials[name]['density'] = inputs['rho'][i_mat] bcm.materials[name]['unit_cost'] = inputs['unit_cost'][i_mat] bcm.materials[name]['waste'] = inputs['waste'][i_mat] * 100. if discrete_inputs['component_id'][i_mat] > 1: # It's a composite bcm.materials[name]['fiber_density'] = inputs['rho_fiber'][ i_mat] bcm.materials[name]['area_density_dry'] = inputs[ 'rho_area_dry'][i_mat] bcm.materials[name]['fvf'] = inputs['fvf'][i_mat] * 100. bcm.materials[name]['fwf'] = inputs['fwf'][i_mat] * 100. bcm.materials[name]['ply_t'] = inputs['ply_t'][i_mat] if discrete_inputs['component_id'][ i_mat] > 3: # The material does not need to be cut@station bcm.materials[name]['cut@station'] = 'N' else: bcm.materials[name]['cut@station'] = 'Y' bcm.materials[name]['roll_mass'] = inputs['roll_mass'][ i_mat] else: bcm.materials[name]['fvf'] = 100. bcm.materials[name]['fwf'] = 100. bcm.materials[name]['cut@station'] = 'N' if discrete_inputs['component_id'][i_mat] <= 0: bcm.materials[name]['ply_t'] = inputs['ply_t'][i_mat] if discrete_inputs['component_id'][i_mat] == 0: bcm.mat_options['coating_mat_id'] = bcm.materials[name][ 'id'] # Assigning the material to the coating elif discrete_inputs['component_id'][i_mat] == 1: bcm.mat_options['core_mat_id'][ bcm.materials[name]['id'] - 1] = 1 # Assigning the material to the core elif discrete_inputs['component_id'][i_mat] == 2: bcm.mat_options['skin_mat_id'] = bcm.materials[name][ 'id'] # Assigning the material to the shell skin elif discrete_inputs['component_id'][i_mat] == 3: bcm.mat_options['skinwebs_mat_id'] = bcm.materials[name][ 'id'] # Assigning the material to the webs skin elif discrete_inputs['component_id'][i_mat] == 4: bcm.mat_options['sc_mat_id'] = bcm.materials[name][ 'id'] # Assigning the material to the spar caps elif discrete_inputs['component_id'][i_mat] == 5: bcm.mat_options['le_reinf_mat_id'] = bcm.materials[name][ 'id'] # Assigning the material to the le reinf bcm.mat_options['te_reinf_mat_id'] = bcm.materials[name][ 'id'] # Assigning the material to the te reinf bcm.upperCS = upperCS bcm.lowerCS = lowerCS bcm.websCS = websCS bcm.profile = profile bcm.chord = inputs['chord'] bcm.r = inputs['r'] - inputs['r'][0] bcm.bladeLength = inputs['r'][-1] - inputs['r'][0] bcm.le_location = inputs['pitch_axis'] blade_cost, blade_mass = bcm.execute_blade_cost_model() outputs['total_blade_cost'] = blade_cost outputs['total_blade_mass'] = blade_mass
def compute(self, inputs, outputs): PBEAM = False # Unpack inputs x_ref = inputs['blade_ref_axis'][:,0] # from PS to SS y_ref = inputs['blade_ref_axis'][:,1] # from LE to TE z_ref = inputs['blade_ref_axis'][:,2] # from root to tip r = util.arc_length(inputs['blade_ref_axis']) blade_length = r[-1] theta = inputs['theta'] chord = inputs['chord'] x_sc = inputs['x_sc'] y_sc = inputs['y_sc'] A = inputs['A'] rhoA = inputs['rhoA'] rhoJ = inputs['rhoJ'] GJ = inputs['GJ'] EA = inputs['EA'] EIxx = inputs['EIxx'] # edge (rotation about x) EIyy = inputs['EIyy'] # flap (rotation about y) EIxy = inputs['EIxy'] lateral_clearance = 0.5*inputs['lateral_clearance'][0] vertical_clearance = inputs['vertical_clearance'][0] max_strains = inputs['max_strains'][0] max_rot = inputs['max_root_rot_deg'][0] max_LV = inputs['max_LV'][0] mass_car_4axle = inputs['max_flatcar_weight_4axle'][0] mass_car_8axle = inputs['max_flatcar_weight_8axle'][0] flatcar_tc_length = inputs['flatcar_tc_length'][0] #------- Get turn radius geometry for horizontal and vertical curves # Horizontal turns- defined as a degree of arc assuming a 100ft "chord" # https://trn.trains.com/railroads/ask-trains/2011/01/measuring-track-curvature angleH_rad = np.deg2rad(inputs['horizontal_angle_deg'][0]) r_curveH = spc.foot * 100. /(2.*np.sin(0.5*angleH_rad)) arcsH = r / r_curveH # Vertical curves on hills and sags defined directly by radius r_curveV = inputs['min_vertical_radius'][0] arcsV = r / r_curveV # ---------- #---------- Put airfoil cross sections into principle axes # Determine principal C.S. (with swap of x, y for profile c.s.) EIxx_cs , EIyy_cs = EIyy.copy() , EIxx.copy() x_sc_cs , y_sc_cs = y_sc.copy() , x_sc.copy() EIxy_cs = EIxy.copy() # translate to elastic center EIxx_cs -= y_sc_cs**2 * EA EIyy_cs -= x_sc_cs**2 * EA EIxy_cs -= x_sc_cs * y_sc_cs * EA # get rotation angle alpha = 0.5*np.arctan2(2*EIxy_cs, EIyy_cs-EIxx_cs) # get moments and positions in principal axes EI11 = EIxx_cs - EIxy_cs*np.tan(alpha) EI22 = EIyy_cs + EIxy_cs*np.tan(alpha) ca = np.cos(alpha) sa = np.sin(alpha) def rotate(x,y): x2 = x*ca + y*sa y2 = -x*sa + y*ca return x2, y2 # Now store alpha for later use in degrees alpha = np.rad2deg(alpha) # ------------------- # ---------- Frame3dd blade prep # Nodes: Prep data, but node x,y,z will shift for vertical and horizontal curves rad = np.zeros(self.n_span) # 'radius' of rigidity at node- set to zero inode = 1+np.arange(self.n_span) # Node numbers (will convert to 1-based indexing later) L = np.diff(r) # Reactions: attachment at root ireact = np.array([inode[0]]) rigid = 1e16 pinned = rigid*np.ones(1) reactions = pyframe3dd.ReactionData(ireact, pinned, pinned, pinned, pinned, pinned, pinned, float(rigid)) # Element data ielem = np.arange(1, self.n_span) # Element Numbers N1 = np.arange(1, self.n_span) # Node number start N2 = np.arange(2, self.n_span+1) # Node number finish E = EA / A rho = rhoA / A J = rhoJ / rho G = GJ / J Ix = EIyy / E if PBEAM else EI11 / E Iy = EIxx / E if PBEAM else EI22 / E Asx = Asy = 1e-6*np.ones(ielem.shape) # Unused when shear=False # Have to convert nodal values to find average at center of element Abar,_ = util.nodal2sectional(A) Ebar,_ = util.nodal2sectional(E) rhobar,_ = util.nodal2sectional(rho) Jbar,_ = util.nodal2sectional(J) Gbar,_ = util.nodal2sectional(G) Ixbar,_ = util.nodal2sectional(Ix) Iybar,_ = util.nodal2sectional(Iy) # Angle of element principal axes relative to global coordinate system # Global c.s. is blade with z from root to tip, y from ss to ps, and x from LE to TE (TE points up) # Local element c.s. is airfoil (twist + principle rotation) if PBEAM: roll = np.zeros(theta.shape) else: roll,_ = util.nodal2sectional(theta + alpha) elements = pyframe3dd.ElementData(ielem, N1, N2, Abar, Asx, Asy, Jbar, Ixbar, Iybar, Ebar, Gbar, roll, rhobar) # Frame3dd options: no need for shear, axial stiffening, or higher resolution force calculations options = pyframe3dd.Options(False, False, -1) #----------- #------ Airfoil positions at which to measure strain # Find the cross sectional points furthest from the elastic center at each spanwise location to be used for strain measurement xps = np.zeros(self.n_span) xss = np.zeros(self.n_span) yps = np.zeros(self.n_span) yss = np.zeros(self.n_span) xle = np.zeros(self.n_span) xte = np.zeros(self.n_span) yle = np.zeros(self.n_span) yte = np.zeros(self.n_span) for i in range(self.n_span): ## Rotate the profiles to the blade reference system profile_i = inputs['coord_xy_interp'][i,:,:] profile_i_rot = np.column_stack(util.rotate(inputs['pitch_axis'][i], 0., profile_i[:,0], profile_i[:,1], np.radians(theta[i]))) # normalize profile_i_rot[:,0] -= min(profile_i_rot[:,0]) profile_i_rot = profile_i_rot/ max(profile_i_rot[:,0]) profile_i_rot_precomp = profile_i_rot.copy() idx_s = 0 idx_le_precomp = np.argmax(profile_i_rot_precomp[:,0]) if idx_le_precomp != 0: if profile_i_rot_precomp[0,0] == profile_i_rot_precomp[-1,0]: idx_s = 1 profile_i_rot_precomp = np.row_stack((profile_i_rot_precomp[idx_le_precomp:], profile_i_rot_precomp[idx_s:idx_le_precomp,:])) profile_i_rot_precomp[:,1] -= profile_i_rot_precomp[np.argmin(profile_i_rot_precomp[:,0]),1] # # renormalize profile_i_rot_precomp[:,0] -= min(profile_i_rot_precomp[:,0]) profile_i_rot_precomp = profile_i_rot_precomp/ max(profile_i_rot_precomp[:,0]) if profile_i_rot_precomp[-1,0] != 1.: profile_i_rot_precomp = np.row_stack((profile_i_rot_precomp, profile_i_rot_precomp[0,:])) # 'web' at trailing edge needed for flatback airfoils if profile_i_rot_precomp[0,1] != profile_i_rot_precomp[-1,1] and profile_i_rot_precomp[0,0] == profile_i_rot_precomp[-1,0]: flatback = True else: flatback = False xnode = profile_i_rot_precomp[:,0] xnode_pa = xnode - inputs['pitch_axis'][i] ynode = profile_i_rot_precomp[:,1] theta_rad = theta[i] * np.pi / 180. xnode_no_theta = xnode_pa * np.cos(-theta_rad) - ynode * np.sin(-theta_rad) ynode_no_theta = xnode_pa * np.sin(-theta_rad) + ynode * np.cos(-theta_rad) xnode_dim_no_theta = xnode_no_theta * chord[i] ynode_dim_no_theta = ynode_no_theta * chord[i] xnode_dim = xnode_dim_no_theta * np.cos(theta_rad) - ynode_dim_no_theta * np.sin(theta_rad) ynode_dim = xnode_dim_no_theta * np.sin(theta_rad) + ynode_dim_no_theta * np.cos(theta_rad) yss[i] = max(ynode_dim) - y_sc_cs[i] yps[i] = y_sc_cs[i] - min(ynode_dim) xte[i] = max(xnode_dim) - x_sc_cs[i] xle[i] = x_sc_cs[i] - min(xnode_dim) # Put these sectional points in airfoil principle directions xps_cs, yps_cs = yps, xps xss_cs, yss_cs = yss, xss ps1, ps2 = rotate(xps_cs, yps_cs) ss1, ss2 = rotate(xss_cs, yss_cs) xle_cs, yle_cs = yle, xle xte_cs, yte_cs = yte, xte le1, le2 = rotate(xle_cs, yle_cs) te1, te2 = rotate(xte_cs, yte_cs) #---------------- #-------- Horizontal curve where we select blade support nodes on flat cars # Gravity field orientation gy = -gravity gx = gz = 0.0 # Set clearance boundary r_envelopeH = r_curveH + lateral_clearance*np.array([-1, 1]) # Use blade shape and clearance envelope to determine node position limits r_envelopeH_inner1 = r_envelopeH.min() + yss r_envelopeH_inner2 = r_envelopeH.min() + yps r_envelopeH_outer1 = r_envelopeH.max() - yps r_envelopeH_outer2 = r_envelopeH.max() - yss # Find rotation angles that keep blade within inner boundary # Function that does the structural analysis to be called during optimization def rotate_blade(ang): # Node location starting points when curving towards SS # (towards the LEFT with LE pointed down and standing at the root looking at tip) x_rot1, z_rot1 = util.rotate(r_curveH, 0.0, r_curveH + x_ref, z_ref, ang[0]) # Node location starting points when curving towards PS # (towards the RIGHT with LE pointed down and standing at the root looking at tip) x_rot2, z_rot2 = util.rotate(-r_curveH, 0.0, -r_curveH + x_ref, z_ref, ang[1]) # Check solved blade shape against envelope r_check1 = np.sqrt( x_rot1**2 + z_rot1**2) r_check2 = np.sqrt( x_rot2**2 + z_rot2**2) # Formulate as constraints for SLSQP cboundary = (np.sum( np.maximum(r_envelopeH_inner1 - r_check1, 0.0) ) + np.sum( np.maximum(r_envelopeH_inner2 - r_check2, 0.0) ) ) return -cboundary # Initiliaze scipy minimization to find the right angle- biggest deflection that keeps blade away from inner envelope const = {} const['type'] = 'ineq' const['fun'] = rotate_blade bounds = [np.deg2rad(max_rot)*np.r_[0,1], np.deg2rad(max_rot)*np.r_[-1,0]] x0 = np.deg2rad( max_rot*np.array([1.0, -1.0]) ) result = minimize(lambda x: -np.sum(np.abs(x)), x0, method='slsqp', bounds=bounds, tol=1e-3, constraints=const) # Now rotate blade at optimized angle # Node location starting points when curving towards SS # (towards the LEFT with LE pointed down and standing at the root looking at tip) x_rot1, z_rot1 = util.rotate(r_curveH, 0.0, r_curveH + x_ref, z_ref, result.x[0]) nodes1 = pyframe3dd.NodeData(inode, x_rot1, y_ref, z_rot1, rad) # Node location starting points when curving towards PS # (towards the RIGHT with LE pointed down and standing at the root looking at tip) x_rot2, z_rot2 = util.rotate(-r_curveH, 0.0, -r_curveH + x_ref, z_ref, result.x[1]) nodes2 = pyframe3dd.NodeData(inode, x_rot2, y_ref, z_rot2, rad) # Initialize Frame3dd objects blade1 = pyframe3dd.Frame(nodes1, reactions, elements, options) blade2 = pyframe3dd.Frame(nodes2, reactions, elements, options) # Tolerance for envelop compliance checking tol = 3e-1 # Function that does the structural analysis to be called during optimization def run_hcurve(FrIn, optFlag=True): Fr = FrIn.reshape((self.n_span-1, 2)) # Will only worry about radial loads Fy = Mx = My = Mz = np.zeros(self.n_span-1) # Output containers RF_derailH = np.zeros(2) # Derailment reaction force r_check = np.zeros((self.n_span, 2)) # Envelope constraint strainPS = np.zeros((self.n_span, 2)) strainSS = np.zeros((self.n_span, 2)) # Look over bend to PS/SS cases for k in range(2): if k == 0: blade, r_outer, angs = blade1, r_envelopeH_outer1, arcsH[1:] else: blade, r_outer, angs = blade2, r_envelopeH_outer2, np.pi-arcsH[1:] # Load case: gravity + blade bending to conform to outer boundary load = pyframe3dd.StaticLoadCase(gx, gy, gz) # Put radial loads in x,z plane Fx = -1e4*Fr[:,k]*np.cos(angs) Fz = -1e4*Fr[:,k]*np.sin(angs) load.changePointLoads(inode[1:], Fx, Fy, Fz, Mx, My, Mz) # Add load case to Frame3DD objects and run blade.clearLoadCases() blade.addLoadCase(load) # Run the case displacements, forces, forces_rxn, internalForces, mass, modal = blade.run() # Check solved blade shape against envelope r_check[:,k] = np.sqrt( (blade.nx+displacements.dx[0,:])**2 + (blade.nz+displacements.dz[0,:])**2) - r_outer # Derailing reaction force on root node # - Lateral force on wheels (multiply by 0.5 for 2 wheel sets) # - Moment around axis perpendicular to ground RF_derailH[k] = 0.5*np.sqrt(forces_rxn.Fx**2 + forces_rxn.Fz**2) + np.abs(forces_rxn.Myy)/flatcar_tc_length # Element shear and bending, one per element, which are already in principle directions in Hansen's notation # Zero-ing out axial stress as there shouldn't be any for pure beam bending Fz = np.r_[-forces.Nx[ 0,0], forces.Nx[ 0, 1::2]] M1 = np.r_[-forces.Myy[0,0], forces.Myy[0, 1::2]] M2 = np.r_[ forces.Mzz[0,0], -forces.Mzz[0, 1::2]] if PBEAM: M1,M2 = rotate(M1,M2) # Compute strain at the two points: pressure/suction side extremes strainPS[:,k] = -(M1/EI11*ps2 - M2/EI22*ps1 + Fz/EA) # negative sign because Hansen c3 is opposite of Precomp z strainSS[:,k] = -(M1/EI11*ss2 - M2/EI22*ss1 + Fz/EA) if optFlag: # First constraint is compliance with outer boundary cboundary = np.maximum(r_check, 0) # Second constraint is reaction forces for derailment: crxn = np.maximum(RF_derailH - (0.5 * mass_car_8axle * gravity)/max_LV, 0.0) # Third constraint is keeping the strains reasonable cstrainPS = np.maximum(np.abs(strainPS) - max_strains, 0.0) cstrainSS = np.maximum(np.abs(strainSS) - max_strains, 0.0) # Accumulate constraints cons = np.array([np.sum(cboundary) , np.sum(crxn) , np.sum(cstrainPS) , np.sum(cstrainSS)]) return -cons else: return RF_derailH, strainPS, strainSS # Initiliaze scipy minimization: Minimize force applied that keeps blade within all constraints const = {} const['type'] = 'ineq' const['fun'] = run_hcurve npts = 2*(self.n_span-1) bounds = [(0.0, 1e2)]*npts x0 = 1e2*np.ones(npts) result = minimize(lambda x: np.sum(np.abs(x)), x0, method='slsqp', bounds=bounds, tol=1e-3, constraints=const, options={'maxiter': 100} ) # if result.success: # Evaluate optimized solution RF_derailH, strainPS, strainSS = run_hcurve(result.x, optFlag=False) # Express derailing force as a constraint outputs['constr_LV_4axle_horiz'] = RF_derailH / (0.5 * mass_car_4axle * gravity) / max_LV outputs['constr_LV_8axle_horiz'] = RF_derailH / (0.5 * mass_car_8axle * gravity) / max_LV # Strain constraint outputs outputs['constr_strainPS'] = np.max(np.abs(strainPS) / max_strains, axis = 1) outputs['constr_strainSS'] = np.max(np.abs(strainSS) / max_strains, axis = 1) # else: # outputs['LV_constraint_4axle_horiz'] = 2. # outputs['LV_constraint_8axle_horiz'] = 2. # outputs['constr_strainPS'] = 2. * np.ones([npts,2]) # outputs['constr_strainSS'] = 2. * np.ones([npts,2]) # print('The optimization cannot satisfy the blade rail transport constraints.') '''