Ejemplo n.º 1
0
    def update_mass_stiff(self):
        '''
        This method can be substituted to produce different wing configs.

        Forthis model, remind that the delta_frame_of_reference is chosen such 
        that the B FoR axis are:
        - xb: along the wing span
        - yb: pointing towards the leading edge (i.e. roughly opposite than xa)
        - zb: upward as za
        '''
        # uniform mass/stiffness

        # ea,ga=1e7,1e6
        ea, ga = 1e9, 1e9
        gj = 0.987581e6
        eiy = 9.77221e6
        eiz = 1e2 * eiy
        base_stiffness = np.diag([ea, ga, ga, self.sigma * gj, self.sigma * eiy, eiz])

        self.stiffness = np.zeros((1, 6, 6))
        self.stiffness[0] = base_stiffness

        m_unit = 35.71
        j_tors = 8.64
        pos_cg_b = np.array([0., self.c_ref * (self.main_cg - self.main_ea), 0.])
        m_chi_cg = algebra.skew(m_unit * pos_cg_b)
        self.mass = np.zeros((1, 6, 6))
        self.mass[0, :, :] = np.diag([m_unit, m_unit, m_unit,
                                      j_tors, .1 * j_tors, .9 * j_tors])

        self.mass[0, :3, 3:] = m_chi_cg
        self.mass[0, 3:, :3] = -m_chi_cg

        self.elem_stiffness = np.zeros((self.num_elem_tot,), dtype=int)
        self.elem_mass = np.zeros((self.num_elem_tot,), dtype=int)
Ejemplo n.º 2
0
    def lump_masses(self):
        for i_lumped in range(self.n_lumped_mass):
            r = self.lumped_mass_position[i_lumped, :]
            m = self.lumped_mass[i_lumped]
            j = self.lumped_mass_inertia[i_lumped, :, :]

            i_lumped_node = self.lumped_mass_nodes[i_lumped]
            i_lumped_master_elem, i_lumped_master_node_local = self.node_master_elem[i_lumped_node]

            # cba = algebra.crv2rot(self.elements[i_lumped_master_elem].psi_def[i_lumped_master_node_local, :]).T

            inertia_tensor = np.zeros((6, 6))
            r_skew = algebra.skew(r)
            inertia_tensor[0:3, 0:3] = m*np.eye(3)
            inertia_tensor[0:3, 3:6] = -m*r_skew
            inertia_tensor[3:6, 0:3] = m*r_skew
            inertia_tensor[3:6, 3:6] = j + m*(np.dot(r_skew.T, r_skew))

            if self.elements[i_lumped_master_elem].rbmass is None:
                # allocate memory
                self.elements[i_lumped_master_elem].rbmass = np.zeros((
                    self.elements[i_lumped_master_elem].max_nodes_elem, 6, 6))

            self.elements[i_lumped_master_elem].rbmass[i_lumped_master_node_local, :, :] += (
                inertia_tensor)
Ejemplo n.º 3
0
def free_modes_principal_axes(phi, mass_matrix, use_euler=False):
    """
    Transforms the rigid body modes defined at with the A frame as reference to the centre of mass position and aligned
    with the principal axes of inertia.


    References:
        Marc Artola, 2020
    """
    if use_euler:
        num_rigid_modes = 9
    else:
        num_rigid_modes = 10

    r_cg = cg(mass_matrix, use_euler)  # centre of gravity
    mrr = mass_matrix[-num_rigid_modes:-num_rigid_modes + 6,
                      -num_rigid_modes:-num_rigid_modes + 6]
    m = mrr[0, 0]  # mass

    # principal axes of inertia matrix and transformation matrix
    j_cm, t_rb = np.linalg.eig(
        mrr[-3:, -3:] +
        algebra.multiply_matrices(algebra.skew(r_cg), algebra.skew(r_cg)) * m)

    # rigid body mass matrix about CM and inertia in principal axes
    m_cm = np.eye(6) * m
    m_cm[-3:, -3:] = np.diag(j_cm)

    # rigid body modes about CG - mass normalised
    rb_cm = np.eye(6)
    rb_cm /= np.sqrt(np.diag(rb_cm.T.dot(m_cm.dot(rb_cm))))

    # transform to A frame reference position
    trb_diag = np.zeros((6, 6))  # matrix with (t_rb, t_rb) in the diagonal
    trb_diag[:3, :3] = t_rb
    trb_diag[-3:, -3:] = t_rb
    rb_a = np.block([[np.eye(3), algebra.skew(r_cg)],
                     [np.zeros((3, 3)), np.eye(3)]]).dot(trb_diag.dot(rb_cm))

    phit = np.block(
        [np.zeros((phi.shape[0], num_rigid_modes)), phi[:, num_rigid_modes:]])
    phit[-num_rigid_modes:-num_rigid_modes + 6, :6] = rb_a

    phit[-num_rigid_modes + 6:, 6:num_rigid_modes] = np.eye(
        num_rigid_modes - 6)  # euler or quaternion modes

    return phit
Ejemplo n.º 4
0
def nc_domegazetadzeta(Surfs, Surfs_star):
    """
    Produces a list of derivative matrix d(omaga x zeta)/dzeta, where omega is
    the rotation speed of the A FoR,
    ASSUMING constant panel norm.

    Each list is such that:
    - the ii-th element is associated to the ii-th bound surface collocation
    point, and will contain a sub-list such that:
        - the j-th element of the sub-list is the dAIC_dzeta matrices w.r.t. the
        zeta d.o.f. of the j-th bound surface.
    Hence, DAIC*[ii][jj] will have size K_ii x Kzeta_jj

    call: ncDOmegaZetavert = nc_domegazetadzeta(Surfs,Surfs_star)
    """
    n_surf = len(Surfs)

    ncDOmegaZetacoll = []
    ncDOmegaZetavert = []

    ### loop output (bound) surfaces
    for ss in range(n_surf):

        # define output bound surface size
        Surf = Surfs[ss]
        skew_omega = algebra.skew(Surf.omega)
        K = Surf.maps.K  # K_out = M*N (number of panels)
        Kzeta = Surf.maps.Kzeta  # Kzeta_out = (M+1)*(N+1) (number of vertices/edges)
        wcv = Surf.get_panel_wcv()
        shape_zeta = Surf.maps.shape_vert_vect  # (3,M,N)

        # The derivatives only depend on the studied surface (Surf)
        ncDvert = np.zeros((K, 3 * Kzeta))

        ##### loop collocation points
        for cc in range(K):

            # get (m,n) indices of collocation point
            mm = Surf.maps.ind_2d_pan_scal[0][cc]
            nn = Surf.maps.ind_2d_pan_scal[1][cc]

            # get normal
            nc_here = Surf.normals[:, mm, nn]

            nc_skew_omega = -1. * np.dot(nc_here, skew_omega)

            # loop panel vertices
            for vv, dm, dn in zip(range(4), dmver, dnver):
                mm_v, nn_v = mm + dm, nn + dn
                ii_v = [
                    np.ravel_multi_index((comp, mm_v, nn_v), shape_zeta)
                    for comp in range(3)
                ]

                ncDvert[cc, ii_v] += nc_skew_omega

        ncDOmegaZetavert.append(ncDvert)

    return ncDOmegaZetavert
Ejemplo n.º 5
0
    def get_total_forces_gain(self, zeta_pole=np.zeros((3, ))):
        """
        Calculates gain matrices to calculate the total force (Kftot) and moment
        (Kmtot, Kmtot_disp) about the pole zeta_pole.

        Being :math:`f` and :math:`\\zeta` the force and position at the vertex (m,n) of the lattice
        these are produced as:

            ftot=sum(f) 					=> dftot += df
            mtot-sum((zeta-zeta_pole) x f)	=>
                    => 	dmtot +=  cross(zeta0-zeta_pole) df - cross(f0) dzeta

        """

        self.Kftot = np.zeros((3, 3 * self.Kzeta))
        self.Kmtot = np.zeros((3, 3 * self.Kzeta))
        self.Kmtot_disp = np.zeros((3, 3 * self.Kzeta))

        Kzeta_start = 0
        for ss in range(self.MS.n_surf):

            M, N = self.MS.Surfs[ss].maps.M, self.MS.Surfs[ss].maps.N

            for nn in range(N + 1):
                for mm in range(M + 1):
                    jjvec = [
                        Kzeta_start + np.ravel_multi_index(
                            (cc, mm, nn), (3, M + 1, N + 1)) for cc in range(3)
                    ]

                    self.Kftot[[0, 1, 2], jjvec] = 1.
                    self.Kmtot[np.ix_(
                        [0, 1, 2],
                        jjvec)] = algebra.skew(self.MS.Surfs[ss].zeta[:, mm,
                                                                      nn] -
                                               zeta_pole)
                    self.Kmtot_disp[np.ix_(
                        [0, 1, 2],
                        jjvec)] = algebra.skew(-self.MS.Surfs[ss].fqs[:, mm,
                                                                      nn])

            Kzeta_start += 3 * self.MS.KKzeta[ss]
Ejemplo n.º 6
0
def mass_matrix_generator(m, xcg, inertia):
    """
    This function takes the mass, position of the center of
    gravity wrt the elastic axis and the inertia matrix J (3x3) and
    returns the complete 6x6 mass matrix.
    """
    mass = np.zeros((6, 6))
    m_chi_cg = algebra.skew(m*xcg)
    mass[np.diag_indices(3)] = m
    mass[3:, 3:] = inertia
    mass[0:3, 3:6] = -m_chi_cg
    mass[3:6, 0:3] = m_chi_cg

    return mass
Ejemplo n.º 7
0
    def update_mass_stiff(self):
        '''
        This method can be substituted to produce different wing configs.

        Remind: the delta_frame_of_reference is chosen such that the B FoR axis 
        are:
        - xb: along the wing span
        - yb: pointing towards the leading edge
        - zb: accordingly
        '''

        ### mass matrix
        # identical for vtp/htp
        m_unit = 35.
        j_tors = 8.
        pos_cg_b = np.array(
            [0., self.chord_vtp_root * (self.main_cg - self.main_ea), 0.])
        m_chi_cg = algebra.skew(m_unit * pos_cg_b)
        self.mass = np.zeros((2, 6, 6))
        self.mass[0, :, :] = np.diag(
            [m_unit, m_unit, m_unit, j_tors, .1 * j_tors, .9 * j_tors])
        self.mass[0, :3, 3:] = +m_chi_cg
        self.mass[0, 3:, :3] = -m_chi_cg
        self.elem_mass = np.zeros((self.num_elem_tot, ), dtype=int)

        ### stiffness
        ea, ga = 1e9, 1e9
        # vtp
        Kvtp = np.diag([ea, ga, ga, 1e6 * self.kv, 1e7, 1e9])
        # htp
        Khtp = np.diag([ea, ga, ga, 1e7 * self.kh, 1e7 * self.kh, 1e9])

        self.stiffness = np.zeros((2, 6, 6))
        self.stiffness[0, :, :] = Kvtp
        self.stiffness[1, :, :] = Khtp
        self.elem_stiffness = np.zeros((self.num_elem_tot, ), dtype=int)
        self.elem_stiffness[self.conn_surf_vtp] = 0
        self.elem_stiffness[self.conn_surf_htpL] = 1
        self.elem_stiffness[self.conn_surf_htpR] = 1
Ejemplo n.º 8
0
    def test_crv_tangential_operator(self):
        """ Checks Cartesian rotation vector tangential operator """

        # linearisation point
        fi0 = -np.pi / 6
        nv0 = np.array([1, 3, 1])
        nv0 = np.array([1, 0, 0])
        nv0 = nv0 / np.linalg.norm(nv0)
        fv0 = fi0 * nv0
        Cab = algebra.crv2rotation(fv0)  # fv0 is rotation from A to B

        # dummy
        fi1 = np.pi / 3
        nv1 = np.array([2, 4, 1])
        nv1 = nv1 / np.linalg.norm(nv1)
        fv1 = fi1 * nv1

        er_tan = 10.
        A = np.array([1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6])
        for a in A:
            # perturbed
            fv = a * fv1 + (1. - a) * fv0
            dfv = fv - fv0

            ### Compute relevant quantities
            dCab = algebra.crv2rotation(fv0 + dfv) - Cab
            T = algebra.crv2tan(fv0)
            Tdfv = np.dot(T, dfv)
            Tdfv_skew = algebra.skew(Tdfv)
            dCab_an = np.dot(Cab, Tdfv_skew)

            er_tan_new = np.max(np.abs(dCab - dCab_an)) / np.max(
                np.abs(dCab_an))
            assert er_tan_new < er_tan, 'crv2tan error not converging to 0'
            er_tan = er_tan_new

        assert er_tan < A[-2], 'crv2tan error too large'
Ejemplo n.º 9
0
def dfqsdvind_gamma(Surfs, Surfs_star):
    """
    Assemble derivative of quasi-steady force w.r.t. induced velocities changes
    due to gamma.
    Note: the routine is memory consuming but avoids unnecessary computations.
    """

    n_surf = len(Surfs)
    assert len(Surfs_star) == n_surf, \
        'Number of bound and wake surfaces much be equal'

    ### compute all influence coeff matrices (high RAM, low CPU)
    # AIC_list,AIC_star_list=AICs(Surfs,Surfs_star,target='segments',Project=False)

    Der_list = []
    Der_star_list = []
    for ss_out in range(n_surf):

        Surf_out = Surfs[ss_out]
        M_out, N_out = Surf_out.maps.M, Surf_out.maps.N
        K_out = Surf_out.maps.K
        Kzeta_out = Surf_out.maps.Kzeta
        shape_fqs = Surf_out.maps.shape_vert_vect  # (3,M+1,N+1)

        # get AICs over Surf_out
        AICs = []
        AICs_star = []
        for ss_in in range(n_surf):
            AICs.append(Surfs[ss_in].get_aic_over_surface(Surf_out,
                                                          target='segments',
                                                          Project=False))
            AICs_star.append(Surfs_star[ss_in].get_aic_over_surface(
                Surf_out, target='segments', Project=False))

        # allocate all derivative matrices
        Der_list_sub = []
        Der_star_list_sub = []
        for ss_in in range(n_surf):
            # bound
            K_in = Surfs[ss_in].maps.K
            Der_list_sub.append(np.zeros((3 * Kzeta_out, K_in)))
            # wake
            K_in = Surfs_star[ss_in].maps.K
            Der_star_list_sub.append(np.zeros((3 * Kzeta_out, K_in)))

        ### loop bound panels
        for pp_out in range(K_out):
            # get (m,n) indices of panel
            mm_out = Surf_out.maps.ind_2d_pan_scal[0][pp_out]
            nn_out = Surf_out.maps.ind_2d_pan_scal[1][pp_out]
            # get panel vertices
            # zetav_here=Surf_out.get_panel_vertices_coords(mm_out,nn_out)
            zetav_here = Surf_out.zeta[:, [
                mm_out + 0, mm_out + 1, mm_out + 1, mm_out + 0
            ], [nn_out + 0, nn_out + 0, nn_out + 1, nn_out + 1]].T

            for ll, aa, bb in zip(svec, avec, bvec):

                # get segment
                lv = zetav_here[bb, :] - zetav_here[aa, :]
                Lskew = algebra.skew(
                    (-0.5 * Surf_out.rho * Surf_out.gamma[mm_out, nn_out]) *
                    lv)

                # get vertices m,n indices
                mm_a, nn_a = mm_out + dmver[aa], nn_out + dnver[aa]
                mm_b, nn_b = mm_out + dmver[bb], nn_out + dnver[bb]

                # get vertices 1d index
                ii_a = [
                    np.ravel_multi_index((cc, mm_a, nn_a), shape_fqs)
                    for cc in range(3)
                ]
                ii_b = [
                    np.ravel_multi_index((cc, mm_b, nn_b), shape_fqs)
                    for cc in range(3)
                ]

                # update all derivatives
                for ss_in in range(n_surf):
                    # derivatives: size (3,K_in)
                    Dfs = np.dot(Lskew, AICs[ss_in][:, :, ll, mm_out, nn_out])
                    Dfs_star = np.dot(
                        Lskew, AICs_star[ss_in][:, :, ll, mm_out, nn_out])
                    # allocate
                    Der_list_sub[ss_in][ii_a, :] += Dfs
                    Der_list_sub[ss_in][ii_b, :] += Dfs
                    Der_star_list_sub[ss_in][ii_a, :] += Dfs_star
                    Der_star_list_sub[ss_in][ii_b, :] += Dfs_star

        ### loop again trailing edge
        # here we add the Gammaw_0*rho*skew(lv)*dvind/dgamma contribution hence:
        # - we use Gammaw_0 over the TE
        # - we run along the positive direction as defined in the first row of
        # wake panels
        for nn_out in range(N_out):

            # get TE bound vertices m,n indices
            nn_a = nn_out + dnver[2]
            nn_b = nn_out + dnver[1]

            # get segment
            lv = Surf_out.zeta[:, M_out, nn_b] - Surf_out.zeta[:, M_out, nn_a]
            Lskew = algebra.skew(
                (-0.5 * Surf_out.rho * Surfs_star[ss_out].gamma[0, nn_out]) *
                lv)

            # get vertices 1d index on bound
            ii_a = [
                np.ravel_multi_index((cc, M_out, nn_a), shape_fqs)
                for cc in range(3)
            ]
            ii_b = [
                np.ravel_multi_index((cc, M_out, nn_b), shape_fqs)
                for cc in range(3)
            ]

            # update all derivatives
            for ss_in in range(n_surf):
                # derivatives: size (3,K_in)
                Dfs = np.dot(Lskew, AICs[ss_in][:, :, 1, M_out - 1, nn_out])
                Dfs_star = np.dot(Lskew, AICs_star[ss_in][:, :, 1, M_out - 1,
                                                          nn_out])
                # allocate
                Der_list_sub[ss_in][ii_a, :] += Dfs
                Der_list_sub[ss_in][ii_b, :] += Dfs
                Der_star_list_sub[ss_in][ii_a, :] += Dfs_star
                Der_star_list_sub[ss_in][ii_b, :] += Dfs_star

        Der_list.append(Der_list_sub)
        Der_star_list.append(Der_star_list_sub)

    return Der_list, Der_star_list
