Example #1
0
    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)
Example #2
0
    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]
Example #3
0

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])
Example #4
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))
Example #5
0
    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.')

        
        '''
Example #6
0
    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)