def compute(self, inputs, outputs): frame3dd_opt = self.options["frame3dd_opt"] # ------- node data ---------------- z = inputs["z"] n = len(z) node = np.arange(1, n + 1) x = np.zeros(n) y = np.zeros(n) r = np.zeros(n) nodes = pyframe3dd.NodeData(node, x, y, z, r) # ----------------------------------- # ------ reaction data ------------ # rigid base node = inputs["kidx"] + np.ones(len(inputs["kidx"])) # add one because 0-based index but 1-based node numbering rigid = RIGID reactions = pyframe3dd.ReactionData( node, inputs["kx"], inputs["ky"], inputs["kz"], inputs["ktx"], inputs["kty"], inputs["ktz"], rigid ) # ----------------------------------- # ------ frame element data ------------ element = np.arange(1, n) N1 = np.arange(1, n) N2 = np.arange(2, n + 1) roll = np.zeros(n - 1) # average across element b.c. frame3dd uses constant section elements # TODO: Use nodal2sectional Az = inputs["Az"] Asx = inputs["Asx"] Asy = inputs["Asy"] Jz = inputs["Jz"] Ixx = inputs["Ixx"] Iyy = inputs["Iyy"] E = inputs["E"] G = inputs["G"] rho = inputs["rho"] elements = pyframe3dd.ElementData(element, N1, N2, Az, Asx, Asy, Jz, Ixx, Iyy, E, G, roll, rho) # ----------------------------------- # ------ options ------------ dx = -1.0 options = pyframe3dd.Options(frame3dd_opt["shear"], frame3dd_opt["geom"], dx) # ----------------------------------- # initialize frame3dd object cylinder = pyframe3dd.Frame(nodes, reactions, elements, options) # ------ add extra mass ------------ # extra node inertia data N = inputs["midx"] + np.ones(len(inputs["midx"])) add_gravity = True cylinder.changeExtraNodeMass( N, inputs["m"], inputs["mIxx"], inputs["mIyy"], inputs["mIzz"], inputs["mIxy"], inputs["mIxz"], inputs["mIyz"], inputs["mrhox"], inputs["mrhoy"], inputs["mrhoz"], add_gravity, ) # ------------------------------------ # ------- enable dynamic analysis ---------- Mmethod = 1 lump = 0 shift = 0.0 # Run twice the number of modes to ensure that we can ignore the torsional modes and still get the desired number of fore-aft, side-side modes cylinder.enableDynamics(2 * NFREQ, Mmethod, lump, frame3dd_opt["tol"], shift) # ---------------------------- # ------ static load case 1 ------------ # gravity in the X, Y, Z, directions (global) gx = 0.0 gy = 0.0 gz = -gravity load = pyframe3dd.StaticLoadCase(gx, gy, gz) # point loads nF = inputs["plidx"] + np.ones(len(inputs["plidx"])) load.changePointLoads(nF, inputs["Fx"], inputs["Fy"], inputs["Fz"], inputs["Mxx"], inputs["Myy"], inputs["Mzz"]) # distributed loads Px, Py, Pz = inputs["Pz"], inputs["Py"], -inputs["Px"] # switch to local c.s. z = inputs["z"] # trapezoidally distributed loads EL = np.arange(1, n) xx1 = xy1 = xz1 = np.zeros(n - 1) xx2 = xy2 = xz2 = np.diff(z) - 1e-6 # subtract small number b.c. of precision wx1 = Px[:-1] wx2 = Px[1:] wy1 = Py[:-1] wy2 = Py[1:] wz1 = Pz[:-1] wz2 = Pz[1:] load.changeTrapezoidalLoads(EL, xx1, xx2, wx1, wx2, xy1, xy2, wy1, wy2, xz1, xz2, wz1, wz2) cylinder.addLoadCase(load) # Debugging # cylinder.write('temp.3dd') # ----------------------------------- # run the analysis displacements, forces, reactions, internalForces, mass, modal = cylinder.run() iCase = 0 # mass outputs["mass"] = mass.struct_mass # natural frequncies outputs["f1"] = modal.freq[0] outputs["f2"] = modal.freq[1] outputs["freqs"] = modal.freq[:NFREQ] # Get all mode shapes in batch NFREQ2 = int(NFREQ / 2) freq_x, freq_y, mshapes_x, mshapes_y = util.get_xy_mode_shapes( z, modal.freq, modal.xdsp, modal.ydsp, modal.zdsp, modal.xmpf, modal.ympf, modal.zmpf ) outputs["x_mode_freqs"] = freq_x[:NFREQ2] outputs["y_mode_freqs"] = freq_y[:NFREQ2] outputs["x_mode_shapes"] = mshapes_x[:NFREQ2, :] outputs["y_mode_shapes"] = mshapes_y[:NFREQ2, :] # deflections due to loading (from cylinder top and wind/wave loads) outputs["top_deflection"] = displacements.dx[iCase, n - 1] # in yaw-aligned direction # shear and bending, one per element (convert from local to global c.s.) Fz = forces.Nx[iCase, 1::2] Vy = forces.Vy[iCase, 1::2] Vx = -forces.Vz[iCase, 1::2] Mzz = forces.Txx[iCase, 1::2] Myy = forces.Myy[iCase, 1::2] Mxx = -forces.Mzz[iCase, 1::2] # Record total forces and moments outputs["base_F"] = -1.0 * np.array([reactions.Fx.sum(), reactions.Fy.sum(), reactions.Fz.sum()]) outputs["base_M"] = -1.0 * np.array([reactions.Mxx.sum(), reactions.Myy.sum(), reactions.Mzz.sum()]) outputs["Fz_out"] = Fz outputs["Vx_out"] = Vx outputs["Vy_out"] = Vy outputs["Mxx_out"] = Mxx outputs["Myy_out"] = Myy outputs["Mzz_out"] = Mzz # axial and shear stress d, _ = util.nodal2sectional(inputs["d"]) qdyn, _ = util.nodal2sectional(inputs["qdyn"]) ##R = self.d/2.0 ##x_stress = R*np.cos(self.theta_stress) ##y_stress = R*np.sin(self.theta_stress) ##axial_stress = Fz/self.Az + Mxx/self.Ixx*y_stress - Myy/self.Iyy*x_stress # V = Vy*x_stress/R - Vx*y_stress/R # shear stress orthogonal to direction x,y # shear_stress = 2. * V / self.Az # coefficient of 2 for a hollow circular section, but should be conservative for other shapes outputs["axial_stress"] = ( Fz / inputs["Az"] - np.sqrt(Mxx ** 2 + Myy ** 2) / inputs["Iyy"] * d / 2.0 ) # More conservative, just use the tilted bending and add total max shear as well at the same point, if you do not like it go back to the previous lines outputs["shear_stress"] = ( 2.0 * np.sqrt(Vx ** 2 + Vy ** 2) / inputs["Az"] ) # coefficient of 2 for a hollow circular section, but should be conservative for other shapes # hoop_stress (Eurocode method) L_reinforced = self.options["buckling_length"] * np.ones(Fz.shape) outputs["hoop_stress_euro"] = hoopStressEurocode(inputs["z"], d, inputs["t"], L_reinforced, qdyn) # Simpler hoop stress used in API calculations outputs["hoop_stress"] = hoopStress(d, inputs["t"], qdyn)
def compute(self, inputs, outputs): frame3dd_opt = self.options["frame3dd_opt"] # ------- node data ---------------- z = inputs["z"] n = len(z) node = np.arange(1, n + 1) x = np.zeros(n) y = np.zeros(n) r = np.zeros(n) nodes = pyframe3dd.NodeData(node, x, y, z, r) # ----------------------------------- # ------ reaction data ------------ # rigid base node = inputs["kidx"] + np.ones(len(inputs["kidx"])) # add one because 0-based index but 1-based node numbering rigid = RIGID reactions = pyframe3dd.ReactionData( node, inputs["kx"], inputs["ky"], inputs["kz"], inputs["ktx"], inputs["kty"], inputs["ktz"], rigid ) # ----------------------------------- # ------ frame element data ------------ element = np.arange(1, n) N1 = np.arange(1, n) N2 = np.arange(2, n + 1) roll = np.zeros(n - 1) # Element properties Az = inputs["Az"] Asx = inputs["Asx"] Asy = inputs["Asy"] Jz = inputs["Jz"] Ixx = inputs["Ixx"] Iyy = inputs["Iyy"] E = inputs["E"] G = inputs["G"] rho = inputs["rho"] elements = pyframe3dd.ElementData(element, N1, N2, Az, Asx, Asy, Jz, Ixx, Iyy, E, G, roll, rho) # ----------------------------------- # ------ options ------------ dx = -1.0 options = pyframe3dd.Options(frame3dd_opt["shear"], frame3dd_opt["geom"], dx) # ----------------------------------- # initialize frame3dd object cylinder = pyframe3dd.Frame(nodes, reactions, elements, options) # ------ add extra mass ------------ # extra node inertia data N = inputs["midx"] + np.ones(len(inputs["midx"])) add_gravity = True cylinder.changeExtraNodeMass( N, inputs["m"], inputs["mIxx"], inputs["mIyy"], inputs["mIzz"], inputs["mIxy"], inputs["mIxz"], inputs["mIyz"], inputs["mrhox"], inputs["mrhoy"], inputs["mrhoz"], add_gravity, ) # ------------------------------------ # ------- enable dynamic analysis ---------- Mmethod = 1 lump = 0 shift = 0.0 # Run twice the number of modes to ensure that we can ignore the torsional modes and still get the desired number of fore-aft, side-side modes cylinder.enableDynamics(2 * NFREQ, Mmethod, lump, frame3dd_opt["tol"], shift) # ---------------------------- # ------ static load case 1 ------------ # gravity in the X, Y, Z, directions (global) gx = 0.0 gy = 0.0 gz = -gravity load = pyframe3dd.StaticLoadCase(gx, gy, gz) # point loads nF = inputs["plidx"] + np.ones(len(inputs["plidx"])) load.changePointLoads(nF, inputs["Fx"], inputs["Fy"], inputs["Fz"], inputs["Mxx"], inputs["Myy"], inputs["Mzz"]) # distributed loads Px, Py, Pz = inputs["Pz"], inputs["Py"], -inputs["Px"] # switch to local c.s. z = inputs["z"] # trapezoidally distributed loads EL = np.arange(1, n) xx1 = xy1 = xz1 = np.zeros(n - 1) xx2 = xy2 = xz2 = np.diff(z) - 1e-6 # subtract small number b.c. of precision wx1 = Px[:-1] wx2 = Px[1:] wy1 = Py[:-1] wy2 = Py[1:] wz1 = Pz[:-1] wz2 = Pz[1:] load.changeTrapezoidalLoads(EL, xx1, xx2, wx1, wx2, xy1, xy2, wy1, wy2, xz1, xz2, wz1, wz2) cylinder.addLoadCase(load) # Debugging # cylinder.write('temp.3dd') # ----------------------------------- # run the analysis displacements, forces, reactions, internalForces, mass, modal = cylinder.run() ic = 0 # mass outputs["mass"] = mass.struct_mass # natural frequncies outputs["f1"] = modal.freq[0] outputs["f2"] = modal.freq[1] outputs["structural_frequencies"] = modal.freq[:NFREQ] # Get all mode shapes in batch NFREQ2 = int(NFREQ / 2) freq_x, freq_y, freq_z, mshapes_x, mshapes_y, mshapes_z = util.get_xyz_mode_shapes( z, modal.freq, modal.xdsp, modal.ydsp, modal.zdsp, modal.xmpf, modal.ympf, modal.zmpf ) outputs["fore_aft_freqs"] = freq_x[:NFREQ2] outputs["side_side_freqs"] = freq_y[:NFREQ2] outputs["torsion_freqs"] = freq_z[:NFREQ2] outputs["fore_aft_modes"] = mshapes_x[:NFREQ2, :] outputs["side_side_modes"] = mshapes_y[:NFREQ2, :] outputs["torsion_modes"] = mshapes_z[:NFREQ2, :] # deflections due to loading (from cylinder top and wind/wave loads) outputs["tower_deflection"] = np.sqrt( displacements.dx[ic, :] ** 2 + displacements.dy[ic, :] ** 2 ) # in yaw-aligned direction outputs["top_deflection"] = outputs["tower_deflection"][-1] # Record total forces and moments ibase = 2 * int(inputs["kidx"].max()) outputs["base_F"] = -np.r_[-forces.Vz[ic, ibase], forces.Vy[ic, ibase], forces.Nx[ic, ibase]] outputs["base_M"] = -np.r_[-forces.Mzz[ic, ibase], forces.Myy[ic, ibase], forces.Txx[ic, ibase]] # Forces and moments along the structure outputs["tower_Fz"] = forces.Nx[ic, 1::2] outputs["tower_Vx"] = -forces.Vz[ic, 1::2] outputs["tower_Vy"] = forces.Vy[ic, 1::2] outputs["tower_Mxx"] = -forces.Mzz[ic, 1::2] outputs["tower_Myy"] = forces.Myy[ic, 1::2] outputs["tower_Mzz"] = forces.Txx[ic, 1::2]
elements = pyframe3dd.ElementData(ielement, N1, N2, Ax, Asy, Asz, Jx, Iy, Iz, E, G, roll, density) # 2 ----------------------------------- # 3 ------ other data ------------ shear = True # 1: include shear deformation geom = True # 1: include geometric stiffness dx = 20.0 # x-axis increment for internal forces other = pyframe3dd.Options(shear, geom, dx) # 3 ----------------------------------- # 4 ------ initialize frame3dd object frame = pyframe3dd.Frame(nodes, reactions, elements, other) # 4 ----------------------------------- # 5 ------ static load case 1 ------------ # gravity in the X, Y, Z, directions (global) gx = 0.0 gy = 0.0 gz = -9806.33 load = pyframe3dd.StaticLoadCase(gx, gy, gz) # point load nF = np.array([1]) Fx = np.array([100.0]) Fy = np.array([-200.0]) Fz = np.array([-100.0])
def compute(self, inputs, outputs): # Unpack variables opt = self.options["options"] n_attach = opt["mooring"]["n_attach"] m_rna = float(inputs["rna_mass"]) cg_rna = inputs["rna_cg"] I_rna = inputs["rna_I"] I_trans = inputs["transition_piece_I"] fairlead_joints = inputs["mooring_fairlead_joints"] mooringF = inputs["mooring_neutral_load"] # Create frame3dd instance: nodes, elements, reactions, and options for frame in ["tower", "system"]: nodes = inputs[frame + "_nodes"] nnode = np.where(nodes[:, 0] == NULL)[0][0] nodes = nodes[:nnode, :] rnode = np.zeros(nnode) # inputs[frame + "_Rnode"][:nnode] Fnode = inputs[frame + "_Fnode"][:nnode, :] Mnode = np.zeros((nnode, 3)) ihub = np.argmax(nodes[:, 2]) - 1 itrans = util.closest_node(nodes, inputs["transition_node"]) N1 = np.int_(inputs[frame + "_elem_n1"]) nelem = np.where(N1 == NULL)[0][0] N1 = N1[:nelem] N2 = np.int_(inputs[frame + "_elem_n2"][:nelem]) A = inputs[frame + "_elem_A"][:nelem] Asx = inputs[frame + "_elem_Asx"][:nelem] Asy = inputs[frame + "_elem_Asy"][:nelem] Ixx = inputs[frame + "_elem_Ixx"][:nelem] Iyy = inputs[frame + "_elem_Iyy"][:nelem] Izz = inputs[frame + "_elem_Izz"][:nelem] rho = inputs[frame + "_elem_rho"][:nelem] E = inputs[frame + "_elem_E"][:nelem] G = inputs[frame + "_elem_G"][:nelem] roll = np.zeros(nelem) inodes = np.arange(nnode) + 1 node_obj = pyframe3dd.NodeData(inodes, nodes[:, 0], nodes[:, 1], nodes[:, 2], rnode) ielem = np.arange(nelem) + 1 elem_obj = pyframe3dd.ElementData(ielem, N1 + 1, N2 + 1, A, Asx, Asy, Izz, Ixx, Iyy, E, G, roll, rho) # TODO: Hydro_K + Mooring_K for tower (system too?) rid = np.array([itrans]) # np.array([np.argmin(nodes[:, 2])]) Rx = Ry = Rz = Rxx = Ryy = Rzz = np.array([RIGID]) react_obj = pyframe3dd.ReactionData(rid + 1, Rx, Ry, Rz, Rxx, Ryy, Rzz, rigid=RIGID) frame3dd_opt = opt["WISDEM"]["FloatingSE"]["frame3dd"] opt_obj = pyframe3dd.Options(frame3dd_opt["shear"], frame3dd_opt["geom"], -1.0) myframe = pyframe3dd.Frame(node_obj, react_obj, elem_obj, opt_obj) # Added mass m_trans = float(inputs["transition_piece_mass"]) if frame == "tower": # TODO: Added mass and stiffness m_trans += float(inputs["platform_mass"]) cg_trans = inputs["transition_node"] - inputs[ "platform_center_of_mass"] else: cg_trans = np.zeros(3) add_gravity = True mID = np.array([itrans, ihub], dtype=np.int_).flatten() m_add = np.array([m_trans, m_rna]).flatten() I_add = np.c_[I_trans, I_rna] cg_add = np.c_[cg_trans, cg_rna] myframe.changeExtraNodeMass( mID + 1, m_add, I_add[0, :], I_add[1, :], I_add[2, :], I_add[3, :], I_add[4, :], I_add[5, :], cg_add[0, :], cg_add[1, :], cg_add[2, :], add_gravity, ) # Dynamics if frame == "tower" and frame3dd_opt["modal"]: Mmethod = 1 lump = 0 shift = 0.0 myframe.enableDynamics(2 * NFREQ, Mmethod, lump, frame3dd_opt["tol"], shift) # Initialize loading with gravity, mooring line forces, and buoyancy (already in nodal forces) gx = gy = 0.0 gz = -gravity load_obj = pyframe3dd.StaticLoadCase(gx, gy, gz) if frame == "system": for k in range(n_attach): ind = util.closest_node(nodes, fairlead_joints[k, :]) Fnode[ind, :] += mooringF[k, :] Fnode[ihub, :] += inputs["rna_F"] Mnode[ihub, :] += inputs["rna_M"] nF = np.where(np.abs(Fnode).sum(axis=1) > 0.0)[0] load_obj.changePointLoads(nF + 1, Fnode[nF, 0], Fnode[nF, 1], Fnode[nF, 2], Mnode[nF, 0], Mnode[nF, 1], Mnode[nF, 2]) # Add the load case and run myframe.addLoadCase(load_obj) # myframe.write(frame + ".3dd") displacements, forces, reactions, internalForces, mass, modal = myframe.run( ) # natural frequncies if frame == "tower" and frame3dd_opt["modal"]: outputs[frame + "_freqs"] = modal.freq[:NFREQ] # Get all mode shapes in batch NFREQ2 = int(NFREQ / 2) freq_x, freq_y, mshapes_x, mshapes_y = util.get_xy_mode_shapes( nodes[:, 2], modal.freq, modal.xdsp, modal.ydsp, modal.zdsp, modal.xmpf, modal.ympf, modal.zmpf) outputs[frame + "_fore_aft_freqs"] = freq_x[:NFREQ2] outputs[frame + "_side_side_freqs"] = freq_y[:NFREQ2] outputs[frame + "_fore_aft_modes"] = mshapes_x[:NFREQ2, :] outputs[frame + "_side_side_modes"] = mshapes_y[:NFREQ2, :] # Determine forces F_sum = -1.0 * np.array( [reactions.Fx.sum(), reactions.Fy.sum(), reactions.Fz.sum()]) M_sum = -1.0 * np.array([ reactions.Mxx.sum(), reactions.Myy.sum(), reactions.Mzz.sum() ]) L = np.sqrt(np.sum((nodes[N2, :] - nodes[N1, :])**2, axis=1))
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.') '''
def compute(self, inputs, outputs): frame3dd_opt = self.options['frame3dd_opt'] # ------- node data ---------------- z = inputs['z'] n = len(z) node = np.arange(1, n + 1) x = np.zeros(n) y = np.zeros(n) r = np.zeros(n) nodes = pyframe3dd.NodeData(node, x, y, z, r) # ----------------------------------- # ------ reaction data ------------ # rigid base node = inputs['kidx'] + np.ones( len(inputs['kidx'] )) # add one because 0-based index but 1-based node numbering rigid = RIGID reactions = pyframe3dd.ReactionData(node, inputs['kx'], inputs['ky'], inputs['kz'], inputs['ktx'], inputs['kty'], inputs['ktz'], rigid) # ----------------------------------- # ------ frame element data ------------ element = np.arange(1, n) N1 = np.arange(1, n) N2 = np.arange(2, n + 1) roll = np.zeros(n - 1) # average across element b.c. frame3dd uses constant section elements # TODO: Use nodal2sectional Az = inputs['Az'] Asx = inputs['Asx'] Asy = inputs['Asy'] Jz = inputs['Jz'] Ixx = inputs['Ixx'] Iyy = inputs['Iyy'] E = inputs['E'] G = inputs['G'] rho = inputs['rho'] elements = pyframe3dd.ElementData(element, N1, N2, Az, Asx, Asy, Jz, Ixx, Iyy, E, G, roll, rho) # ----------------------------------- # ------ options ------------ dx = -1.0 options = pyframe3dd.Options(frame3dd_opt['shear'], frame3dd_opt['geom'], dx) # ----------------------------------- # initialize frame3dd object cylinder = pyframe3dd.Frame(nodes, reactions, elements, options) # ------ add extra mass ------------ # extra node inertia data N = inputs['midx'] + np.ones(len(inputs['midx'])) add_gravity = True cylinder.changeExtraNodeMass(N, inputs['m'], inputs['mIxx'], inputs['mIyy'], inputs['mIzz'], inputs['mIxy'], inputs['mIxz'], inputs['mIyz'], inputs['mrhox'], inputs['mrhoy'], inputs['mrhoz'], add_gravity) # ------------------------------------ # ------- enable dynamic analysis ---------- Mmethod = 1 lump = 0 shift = 0.0 cylinder.enableDynamics(NFREQ, Mmethod, lump, frame3dd_opt['tol'], shift) # ---------------------------- # ------ static load case 1 ------------ # gravity in the X, Y, Z, directions (global) gx = 0.0 gy = 0.0 gz = -gravity load = pyframe3dd.StaticLoadCase(gx, gy, gz) # point loads nF = inputs['plidx'] + np.ones(len(inputs['plidx'])) load.changePointLoads(nF, inputs['Fx'], inputs['Fy'], inputs['Fz'], inputs['Mxx'], inputs['Myy'], inputs['Mzz']) # distributed loads Px, Py, Pz = inputs['Pz'], inputs['Py'], -inputs[ 'Px'] # switch to local c.s. z = inputs['z'] # trapezoidally distributed loads EL = np.arange(1, n) xx1 = xy1 = xz1 = np.zeros(n - 1) xx2 = xy2 = xz2 = np.diff( z) - 1e-6 # subtract small number b.c. of precision wx1 = Px[:-1] wx2 = Px[1:] wy1 = Py[:-1] wy2 = Py[1:] wz1 = Pz[:-1] wz2 = Pz[1:] load.changeTrapezoidalLoads(EL, xx1, xx2, wx1, wx2, xy1, xy2, wy1, wy2, xz1, xz2, wz1, wz2) cylinder.addLoadCase(load) # Debugging #cylinder.write('temp.3dd') # ----------------------------------- # run the analysis displacements, forces, reactions, internalForces, mass, modal = cylinder.run( ) iCase = 0 # mass outputs['mass'] = mass.struct_mass # natural frequncies outputs['f1'] = modal.freq[0] outputs['f2'] = modal.freq[1] outputs['freqs'] = modal.freq # Get all mode shapes in batch freq_x, freq_y, mshapes_x, mshapes_y = util.get_mode_shapes( z, modal.freq, modal.xdsp, modal.ydsp, modal.zdsp, modal.xmpf, modal.ympf, modal.zmpf) outputs['x_mode_freqs'] = freq_x outputs['y_mode_freqs'] = freq_y outputs['x_mode_shapes'] = mshapes_x outputs['y_mode_shapes'] = mshapes_y # deflections due to loading (from cylinder top and wind/wave loads) outputs['top_deflection'] = displacements.dx[ iCase, n - 1] # in yaw-aligned direction # shear and bending, one per element (convert from local to global c.s.) Fz = forces.Nx[iCase, 1::2] Vy = forces.Vy[iCase, 1::2] Vx = -forces.Vz[iCase, 1::2] Mzz = forces.Txx[iCase, 1::2] Myy = forces.Myy[iCase, 1::2] Mxx = -forces.Mzz[iCase, 1::2] # Record total forces and moments outputs['base_F'] = -1.0 * np.array( [reactions.Fx.sum(), reactions.Fy.sum(), reactions.Fz.sum()]) outputs['base_M'] = -1.0 * np.array( [reactions.Mxx.sum(), reactions.Myy.sum(), reactions.Mzz.sum()]) outputs['Fz_out'] = Fz outputs['Vx_out'] = Vx outputs['Vy_out'] = Vy outputs['Mxx_out'] = Mxx outputs['Myy_out'] = Myy outputs['Mzz_out'] = Mzz # axial and shear stress d, _ = util.nodal2sectional(inputs['d']) qdyn, _ = util.nodal2sectional(inputs['qdyn']) ##R = self.d/2.0 ##x_stress = R*np.cos(self.theta_stress) ##y_stress = R*np.sin(self.theta_stress) ##axial_stress = Fz/self.Az + Mxx/self.Ixx*y_stress - Myy/self.Iyy*x_stress # V = Vy*x_stress/R - Vx*y_stress/R # shear stress orthogonal to direction x,y # shear_stress = 2. * V / self.Az # coefficient of 2 for a hollow circular section, but should be conservative for other shapes outputs['axial_stress'] = Fz / inputs['Az'] - np.sqrt( Mxx**2 + Myy**2 ) / inputs[ 'Iyy'] * d / 2.0 #More conservative, just use the tilted bending and add total max shear as well at the same point, if you do not like it go back to the previous lines outputs['shear_stress'] = 2. * np.sqrt(Vx**2 + Vy**2) / inputs[ 'Az'] # coefficient of 2 for a hollow circular section, but should be conservative for other shapes # hoop_stress (Eurocode method) L_reinforced = self.options['buckling_length'] * np.ones(Fz.shape) outputs['hoop_stress_euro'] = hoopStressEurocode( inputs['z'], d, inputs['t'], L_reinforced, qdyn) # Simpler hoop stress used in API calculations outputs['hoop_stress'] = hoopStress(d, inputs['t'], qdyn)