Ejemplo n.º 10
0
    def get_gebm2uvlm_gains(self):
        """
        Gain matrix to transfer GEBM dofs to UVLM lattice vertices and stiffening
        term due to non-zero forces at the linearisation point.

        The function produces the matrices:

            - ``Kdisp``: from GEBM to UVLM grid displacements
            - ``Kvel_disp``: influence of GEBM dofs displacements to UVLM grid
              velocities.
            - ``Kvel_vel``: influence of GEBM dofs displacements to UVLM grid
              displacements.
            - ``Kforces`` (UVLM->GEBM) dimensions are the transpose than the Kdisp and
              Kvel* matrices. Hence, when allocation this term, ``ii`` and ``jj`` indices
              will unintuitively refer to columns and rows, respectively.
            - ``Kss``: stiffness factor accounting for non-zero forces at the
              linearisation point. (flexible dof -> flexible dof)
            - ``Ksr``: stiffness factor accounting for non-zero forces at the
              linearisation point. (rigid dof -> flexible dof)


        Notes:
            - The following terms have been verified against SHARPy (to ensure same sign conventions and accuracy):
                - :math:`\\mathbf{C}^{AB}`
                - accuracy of :math:`X^B=\\mathbf{C}^{AB}*X^A`
                - accuracy of :math:`X^G` and :math:`X^A`


        """

        data = self.data
        aero = self.data.aero
        structure = self.data.structure  # data.aero.beam
        tsaero = self.tsaero
        tsstr = self.tsstr

        # allocate output
        Kdisp = np.zeros((3 * self.linuvlm.Kzeta, self.num_dof_str))
        Kvel_disp = np.zeros((3 * self.linuvlm.Kzeta, self.num_dof_str))
        Kvel_vel = np.zeros((3 * self.linuvlm.Kzeta, self.num_dof_str))
        Kforces = np.zeros((self.num_dof_str, 3 * self.linuvlm.Kzeta))

        Kss = np.zeros((self.num_dof_flex, self.num_dof_flex))
        Ksr = np.zeros((self.num_dof_flex, self.num_dof_rig))

        # get projection matrix A->G
        # (and other quantities indep. from nodal position)
        Cga = algebra.quat2rotation(tsstr.quat)
        Cag = Cga.T

        # for_pos=tsstr.for_pos
        for_tra = tsstr.for_vel[:3]
        for_rot = tsstr.for_vel[3:]
        skew_for_rot = algebra.skew(for_rot)
        Der_vel_Ra = np.dot(Cga, skew_for_rot)

        # GEBM degrees of freedom
        jj_for_tra = range(self.num_dof_str - 10, self.num_dof_str - 7)
        jj_for_rot = range(self.num_dof_str - 7, self.num_dof_str - 4)
        jj_quat = range(self.num_dof_str - 4, self.num_dof_str)

        jj = 0  # nodal dof index
        for node_glob in range(structure.num_node):

            ### detect bc at node (and no. of dofs)
            bc_here = structure.boundary_conditions[node_glob]

            if bc_here == 1:  # clamp (only rigid-body)
                dofs_here = 0
                jj_tra, jj_rot = [], []
            # continue

            elif bc_here == -1 or bc_here == 0:  # (rigid+flex body)
                dofs_here = 6
                jj_tra = 6 * structure.vdof[node_glob] + np.array([0, 1, 2],
                                                                  dtype=int)
                jj_rot = 6 * structure.vdof[node_glob] + np.array([3, 4, 5],
                                                                  dtype=int)
            # jj_tra=[jj  ,jj+1,jj+2]
            # jj_rot=[jj+3,jj+4,jj+5]
            else:
                raise NameError('Invalid boundary condition (%d) at node %d!' \
                                % (bc_here, node_glob))

            jj += dofs_here

            # retrieve element and local index
            ee, node_loc = structure.node_master_elem[node_glob, :]

            # get position, crv and rotation matrix
            Ra = tsstr.pos[node_glob, :]  # in A FoR, w.r.t. origin A-G
            Rg = np.dot(Cag.T, Ra)  # in G FoR, w.r.t. origin A-G
            psi = tsstr.psi[ee, node_loc, :]
            psi_dot = tsstr.psi_dot[ee, node_loc, :]
            Cab = algebra.crv2rotation(psi)
            Cba = Cab.T
            Cbg = np.dot(Cab.T, Cag)

            ### str -> aero mapping
            # some nodes may be linked to multiple surfaces...
            for str2aero_here in aero.struct2aero_mapping[node_glob]:

                # detect surface/span-wise coordinate (ss,nn)
                nn, ss = str2aero_here['i_n'], str2aero_here['i_surf']
                # print('%.2d,%.2d'%(nn,ss))

                # surface panelling
                M = aero.aero_dimensions[ss][0]
                N = aero.aero_dimensions[ss][1]

                Kzeta_start = 3 * sum(self.linuvlm.MS.KKzeta[:ss])
                shape_zeta = (3, M + 1, N + 1)

                for mm in range(M + 1):
                    # get bound vertex index
                    ii_vert = [
                        Kzeta_start + np.ravel_multi_index(
                            (cc, mm, nn), shape_zeta) for cc in range(3)
                    ]

                    # get aero force
                    faero = tsaero.forces[ss][:3, mm, nn]

                    # get position vectors
                    zetag = tsaero.zeta[ss][:, mm,
                                            nn]  # in G FoR, w.r.t. origin A-G
                    zetaa = np.dot(Cag, zetag)  # in A FoR, w.r.t. origin A-G
                    Xg = zetag - Rg  # in G FoR, w.r.t. origin B
                    Xb = np.dot(Cbg, Xg)  # in B FoR, w.r.t. origin B

                    # get rotation terms
                    Xbskew = algebra.skew(Xb)
                    Tan = algebra.crv2tan(psi)
                    XbskewTan = np.dot(Xbskew, Tan)

                    # get velocity terms
                    zetag_dot = tsaero.zeta_dot[
                        ss][:, mm, nn]  # in G FoR, w.r.t. origin A-G
                    zetaa_dot = np.dot(
                        Cag, zetag_dot)  # in A FoR, w.r.t. origin A-G

                    ### ---------------------------------------- allocate Kdisp

                    if bc_here != 1:
                        # wrt pos
                        Kdisp[np.ix_(ii_vert, jj_tra)] += Cga

                        # wrt psi
                        Kdisp[np.ix_(ii_vert,
                                     jj_rot)] -= np.dot(Cbg.T, XbskewTan)

                    # w.r.t. position of FoR A (w.r.t. origin G)
                    # null as A and G have always same origin in SHARPy

                    # # ### w.r.t. quaternion (attitude changes)
                    Kdisp[np.ix_(ii_vert, jj_quat)] = \
                        algebra.der_Cquat_by_v(tsstr.quat, zetaa)

                    ### ------------------------------------ allocate Kvel_disp

                    if bc_here != 1:
                        # # wrt pos
                        Kvel_disp[np.ix_(ii_vert, jj_tra)] += Der_vel_Ra

                        # wrt psi (at zero psi_dot)
                        Kvel_disp[np.ix_(ii_vert, jj_rot)] -= \
                            np.dot(Cga,
                                   np.dot(skew_for_rot,
                                          np.dot(Cab, XbskewTan)))

                        # # wrt psi (psi_dot contributions - verified)
                        Kvel_disp[np.ix_(ii_vert, jj_rot)] += np.dot(
                            Cbg.T,
                            np.dot(algebra.skew(np.dot(XbskewTan, psi_dot)),
                                   Tan))

                        Kvel_disp[np.ix_(ii_vert, jj_rot)] -= \
                            np.dot(Cbg.T,
                                   np.dot(Xbskew,
                                          algebra.der_Tan_by_xv(psi, psi_dot)))

                    # # w.r.t. position of FoR A (w.r.t. origin G)
                    # # null as A and G have always same origin in SHARPy

                    # # ### w.r.t. quaternion (attitude changes)
                    Kvel_disp[np.ix_(ii_vert, jj_quat)] = \
                        algebra.der_Cquat_by_v(tsstr.quat, zetaa_dot)

                    ### ------------------------------------- allocate Kvel_vel

                    if bc_here != 1:
                        # wrt pos_dot
                        Kvel_vel[np.ix_(ii_vert, jj_tra)] += Cga

                        # # wrt crv_dot
                        Kvel_vel[np.ix_(ii_vert,
                                        jj_rot)] -= np.dot(Cbg.T, XbskewTan)

                    # # wrt velocity of FoR A
                    Kvel_vel[np.ix_(ii_vert, jj_for_tra)] += Cga
                    Kvel_vel[np.ix_(ii_vert, jj_for_rot)] -= \
                        np.dot(Cga, algebra.skew(zetaa))

                    # wrt rate of change of quaternion: not implemented!

                    ### -------------------------------------- allocate Kforces

                    if bc_here != 1:
                        # nodal forces
                        Kforces[np.ix_(jj_tra, ii_vert)] += Cbg

                        # nodal moments
                        Kforces[np.ix_(jj_rot, ii_vert)] += \
                            np.dot(Cbg, algebra.skew(Xg))
                    # or, equivalently, np.dot( algebra.skew(Xb),Cbg)

                    # total forces
                    Kforces[np.ix_(jj_for_tra, ii_vert)] += Cag

                    # total moments
                    Kforces[np.ix_(jj_for_rot, ii_vert)] += \
                        np.dot(Cag, algebra.skew(zetag))

                    # quaternion equation
                    # null, as not dep. on external forces

                    ### --------------------------------------- allocate Kstiff

                    if bc_here != 1:
                        # forces
                        Dfdcrv = algebra.der_CcrvT_by_v(
                            psi, np.dot(Cag, faero))
                        Dfdquat = np.dot(
                            Cba, algebra.der_CquatT_by_v(tsstr.quat, faero))
                        Kss[np.ix_(jj_tra, jj_rot)] -= Dfdcrv
                        Ksr[jj_tra, -4:] -= Dfdquat

                        # moments
                        Kss[np.ix_(jj_rot, jj_rot)] -= np.dot(Xbskew, Dfdcrv)
                        Ksr[jj_rot, -4:] -= np.dot(Xbskew, Dfdquat)

                # embed()

        # transfer
        self.Kdisp = Kdisp
        self.Kvel_disp = Kvel_disp
        self.Kvel_vel = Kvel_vel
        self.Kforces = Kforces

        # stiffening factors
        self.Kss = Kss
        self.Ksr = Ksr
Ejemplo n.º 11
0
    def free_free_modes(self, phi, M):
        r"""
        Returns the rigid body modes defined with respect to the centre of gravity

        The transformation from the modes defined at the FoR A origin, :math:`\boldsymbol{\Phi}`, to the modes defined
        using the centre of gravity as a reference is


        .. math:: \boldsymbol{\Phi}_{rr,CG}|_{TRA} = \boldsymbol{\Phi}_{RR}|_{TRA} + \tilde{\mathbf{r}}_{CG}
            \boldsymbol{\Phi}_{RR}|_{ROT}

        .. math:: \boldsymbol{\Phi}_{rr,CG}|_{ROT} = \boldsymbol{\Phi}_{RR}|_{ROT}

        Returns:
            (np.array): Transformed eigenvectors
        """

        # NG - 26/7/19 This is the transformation being performed by K_vec
        # Leaving this here for now in case it becomes necessary
        # .. math:: \boldsymbol{\Phi}_{ss,CG}|_{TRA} = \boldsymbol{\Phi}_{SS}|_{TRA} +\boldsymbol{\Phi}_{RS}|_{TRA}  -
        # \tilde{\mathbf{r}}_{A}\boldsymbol{\Phi}_{RS}|_{ROT}
        #
        # .. math:: \boldsymbol{\Phi}_{ss,CG}|_{ROT} = \boldsymbol{\Phi}_{SS}|_{ROT}
        # + (\mathbf{T}(\boldsymbol{\Psi})^\top)^{-1}\boldsymbol{\Phi}_{RS}|_{ROT}

        if not self.rigid_body_motion:
            warnings.warn('No rigid body modes to transform because the structure is clamped')
            return phi
        else:
            pos = self.data.structure.timestep_info[self.data.ts].pos
            r_cg = modalutils.cg(M)

            jj = 0
            K_vec = np.zeros((phi.shape[0], phi.shape[0]))

            jj_for_vel = range(self.data.structure.num_dof.value, self.data.structure.num_dof.value + 3)
            jj_for_rot = range(self.data.structure.num_dof.value + 3, self.data.structure.num_dof.value + 6)

            for node_glob in range(self.data.structure.num_node):
                ### detect bc at node (and no. of dofs)
                bc_here = self.data.structure.boundary_conditions[node_glob]

                if bc_here == 1:  # clamp (only rigid-body)
                    dofs_here = 0
                    jj_tra, jj_rot = [], []
                    continue

                elif bc_here == -1 or bc_here == 0:  # (rigid+flex body)
                    dofs_here = 6
                    jj_tra = 6 * self.data.structure.vdof[node_glob] + np.array([0, 1, 2], dtype=int)
                    jj_rot = 6 * self.data.structure.vdof[node_glob] + np.array([3, 4, 5], dtype=int)
                # jj_tra=[jj  ,jj+1,jj+2]
                # jj_rot=[jj+3,jj+4,jj+5]
                else:
                    raise NameError('Invalid boundary condition (%d) at node %d!' \
                                    % (bc_here, node_glob))

                jj += dofs_here

                ee, node_loc = self.data.structure.node_master_elem[node_glob, :]
                psi = self.data.structure.timestep_info[self.data.ts].psi[ee, node_loc, :]

                Ra = pos[node_glob, :]  # in A FoR with respect to G

                K_vec[np.ix_(jj_tra, jj_tra)] += np.eye(3)
                K_vec[np.ix_(jj_tra, jj_for_vel)] += np.eye(3)
                K_vec[np.ix_(jj_tra, jj_for_rot)] -= algebra.skew(Ra)

                K_vec[np.ix_(jj_rot, jj_rot)] += np.eye(3)
                K_vec[np.ix_(jj_rot, jj_for_rot)] += np.linalg.inv(algebra.crv2tan(psi).T)

            # Rigid-Rigid modes transform
            Krr = np.eye(10)
            Krr[np.ix_([0, 1, 2], [3, 4, 5])] += algebra.skew(r_cg)

            # Assemble transformed modes
            phirr = Krr.dot(phi[-10:, :10])
            phiss = K_vec.dot(phi[:, 10:])

            # Get rigid body modes to be positive in translation and rotation
            for i in range(10):
                ind = np.argmax(np.abs(phirr[:, i]))
                phirr[:, i] = np.sign(phirr[ind, i]) * phirr[:, i]

            # NG - 26/7/19 - Transformation of the rigid part of the elastic modes ended up not being necessary but leaving
            # here in case it becomes useful in the future
            phit = np.block([np.zeros((phi.shape[0], 10)), phi[:, 10:]])
            phit[-10:, :10] = phirr

            return phit
Ejemplo n.º 12
0
    def test_nc_domegazetadzeta(self):
        """
        Variation at colocation points due to geometrical variations at vertices
        Needs to be tested with a case that actually rotates
        """

        print(
            '----------------------------- Testing assembly.test_nc_domegazetadzeta'
        )

        MS = self.MS
        n_surf = MS.n_surf

        # analytical
        Dervert_list = assembly.nc_domegazetadzeta(MS.Surfs, MS.Surfs_star)

        # allocate numerical
        # Derlist_num=[]
        # for ii in range(n_surf):
        #     sub=[]
        #     for jj in range(n_surf):
        #         sub.append(0.0*Dervert_list[ii][jj])
        #     Derlist_num.append(sub)

        # Store the initial values of the variabes
        Zeta0 = []
        Zeta0_star = []
        N0 = []
        ZetaC0 = []
        for ss in range(n_surf):
            Zeta0.append(MS.Surfs[ss].zeta.copy())
            ZetaC0.append(MS.Surfs[ss].zetac.copy('F'))
            Zeta0_star.append(MS.Surfs_star[ss].zeta.copy())
            N0.append(MS.Surfs[ss].normals.copy())

        # Computation
        Steps = [1e-2, 1e-4, 1e-6]
        nsteps = len(Steps)
        error = np.zeros((nsteps, ))
        for istep in range(nsteps):
            step = Steps[istep]
            for ss in range(n_surf):
                Surf = MS.Surfs[ss]
                Surf_star = MS.Surfs_star[ss]
                M, N = Surf.maps.M, Surf.maps.N

                perturb_vector = np.zeros(3 * Surf.maps.Kzeta)

                # PERTURBATION OF THE SURFACE
                for kk in range(3 * Surf.maps.Kzeta):
                    # generate a random perturbation between the 90% and the 110% of the step
                    perturb_vector[kk] += step * (0.2 * np.random.rand() + 0.9)
                    cc, mm, nn = np.unravel_index(kk, (3, M + 1, N + 1))

                    # perturb bound. vertices and collocation
                    Surf.zeta = Zeta0[ss].copy()
                    Surf.zeta[cc, mm, nn] += perturb_vector[kk]

                    # perturb wake TE
                    if mm == M:
                        Surf_star.zeta = Zeta0_star[ss].copy()
                        Surf_star.zeta[cc, 0, nn] += perturb_vector[kk]

                Surf.generate_collocations()

                # COMPUTE THE DERIVATIVES
                Der_an = np.zeros(Surf.maps.K)
                Der_an = np.dot(Dervert_list[ss], perturb_vector)
                Der_num = np.zeros(Surf.maps.K)
                ipanel = 0
                skew_omega = algebra.skew(Surf.omega)
                for mm in range(M):
                    for nn in range(N):
                        Der_num[ipanel] = (
                            np.dot(N0[ss][:, mm, nn],
                                   np.dot(skew_omega, ZetaC0[ss][:, mm, nn])) -
                            np.dot(N0[ss][:, mm, nn],
                                   np.dot(skew_omega, Surf.zetac[:, mm, nn])))
                        ipanel += 1

                # COMPUTE THE ERROR
                error[istep] = np.maximum(error[istep],
                                          np.absolute(Der_num - Der_an).max())

            print('FD step: %.2e ---> Max error: %.2e' % (step, error[istep]))
            assert error[
                istep] < 5e1 * step, 'Error larger than 50 times the step size'

            if istep > 0:
                assert error[istep] <= error[istep - 1], \
                    'Error not decreasing as FD step size is reduced'

        print(
            '------------------------------------------------------------ OK')
def generate_fem_file():
    # placeholders
    # coordinates
    global x, y, z
    global sigma
    x = np.zeros((num_node, ))
    y = np.zeros((num_node, ))
    z = np.zeros((num_node, ))
    # struct twist
    structural_twist = np.zeros((num_elem, 3))
    # beam number
    beam_number = np.zeros((num_elem, ), dtype=int)
    # frame of reference delta
    frame_of_reference_delta = np.zeros((num_elem, num_node_elem, 3))
    # connectivities
    conn = np.zeros((num_elem, num_node_elem), dtype=int)
    # stiffness
    num_stiffness = 1
    ea = 1e5
    ga = 1e5
    gj = 0.987581e6
    eiy = 9.77221e6
    eiz = 9.77221e8
    base_stiffness = sigma * np.diag([ea, ga, ga, gj, eiy, eiz])
    stiffness = np.zeros((num_stiffness, 6, 6))
    stiffness[0, :, :] = main_sigma * base_stiffness
    elem_stiffness = np.zeros((num_elem, ), dtype=int)
    # mass
    num_mass = 1
    m_base = 1
    j_base = 0.5
    import sharpy.utils.algebra as algebra
    # m_chi_cg = algebra.skew(m_base*np.array([0., -(main_ea - main_cg), 0.]))
    m_chi_cg = algebra.skew(m_base * np.array([0., (main_ea - main_cg), 0.]))
    base_mass = np.diag(
        [m_base, m_base, m_base, j_base, 0.5 * j_base, 0.5 * j_base])
    base_mass[0:3, 3:6] = -m_chi_cg
    base_mass[3:6, 0:3] = m_chi_cg
    mass = np.zeros((num_mass, 6, 6))
    mass[0, :, :] = base_mass
    elem_mass = np.zeros((num_elem, ), dtype=int)
    # boundary conditions
    boundary_conditions = np.zeros((num_node, ), dtype=int)
    boundary_conditions[0] = 1
    # applied forces
    # n_app_forces = 2
    # node_app_forces = np.zeros((n_app_forces,), dtype=int)
    app_forces = np.zeros((num_node, 6))

    spacing_param = 10

    # right wing (beam 0) --------------------------------------------------------------
    working_elem = 0
    working_node = 0
    beam_number[working_elem:working_elem + num_elem_main] = 0
    domain = np.linspace(0, 1.0, num_node_main)
    # 16 - (np.geomspace(20, 4, 10) - 4)
    x[working_node:working_node + num_node_main] = np.sin(sweep) * (
        main_span - (np.geomspace(main_span + spacing_param, 0 + spacing_param,
                                  num_node_main) - spacing_param))
    y[working_node:working_node + num_node_main] = np.abs(
        np.cos(sweep) *
        (main_span -
         (np.geomspace(main_span + spacing_param, 0 + spacing_param,
                       num_node_main) - spacing_param)))
    y[0] = 0
    # y[working_node:working_node + num_node_main] = np.cos(sweep)*np.linspace(0.0, main_span, num_node_main)
    # x[working_node:working_node + num_node_main] = np.sin(sweep)*np.linspace(0.0, main_span, num_node_main)
    for ielem in range(num_elem_main):
        for inode in range(num_node_elem):
            frame_of_reference_delta[working_elem + ielem,
                                     inode, :] = [-1, 0, 0]
    # connectivity
    for ielem in range(num_elem_main):
        conn[working_elem + ielem, :] = ((np.ones(
            (3, )) * (working_elem + ielem) * (num_node_elem - 1)) + [0, 2, 1])
    elem_stiffness[working_elem:working_elem + num_elem_main] = 0
    elem_mass[working_elem:working_elem + num_elem_main] = 0
    boundary_conditions[0] = 1
    boundary_conditions[working_node + num_node_main - 1] = -1
    working_elem += num_elem_main
    working_node += num_node_main

    # # left wing (beam 1) --------------------------------------------------------------
    # beam_number[working_elem:working_elem + num_elem_main] = 1
    # domain = np.linspace(0.0, 1.0, num_node_main)
    # tempy = np.linspace(0.0, main_span, num_node_main)
    # # x[working_node:working_node + num_node_main - 1] = -np.sin(sweep)*tempy[0:-1]
    # # y[working_node:working_node + num_node_main - 1] = np.cos(sweep)*tempy[0:-1]
    # x[working_node:working_node + num_node_main - 1] = -np.sin(sweep)*(main_span - (np.geomspace(0 + spacing_param,
    # main_span + spacing_param,
    # num_node_main)[:-1]
    # - spacing_param))
    # y[working_node:working_node + num_node_main - 1] = -np.abs(np.cos(sweep)*(main_span - (np.geomspace(0 + spacing_param,
    # main_span + spacing_param,
    # num_node_main)[:-1]
    # - spacing_param)))
    # for ielem in range(num_elem_main):
    # for inode in range(num_node_elem):
    # frame_of_reference_delta[working_elem + ielem, inode, :] = [1, 0, 0]
    # # connectivity
    # for ielem in range(num_elem_main):
    # conn[working_elem + ielem, :] = ((np.ones((3,))*(working_elem + ielem)*(num_node_elem - 1)) +
    # [0, 2, 1])
    # conn[working_elem , 0] = 0
    # elem_stiffness[working_elem:working_elem + num_elem_main] = 0
    # elem_mass[working_elem:working_elem + num_elem_main] = 0
    # boundary_conditions[working_node + num_node_main - 1 - 1] = -1
    # working_elem += num_elem_main
    # working_node += num_node_main - 1

    with h5.File(route + '/' + case_name + '.fem.h5', 'a') as h5file:
        coordinates = h5file.create_dataset('coordinates',
                                            data=np.column_stack((x, y, z)))
        conectivities = h5file.create_dataset('connectivities', data=conn)
        num_nodes_elem_handle = h5file.create_dataset('num_node_elem',
                                                      data=num_node_elem)
        num_nodes_handle = h5file.create_dataset('num_node', data=num_node)
        num_elem_handle = h5file.create_dataset('num_elem', data=num_elem)
        stiffness_db_handle = h5file.create_dataset('stiffness_db',
                                                    data=stiffness)
        stiffness_handle = h5file.create_dataset('elem_stiffness',
                                                 data=elem_stiffness)
        mass_db_handle = h5file.create_dataset('mass_db', data=mass)
        mass_handle = h5file.create_dataset('elem_mass', data=elem_mass)
        frame_of_reference_delta_handle = h5file.create_dataset(
            'frame_of_reference_delta', data=frame_of_reference_delta)
        structural_twist_handle = h5file.create_dataset('structural_twist',
                                                        data=structural_twist)
        bocos_handle = h5file.create_dataset('boundary_conditions',
                                             data=boundary_conditions)
        beam_handle = h5file.create_dataset('beam_number', data=beam_number)
        app_forces_handle = h5file.create_dataset('app_forces',
                                                  data=app_forces)
        body_number_handle = h5file.create_dataset('body_number',
                                                   data=np.zeros((num_elem, ),
                                                                 dtype=int))
Ejemplo n.º 14
0
def generate_strip(node_info, airfoil_db, aligned_grid, orientation_in=np.array([1, 0, 0]), calculate_zeta_dot = False):
    """
    Returns a strip in "a" frame of reference, it has to be then rotated to
    simulate angles of attack, etc
    :param node_info:
    :param airfoil_db:
    :param aligned_grid:
    :param orientation_in:
    :return:
    """
    strip_coordinates_a_frame = np.zeros((3, node_info['M'] + 1), dtype=ct.c_double)
    strip_coordinates_b_frame = np.zeros((3, node_info['M'] + 1), dtype=ct.c_double)

    # airfoil coordinates
    # we are going to store everything in the x-z plane of the b
    # FoR, so that the transformation Cab rotates everything in place.
    if node_info['M_distribution'] == 'uniform':
        strip_coordinates_b_frame[1, :] = np.linspace(0.0, 1.0, node_info['M'] + 1)
    elif node_info['M_distribution'] == '1-cos':
        domain = np.linspace(0, 1.0, node_info['M'] + 1)
        strip_coordinates_b_frame[1, :] = 0.5*(1.0 - np.cos(domain*np.pi))
    else:
        raise NotImplemented('M_distribution is ' + node_info['M_distribution'] +
                             ' and it is not yet supported')
    strip_coordinates_b_frame[2, :] = airfoil_db[node_info['airfoil']](
                                            strip_coordinates_b_frame[1, :])

    # elastic axis correction
    for i_M in range(node_info['M'] + 1):
        strip_coordinates_b_frame[1, i_M] -= node_info['eaxis']

    chord_line_b_frame = strip_coordinates_b_frame[:, -1] - strip_coordinates_b_frame[:, 0]

    # control surface deflection
    if node_info['control_surface'] is not None:
        b_frame_hinge_coords = strip_coordinates_b_frame[:, node_info['M'] - node_info['control_surface']['chord']]
        # support for different hinge location for fully articulated control surfaces
        if node_info['control_surface']['hinge_coords'] is not None:
            # make sure the hinge coordinates are only applied when M == cs_chord
            if not node_info['M'] - node_info['control_surface']['chord'] == 0:
                cout.cout_wrap('The hinge coordinates parameter is only supported when M == cs_chord')
                node_info['control_surface']['hinge_coords'] = None
            else:
                b_frame_hinge_coords =  node_info['control_surface']['hinge_coords']

        for i_M in range(node_info['M'] - node_info['control_surface']['chord'], node_info['M'] + 1):
            relative_coords = strip_coordinates_b_frame[:, i_M] - b_frame_hinge_coords
            # rotate the control surface
            relative_coords = np.dot(algebra.rotation3d_x(-node_info['control_surface']['deflection']),
                                     relative_coords)
            # restore coordinates
            relative_coords += b_frame_hinge_coords

            # substitute with new coordinates
            strip_coordinates_b_frame[:, i_M] = relative_coords

    # chord scaling
    strip_coordinates_b_frame *= node_info['chord']

    # twist transformation (rotation around x_b axis)
    if np.abs(node_info['twist']) > 1e-6:
        Ctwist = algebra.rotation3d_x(node_info['twist'])
    else:
        Ctwist = np.eye(3)

    # Cab transformation
    Cab = algebra.crv2rot(node_info['beam_psi'])

    rot_angle = algebra.angle_between_vectors_sign(orientation_in, Cab[:, 1], Cab[:, 2])
    Crot = algebra.rotation3d_z(-rot_angle)

    c_sweep = np.eye(3)
    if np.abs(node_info['sweep']) > 1e-6:
        c_sweep = algebra.rotation3d_z(node_info['sweep'])

    # transformation from beam to beam prime (with sweep and twist)
    for i_M in range(node_info['M'] + 1):
        strip_coordinates_b_frame[:, i_M] = np.dot(c_sweep, np.dot(Crot,
                                                   np.dot(Ctwist, strip_coordinates_b_frame[:, i_M])))
        strip_coordinates_a_frame[:, i_M] = np.dot(Cab, strip_coordinates_b_frame[:, i_M])

    # zeta_dot
    if calculate_zeta_dot:
        zeta_dot_a_frame = np.zeros((3, node_info['M'] + 1), dtype=ct.c_double)

        # velocity due to pos_dot
        for i_M in range(node_info['M'] + 1):
            zeta_dot_a_frame[:, i_M] += node_info['pos_dot']

        # velocity due to psi_dot
        Omega_b = algebra.crv_dot2Omega(node_info['beam_psi'], node_info['psi_dot'])
        for i_M in range(node_info['M'] + 1):
            zeta_dot_a_frame[:, i_M] += (
                np.dot(algebra.skew(Omega_b), strip_coordinates_a_frame[:, i_M]))

    else:
        zeta_dot_a_frame = np.zeros((3, node_info['M'] + 1), dtype=ct.c_double)

    # add node coords
    for i_M in range(node_info['M'] + 1):
        strip_coordinates_a_frame[:, i_M] += node_info['beam_coord']

    # rotation from a to g
    for i_M in range(node_info['M'] + 1):
        strip_coordinates_a_frame[:, i_M] = np.dot(node_info['cga'],
                                                   strip_coordinates_a_frame[:, i_M])
        zeta_dot_a_frame[:, i_M] = np.dot(node_info['cga'],
                                          zeta_dot_a_frame[:, i_M])

    return strip_coordinates_a_frame, zeta_dot_a_frame
Ejemplo n.º 15
0
    def update_mass_stiffness(self, sigma=1., sigma_mass=1.):
        """
        Set's the mass and stiffness properties of the default wing

        Returns:

        """

        n_elem_fuselage = self.n_elem_fuselage
        n_elem_wing = self.n_elem_wing
        n_node_wing = self.n_node_wing
        n_node_fuselage = self.n_node_fuselage
        c_root = self.c_root
        taper_ratio = self.taper_ratio

        # Local chord to root chord initialisation
        c_bar_temp = np.linspace(c_root, taper_ratio * c_root, n_elem_wing)

        # Structural properties at the wing root section from Richards 2016
        ea = 1e6
        ga = 1e6
        gj = 4.24e5
        eiy = 3.84e5
        eiz = 2.46e7

        root_i_beam = IBeam()
        root_i_beam.build(c_root)
        root_i_beam.rotation_axes = np.array([0, self.main_ea_root - 0.25, 0])

        root_airfoil = Airfoil()
        root_airfoil.build(c_root)
        root_i_beam.rotation_axes = np.array([0, self.main_ea_root - 0.25, 0])

        mu_0 = root_i_beam.mass + root_airfoil.mass
        j_xx = root_i_beam.ixx + root_airfoil.ixx
        j_yy = root_i_beam.iyy + root_airfoil.iyy
        j_zz = root_i_beam.izz + root_airfoil.izz

        # Number of stiffnesses used
        n_stiffness = self.n_stiffness

        # Initialise the stiffness database
        base_stiffness = self.base_stiffness

        stiffness_root = sigma * np.diag([ea, ga, ga, gj, eiy, eiz])
        stiffness_tip = taper_ratio**2 * stiffness_root

        # Assume a linear variation in the stiffness. Richards et al. use VABS on the linearly tapered wing to find the
        # spanwise properties
        alpha = np.linspace(0, 1, self.n_elem_wing)
        for i_elem in range(0, self.n_elem_wing):
            base_stiffness[i_elem + 1, :, :] = stiffness_root * (
                1 - alpha[i_elem]**2) + stiffness_tip * alpha[i_elem]**2
        base_stiffness[0] = base_stiffness[1]

        # Mass variation along the span
        # Right wing centre of mass - wrt to 0.25c
        cm = (root_airfoil.centre_mass * root_airfoil.mass + root_i_beam.centre_mass * root_i_beam.mass) \
             / np.sum(root_airfoil.mass + root_i_beam.mass)
        cg = np.array(
            [0, -(cm[0] + 0.25 * self.c_root - self.main_ea_root), 0]) * 1
        n_mass = self.n_mass
        # sigma_mass = 1.25

        # Initialise database
        base_mass = self.base_mass

        mass_root_right = np.diag([mu_0, mu_0, mu_0, j_xx, j_yy, j_zz
                                   ]) * sigma_mass
        mass_root_right[:3, -3:] = -algebra.skew(cg) * mu_0
        mass_root_right[-3:, :3] = algebra.skew(cg) * mu_0

        mass_root_left = np.diag([mu_0, mu_0, mu_0, j_xx, j_yy, j_zz
                                  ]) * sigma_mass
        mass_root_left[:3, -3:] = -algebra.skew(-cg) * mu_0
        mass_root_left[-3:, :3] = algebra.skew(-cg) * mu_0

        mass_tip_right = taper_ratio * mass_root_right
        mass_tip_left = taper_ratio * mass_root_left

        ixx_dummy = []
        iyy_dummy = []
        izz_dummy = []

        for i_elem in range(self.n_elem_wing):
            # Create full cross section
            c_bar = self.c_root * (
                (1 - alpha[i_elem]) + self.taper_ratio * alpha[i_elem])
            x_section = WingCrossSection(c_bar)
            print(i_elem)
            print('Section Mass: %.2f ' % x_section.mass)
            print('Linear Mass: %.2f' %
                  (mu_0 * (1 - alpha[i_elem]) +
                   mu_0 * self.taper_ratio * alpha[i_elem]))
            print('Section Ixx: %.4f' % x_section.ixx)
            print('Section Iyy: %.4f' % x_section.iyy)
            print('Section Izz: %.4f' % x_section.izz)
            print('Linear Ixx: %.2f' %
                  (j_xx * (1 - alpha[i_elem]) +
                   j_xx * self.taper_ratio * alpha[i_elem]))
            # base_mass[i_elem, :, :] = mass_root_right*(1-alpha[i_elem]) + mass_tip_right*alpha[i_elem]
            # base_mass[i_elem + self.n_elem_wing + self.n_elem_fuselage - 1] = mass_root_left*(1-alpha[i_elem]) + mass_tip_left*alpha[i_elem]

            base_mass[i_elem, :, :] = np.diag([
                x_section.mass, x_section.mass, x_section.mass, x_section.ixx,
                x_section.iyy, x_section.izz
            ])
            cg = np.array([
                0, -(x_section.centre_mass[0] +
                     (0.25 - self.main_ea_root) * c_bar / self.c_root), 0
            ]) * 1
            base_mass[i_elem, :3, -3:] = -algebra.skew(cg) * x_section.mass
            base_mass[i_elem, -3:, :3] = algebra.skew(cg) * x_section.mass

            base_mass[i_elem + self.n_elem_wing + self.n_elem_fuselage -
                      1, :, :] = np.diag([
                          x_section.mass, x_section.mass, x_section.mass,
                          x_section.ixx, x_section.iyy, x_section.izz
                      ])
            cg = np.array([
                0, -(x_section.centre_mass[0] +
                     (0.25 - self.main_ea_root) * c_bar / self.c_root), 0
            ]) * 1
            base_mass[i_elem + self.n_elem_wing + self.n_elem_fuselage - 1, :3,
                      -3:] = -algebra.skew(-cg) * x_section.mass
            base_mass[i_elem + self.n_elem_wing + self.n_elem_fuselage - 1,
                      -3:, :3] = algebra.skew(-cg) * x_section.mass

            ixx_dummy.append(x_section.ixx)
            iyy_dummy.append(x_section.iyy)
            izz_dummy.append(x_section.izz)

            # for item in x_section.items:
            #     plt.plot(item.y, item.z)
            # plt.scatter(x_section.centre_mass[0], x_section.centre_mass[1])
            # plt.show()
            # print(x_section.centre_mass)
            # print(cg)

        # plt.plot(range(self.n_elem_wing), ixx_dummy)
        # plt.plot(range(self.n_elem_wing), iyy_dummy)
        # plt.plot(range(self.n_elem_wing), izz_dummy)
        # plt.show()
        # Lumped mass initialisation
        lumped_mass_nodes = self.lumped_mass_nodes
        lumped_mass = self.lumped_mass
        lumped_mass_inertia = self.lumped_mass_inertia
        lumped_mass_position = self.lumped_mass_position

        # Lumped masses nodal position
        # 0 - Right engine
        # 1 - Left engine
        # 2 - Fuselage
        lumped_mass_nodes[0] = 2
        lumped_mass_nodes[1] = n_node_fuselage + n_node_wing + 1
        lumped_mass_nodes[2] = 0

        # Lumped mass value from Richards 2013
        lumped_mass[0:2] = 51.445 / 9.81
        lumped_mass[2] = 150 / 9.81
        # lumped_mass_position[2] = [0, 0, -10.]

        # Lumped mass inertia
        lumped_mass_inertia[0, :, :] = np.diag([0.29547, 0.29322, 0.29547])
        lumped_mass_inertia[1, :, :] = np.diag([0.29547, 0.29322, 0.29547])
        lumped_mass_inertia[2, :, :] = np.diag([0.5, 1, 1]) * lumped_mass[2]

        # Define class attributes
        self.lumped_mass = lumped_mass * 1
        self.lumped_mass_nodes = lumped_mass_nodes * 1
        self.lumped_mass_inertia = lumped_mass_inertia * 1
        self.lumped_mass_position = lumped_mass_position * 1

        self.base_stiffness = base_stiffness
        self.base_mass = base_mass
    def generate(self, linuvlm=None, tsaero0=None, tsstruct0=None, aero=None, structure=None):
        """
        Generates a matrix mapping a linear control surface deflection onto the aerodynamic grid.

        The parsing of arguments is temporary since this state space element will include a full actuator model.

        The parsing of arguments is optional if the class has been previously initialised.

        Args:
            linuvlm:
            tsaero0:
            tsstruct0:
            aero:
            structure:

        Returns:

        """

        if self.aero is not None:
            aero = self.aero
            structure = self.structure
            linuvlm = self.linuvlm
            tsaero0 = self.tsaero0
            tsstruct0 = self.tsstruct0

        # Find the vertices corresponding to a control surface from beam coordinates to aerogrid
        aero_dict = aero.aero_dict
        n_surf = aero.timestep_info[0].n_surf
        n_control_surfaces = self.n_control_surfaces

        if self.under_development:
            import matplotlib.pyplot as plt  # Part of the testing process
        Kdisp = np.zeros((3 * linuvlm.Kzeta, n_control_surfaces))
        Kvel = np.zeros((3 * linuvlm.Kzeta, n_control_surfaces))
        Kmom = np.zeros((3 * linuvlm.Kzeta, n_control_surfaces))
        zeta0 = np.concatenate([tsaero0.zeta[i_surf].reshape(-1, order='C') for i_surf in range(n_surf)])

        Cga = algebra.quat2rotation(tsstruct0.quat).T
        Cag = Cga.T

        # Initialise these parameters
        hinge_axis = None  # Will be set once per control surface to the hinge axis
        with_control_surface = False  # Will be set to true if the spanwise node contains a control surface

        for global_node in range(structure.num_node):

            # Retrieve elements and local nodes to which a single node is attached
            for i_elem in range(structure.num_elem):
                if global_node in structure.connectivities[i_elem, :]:
                    i_local_node = np.where(structure.connectivities[i_elem, :] == global_node)[0][0]

                    for_delta = structure.frame_of_reference_delta[i_elem, :, 0]

                    # CRV to transform from G to B frame
                    psi = tsstruct0.psi[i_elem, i_local_node]
                    Cab = algebra.crv2rotation(psi)
                    Cba = Cab.T
                    Cbg = np.dot(Cab.T, Cag)
                    Cgb = Cbg.T

                    # print(global_node)
                    if self.under_development:
                        print('Node -- ' + str(global_node))
                    # Map onto aerodynamic coordinates. Some nodes may be part of two aerodynamic surfaces. This will happen
                    # at the surface boundary
                    for structure2aero_node in aero.struct2aero_mapping[global_node]:
                        # Retrieve surface and span-wise coordinate
                        i_surf, i_node_span = structure2aero_node['i_surf'], structure2aero_node['i_n']

                        # Surface panelling
                        M = aero.aero_dimensions[i_surf][0]
                        N = aero.aero_dimensions[i_surf][1]

                        K_zeta_start = 3 * sum(linuvlm.MS.KKzeta[:i_surf])
                        shape_zeta = (3, M + 1, N + 1)

                        i_control_surface = aero_dict['control_surface'][i_elem, i_local_node]
                        if i_control_surface >= 0:
                            if not with_control_surface:
                                i_start_of_cs = i_node_span.copy()
                                with_control_surface = True
                            control_surface_chord = aero_dict['control_surface_chord'][i_control_surface]
                            i_node_hinge = M - control_surface_chord
                            i_vertex_hinge = [K_zeta_start +
                                              np.ravel_multi_index((i_axis, i_node_hinge, i_node_span), shape_zeta)
                                              for i_axis in range(3)]
                            i_vertex_next_hinge = [K_zeta_start +
                                                   np.ravel_multi_index((i_axis, i_node_hinge, i_start_of_cs + 1),
                                                                        shape_zeta) for i_axis in range(3)]
                            zeta_hinge = zeta0[i_vertex_hinge]
                            zeta_next_hinge = zeta0[i_vertex_next_hinge]

                            if hinge_axis is None:
                                # Hinge axis not yet set for current control surface
                                # Hinge axis is in G frame
                                hinge_axis = zeta_next_hinge - zeta_hinge
                                hinge_axis = hinge_axis / np.linalg.norm(hinge_axis)
                            for i_node_chord in range(M + 1):
                                i_vertex = [K_zeta_start +
                                            np.ravel_multi_index((i_axis, i_node_chord, i_node_span), shape_zeta)
                                            for i_axis in range(3)]

                                if i_node_chord > i_node_hinge:
                                    # Zeta in G frame
                                    zeta_node = zeta0[i_vertex]  # Gframe
                                    zeta_nodeA = Cag.dot(zeta_node)
                                    zeta_hingeA = Cag.dot(zeta_hinge)
                                    zeta_hingeB = Cbg.dot(zeta_hinge)
                                    zeta_nodeB = Cbg.dot(zeta_node)
                                    chord_vec = (zeta_node - zeta_hinge)
                                    if self.under_development:
                                        print('G Frame')
                                        print('Hinge axis = ' + str(hinge_axis))
                                        print('\tHinge = ' + str(zeta_hinge))
                                        print('\tNode = ' + str(zeta_node))
                                        print('A Frame')
                                        print('\tHinge = ' + str(zeta_hingeA))
                                        print('\tNode = ' + str(zeta_nodeA))
                                        print('B Frame')
                                        print('\tHinge axis = ' + str(Cbg.dot(hinge_axis)))
                                        print('\tHinge = ' + str(zeta_hingeB))
                                        print('\tNode = ' + str(zeta_nodeB))
                                        print('Chordwise Vector')
                                        print('GVec = ' + str(chord_vec/np.linalg.norm(chord_vec)))
                                        print('BVec = ' + str(Cbg.dot(chord_vec/np.linalg.norm(chord_vec))))
                                        # pass
                                    # Removing the += because cs where being added twice
                                    Kdisp[i_vertex, i_control_surface] = \
                                        Cgb.dot(der_R_arbitrary_axis_times_v(Cbg.dot(hinge_axis),
                                                                             0,
                                                                             -for_delta * Cbg.dot(chord_vec)))
                                    # Kdisp[i_vertex, i_control_surface] = \
                                    #     der_R_arbitrary_axis_times_v(hinge_axis, 0, chord_vec)

                                    # Flap velocity
                                    Kvel[i_vertex, i_control_surface] = -algebra.skew(chord_vec).dot(
                                        hinge_axis)

                                    # Flap hinge moment - future work
                                    # Kmom[i_vertex, i_control_surface] += algebra.skew(chord_vec)

                                    # Testing progress
                                    if self.under_development:
                                        plt.scatter(zeta_hingeB[1], zeta_hingeB[2], color='k')
                                        plt.scatter(zeta_nodeB[1], zeta_nodeB[2], color='b')
                                        # plt.scatter(zeta_hinge[1], zeta_hinge[2], color='k')
                                        # plt.scatter(zeta_node[1], zeta_node[2], color='b')

                                        # Testing out
                                        delta = 5*np.pi/180
                                        # zeta_newB = Cbg.dot(Kdisp[i_vertex, 1].dot(delta)) + zeta_nodeB
                                        zeta_newB = Cbg.dot(Kdisp[i_vertex, -1].dot(delta)) + zeta_nodeB
                                        plt.scatter(zeta_newB[1], zeta_newB[2], color='r')

                                        old_vector = zeta_nodeB - zeta_hingeB
                                        new_vector = zeta_newB - zeta_hingeB

                                        angle = np.arccos(new_vector.dot(old_vector) /
                                                          (np.linalg.norm(new_vector) * np.linalg.norm(old_vector)))
                                        print(angle)

                            if self.under_development:
                                plt.axis('equal')
                                plt.show()
                        else:
                            with_control_surface = False
                            hinge_axis = None  # Reset for next control surface

        self.Kzeta_delta = Kdisp
        self.Kdzeta_ddelta = Kvel
        # self.Kmom = Kmom
        return Kdisp, Kvel
Ejemplo n.º 17
0
    def change_to_global_AFoR(self, global_ibody):
        """
        change_to_global_AFoR

        Reference a StructTimeStepInfo to the global A frame of reference

        Given 'self' as a StructTimeStepInfo class, this function references
        it to the global A frame of reference

        Args:
            self(StructTimeStepInfo): timestep information
            global_ibody(int): body number (as defined in the mutibody system) to be modified

        Returns:

        Examples:

        Notes:

        """

        # Define the rotation matrices between the different FoR
        CAslaveG = algebra.quat2rotation(self.mb_quat[global_ibody, :]).T
        CGAmaster = algebra.quat2rotation(self.mb_quat[0, :])
        Csm = np.dot(CAslaveG, CGAmaster)

        delta_pos_ms = self.mb_FoR_pos[global_ibody, :] - self.mb_FoR_pos[0, :]
        delta_vel_ms = self.mb_FoR_vel[global_ibody, :] - self.mb_FoR_vel[0, :]

        for inode in range(self.pos.shape[0]):
            pos_previous = self.pos[inode, :] + np.zeros((3, ), )
            self.pos[inode, :] = (
                np.dot(np.transpose(Csm), self.pos[inode, :]) +
                np.dot(np.transpose(CGAmaster), delta_pos_ms[0:3]))
            self.pos_dot[inode, :] = (
                np.dot(np.transpose(Csm), self.pos_dot[inode, :]) +
                np.dot(np.transpose(CGAmaster), delta_vel_ms[0:3]) + np.dot(
                    Csm.T,
                    np.dot(
                        algebra.skew(
                            np.dot(CAslaveG, self.mb_FoR_vel[global_ibody,
                                                             3:6])),
                        pos_previous)) -
                np.dot(
                    algebra.skew(np.dot(CGAmaster.T, self.mb_FoR_vel[0, 3:6])),
                    self.pos[inode, :]))
            self.gravity_forces[inode,
                                0:3] = np.dot(Csm.T, self.gravity_forces[inode,
                                                                         0:3])
            self.gravity_forces[inode,
                                3:6] = np.dot(Csm.T, self.gravity_forces[inode,
                                                                         3:6])
            # np.cross(np.dot(CGAmaster.T, delta_vel_ms[3:6]), pos_previous))

        for ielem in range(self.psi.shape[0]):
            for inode in range(3):
                psi_previous = self.psi[ielem, inode, :] + np.zeros((3, ), )
                self.psi[ielem, inode, :] = algebra.rotation2crv(
                    np.dot(Csm.T,
                           algebra.crv2rotation(self.psi[ielem, inode, :])))
                self.psi_dot[ielem, inode, :] = np.dot(
                    algebra.crv2tan(self.psi[ielem, inode, :]), (np.dot(
                        Csm.T,
                        np.dot(
                            algebra.crv2tan(psi_previous).T,
                            self.psi_dot[ielem, inode, :])) + np.dot(
                                algebra.quat2rotation(self.mb_quat[0, :]).T,
                                delta_vel_ms[3:6])))

        # Set the output FoR variables
        self.for_pos = self.mb_FoR_pos[0, :].astype(dtype=ct.c_double,
                                                    order='F',
                                                    copy=True)
        self.for_vel[0:3] = np.dot(np.transpose(CGAmaster),
                                   self.mb_FoR_vel[0, 0:3])
        self.for_vel[3:6] = np.dot(np.transpose(CGAmaster),
                                   self.mb_FoR_vel[0, 3:6])
        self.for_acc[0:3] = np.dot(np.transpose(CGAmaster),
                                   self.mb_FoR_acc[0, 0:3])
        self.for_acc[3:6] = np.dot(np.transpose(CGAmaster),
                                   self.mb_FoR_acc[0, 3:6])
        self.quat = self.mb_quat[0, :].astype(dtype=ct.c_double,
                                              order='F',
                                              copy=True)
Ejemplo n.º 18
0
    def get_gebm2uvlm_gains(self, data):
        r"""
        Provides:
            - the gain matrices required to connect the linearised GEBM and UVLM
             inputs/outputs
            - the stiffening and damping factors to be added to the linearised
            GEBM equations in order to account for non-zero aerodynamic loads at
            the linearisation point.

        The function produces the gain matrices:

            - ``Kdisp``: gains from GEBM to UVLM grid displacements
            - ``Kvel_disp``: influence of GEBM dofs displacements to UVLM grid
              velocities.
            - ``Kvel_vel``: influence of GEBM dofs displacements to UVLM grid
              displacements.
            - ``Kforces`` (UVLM->GEBM) dimensions are the transpose than the
            Kdisp and Kvel* matrices. Hence, when allocation this term, ``ii``
            and ``jj`` indices will unintuitively refer to columns and rows,
            respectively.

        And the stiffening/damping terms accounting for non-zero aerodynamic
        forces at the linearisation point:

            - ``Kss``: stiffness factor (flexible dof -> flexible dof) accounting
            for non-zero forces at the linearisation point.
            - ``Csr``: damping factor  (rigid dof -> flexible dof)
            - ``Crs``: damping factor (flexible dof -> rigid dof)
            - ``Crr``: damping factor (rigid dof -> rigid dof)


        Stiffening and damping related terms due to the non-zero aerodynamic forces at the linearisation point:

        .. math::
            \mathbf{F}_{A,n} = C^{AG}(\mathbf{\chi})\sum_j \mathbf{f}_{G,j} \rightarrow
            \delta\mathbf{F}_{A,n} = C^{AG}_0 \sum_j \delta\mathbf{f}_{G,j} + \frac{\partial}{\partial\chi}(C^{AG}\sum_j
            \mathbf{f}_{G,j}^0)\delta\chi

        The term multiplied by the variation in the quaternion, :math:`\delta\chi`, couples the forces with the rigid
        body equations and becomes part of :math:`\mathbf{C}_{sr}`.

        Similarly, the linearisation of the moments results in expression that contribute to the stiffness and
        damping matrices.

        .. math::
            \mathbf{M}_{B,n} = \sum_j \tilde{X}_B C^{BA}(\Psi)C^{AG}(\chi)\mathbf{f}_{G,j}

        .. math::
            \delta\mathbf{M}_{B,n} = \sum_j \tilde{X}_B\left(C_0^{BG}\delta\mathbf{f}_{G,j}
            + \frac{\partial}{\partial\Psi}(C^{BA}\delta\mathbf{f}^0_{A,j})\delta\Psi
            + \frac{\partial}{\partial\chi}(C^{BA}_0 C^{AG} \mathbf{f}_{G,j})\delta\chi\right)

        The linearised equations of motion for the geometrically exact beam model take the input term :math:`\delta
        \mathbf{Q}_n = \{\delta\mathbf{F}_{A,n},\, T_0^T\delta\mathbf{M}_{B,n}\}`, which means that the moments
        should be provided as :math:`T^T(\Psi)\mathbf{M}_B` instead of :math:`\mathbf{M}_A = C^{AB}\mathbf{M}_B`,
        where :math:`T(\Psi)` is the tangential operator.

        .. math::
            \delta(T^T\mathbf{M}_B) = T^T_0\delta\mathbf{M}_B
            + \frac{\partial}{\partial\Psi}(T^T\delta\mathbf{M}_B^0)\delta\Psi

        is the linearised expression for the moments, where the first term would correspond to the input terms to the
        beam equations and the second arises due to the non-zero aerodynamic moment at the linearisation point and
        must be subtracted (since it comes from the forces) to form part of :math:`\mathbf{K}_{ss}`. In addition, the
        :math:`\delta\mathbf{M}_B` term depends on both :math:`\delta\Psi` and :math:`\delta\chi`, therefore those
        terms would also contribute to :math:`\mathbf{K}_{ss}` and :math:`\mathbf{C}_{sr}`, respectively.

        The contribution from the total forces and moments will be accounted for in :math:`\mathbf{C}_{rr}` and
        :math:`\mathbf{C}_{rs}`.

        .. math::
            \delta\mathbf{F}_{tot,A} = \sum_n\left(C^{GA}_0 \sum_j \delta\mathbf{f}_{G,j}
            + \frac{\partial}{\partial\chi}(C^{AG}\sum_j
            \mathbf{f}_{G,j}^0)\delta\chi\right)

        Therefore, after running this method, the beam matrices will be updated as:

        >>> K_beam[:flex_dof, :flex_dof] += Kss
        >>> C_beam[:flex_dof, -rigid_dof:] += Csr
        >>> C_beam[-rigid_dof:, :flex_dof] += Crs
        >>> C_beam[-rigid_dof:, -rigid_dof:] += Crr

        Track body option

        The ``track_body`` setting restricts the UVLM grid to linear translation motions and therefore should be used to
        ensure that the forces are computed using the reference linearisation frame.

        The UVLM and beam are linearised about a reference equilibrium condition. The UVLM is defined in the inertial
        reference frame while the beam employs the body attached frame and therefore a projection from one frame onto
        another is required during the coupling process.

        However, the inputs to the UVLM (i.e. the lattice grid coordinates) are obtained from the beam deformation which
        is expressed in A frame and therefore the grid coordinates need to be projected onto the inertial frame ``G``.
        As the beam rotates, the projection onto the ``G`` frame of the lattice grid coordinates will result in a grid
        that is not coincident with that at the linearisation reference and therefore the grid coordinates must be
        projected onto the original frame, which will be referred to as ``U``. The transformation between the inertial
        frame ``G`` and the ``U`` frame is a function of the rotation of the ``A`` frame and the original position:

        .. math:: C^{UG}(\chi) = C^{GA}(\chi_0)C^{AG}(\chi)

        Therefore, the grid coordinates obtained in ``A`` frame and projected onto the ``G`` frame can be transformed
        to the ``U`` frame using

        .. math:: \zeta_U = C^{UG}(\chi) \zeta_G

        which allows the grid lattice coordinates to be projected onto the original linearisation frame.

        In a similar fashion, the output lattice vertex forces of the UVLM are defined in the original linearisation
        frame ``U`` and need to be transformed onto the inertial frame ``G`` prior to projecting them onto the ``A``
        frame to use them as the input forces to the beam system.

        .. math:: \boldsymbol{f}_G = C^{GU}(\chi)\boldsymbol{f}_U

        The linearisation of the above relations lead to the following expressions that have to be added to the
        coupling matrices:

            * ``Kdisp_vel`` terms:

                .. math::
                    \delta\boldsymbol{\zeta}_U= C^{GA}_0 \frac{\partial}{\partial \boldsymbol{\chi}}
                    \left(C^{AG}\boldsymbol{\zeta}_{G,0}\right)\delta\boldsymbol{\chi} + \delta\boldsymbol{\zeta}_G

            * ``Kvel_vel`` terms:

                .. math::
                    \delta\dot{\boldsymbol{\zeta}}_U= C^{GA}_0 \frac{\partial}{\partial \boldsymbol{\chi}}
                    \left(C^{AG}\dot{\boldsymbol{\zeta}}_{G,0}\right)\delta\boldsymbol{\chi}
                    + \delta\dot{\boldsymbol{\zeta}}_G

        The transformation of the forces and moments introduces terms that are functions of the orientation and
        are included as stiffening and damping terms in the beam's matrices:

            * ``Csr`` damping terms relating to translation forces:

                .. math::
                    C_{sr}^{tra} -= \frac{\partial}{\partial\boldsymbol{\chi}}
                    \left(C^{GA} C^{AG}_0 \boldsymbol{f}_{G,0}\right)\delta\boldsymbol{\chi}

            * ``Csr`` damping terms related to moments:

                .. math::
                    C_{sr}^{rot} -= T^\top\widetilde{\mathbf{X}}_B C^{BG}
                    \frac{\partial}{\partial\boldsymbol{\chi}}
                    \left(C^{GA} C^{AG}_0 \boldsymbol{f}_{G,0}\right)\delta\boldsymbol{\chi}


        The ``track_body`` setting.

        When ``track_body`` is enabled, the UVLM grid is no longer coincident with the inertial reference frame
        throughout the simulation but rather it is able to rotate as the ``A`` frame rotates. This is to simulate a free
        flying vehicle, where, for instance, the orientation does not affect the aerodynamics. The UVLM defined in this
        frame of reference, named ``U``, satisfies the following convention:

            * The ``U`` frame is coincident with the ``G`` frame at the time of linearisation.

            * The ``U`` frame rotates as the ``A`` frame rotates.

        Transformations related to the ``U`` frame of reference:

            * The angle between the ``U`` frame and the ``A`` frame is always constant and equal
              to :math:`\boldsymbol{\Theta}_0`.

            * The angle between the ``A`` frame and the ``G`` frame is :math:`\boldsymbol{\Theta}=\boldsymbol{\Theta}_0
              + \delta\boldsymbol{\Theta}`

            * The projection of a vector expressed in the ``G`` frame onto the ``U`` frame is expressed by:

                .. math:: \boldsymbol{v}^U = C^{GA}_0 C^{AG} \boldsymbol{v}^G

            * The reverse, a projection of a vector expressed in the ``U`` frame onto the ``G`` frame, is expressed by

                .. math:: \boldsymbol{v}^U = C^{GA} C^{AG}_0 \boldsymbol{v}^U

        The effect this has on the aeroelastic coupling between the UVLM and the structural dynamics is that the
        orientation and change of orientation of the vehicle has no effect on the aerodynamics. The aerodynamics are
        solely affected by the contribution of the 6-rigid body velocities (as well as the flexible DOFs velocities).

        """

        aero = data.aero
        structure = data.structure
        tsaero = self.uvlm.tsaero0
        tsstr = self.beam.tsstruct0

        Kzeta = self.uvlm.sys.Kzeta
        num_dof_str = self.beam.sys.num_dof_str
        num_dof_rig = self.beam.sys.num_dof_rig
        num_dof_flex = self.beam.sys.num_dof_flex
        use_euler = self.beam.sys.use_euler

        # allocate output
        Kdisp = np.zeros((3 * Kzeta, num_dof_str))
        Kdisp_vel = np.zeros(
            (3 * Kzeta, num_dof_str))  # Orientation is in velocity DOFs
        Kvel_disp = np.zeros((3 * Kzeta, num_dof_str))
        Kvel_vel = np.zeros((3 * Kzeta, num_dof_str))
        Kforces = np.zeros((num_dof_str, 3 * Kzeta))

        Kss = np.zeros((num_dof_flex, num_dof_flex))
        Csr = np.zeros((num_dof_flex, num_dof_rig))
        Crs = np.zeros((num_dof_rig, num_dof_flex))
        Crr = np.zeros((num_dof_rig, num_dof_rig))
        Krs = np.zeros((num_dof_rig, num_dof_flex))

        # get projection matrix A->G
        # (and other quantities indep. from nodal position)
        Cga = algebra.quat2rotation(tsstr.quat)  # NG 6-8-19 removing .T
        Cag = Cga.T

        # for_pos=tsstr.for_pos
        for_vel = tsstr.for_vel[:3]
        for_rot = tsstr.for_vel[3:]
        skew_for_rot = algebra.skew(for_rot)
        Der_vel_Ra = np.dot(Cga, skew_for_rot)

        Faero = np.zeros(3)
        FaeroA = np.zeros(3)

        # GEBM degrees of freedom
        jj_for_tra = range(num_dof_str - num_dof_rig,
                           num_dof_str - num_dof_rig + 3)
        jj_for_rot = range(num_dof_str - num_dof_rig + 3,
                           num_dof_str - num_dof_rig + 6)

        if use_euler:
            jj_euler = range(num_dof_str - 3, num_dof_str)
            euler = algebra.quat2euler(tsstr.quat)
            tsstr.euler = euler
        else:
            jj_quat = range(num_dof_str - 4, num_dof_str)

        jj = 0  # nodal dof index
        for node_glob in range(structure.num_node):

            ### detect bc at node (and no. of dofs)
            bc_here = structure.boundary_conditions[node_glob]

            if bc_here == 1:  # clamp (only rigid-body)
                dofs_here = 0
                jj_tra, jj_rot = [], []
            # continue

            elif bc_here == -1 or bc_here == 0:  # (rigid+flex body)
                dofs_here = 6
                jj_tra = 6 * structure.vdof[node_glob] + np.array([0, 1, 2],
                                                                  dtype=int)
                jj_rot = 6 * structure.vdof[node_glob] + np.array([3, 4, 5],
                                                                  dtype=int)
            else:
                raise NameError('Invalid boundary condition (%d) at node %d!' \
                                % (bc_here, node_glob))

            jj += dofs_here

            # retrieve element and local index
            ee, node_loc = structure.node_master_elem[node_glob, :]

            # get position, crv and rotation matrix
            Ra = tsstr.pos[node_glob, :]  # in A FoR, w.r.t. origin A-G
            Rg = np.dot(Cag.T, Ra)  # in G FoR, w.r.t. origin A-G
            psi = tsstr.psi[ee, node_loc, :]
            psi_dot = tsstr.psi_dot[ee, node_loc, :]
            Cab = algebra.crv2rotation(psi)
            Cba = Cab.T
            Cbg = np.dot(Cab.T, Cag)
            Tan = algebra.crv2tan(psi)

            track_body = self.settings['track_body']

            ### str -> aero mapping
            # some nodes may be linked to multiple surfaces...
            for str2aero_here in aero.struct2aero_mapping[node_glob]:

                # detect surface/span-wise coordinate (ss,nn)
                nn, ss = str2aero_here['i_n'], str2aero_here['i_surf']
                # print('%.2d,%.2d'%(nn,ss))

                # surface panelling
                M = aero.aero_dimensions[ss][0]
                N = aero.aero_dimensions[ss][1]

                Kzeta_start = 3 * sum(self.uvlm.sys.MS.KKzeta[:ss])
                shape_zeta = (3, M + 1, N + 1)

                for mm in range(M + 1):
                    # get bound vertex index
                    ii_vert = [
                        Kzeta_start + np.ravel_multi_index(
                            (cc, mm, nn), shape_zeta) for cc in range(3)
                    ]

                    # get position vectors
                    zetag = tsaero.zeta[ss][:, mm,
                                            nn]  # in G FoR, w.r.t. origin A-G
                    zetaa = np.dot(Cag, zetag)  # in A FoR, w.r.t. origin A-G
                    Xg = zetag - Rg  # in G FoR, w.r.t. origin B
                    Xb = np.dot(Cbg, Xg)  # in B FoR, w.r.t. origin B

                    # get rotation terms
                    Xbskew = algebra.skew(Xb)
                    XbskewTan = np.dot(Xbskew, Tan)

                    # get velocity terms
                    zetag_dot = tsaero.zeta_dot[ss][:, mm, nn] - Cga.dot(
                        for_vel)  # in G FoR, w.r.t. origin A-G
                    zetaa_dot = np.dot(
                        Cag, zetag_dot)  # in A FoR, w.r.t. origin A-G

                    # get aero force
                    faero = tsaero.forces[ss][:3, mm, nn]
                    Faero += faero
                    faero_a = np.dot(Cag, faero)
                    FaeroA += faero_a
                    maero_g = np.cross(Xg, faero)
                    maero_b = np.dot(Cbg, maero_g)

                    ### ---------------------------------------- allocate Kdisp

                    if bc_here != 1:
                        # wrt pos - Eq 25 second term
                        Kdisp[np.ix_(ii_vert, jj_tra)] += Cga

                        # wrt psi - Eq 26
                        Kdisp[np.ix_(ii_vert,
                                     jj_rot)] -= np.dot(Cbg.T, XbskewTan)

                    # w.r.t. position of FoR A (w.r.t. origin G)
                    # null as A and G have always same origin in SHARPy

                    # # ### w.r.t. quaternion (attitude changes)
                    if use_euler:
                        Kdisp_vel[np.ix_(ii_vert, jj_euler)] += \
                            algebra.der_Ceuler_by_v(tsstr.euler, zetaa)

                        # Track body - project inputs as for A not moving
                        if track_body:
                            Kdisp_vel[np.ix_(ii_vert, jj_euler)] += \
                                Cga.dot(algebra.der_Peuler_by_v(tsstr.euler, zetag))
                    else:
                        # Equation 25
                        # Kdisp[np.ix_(ii_vert, jj_quat)] += \
                        #     algebra.der_Cquat_by_v(tsstr.quat, zetaa)
                        Kdisp_vel[np.ix_(ii_vert, jj_quat)] += \
                            algebra.der_Cquat_by_v(tsstr.quat, zetaa)

                        # Track body - project inputs as for A not moving
                        if track_body:
                            Kdisp_vel[np.ix_(ii_vert, jj_quat)] += \
                                Cga.dot(algebra.der_CquatT_by_v(tsstr.quat, zetag))

                    ### ------------------------------------ allocate Kvel_disp

                    if bc_here != 1:
                        # # wrt pos
                        Kvel_disp[np.ix_(ii_vert, jj_tra)] += Der_vel_Ra

                        # wrt psi (at zero psi_dot)
                        Kvel_disp[np.ix_(ii_vert, jj_rot)] -= \
                            np.dot(Cga,
                                   np.dot(skew_for_rot,
                                          np.dot(Cab, XbskewTan)))

                        # # wrt psi (psi_dot contributions - verified)
                        Kvel_disp[np.ix_(ii_vert, jj_rot)] += np.dot(
                            Cbg.T,
                            np.dot(algebra.skew(np.dot(XbskewTan, psi_dot)),
                                   Tan))

                        if np.linalg.norm(psi) >= 1e-6:
                            Kvel_disp[np.ix_(ii_vert, jj_rot)] -= \
                                np.dot(Cbg.T,
                                       np.dot(Xbskew,
                                              algebra.der_Tan_by_xv(psi, psi_dot)))

                    # # w.r.t. position of FoR A (w.r.t. origin G)
                    # # null as A and G have always same origin in SHARPy

                    # # ### w.r.t. quaternion (attitude changes) - Eq 30
                    if use_euler:
                        Kvel_vel[np.ix_(ii_vert, jj_euler)] += \
                            algebra.der_Ceuler_by_v(tsstr.euler, zetaa_dot)

                        # Track body if ForA is rotating
                        if track_body:
                            Kvel_vel[np.ix_(ii_vert, jj_euler)] += \
                                Cga.dot(algebra.der_Peuler_by_v(tsstr.euler, zetag_dot))
                    else:
                        Kvel_vel[np.ix_(ii_vert, jj_quat)] += \
                            algebra.der_Cquat_by_v(tsstr.quat, zetaa_dot)

                        # Track body if ForA is rotating
                        if track_body:
                            Kvel_vel[np.ix_(ii_vert, jj_quat)] += \
                                Cga.dot(algebra.der_CquatT_by_v(tsstr.quat, zetag_dot))

                    ### ------------------------------------- allocate Kvel_vel

                    if bc_here != 1:
                        # wrt pos_dot
                        Kvel_vel[np.ix_(ii_vert, jj_tra)] += Cga

                        # # wrt crv_dot
                        Kvel_vel[np.ix_(ii_vert,
                                        jj_rot)] -= np.dot(Cbg.T, XbskewTan)

                    # # wrt velocity of FoR A
                    Kvel_vel[np.ix_(ii_vert, jj_for_tra)] += Cga
                    Kvel_vel[np.ix_(ii_vert, jj_for_rot)] -= \
                        np.dot(Cga, algebra.skew(zetaa))

                    # wrt rate of change of quaternion: not implemented!

                    ### -------------------------------------- allocate Kforces

                    if bc_here != 1:
                        # nodal forces
                        Kforces[np.ix_(jj_tra, ii_vert)] += Cag

                        # nodal moments
                        Kforces[np.ix_(jj_rot, ii_vert)] += \
                            np.dot(Tan.T, np.dot(Cbg, algebra.skew(Xg)))
                    # or, equivalently, np.dot( algebra.skew(Xb),Cbg)

                    # total forces
                    Kforces[np.ix_(jj_for_tra, ii_vert)] += Cag

                    # total moments
                    Kforces[np.ix_(jj_for_rot, ii_vert)] += \
                        np.dot(Cag, algebra.skew(zetag))

                    # quaternion equation
                    # null, as not dep. on external forces

                    ### --------------------------------------- allocate Kstiff

                    ### flexible dof equations (Kss and Csr)
                    if bc_here != 1:
                        # nodal forces
                        if use_euler:
                            if not track_body:
                                Csr[jj_tra, -3:] -= algebra.der_Peuler_by_v(
                                    tsstr.euler, faero)
                                # Csr[jj_tra, -3:] -= algebra.der_Ceuler_by_v(tsstr.euler, Cga.T.dot(faero))

                        else:
                            if not track_body:
                                Csr[jj_tra, -4:] -= algebra.der_CquatT_by_v(
                                    tsstr.quat, faero)

                            # Track body
                            # if track_body:
                            #     Csr[jj_tra, -4:] -= algebra.der_Cquat_by_v(tsstr.quat, Cga.T.dot(faero))

                        ### moments
                        TanTXbskew = np.dot(Tan.T, Xbskew)
                        # contrib. of TanT (dpsi) - Eq 37 - Integration of UVLM and GEBM
                        Kss[np.ix_(jj_rot, jj_rot)] -= algebra.der_TanT_by_xv(
                            psi, maero_b)
                        # contrib of delta aero moment (dpsi) - Eq 36
                        Kss[np.ix_(jj_rot, jj_rot)] -= \
                            np.dot(TanTXbskew, algebra.der_CcrvT_by_v(psi, np.dot(Cag, faero)))
                        # contribution of delta aero moment (dquat)
                        if use_euler:
                            if not track_body:
                                Csr[jj_rot, -3:] -= \
                                    np.dot(TanTXbskew,
                                           np.dot(Cba,
                                                  algebra.der_Peuler_by_v(tsstr.euler, faero)))

                            # if track_body:
                            #     Csr[jj_rot, -3:] -= \
                            #         np.dot(TanTXbskew,
                            #                np.dot(Cbg,
                            #                       algebra.der_Peuler_by_v(tsstr.euler, Cga.T.dot(faero))))
                        else:
                            if not track_body:
                                Csr[jj_rot, -4:] -= \
                                    np.dot(TanTXbskew,
                                           np.dot(Cba,
                                                  algebra.der_CquatT_by_v(tsstr.quat, faero)))

                            # Track body
                            # if track_body:
                            #     Csr[jj_rot, -4:] -= \
                            #         np.dot(TanTXbskew,
                            #                np.dot(Cbg,
                            #                       algebra.der_CquatT_by_v(tsstr.quat, Cga.T.dot(faero))))

                    ### rigid body eqs (Crs and Crr)

                    if bc_here != 1:
                        # Changed Crs to Krs - NG 14/5/19
                        # moments contribution due to delta_Ra (+ sign intentional)
                        Krs[3:6, jj_tra] += algebra.skew(faero_a)
                        # moment contribution due to delta_psi (+ sign intentional)
                        Krs[3:6,
                            jj_rot] += np.dot(algebra.skew(faero_a),
                                              algebra.der_Ccrv_by_v(psi, Xb))

                    if use_euler:
                        if not track_body:
                            # total force
                            Crr[:3, -3:] -= algebra.der_Peuler_by_v(
                                tsstr.euler, faero)

                            # total moment contribution due to change in euler angles
                            Crr[3:6, -3:] -= algebra.der_Peuler_by_v(
                                tsstr.euler, np.cross(zetag, faero))
                            Crr[3:6, -3:] += np.dot(
                                np.dot(Cag, algebra.skew(faero)),
                                algebra.der_Peuler_by_v(
                                    tsstr.euler, np.dot(Cab, Xb)))

                    else:
                        if not track_body:
                            # total force
                            Crr[:3, -4:] -= algebra.der_CquatT_by_v(
                                tsstr.quat, faero)

                            # total moment contribution due to quaternion
                            Crr[3:6, -4:] -= algebra.der_CquatT_by_v(
                                tsstr.quat, np.cross(zetag, faero))
                            Crr[3:6, -4:] += np.dot(
                                np.dot(Cag, algebra.skew(faero)),
                                algebra.der_CquatT_by_v(
                                    tsstr.quat, np.dot(Cab, Xb)))

                        # # Track body
                        # if track_body:
                        #     # NG 20/8/19 - is the Cag needed here? Verify
                        #     Crr[:3, -4:] -= Cag.dot(algebra.der_Cquat_by_v(tsstr.quat, Cga.T.dot(faero)))
                        #
                        #     Crr[3:6, -4:] -= Cag.dot(algebra.skew(zetag).dot(algebra.der_Cquat_by_v(tsstr.quat, Cga.T.dot(faero))))
                        #     Crr[3:6, -4:] += Cag.dot(algebra.skew(faero)).dot(algebra.der_Cquat_by_v(tsstr.quat, Cga.T.dot(zetag)))

        # transfer
        self.Kdisp = Kdisp
        self.Kvel_disp = Kvel_disp
        self.Kdisp_vel = Kdisp_vel
        self.Kvel_vel = Kvel_vel
        self.Kforces = Kforces

        # stiffening factors
        self.Kss = Kss
        self.Krs = Krs
        self.Csr = Csr
        self.Crs = Crs
        self.Crr = Crr
Ejemplo n.º 19
0
def dfqsdzeta_omega(Surfs, Surfs_star):
    """
    Assemble derivative of quasi-steady force w.r.t. to zeta
    The contribution implemented is related with the omega x zeta term
    call: Der_list = dfqsdzeta_omega(Surfs,Surfs_star)
    """

    Der_list = []
    n_surf = len(Surfs)

    for ss in range(n_surf):

        Surf = Surfs[ss]
        skew_omega = algebra.skew(Surf.omega)
        M, N = Surf.maps.M, Surf.maps.N
        K = Surf.maps.K
        Kzeta = Surf.maps.Kzeta
        shape_fqs = Surf.maps.shape_vert_vect  # (3,M+1,N+1)

        ##### omega x zeta contribution
        Der = np.zeros((3 * Kzeta, 3 * Kzeta))

        # loop panels (input, i.e. matrix columns)
        for pp_in in range(K):
            # get (m,n) indices of panel
            mm_in = Surf.maps.ind_2d_pan_scal[0][pp_in]
            nn_in = Surf.maps.ind_2d_pan_scal[1][pp_in]

            # get panel vertices
            zetav_here = Surf.zeta[:, [
                mm_in + 0, mm_in + 1, mm_in + 1, mm_in + 0
            ], [nn_in + 0, nn_in + 0, nn_in + 1, nn_in + 1]].T

            for ll, aa, bb in zip(svec, avec, bvec):
                # get segment
                lv = zetav_here[bb, :] - zetav_here[aa, :]
                Df = (0.25 * Surf.rho * Surf.gamma[mm_in, nn_in]
                      ) * algebra.skew(lv).dot(skew_omega)

                # get vertices m,n indices
                mm_a, nn_a = mm_in + dmver[aa], nn_in + dnver[aa]
                mm_b, nn_b = mm_in + dmver[bb], nn_in + dnver[bb]

                # get vertices 1d index
                ii_a = [
                    np.ravel_multi_index((cc, mm_a, nn_a), shape_fqs)
                    for cc in range(3)
                ]
                ii_b = [
                    np.ravel_multi_index((cc, mm_b, nn_b), shape_fqs)
                    for cc in range(3)
                ]
                Der[np.ix_(ii_a, ii_a)] += Df
                Der[np.ix_(ii_b, ii_a)] += Df
                Der[np.ix_(ii_a, ii_b)] += Df
                Der[np.ix_(ii_b, ii_b)] += Df

        ##### contribution of WAKE TE segments.
        # This is added to Der, as only velocities at the bound vertices are
        # included in the input of the state-space model

        # loop TE bound segment but:
        # - using wake gamma
        # - using orientation of wake panel
        for nn_in in range(N):
            # get TE bound vertices m,n indices
            nn_a = nn_in + dnver[2]
            nn_b = nn_in + dnver[1]

            # get segment
            lv = Surf.zeta[:, M, nn_b] - Surf.zeta[:, M, nn_a]
            Df = (0.25 * Surf.rho *
                  Surf.gamma[mm_in, nn_in]) * algebra.skew(lv).dot(skew_omega)

            # get vertices 1d index on bound
            ii_a = [
                np.ravel_multi_index((cc, M, nn_a), shape_fqs)
                for cc in range(3)
            ]
            ii_b = [
                np.ravel_multi_index((cc, M, nn_b), shape_fqs)
                for cc in range(3)
            ]

            Der[np.ix_(ii_a, ii_a)] += Df
            Der[np.ix_(ii_b, ii_a)] += Df
            Der[np.ix_(ii_a, ii_b)] += Df
            Der[np.ix_(ii_b, ii_b)] += Df

        Der_list.append(Der)

    return Der_list
Ejemplo n.º 20
0
def dfqsduinput(Surfs, Surfs_star):
    """
    Assemble derivative of quasi-steady force w.r.t. external input velocity.
    """

    Der_list = []
    n_surf = len(Surfs)
    assert len(Surfs_star) == n_surf, \
        'Number of bound and wake surfaces much be equal'

    for ss in range(n_surf):

        Surf = Surfs[ss]
        if Surf.u_input_seg is None:
            raise NameError('Input velocities at segments missing')

        M, N = Surf.maps.M, Surf.maps.N
        K = Surf.maps.K
        Kzeta = Surf.maps.Kzeta
        shape_fqs = Surf.maps.shape_vert_vect  # (3,M+1,N+1)

        ##### unit gamma contribution of BOUND panels
        Der = np.zeros((3 * Kzeta, 3 * Kzeta))

        # loop panels (input, i.e. matrix columns)
        for pp_in in range(K):
            # get (m,n) indices of panel
            mm_in = Surf.maps.ind_2d_pan_scal[0][pp_in]
            nn_in = Surf.maps.ind_2d_pan_scal[1][pp_in]

            # get panel vertices
            # zetav_here=Surf.get_panel_vertices_coords(mm_in,nn_in)
            zetav_here = Surf.zeta[:, [
                mm_in + 0, mm_in + 1, mm_in + 1, mm_in + 0
            ], [nn_in + 0, nn_in + 0, nn_in + 1, nn_in + 1]].T

            for ll, aa, bb in zip(svec, avec, bvec):
                # get segment
                lv = zetav_here[bb, :] - zetav_here[aa, :]
                Df = algebra.skew(
                    (-0.25 * Surf.rho * Surf.gamma[mm_in, nn_in]) * lv)

                # get vertices m,n indices
                mm_a, nn_a = mm_in + dmver[aa], nn_in + dnver[aa]
                mm_b, nn_b = mm_in + dmver[bb], nn_in + dnver[bb]

                # get vertices 1d index
                ii_a = [
                    np.ravel_multi_index((cc, mm_a, nn_a), shape_fqs)
                    for cc in range(3)
                ]
                ii_b = [
                    np.ravel_multi_index((cc, mm_b, nn_b), shape_fqs)
                    for cc in range(3)
                ]
                Der[np.ix_(ii_a, ii_a)] += Df
                Der[np.ix_(ii_b, ii_a)] += Df
                Der[np.ix_(ii_a, ii_b)] += Df
                Der[np.ix_(ii_b, ii_b)] += Df

        ##### contribution of WAKE TE segments.
        # This is added to Der, as only velocities at the bound vertices are
        # included in the input of the state-space model

        # loop TE bound segment but:
        # - using wake gamma
        # - using orientation of wake panel
        for nn_in in range(N):
            # get TE bound vertices m,n indices
            nn_a = nn_in + dnver[2]
            nn_b = nn_in + dnver[1]

            # get segment
            lv = Surf.zeta[:, M, nn_b] - Surf.zeta[:, M, nn_a]
            Df = algebra.skew(
                (-0.25 * Surf.rho * Surf.gamma[mm_in, nn_in]) * lv)

            # get vertices 1d index on bound
            ii_a = [
                np.ravel_multi_index((cc, M, nn_a), shape_fqs)
                for cc in range(3)
            ]
            ii_b = [
                np.ravel_multi_index((cc, M, nn_b), shape_fqs)
                for cc in range(3)
            ]

            Der[np.ix_(ii_a, ii_a)] += Df
            Der[np.ix_(ii_b, ii_a)] += Df
            Der[np.ix_(ii_a, ii_b)] += Df
            Der[np.ix_(ii_b, ii_b)] += Df
        Der_list.append(Der)

    return Der_list
Ejemplo n.º 21
0
def dfqsdzeta_vrel0(Surfs, Surfs_star):
    """
    Assemble derivative of quasi-steady force w.r.t. zeta with fixed relative
    velocity - the changes in induced velocities due to zeta over the surface
    inducing the velocity are not accounted for. The routine exploits the
    available relative velocities at the mid-segment points
    """

    Der_list = []
    n_surf = len(Surfs)
    assert len(Surfs_star) == n_surf, \
        'Number of bound and wake surfaces much be equal'

    for ss in range(n_surf):

        Surf = Surfs[ss]
        if not hasattr(Surf, 'u_ind_seg'):
            raise NameError('Induced velocities at segments missing')
        if Surf.u_input_seg is None:
            raise NameError('Input velocities at segments missing')

        M, N = Surf.maps.M, Surf.maps.N
        K = Surf.maps.K
        Kzeta = Surf.maps.Kzeta
        shape_fqs = Surf.maps.shape_vert_vect  # (3,M+1,N+1)

        ##### unit gamma contribution of BOUND panels
        Der = np.zeros((3 * Kzeta, 3 * Kzeta))

        # loop panels (input, i.e. matrix columns)
        for pp_in in range(K):
            # get (m,n) indices of panel
            mm_in = Surf.maps.ind_2d_pan_scal[0][pp_in]
            nn_in = Surf.maps.ind_2d_pan_scal[1][pp_in]

            for ll, aa, bb in zip(svec, avec, bvec):
                vrel_seg = (Surf.u_input_seg[:, ll, mm_in, nn_in] +
                            Surf.u_ind_seg[:, ll, mm_in, nn_in])
                Df = algebra.skew(
                    (0.5 * Surf.rho * Surf.gamma[mm_in, nn_in]) * vrel_seg)

                # get vertices m,n indices
                mm_a, nn_a = mm_in + dmver[aa], nn_in + dnver[aa]
                mm_b, nn_b = mm_in + dmver[bb], nn_in + dnver[bb]

                # get vertices 1d index
                ii_a = [
                    np.ravel_multi_index((cc, mm_a, nn_a), shape_fqs)
                    for cc in range(3)
                ]
                ii_b = [
                    np.ravel_multi_index((cc, mm_b, nn_b), shape_fqs)
                    for cc in range(3)
                ]
                Der[np.ix_(ii_a, ii_a)] += -Df
                Der[np.ix_(ii_b, ii_a)] += -Df
                Der[np.ix_(ii_a, ii_b)] += Df
                Der[np.ix_(ii_b, ii_b)] += Df

        ##### contribution of WAKE TE segments.
        # This is added to Der, as only the bound vertices are included in the
        # input

        # loop TE bound segment but:
        # - using wake gamma
        # - using orientation of wake panel
        for nn_in in range(N):
            # get velocity at seg.3 of wake TE
            vrel_seg = (Surf.u_input_seg[:, 1, M - 1, nn_in] +
                        Surf.u_ind_seg[:, 1, M - 1, nn_in])
            Df = Df = algebra.skew(
                (0.5 * Surfs_star[ss].rho * Surfs_star[ss].gamma[0, nn_in]) *
                vrel_seg)

            # get TE bound vertices m,n indices
            nn_a = nn_in + dnver[2]
            nn_b = nn_in + dnver[1]
            # get vertices 1d index on bound
            ii_a = [
                np.ravel_multi_index((cc, M, nn_a), shape_fqs)
                for cc in range(3)
            ]
            ii_b = [
                np.ravel_multi_index((cc, M, nn_b), shape_fqs)
                for cc in range(3)
            ]

            Der[np.ix_(ii_a, ii_a)] += -Df
            Der[np.ix_(ii_b, ii_a)] += -Df
            Der[np.ix_(ii_a, ii_b)] += Df
            Der[np.ix_(ii_b, ii_b)] += Df
        Der_list.append(Der)

    return Der_list
Ejemplo n.º 22
0
def dfqsdvind_zeta(Surfs, Surfs_star):
    """
    Assemble derivative of quasi-steady force w.r.t. induced velocities changes
    due to zeta.
    """

    n_surf = len(Surfs)
    assert len(Surfs_star) == n_surf, \
        'Number of bound and wake surfaces much be equal'

    # allocate
    Dercoll_list = []
    Dervert_list = []
    for ss_out in range(n_surf):
        Kzeta_out = Surfs[ss_out].maps.Kzeta
        Dercoll_list.append(np.zeros((3 * Kzeta_out, 3 * Kzeta_out)))
        Dervert_list_sub = []
        for ss_in in range(n_surf):
            Kzeta_in = Surfs[ss_in].maps.Kzeta
            Dervert_list_sub.append(np.zeros((3 * Kzeta_out, 3 * Kzeta_in)))
        Dervert_list.append(Dervert_list_sub)

    for ss_out in range(n_surf):

        Surf_out = Surfs[ss_out]
        M_out, N_out = Surf_out.maps.M, Surf_out.maps.N
        K_out = Surf_out.maps.K
        Kzeta_out = Surf_out.maps.Kzeta
        shape_fqs = Surf_out.maps.shape_vert_vect  # (3,M+1,N+1)
        Dercoll = Dercoll_list[ss_out]  # <--link

        ### Loop out (bound) surface panels
        for pp_out in itertools.product(range(0, M_out), range(0, N_out)):
            mm_out, nn_out = pp_out
            # zeta_panel_out=Surf_out.get_panel_vertices_coords(mm_out,nn_out)
            zeta_panel_out = Surf_out.zeta[:, [
                mm_out + 0, mm_out + 1, mm_out + 1, mm_out + 0
            ], [nn_out + 0, nn_out + 0, nn_out + 1, nn_out + 1]].T

            # Loop segments
            for ll, aa, bb in zip(svec, avec, bvec):
                zeta_mid = 0.5 * (zeta_panel_out[bb, :] +
                                  zeta_panel_out[aa, :])
                lv = zeta_panel_out[bb, :] - zeta_panel_out[aa, :]
                Lskew = algebra.skew(
                    (-Surf_out.rho * Surf_out.gamma[mm_out, nn_out]) * lv)

                # get vertices m,n indices
                mm_a, nn_a = mm_out + dmver[aa], nn_out + dnver[aa]
                mm_b, nn_b = mm_out + dmver[bb], nn_out + dnver[bb]
                # get vertices 1d index
                ii_a = [
                    np.ravel_multi_index((cc, mm_a, nn_a), shape_fqs)
                    for cc in range(3)
                ]
                ii_b = [
                    np.ravel_multi_index((cc, mm_b, nn_b), shape_fqs)
                    for cc in range(3)
                ]
                del mm_a, mm_b, nn_a, nn_b

                ### loop input surfaces coordinates
                for ss_in in range(n_surf):
                    ### Bound
                    Surf_in = Surfs[ss_in]
                    M_in_bound, N_in_bound = Surf_in.maps.M, Surf_in.maps.N
                    shape_zeta_in_bound = (3, M_in_bound + 1, N_in_bound + 1)
                    Dervert = Dervert_list[ss_out][ss_in]  # <- link
                    # deriv wrt induced velocity
                    dvind_mid, dvind_vert = dvinddzeta_cpp(zeta_mid,
                                                           Surf_in,
                                                           is_bound=True)
                    # allocate coll
                    Df = np.dot(0.25 * Lskew, dvind_mid)
                    Dercoll[np.ix_(ii_a, ii_a)] += Df
                    Dercoll[np.ix_(ii_b, ii_a)] += Df
                    Dercoll[np.ix_(ii_a, ii_b)] += Df
                    Dercoll[np.ix_(ii_b, ii_b)] += Df
                    # allocate vert
                    Df = np.dot(0.5 * Lskew, dvind_vert)
                    Dervert[ii_a, :] += Df
                    Dervert[ii_b, :] += Df

                    ### wake
                    # deriv wrt induced velocity
                    dvind_mid, dvind_vert = dvinddzeta_cpp(
                        zeta_mid,
                        Surfs_star[ss_in],
                        is_bound=False,
                        M_in_bound=Surf_in.maps.M)
                    # allocate coll
                    Df = np.dot(0.25 * Lskew, dvind_mid)
                    Dercoll[np.ix_(ii_a, ii_a)] += Df
                    Dercoll[np.ix_(ii_b, ii_a)] += Df
                    Dercoll[np.ix_(ii_a, ii_b)] += Df
                    Dercoll[np.ix_(ii_b, ii_b)] += Df

                    Df = np.dot(0.5 * Lskew, dvind_vert)
                    Dervert[ii_a, :] += Df
                    Dervert[ii_b, :] += Df

        # Loop output surf. TE
        # - we use Gammaw_0 over the TE
        # - we run along the positive direction as defined in the first row of
        # wake panels
        for nn_out in range(N_out):

            # get TE bound vertices m,n indices
            nn_a = nn_out + 1
            nn_b = nn_out

            # get segment and mid-point
            zeta_mid = 0.5 * (Surf_out.zeta[:, M_out, nn_b] +
                              Surf_out.zeta[:, M_out, nn_a])
            lv = Surf_out.zeta[:, M_out, nn_b] - Surf_out.zeta[:, M_out, nn_a]
            Lskew = algebra.skew(
                (-Surf_out.rho * Surfs_star[ss_out].gamma[0, nn_out]) * lv)

            # get vertices 1d index on bound
            ii_a = [
                np.ravel_multi_index((cc, M_out, nn_a), shape_fqs)
                for cc in range(3)
            ]
            ii_b = [
                np.ravel_multi_index((cc, M_out, nn_b), shape_fqs)
                for cc in range(3)
            ]

            ### loop input surfaces coordinates
            for ss_in in range(n_surf):
                ### Bound
                Surf_in = Surfs[ss_in]
                M_in_bound, N_in_bound = Surf_in.maps.M, Surf_in.maps.N
                shape_zeta_in_bound = (3, M_in_bound + 1, N_in_bound + 1)
                Dervert = Dervert_list[ss_out][ss_in]  # <- link
                # deriv wrt induced velocity
                dvind_mid, dvind_vert = dvinddzeta_cpp(zeta_mid,
                                                       Surf_in,
                                                       is_bound=True)
                # allocate coll
                Df = np.dot(0.25 * Lskew, dvind_mid)
                Dercoll[np.ix_(ii_a, ii_a)] += Df
                Dercoll[np.ix_(ii_b, ii_a)] += Df
                Dercoll[np.ix_(ii_a, ii_b)] += Df
                Dercoll[np.ix_(ii_b, ii_b)] += Df
                # allocate vert
                Df = np.dot(0.5 * Lskew, dvind_vert)
                Dervert[ii_a, :] += Df
                Dervert[ii_b, :] += Df

                ### wake
                # deriv wrt induced velocity
                dvind_mid, dvind_vert = dvinddzeta_cpp(
                    zeta_mid,
                    Surfs_star[ss_in],
                    is_bound=False,
                    M_in_bound=Surf_in.maps.M)
                # allocate coll
                Df = np.dot(0.25 * Lskew, dvind_mid)
                Dercoll[np.ix_(ii_a, ii_a)] += Df
                Dercoll[np.ix_(ii_b, ii_a)] += Df
                Dercoll[np.ix_(ii_a, ii_b)] += Df
                Dercoll[np.ix_(ii_b, ii_b)] += Df

                # allocate vert
                Df = np.dot(0.5 * Lskew, dvind_vert)
                Dervert[ii_a, :] += Df
                Dervert[ii_b, :] += Df

    return Dercoll_list, Dervert_list
Ejemplo n.º 23
0
def generate_strip(node_info,
                   airfoil_db,
                   aligned_grid,
                   orientation_in=np.array([1, 0, 0]),
                   calculate_zeta_dot=False):
    """
    Returns a strip of panels in ``A`` frame of reference, it has to be then rotated to
    simulate angles of attack, etc
    """
    strip_coordinates_a_frame = np.zeros((3, node_info['M'] + 1),
                                         dtype=ct.c_double)
    strip_coordinates_b_frame = np.zeros((3, node_info['M'] + 1),
                                         dtype=ct.c_double)
    zeta_dot_a_frame = np.zeros((3, node_info['M'] + 1), dtype=ct.c_double)

    # airfoil coordinates
    # we are going to store everything in the x-z plane of the b
    # FoR, so that the transformation Cab rotates everything in place.
    if node_info['M_distribution'] == 'uniform':
        strip_coordinates_b_frame[1, :] = np.linspace(0.0, 1.0,
                                                      node_info['M'] + 1)
    elif node_info['M_distribution'] == '1-cos':
        domain = np.linspace(0, 1.0, node_info['M'] + 1)
        strip_coordinates_b_frame[1, :] = 0.5 * (1.0 - np.cos(domain * np.pi))
    elif node_info['M_distribution'].lower() == 'user_defined':
        # strip_coordinates_b_frame[1, :-1] = np.linspace(0.0, 1.0 - node_info['last_panel_length'], node_info['M'])
        # strip_coordinates_b_frame[1,-1] = 1.
        strip_coordinates_b_frame[
            1, :] = node_info['user_defined_m_distribution']
    else:
        raise NotImplemented('M_distribution is ' +
                             node_info['M_distribution'] +
                             ' and it is not yet supported')
    strip_coordinates_b_frame[2, :] = airfoil_db[node_info['airfoil']](
        strip_coordinates_b_frame[1, :])

    # elastic axis correction
    for i_M in range(node_info['M'] + 1):
        strip_coordinates_b_frame[1, i_M] -= node_info['eaxis']

    # chord_line_b_frame = strip_coordinates_b_frame[:, -1] - strip_coordinates_b_frame[:, 0]
    cs_velocity = np.zeros_like(strip_coordinates_b_frame)

    # control surface deflection
    if node_info['control_surface'] is not None:
        b_frame_hinge_coords = strip_coordinates_b_frame[:, node_info[
            'M'] - node_info['control_surface']['chord']]
        # support for different hinge location for fully articulated control surfaces
        if node_info['control_surface']['hinge_coords'] is not None:
            # make sure the hinge coordinates are only applied when M == cs_chord
            if not node_info['M'] - node_info['control_surface']['chord'] == 0:
                node_info['control_surface']['hinge_coords'] = None
            else:
                b_frame_hinge_coords = node_info['control_surface'][
                    'hinge_coords']

        for i_M in range(
                node_info['M'] - node_info['control_surface']['chord'],
                node_info['M'] + 1):
            relative_coords = strip_coordinates_b_frame[:,
                                                        i_M] - b_frame_hinge_coords
            # rotate the control surface
            relative_coords = np.dot(
                algebra.rotation3d_x(
                    -node_info['control_surface']['deflection']),
                relative_coords)
            # deflection velocity
            try:
                cs_velocity[:, i_M] += np.cross(
                    np.array([
                        -node_info['control_surface']['deflection_dot'], 0.0,
                        0.0
                    ]), relative_coords)
            except KeyError:
                pass

            # restore coordinates
            relative_coords += b_frame_hinge_coords

            # substitute with new coordinates
            strip_coordinates_b_frame[:, i_M] = relative_coords

    # chord scaling
    strip_coordinates_b_frame *= node_info['chord']

    # twist transformation (rotation around x_b axis)
    if np.abs(node_info['twist']) > 1e-6:
        Ctwist = algebra.rotation3d_x(node_info['twist'])
    else:
        Ctwist = np.eye(3)

    # Cab transformation
    Cab = algebra.crv2rotation(node_info['beam_psi'])

    rot_angle = algebra.angle_between_vectors_sign(orientation_in, Cab[:, 1],
                                                   Cab[:, 2])
    if np.sign(np.dot(orientation_in, Cab[:, 1])) >= 0:
        rot_angle += 0.0
    else:
        rot_angle += -2 * np.pi
    Crot = algebra.rotation3d_z(-rot_angle)

    c_sweep = np.eye(3)
    if np.abs(node_info['sweep']) > 1e-6:
        c_sweep = algebra.rotation3d_z(node_info['sweep'])

    # transformation from beam to beam prime (with sweep and twist)
    for i_M in range(node_info['M'] + 1):
        strip_coordinates_b_frame[:, i_M] = np.dot(
            c_sweep,
            np.dot(Crot, np.dot(Ctwist, strip_coordinates_b_frame[:, i_M])))
        strip_coordinates_a_frame[:, i_M] = np.dot(
            Cab, strip_coordinates_b_frame[:, i_M])

        cs_velocity[:, i_M] = np.dot(Cab, cs_velocity[:, i_M])

    # zeta_dot
    if calculate_zeta_dot:
        # velocity due to pos_dot
        for i_M in range(node_info['M'] + 1):
            zeta_dot_a_frame[:, i_M] += node_info['pos_dot']

        # velocity due to psi_dot
        omega_a = algebra.crv_dot2omega(node_info['beam_psi'],
                                        node_info['psi_dot'])
        for i_M in range(node_info['M'] + 1):
            zeta_dot_a_frame[:,
                             i_M] += (np.dot(algebra.skew(omega_a),
                                             strip_coordinates_a_frame[:,
                                                                       i_M]))

        # control surface deflection velocity contribution
        try:
            if node_info['control_surface'] is not None:
                node_info['control_surface']['deflection_dot']
                for i_M in range(node_info['M'] + 1):
                    zeta_dot_a_frame[:, i_M] += cs_velocity[:, i_M]
        except KeyError:
            pass

    else:
        zeta_dot_a_frame = np.zeros((3, node_info['M'] + 1), dtype=ct.c_double)

    # add node coords
    for i_M in range(node_info['M'] + 1):
        strip_coordinates_a_frame[:, i_M] += node_info['beam_coord']

    # add quarter-chord disp
    delta_c = (strip_coordinates_a_frame[:, -1] -
               strip_coordinates_a_frame[:, 0]) / node_info['M']
    if node_info['M_distribution'] == 'uniform':
        for i_M in range(node_info['M'] + 1):
            strip_coordinates_a_frame[:, i_M] += 0.25 * delta_c
    else:
        warnings.warn(
            "No quarter chord disp of grid for non-uniform grid distributions implemented",
            UserWarning)

    # rotation from a to g
    for i_M in range(node_info['M'] + 1):
        strip_coordinates_a_frame[:, i_M] = np.dot(
            node_info['cga'], strip_coordinates_a_frame[:, i_M])
        zeta_dot_a_frame[:, i_M] = np.dot(node_info['cga'],
                                          zeta_dot_a_frame[:, i_M])

    return strip_coordinates_a_frame, zeta_dot_a_frame
Ejemplo n.º 24
0
    def generate(self):
        """
        Generates a matrix mapping a linear control surface deflection onto the aerodynamic grid.

        This generates two matrices:

            * `Kzeta_delta` maps the deflection angle onto displacements. It has as many columns as independent control
              surfaces.

            * `Kdzeta_ddelta` maps the deflection rate onto grid velocities. Again, it has as many columns as
              independent control surfaces.

        Returns:
            tuple: Tuple containing `Kzeta_delta` and `Kdzeta_ddelta`.

        """
        # For future development
        # In hindsight, building this matrix iterating through structural node was a big mistake that
        # has led to very messy code. Would rework by element and in B frame

        aero = self.aero
        structure = self.structure
        linuvlm = self.linuvlm
        tsaero0 = self.tsaero0
        tsstruct0 = self.tsstruct0

        # Find the vertices corresponding to a control surface from beam coordinates to aerogrid
        aero_dict = aero.aero_dict
        n_surf = tsaero0.n_surf
        n_control_surfaces = self.n_control_surfaces

        Kdisp = np.zeros((3 * linuvlm.Kzeta, n_control_surfaces))
        Kvel = np.zeros((3 * linuvlm.Kzeta, n_control_surfaces))
        zeta0 = np.concatenate([tsaero0.zeta[i_surf].reshape(-1, order='C') for i_surf in range(n_surf)])

        Cga = algebra.quat2rotation(tsstruct0.quat).T
        Cag = Cga.T

        # Initialise these parameters
        hinge_axis = None  # Will be set once per control surface to the hinge axis
        with_control_surface = False  # Will be set to true if the spanwise node contains a control surface

        for global_node in range(structure.num_node):

            # Retrieve elements and local nodes to which a single node is attached
            for i_elem in range(structure.num_elem):
                if global_node in structure.connectivities[i_elem, :]:
                    i_local_node = np.where(structure.connectivities[i_elem, :] == global_node)[0][0]

                    for_delta = structure.frame_of_reference_delta[i_elem, :, 0]

                    # CRV to transform from G to B frame
                    psi = tsstruct0.psi[i_elem, i_local_node]
                    Cab = algebra.crv2rotation(psi)
                    Cba = Cab.T
                    Cbg = np.dot(Cab.T, Cag)
                    Cgb = Cbg.T

                    # Map onto aerodynamic coordinates. Some nodes may be part of two aerodynamic surfaces.
                    for structure2aero_node in aero.struct2aero_mapping[global_node]:
                        # Retrieve surface and span-wise coordinate
                        i_surf, i_node_span = structure2aero_node['i_surf'], structure2aero_node['i_n']

                        # Although a node may be part of 2 aerodynamic surfaces, we need to ensure that the current
                        # element for the given node is indeed part of that surface.
                        elems_in_surf = np.where(aero_dict['surface_distribution'] == i_surf)[0]
                        if i_elem not in elems_in_surf:
                            continue

                        # Surface panelling
                        M = aero.aero_dimensions[i_surf][0]
                        N = aero.aero_dimensions[i_surf][1]

                        K_zeta_start = 3 * sum(linuvlm.MS.KKzeta[:i_surf])
                        shape_zeta = (3, M + 1, N + 1)

                        i_control_surface = aero_dict['control_surface'][i_elem, i_local_node]
                        if i_control_surface >= 0:
                            if not with_control_surface:
                                i_start_of_cs = i_node_span.copy()
                                with_control_surface = True

                            control_surface_chord = aero_dict['control_surface_chord'][i_control_surface]

                            try:
                                control_surface_hinge_coord = \
                                    aero_dict['control_surface_hinge_coord'][i_control_surface] * \
                                    aero_dict['chord'][i_elem, i_local_node]
                            except KeyError:
                                control_surface_hinge_coord = None

                            i_node_hinge = M - control_surface_chord
                            i_vertex_hinge = [K_zeta_start +
                                              np.ravel_multi_index((i_axis, i_node_hinge, i_node_span), shape_zeta)
                                              for i_axis in range(3)]
                            i_vertex_next_hinge = [K_zeta_start +
                                                   np.ravel_multi_index((i_axis, i_node_hinge, i_start_of_cs + 1),
                                                                        shape_zeta) for i_axis in range(3)]

                            if control_surface_hinge_coord is not None and M == control_surface_chord:  # fully articulated control surface
                                zeta_hinge = Cgb.dot(Cba.dot(tsstruct0.pos[global_node]) + for_delta * np.array([0, control_surface_hinge_coord, 0]))
                                zeta_next_hinge = Cgb.dot(Cbg.dot(zeta_hinge) + np.array([1, 0, 0]))  # parallel to the x_b vector
                            else:
                                zeta_hinge = zeta0[i_vertex_hinge]
                                zeta_next_hinge = zeta0[i_vertex_next_hinge]

                            if hinge_axis is None:
                                # Hinge axis not yet set for current control surface
                                # Hinge axis is in G frame
                                hinge_axis = zeta_next_hinge - zeta_hinge
                                hinge_axis = hinge_axis / np.linalg.norm(hinge_axis)
                            for i_node_chord in range(M + 1):
                                i_vertex = [K_zeta_start +
                                            np.ravel_multi_index((i_axis, i_node_chord, i_node_span), shape_zeta)
                                            for i_axis in range(3)]

                                if i_node_chord >= i_node_hinge:
                                    # Zeta in G frame
                                    zeta_node = zeta0[i_vertex]  # Gframe
                                    chord_vec = (zeta_node - zeta_hinge)

                                    Kdisp[i_vertex, i_control_surface] = \
                                        Cgb.dot(der_R_arbitrary_axis_times_v(Cbg.dot(hinge_axis),
                                                                             0,
                                                                             -for_delta * Cbg.dot(chord_vec))) * for_delta * -1

                                    # Flap velocity
                                    Kvel[i_vertex, i_control_surface] = -algebra.skew(-for_delta * chord_vec).dot(
                                        hinge_axis) * for_delta * -1

                        else:
                            with_control_surface = False
                            hinge_axis = None  # Reset for next control surface

        # >>>> Merge control surfaces 0 and 1
        # Kdisp[:, 0] -= Kdisp[:, 1]
        # Kvel[:, 0] -= Kvel[:, 1]

        self.Kzeta_delta = Kdisp
        self.Kdzeta_ddelta = Kvel
        return Kdisp, Kvel