Esempio n. 1
0
    def nodal_b_for_2_a_for(self, nodal, tstep, filter=np.array([True] * 6)):
        """
        Projects a nodal variable from the local, body-attached frame (B) to the reference A frame.

        Args:
            nodal (np.array): Nodal variable of size ``(num_node, 6)``
            tstep (sharpy.datastructures.StructTimeStepInfo): structural time step info.
            filter (np.array): optional argument that filters and does not convert a specific degree of
              freedom. Defaults to ``np.array([True, True, True, True, True, True])``.

        Returns:
            np.array: the ``nodal`` argument projected onto the reference ``A`` frame.
        """
        nodal_a = nodal.copy(order='F')
        for i_node in range(self.num_node):
            # get master elem and i_local_node
            i_master_elem, i_local_node = self.node_master_elem[i_node, :]
            crv = tstep.psi[i_master_elem, i_local_node, :]
            cab = algebra.crv2rotation(crv)
            temp = np.zeros((6, ))
            temp[0:3] = np.dot(cab, nodal[i_node, 0:3])
            temp[3:6] = np.dot(cab, nodal[i_node, 3:6])
            for i in range(6):
                if filter[i]:
                    nodal_a[i_node, i] = temp[i]

        return nodal_a
Esempio n. 2
0
    def get_deflection_at_line(self, reference_line=np.array([0, 0, 0.])):
        if self.crv is None:
            return self.deflection

        def_at_line = np.zeros((self.deflection.shape[0], 3))
        for i_node in range(def_at_line.shape[0]):
            def_at_line[i_node] = self.deflection[
                i_node, -3:] + algebra.crv2rotation(
                    self.crv[i_node]).dot(reference_line)

        return def_at_line
Esempio n. 3
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'
Esempio n. 4
0
    def nodal_b_for_2_a_for(self, nodal, tstep, filter=np.array([True]*6)):
        nodal_a = nodal.copy(order='F')
        for i_node in range(self.num_node):
            # get master elem and i_local_node
            i_master_elem, i_local_node = self.node_master_elem[i_node, :]
            crv = tstep.psi[i_master_elem, i_local_node, :]
            cab = algebra.crv2rotation(crv)
            temp = np.zeros((6,))
            temp[0:3] = np.dot(cab, nodal[i_node, 0:3])
            temp[3:6] = np.dot(cab, nodal[i_node, 3:6])
            for i in range(6):
                if filter[i]:
                    nodal_a[i_node, i] = temp[i]

        return nodal_a
Esempio n. 5
0
    def generate(self, tsstruct0, sys):

        structure = sys.structure
        K_thrust = np.zeros_like(sys.Kstr)
        thrust0B = np.zeros((structure.num_dof.value, 3))
        thrust0B[2, :] = np.array([0, self.thrust0, 0])
        thrust0B[26, :] = np.array([0, -self.thrust0, 0])

        for i_node in self.thrust_nodes:
            ee, node_loc = structure.node_master_elem[i_node, :]
            psi = tsstruct0.psi[ee, node_loc, :]
            Cab = algebra.crv2rotation(psi)

            jj = 0  # nodal dof index
            bc_at_node = structure.boundary_conditions[
                i_node]  # Boundary conditions at the node

            if bc_at_node == 1:  # clamp (only rigid-body)
                dofs_at_node = 0
                jj_tra, jj_rot = [], []

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

            jj += dofs_at_node

            if bc_at_node != 1:
                K_thrust[np.ix_(jj_tra, jj_rot)] -= algebra.der_Ccrv_by_v(
                    psi, thrust0B[i_node])

                K_thrust[-10:-7, jj_rot] -= algebra.der_Ccrv_by_v(
                    psi, thrust0B[i_node])

        return K_thrust
Esempio n. 6
0
def get_mode_zeta(data, eigvect):
    """
    Retrieves the UVLM grid nodal displacements associated to the eigenvector ``eigvect``
    """

    ### initialise
    aero = data.aero
    struct = data.structure
    tsaero = data.aero.timestep_info[data.ts]
    tsstr = data.structure.timestep_info[data.ts]

    try:
        num_dof = struct.num_dof.value
    except AttributeError:
        num_dof = struct.num_dof

    eigvect = eigvect[:num_dof]

    zeta_mode = []
    for ss in range(aero.n_surf):
        zeta_mode.append(tsaero.zeta[ss].copy())

    jj = 0  # structural dofs index
    Cga0 = algebra.quat2rotation(tsstr.quat)
    Cag0 = Cga0.T
    for node_glob in range(struct.num_node):

        ### detect bc at node (and no. of dofs)
        bc_here = struct.boundary_conditions[node_glob]
        if bc_here == 1:  # clamp
            dofs_here = 0
            continue
        elif bc_here == -1 or bc_here == 0:
            dofs_here = 6
            jj_tra = [jj, jj + 1, jj + 2]
            jj_rot = [jj + 3, jj + 4, jj + 5]
        jj += dofs_here

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

        # get original position and crv
        Ra0 = tsstr.pos[node_glob, :]
        psi0 = tsstr.psi[ee, node_loc, :]
        Rg0 = np.dot(Cga0, Ra0)
        Cab0 = algebra.crv2rotation(psi0)
        Cbg0 = np.dot(Cab0.T, Cag0)

        # update position and crv of mode
        Ra = tsstr.pos[node_glob, :] + eigvect[jj_tra]
        psi = tsstr.psi[ee, node_loc, :] + eigvect[jj_rot]
        Rg = np.dot(Cga0, Ra)
        Cab = algebra.crv2rotation(psi)
        Cbg = np.dot(Cab.T, Cag0)

        ### 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]

            for mm in range(M + 1):
                # get position of vertex in B FoR
                zetag0 = tsaero.zeta[ss][:, mm,
                                         nn]  # in G FoR, w.r.t. origin A-G
                Xb = np.dot(Cbg0, zetag0 - Rg0)  # in B FoR, w.r.t. origin B

                # update vertex position
                zeta_mode[ss][:, mm, nn] = Rg + np.dot(np.dot(Cga0, Cab), Xb)

    return zeta_mode
    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
Esempio n. 8
0
    ['...', route_main + case_main + '.solver.txt'])
tsaero0 = data0.aero.timestep_info[0]
tsaero0.rho = ws.config['LinearUvlm']['density']

### ----- retrieve transformation matrices
# this is necessary so as to project input motion and output forces in the
# rolled frame of reference R
tsstr0 = data0.structure.timestep_info[0]
### check all CRV are the same
crv_ref = tsstr0.psi[0][0]
for ee in range(data0.structure.num_elem):
    for nn_node in range(3):
        assert np.linalg.norm(crv_ref - tsstr0.psi[ee, nn_node, :]) < 1e-13, \
            'CRV distribution along beam nodes not uniform!'
Cga = algebra.quat2rotation(tsstr0.quat)
Cab = algebra.crv2rotation(tsstr0.psi[0][0])
Cgb = np.dot(Cga, Cab)

### rolled FoR:
# note that, if RollNodes is False, this is equivalent to the FoR A. While
# this transformation is redundant for RollNodes=False, we keep it for debug
Roll0Rad = np.pi / 180. * Roll0Deg
crv_roll = Roll0Rad * np.array([1., 0., 0.])
Cgr = algebra.crv2rotation(crv_roll)
Crg = Cgr.T
Crb = np.dot(Crg, Cgb)
Cra = np.dot(Crg, Cga)

### ----- linearisation
Sol = lin_aeroelastic.LinAeroEla(data0)
Sol.linuvlm.assemble_ss()
Esempio n. 9
0
    def run_wagner(self, Nsurf, integr_ord, RemovePred, UseSparse, RollNodes):
        """
        see test_wagner
        """

        ### ----- set reference solution
        self.setUp_from_params(Nsurf, integr_ord, RemovePred, UseSparse,
                               RollNodes)
        tsaero0 = self.tsaero0
        ws = self.ws
        M = self.M
        N = self.N
        Mstar_fact = self.Mstar_fact
        Uinf0 = self.Uinf0

        ### ----- linearisation
        uvlm = linuvlm.Dynamic(tsaero0,
                               dynamic_settings=ws.config['LinearUvlm'])
        uvlm.assemble_ss()
        zeta_pole = np.array([0., 0., 0.])
        uvlm.get_total_forces_gain(zeta_pole=zeta_pole)
        uvlm.get_rigid_motion_gains(zeta_rotation=zeta_pole)
        uvlm.get_sect_forces_gain()

        ### ----- Scale gains
        Fref_tot = self.Fref_tot
        Fref_span = self.Fref_span
        uvlm.Kftot = uvlm.Kftot / Fref_tot
        uvlm.Kmtot = uvlm.Kmtot / Fref_tot / ws.c_ref
        uvlm.Kfsec /= Fref_span
        uvlm.Kmsec /= (Fref_span * ws.c_ref)

        ### ----- step input
        # rotate incoming flow, wing lattice and wing lattice speed about
        # the (rolled) wing elastic axis to create an effective angle of attack.
        # Rotation is expressed through a CRV.
        delta_AlphaEffDeg = 1e-2
        delta_AlphaEffRad = 1e-2 * np.pi / 180.

        Roll0Rad = self.Roll0Deg / 180. * np.pi
        dcrv = -delta_AlphaEffRad * np.array(
            [0., np.cos(Roll0Rad), np.sin(Roll0Rad)])
        uvec0 = np.array([Uinf0, 0, 0])
        uvec = np.dot(algebra.crv2rotation(dcrv), uvec0)
        duvec = uvec - uvec0
        dzeta = np.zeros((Nsurf, 3, M + 1, N // Nsurf + 1))
        dzeta_dot = np.zeros((Nsurf, 3, M + 1, N // Nsurf + 1))
        du_ext = np.zeros((Nsurf, 3, M + 1, N // Nsurf + 1))

        for ss in range(Nsurf):
            for mm in range(M + 1):
                for nn in range(N // Nsurf + 1):
                    dzeta_dot[ss, :, mm, nn] = -1. / 3 * duvec
                    du_ext[ss, :, mm, nn] = +1. / 3 * duvec
                    dzeta = 1. / 3 * np.dot(uvlm.Krot, dcrv)
        Uaero = np.concatenate(
            (dzeta.reshape(-1), dzeta_dot.reshape(-1), du_ext.reshape(-1)))

        ### ----- Steady state solution
        xste, yste = uvlm.solve_steady(Uaero)
        Ftot_ste = np.dot(uvlm.Kftot, yste)
        Mtot_ste = np.dot(uvlm.Kmtot, yste)
        # first check of gain matrices...
        Ftot_ste_ref = np.zeros((3, ))
        Mtot_ste_ref = np.zeros((3, ))
        fnodes = yste.reshape((Nsurf, 3, M + 1, N // Nsurf + 1))
        for ss in range(Nsurf):
            for nn in range(N // Nsurf + 1):
                for mm in range(M + 1):
                    Ftot_ste_ref += fnodes[ss, :, mm, nn]
                    Mtot_ste_ref += np.cross(uvlm.MS.Surfs[ss].zeta[:, mm, nn],
                                             fnodes[ss, :, mm, nn])
        Ftot_ste_ref /= Fref_tot
        Mtot_ste_ref /= (Fref_tot * ws.c_ref)
        Fmag = np.linalg.norm(Ftot_ste_ref)
        er_f = np.max(np.abs(Ftot_ste - Ftot_ste_ref)) / Fmag
        er_m = np.max(np.abs(Mtot_ste - Mtot_ste_ref)) / Fmag / ws.c_ref
        assert (er_f < 1e-8 and er_m < 1e-8), \
            'Error of total forces (%.2e) and moment (%.2e) too large!' % (er_f, er_m) + \
            'Verify gains produced by linuvlm.Dynamic.get_total_forces_gain.'

        # then compare against analytical ...
        Cl_inf = delta_AlphaEffRad * np.pi * 2.
        Cfvec_inf = Cl_inf * np.array(
            [0., -np.sin(Roll0Rad), np.cos(Roll0Rad)])
        er_f = np.abs(np.linalg.norm(Ftot_ste) / Cl_inf - 1.)
        assert (er_f < 1e-2), \
            'Error of total lift coefficient (%.2e) too large!' % (er_f,) + \
            'Verify linuvlm.Dynamic.'
        er_f = np.abs(np.linalg.norm(Ftot_ste - Cfvec_inf) / Cl_inf)
        assert (er_f < 1e-2), \
            'Error of total aero force (%.2e) too large!' % (er_f,) + \
            'Verify linuvlm.Dynamic.'

        # ... and finally compare against non-linear UVLM
        # ps: here we roll the wing and rotate the incoming flow to generate an effective
        # angle of attack
        case_pert = 'wagner_r%.4daeff%.2d_rnodes%s_Nsurf%.2dM%.2dN%.2dwk%.2d' \
                    % (int(np.round(100 * self.Roll0Deg)),
                       int(np.round(100 * delta_AlphaEffDeg)),
                       RollNodes,
                       Nsurf, M, N, Mstar_fact)
        ws_pert = flying_wings.QuasiInfinite(M=M,
                                             N=N,
                                             Mstar_fact=Mstar_fact,
                                             n_surfaces=Nsurf,
                                             u_inf=Uinf0,
                                             alpha=self.Alpha0Deg,
                                             roll=self.Roll0Deg,
                                             aspect_ratio=1e5,
                                             route=self.route_main,
                                             case_name=case_pert,
                                             RollNodes=RollNodes)
        ws_pert.u_inf_direction = uvec / Uinf0
        ws_pert.main_ea = ws.main_ea
        ws_pert.clean_test_files()
        ws_pert.update_derived_params()
        ws_pert.generate_fem_file()
        ws_pert.generate_aero_file()

        # solution flow
        ws_pert.set_default_config_dict()
        ws_pert.config['SHARPy']['flow'] = ws.config['SHARPy']['flow']
        ws_pert.config['SHARPy']['write_screen'] = 'off'
        ws_pert.config['SHARPy']['write_log'] = 'off'
        ws_pert.config['SHARPy'][
            'log_folder'] = self.route_test_dir + '/output/' + self.case_code + '/'
        ws_pert.config.write()

        # solve at perturbed point
        data_pert = sharpy.sharpy_main.main(
            ['...', self.route_main + case_pert + '.sharpy'])
        tsaero = data_pert.aero.timestep_info[0]

        self.start_writer()

        # get total forces
        Ftot_ste_pert = np.zeros((3, ))
        Mtot_ste_pert = np.zeros((3, ))
        for ss in range(Nsurf):
            for nn in range(N // Nsurf + 1):
                for mm in range(M + 1):
                    Ftot_ste_pert += tsaero.forces[ss][:3, mm, nn]
                    Mtot_ste_pert += np.cross(
                        uvlm.MS.Surfs[ss].zeta[:, mm, nn],
                        tsaero.forces[ss][:3, mm, nn])
        Ftot_ste_pert /= Fref_tot
        Mtot_ste_pert /= (Fref_tot * ws.c_ref)
        Fmag = np.linalg.norm(Ftot_ste_pert)
        er_f = np.max(np.abs(Ftot_ste - Ftot_ste_pert)) / Fmag
        er_m = np.max(np.abs(Mtot_ste - Mtot_ste_pert)) / Fmag / ws.c_ref
        assert (er_f < 2e-4 and er_m < 2e-4), \
            'Error of total forces (%.2e) and moment (%.2e) ' % (er_f, er_m) + \
            'with respect to geometrically-exact UVLM too large!'

        # and check non-linear uvlm against analytical solution
        er_f = np.abs(np.linalg.norm(Ftot_ste_pert - Cfvec_inf) / Cl_inf)
        assert (er_f <= 1.5e-2), \
            'Error of total aero force components (%.2e) too large!' % (er_f,) + \
            'Verify StaticUvlm'

        ### ----- Analytical step response (Wagner solution)

        NT = 251
        tv = np.linspace(0., uvlm.dt * (NT - 1), NT)
        Clv_an = an.wagner_imp_start(delta_AlphaEffRad, Uinf0, ws.c_ref, tv)
        assert np.abs(Clv_an[-1] / Cl_inf - 1.) < 1e-2, \
            'Did someone modify this test case?! The time should be enough to reach ' \
            'the steady-state CL with a 1 perc. tolerance...'

        Cfvec_an = np.zeros((NT, 3))
        Cfvec_an[:, 1] = -np.sin(Roll0Rad) * Clv_an
        Cfvec_an[:, 2] = np.cos(Roll0Rad) * Clv_an

        ### ----- Dynamic step response

        Fsect = np.zeros((NT, Nsurf, 3, N // Nsurf + 1))
        # Fbeam=np.zeros((NT,6,N//Nsurf+1))
        Ftot = np.zeros((NT, 3))
        Er_f_tot = np.zeros((NT, ))

        # Ybeam=[]
        gamma = np.zeros((NT, Nsurf, M, N // Nsurf))
        gamma_dot = np.zeros((NT, Nsurf, M, N // Nsurf))
        gamma_star = np.zeros((NT, Nsurf, int(M * Mstar_fact), N // Nsurf))
        xold = np.zeros((uvlm.SS.A.shape[0], ))
        for tt in range(1, NT):
            xnew, ynew = uvlm.solve_step(xold, Uaero)
            change = np.linalg.norm(xnew - xold)
            xold = xnew

            # record state ?
            if uvlm.remove_predictor is False:
                gv, gvstar, gvdot = uvlm.unpack_state(xnew)
                gamma[tt, :, :, :] = gv.reshape((Nsurf, M, N // Nsurf))
                gamma_dot[tt, :, :, :] = gvdot.reshape((Nsurf, M, N // Nsurf))
                gamma_star[tt, :, :, :] = gvstar.reshape(
                    (Nsurf, int(M * Mstar_fact), N // Nsurf))

            # calculate forces (and error)
            Ftot[tt, :3] = np.dot(uvlm.Kftot, ynew)
            Er_f_tot[tt] = np.linalg.norm(Ftot[tt, :] -
                                          Cfvec_an[tt, :]) / Clv_an[tt]
            Fsect[tt, :, :, :] = np.dot(uvlm.Kfsec, ynew).reshape(
                (Nsurf, 3, N // Nsurf + 1))

            # ### beam forces
            # Ybeam.append(np.dot(Sol.Kforces[:-10,:],ynew))
            # Fdummy=Ybeam[-1].reshape((N//Nsurf,6)).T
            # Fbeam[tt,:,:N//Nsurf]=Fdummy[:,:N//Nsurf]
            # Fbeam[tt,:,N//Nsurf+1:]=Fdummy[:,N//Nsurf:]

        if RemovePred:
            ts2perc, ts1perc = 6, 6
        else:
            ts2perc, ts1perc = 16, 36
        er_th_2perc = np.max(Er_f_tot[ts2perc:])
        er_th_1perc = np.max(Er_f_tot[ts1perc:])

        ### ----- generate plot
        if self.ProducePlots:

            # sections to plot
            if Nsurf == 1:
                Nplot = [0, N // 2, N]
                labs = [r'tip', r'root', r'tip']
            elif Nsurf == 2:
                Nplot = [0, N // 2 - 1]
                labs = [r'tip', r'near root', r'tip', r'near root']
            axtitle = [r'$C_{F_y}$', r'$C_{F_z}$']

            # non-dimensional time
            sv = 2.0 * Uinf0 * tv / ws.c_ref

            # generate figure
            clist = ['#003366', '#CC3333', '#336633', '#FF6600'] * 4
            fontlabel = 12
            std_params = {
                'legend.fontsize': 10,
                'font.size': fontlabel,
                'xtick.labelsize': fontlabel - 2,
                'ytick.labelsize': fontlabel - 2,
                'figure.autolayout': True,
                'legend.numpoints': 1
            }
            # plt.rcParams.update(std_params)

            # fig = plt.figure('Lift time-history', (12, 6))
            # axvec = fig.subplots(1, 2)
            # for aa in [0, 1]:
            # comp = aa + 1
            # axvec[aa].set_title(axtitle[aa])
            # axvec[aa].plot(sv, Cfvec_an[:, comp] / Cl_inf, lw=4, ls='-',
            # alpha=0.5, color='r', label=r'Wagner')
            # axvec[aa].plot(sv, Ftot[:, comp] / Cl_inf, lw=5, ls=':',
            # alpha=0.7, color='k', label=r'Total')
            # cc = 0
            # for ss in range(Nsurf):
            # for nn in Nplot:
            # axvec[aa].plot(sv, Fsect[:, ss, comp, nn] / Cl_inf,
            # lw=4 - cc, ls='--', alpha=0.7, color=clist[cc],
            # label=r'Surf. %.1d, n=%.2d (%s)' % (ss, nn, labs[cc]))
            # cc += 1
            # axvec[aa].grid(color='0.8', ls='-')
            # axvec[aa].grid(color='0.85', ls='-', which='minor')
            # axvec[aa].set_xlabel(r'normalised time $t=2 U_\infty \tilde{t}/c$')
            # axvec[aa].set_ylabel(axtitle[aa] + r'$/C_{l_\infty}$')
            # axvec[aa].set_xlim(0, sv[-1])
            # if Cfvec_inf[comp] > 0.:
            # axvec[aa].set_ylim(0, 1.1)
            # else:
            # axvec[aa].set_ylim(-1.1, 0)
            # plt.legend(ncol=1)
            # # plt.show()
            # fig.savefig(self.figfold + self.case_main + '.png')
            # fig.savefig(self.figfold + self.case_main + '.pdf')
            # plt.close()

        assert er_th_2perc < 2e-2 and er_th_1perc < 1e-2, \
            'Error of dynamic step response at time-steps 16 and 36 ' + \
            '(%.2e and %.2e) too large. Verify Linear UVLM.' % (er_th_2perc, er_th_1perc)
Esempio n. 10
0
def polars(data, aero_kstep, structural_kstep, struct_forces, **kwargs):
    r"""
    This function corrects the aerodynamic forces from UVLM based on the airfoil polars provided by the user in the aero.h5 file

    These are the steps needed to correct the forces:

        * The force coming from UVLM is divided into induced drag (parallel to the incoming flow velocity) and lift (the remaining force).
        * The angle of attack is computed based on that lift force and the angle of zero lift computed form the airfoil polar and assuming a slope of :math:`2 \pi`
        * The drag force is computed based on the angle of attack and the polars provided by the user

    Args:
        data (:class:`sharpy.PreSharpy`): SHARPy data
        aero_kstep (:class:`sharpy.utils.datastructures.AeroTimeStepInfo`): Current aerodynamic substep
        structural_kstep (:class:`sharpy.utils.datastructures.StructTimeStepInfo`): Current structural substep
        struct_forces (np.array): Array with the aerodynamic forces mapped on the structure in the B frame of reference

    Keyword Arguments:
        rho (float): air density
        correct_lift (bool): correct also lift coefficient according to the polars
        cd_from_cl (bool): interpolate drag from lift instead of computing the AoA first
        
    Returns:
         np.ndarray: corresponding aerodynamic force at the structural node from the force and moment at a grid vertex
    """

    aerogrid = data.aero
    beam = data.structure
    rho = kwargs.get('rho', 1.225)
    correct_lift = kwargs.get('correct_lift', False)
    cd_from_cl = kwargs.get('cd_from_cl', False)
    aero_dict = aerogrid.aero_dict
    if aerogrid.polars is None:
        return struct_forces
    new_struct_forces = np.zeros_like(struct_forces)

    nnode = struct_forces.shape[0]
    for inode in range(nnode):
        new_struct_forces[inode, :] = struct_forces[inode, :].copy()
        if aero_dict['aero_node'][inode]:

            ielem, inode_in_elem = beam.node_master_elem[inode]
            iairfoil = aero_dict['airfoil_distribution'][ielem, inode_in_elem]
            isurf = aerogrid.struct2aero_mapping[inode][0]['i_surf']
            i_n = aerogrid.struct2aero_mapping[inode][0]['i_n']
            N = aerogrid.aero_dimensions[isurf, 1]
            polar = aerogrid.polars[iairfoil]
            cab = algebra.crv2rotation(structural_kstep.psi[ielem, inode_in_elem, :])
            cga = algebra.quat2rotation(structural_kstep.quat)
            cgb = np.dot(cga, cab)

            # Deal with the extremes
            if i_n == 0:
                node1 = 0
                node2 = 1
            elif i_n == N:
                node1 = nnode - 1
                node2 = nnode - 2
            else:
                node1 = inode + 1
                node2 = inode - 1

            # Define the span and the span direction
            dir_span = 0.5*np.dot(cga,
                              structural_kstep.pos[node1, :] - structural_kstep.pos[node2, :])
            span = np.linalg.norm(dir_span)
            dir_span = algebra.unit_vector(dir_span)

            # Define the chord and the chord direction
            dir_chord = aero_kstep.zeta[isurf][:, -1, i_n] - aero_kstep.zeta[isurf][:, 0, i_n]
            chord = np.linalg.norm(dir_chord)
            dir_chord = algebra.unit_vector(dir_chord)

            # Define the relative velocity and its direction
            urel = (structural_kstep.pos_dot[inode, :] +
                    structural_kstep.for_vel[0:3] +
                    np.cross(structural_kstep.for_vel[3:6],
                             structural_kstep.pos[inode, :]))
            urel = -np.dot(cga, urel)
            urel += np.average(aero_kstep.u_ext[isurf][:, :, i_n], axis=1)
            # uind = uvlmlib.uvlm_calculate_total_induced_velocity_at_points(aero_kstep,
            #                                                                np.array([structural_kstep.pos[inode, :] - np.array([0, 0, 1])]),
            #                                                                structural_kstep.for_pos,
            #                                                                ct.c_uint(8))[0]
            # print(inode, urel, uind)
            # urel -= uind
            dir_urel = algebra.unit_vector(urel)


            # Force in the G frame of reference
            force = np.dot(cgb,
                           struct_forces[inode, 0:3])
            dir_force = algebra.unit_vector(force)

            # Coefficient to change from aerodynamic coefficients to forces (and viceversa)
            coef = 0.5*rho*np.linalg.norm(urel)**2*chord*span

            # Divide the force in drag and lift
            drag_force = np.dot(force, dir_urel)*dir_urel
            lift_force = force - drag_force

            # Compute the associated lift
            cl = np.linalg.norm(lift_force)/coef
            cd_sharpy = np.linalg.norm(drag_force)/coef

            if cd_from_cl:
                # Compute the drag from the lift
                cd, cm = polar.get_cdcm_from_cl(cl)

            else:
                # Compute the angle of attack assuming that UVLM gives a 2pi polar
                aoa_deg_2pi = polar.get_aoa_deg_from_cl_2pi(cl)

                # Compute the coefficients assocaited to that angle of attack
                cl_new, cd, cm = polar.get_coefs(aoa_deg_2pi)
                # print(cl, cl_new)
    
                if correct_lift:
                    cl = cl_new

            # Recompute the forces based on the coefficients
            lift_force = cl*algebra.unit_vector(lift_force)*coef
            drag_force += cd*dir_urel*coef
            force = lift_force + drag_force
            new_struct_forces[inode, 0:3] = np.dot(cgb.T,
                                               force)

    return new_struct_forces
Esempio n. 11
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)
Esempio n. 12
0
    def test_quat_wrt_rot(self):
        """
        We define:
        - G: initial frame
        - A: frame obtained upon rotation, Cga, defined by the quaternion q0
        - B: frame obtained upon further rotation, Cab, of A defined by 
        the "infinitesimal" Cartesian rotation vector dcrv
        The test verifies that:
        1. the total rotation matrix Cgb(q0+dq) is equal to
            Cgb = Cga(q0) Cab(dcrv)
        where 
            dq = algebra.der_quat_wrt_crv(q0)
        2. the difference between analytically computed delta quaternion, dq, 
        and the numerical delta
            dq_num = algebra.crv2quat(algebra.rotation2crv(Cgb_ref))-q0
        is comparable to the step used to compute the delta dcrv
        3. The equality:
            d(Cga(q0)*v)*dq = Cga(q0) * d(Cab*dv)*dcrv
        where d(Cga(q0)*v) and d(Cab*dv) are the derivatives computed through
            algebra.der_Cquat_by_v and algebra.der_Ccrv_by_v
        for a random vector v.

        Warning:
        - The relation dcrv->dquat is not uniquely defined. However, the 
        resulting rotation matrix is, namely:
            Cga(q0+dq)=Cga(q0)*Cab(dcrv) 
        """

        ### case 1: simple rotation about the same axis

        # linearisation point
        a0 = 30. * np.pi / 180
        n0 = np.array([0, 0, 1])
        n0 = n0 / np.linalg.norm(n0)
        q0 = algebra.crv2quat(a0 * n0)
        Cga = algebra.quat2rotation(q0)

        # direction of perturbation
        n2 = n0

        A = np.array([1e-2, 1e-3, 1e-4, 1e-5, 1e-6])
        for a in A:
            drot = a * n2

            # build rotation manually
            atot = a0 + a
            Cgb_exp = algebra.crv2rotation(atot * n0)  # ok

            # build combined rotation
            Cab = algebra.crv2rotation(drot)
            Cgb_ref = np.dot(Cga, Cab)

            # verify expected vs combined rotation matrices
            assert np.linalg.norm(Cgb_exp - Cgb_ref) / a < 1e-8, \
                'Verify test case - these matrices need to be identical'

            # verify analytical rotation matrix
            dq_an = np.dot(algebra.der_quat_wrt_crv(q0), drot)
            Cgb_an = algebra.quat2rotation(q0 + dq_an)
            erel_rot = np.linalg.norm(Cgb_an - Cgb_ref) / a
            assert erel_rot < 3e-3, \
                'Relative error of rotation matrix (%.2e) too large!' % erel_rot

            # verify delta quaternion
            erel_dq = np.linalg.norm(Cgb_an - Cgb_ref)
            dq_num = algebra.crv2quat(algebra.rotation2crv(Cgb_ref)) - q0
            erel_dq = np.linalg.norm(dq_num -
                                     dq_an) / np.linalg.norm(dq_an) / a
            assert erel_dq < .3, \
                'Relative error delta quaternion (%.2e) too large!' % erel_dq

            # verify algebraic relation
            v = np.ones((3, ))
            D1 = algebra.der_Cquat_by_v(q0, v)
            D2 = algebra.der_Ccrv_by_v(np.zeros((3, )), v)
            res = np.dot(D1, dq_num) - np.dot(np.dot(Cga, D2), drot)
            erel_res = np.linalg.norm(res) / a
            assert erel_res < 5e-1 * a, \
                'Relative error of residual (%.2e) too large!' % erel_res

        ### case 2: random rotation

        # linearisation point
        a0 = 30. * np.pi / 180
        n0 = np.array([-2, -1, 1])
        n0 = n0 / np.linalg.norm(n0)
        q0 = algebra.crv2quat(a0 * n0)
        Cga = algebra.quat2rotation(q0)

        # direction of perturbation
        n2 = np.array([0.5, 1., -2.])
        n2 = n2 / np.linalg.norm(n2)

        A = np.array([1e-2, 1e-3, 1e-4, 1e-5, 1e-6])
        for a in A:
            drot = a * n2

            # build combined rotation
            Cab = algebra.crv2rotation(drot)
            Cgb_ref = np.dot(Cga, Cab)

            # verify analytical rotation matrix
            dq_an = np.dot(algebra.der_quat_wrt_crv(q0), drot)
            Cgb_an = algebra.quat2rotation(q0 + dq_an)
            erel_rot = np.linalg.norm(Cgb_an - Cgb_ref) / a
            assert erel_rot < 3e-3, \
                'Relative error of rotation matrix (%.2e) too large!' % erel_rot

            # verify delta quaternion
            erel_dq = np.linalg.norm(Cgb_an - Cgb_ref)
            dq_num = algebra.crv2quat(algebra.rotation2crv(Cgb_ref)) - q0
            erel_dq = np.linalg.norm(dq_num -
                                     dq_an) / np.linalg.norm(dq_an) / a
            assert erel_dq < .3, \
                'Relative error delta quaternion (%.2e) too large!' % erel_dq

            # verify algebraic relation
            v = np.ones((3, ))
            D1 = algebra.der_Cquat_by_v(q0, v)
            D2 = algebra.der_Ccrv_by_v(np.zeros((3, )), v)
            res = np.dot(D1, dq_num) - np.dot(np.dot(Cga, D2), drot)
            erel_res = np.linalg.norm(res) / a
            assert erel_res < 5e-1 * a, \
                'Relative error of residual (%.2e) too large!' % erel_res
Esempio n. 13
0
def aero2struct_force_mapping(aero_forces,
                              struct2aero_mapping,
                              zeta,
                              pos_def,
                              psi_def,
                              master,
                              conn,
                              cag=np.eye(3),
                              aero_dict=None):
    r"""
    Maps the aerodynamic forces at the lattice to the structural nodes

    The aerodynamic forces from the UVLM are always in the inertial ``G`` frame of reference and have to be transformed
    to the body or local ``B`` frame of reference in which the structural forces are defined.

    Since the structural nodes and aerodynamic panels are coincident in a spanwise direction, the aerodynamic forces
    that correspond to a structural node are the summation of the ``M+1`` forces defined at the lattice at that
    spanwise location.

    .. math::
        \mathbf{f}_{struct}^B &= \sum\limits_{i=0}^{m+1}C^{BG}\mathbf{f}_{i,aero}^G \\
        \mathbf{m}_{struct}^B &= \sum\limits_{i=0}^{m+1}C^{BG}(\mathbf{m}_{i,aero}^G +
        \tilde{\boldsymbol{\zeta}}^G\mathbf{f}_{i, aero}^G)

    where :math:`\tilde{\boldsymbol{\zeta}}^G` is the skew-symmetric matrix of the vector between the lattice
    grid vertex and the structural node.

    Args:
        aero_forces (list): Aerodynamic forces from the UVLM in inertial frame of reference
        struct2aero_mapping (dict): Structural to aerodynamic node mapping
        zeta (list): Aerodynamic grid coordinates
        pos_def (np.ndarray): Vector of structural node displacements
        psi_def (np.ndarray): Vector of structural node rotations (CRVs)
        master: Unused
        conn (np.ndarray): Connectivities matrix
        cag (np.ndarray): Transformation matrix between inertial and body-attached reference ``A``
        aero_dict (dict): Dictionary containing the grid's information.

    Returns:
        np.ndarray: structural forces in an ``n_node x 6`` vector
    """

    n_node, _ = pos_def.shape
    n_elem, _, _ = psi_def.shape
    struct_forces = np.zeros((n_node, 6))

    nodes = []

    for i_elem in range(n_elem):
        for i_local_node in range(3):

            i_global_node = conn[i_elem, i_local_node]
            if i_global_node in nodes:
                continue

            nodes.append(i_global_node)
            for mapping in struct2aero_mapping[i_global_node]:
                i_surf = mapping['i_surf']
                i_n = mapping['i_n']
                _, n_m, _ = aero_forces[i_surf].shape

                crv = psi_def[i_elem, i_local_node, :]
                cab = algebra.crv2rotation(crv)
                cbg = np.dot(cab.T, cag)

                for i_m in range(n_m):
                    chi_g = zeta[i_surf][:, i_m, i_n] - np.dot(cag.T, pos_def[i_global_node, :])
                    struct_forces[i_global_node, 0:3] += np.dot(cbg, aero_forces[i_surf][0:3, i_m, i_n])
                    struct_forces[i_global_node, 3:6] += np.dot(cbg, aero_forces[i_surf][3:6, i_m, i_n])
                    struct_forces[i_global_node, 3:6] += np.dot(cbg, algebra.cross3(chi_g, aero_forces[i_surf][0:3, i_m, i_n]))

    return struct_forces
Esempio n. 14
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
Esempio n. 15
0
    def test_rotation_matrices_derivatives(self):
        """
        Checks derivatives of rotation matrix derivatives with respect to
        quaternions and Cartesian rotation vectors

        Note: test only includes CRV <-> quaternions conversions
        """

        ### linearisation point
        # fi0=np.pi/6
        # nv0=np.array([1,3,1])
        fi0 = 2.0 * np.pi * random.random() - np.pi
        nv0 = np.array([random.random(), random.random(), random.random()])
        nv0 = nv0 / np.linalg.norm(nv0)
        fv0 = fi0 * nv0
        qv0 = algebra.crv2quat(fv0)
        ev0 = algebra.quat2euler(qv0)

        # direction of perturbation
        # fi1=np.pi/3
        # nv1=np.array([-2,4,1])
        fi1 = 2.0 * np.pi * random.random() - np.pi
        nv1 = np.array([random.random(), random.random(), random.random()])
        nv1 = nv1 / np.linalg.norm(nv1)
        fv1 = fi1 * nv1
        qv1 = algebra.crv2quat(fv1)
        ev1 = algebra.quat2euler(qv1)

        # linearsation point
        Cga0 = algebra.quat2rotation(qv0)
        Cag0 = Cga0.T
        Cab0 = algebra.crv2rotation(fv0)
        Cba0 = Cab0.T
        Cga0_euler = algebra.euler2rot(ev0)
        Cag0_euler = Cga0_euler.T

        # derivatives
        # xv=np.ones((3,)) # dummy vector
        xv = np.array([random.random(),
                       random.random(),
                       random.random()])  # dummy vector
        derCga = algebra.der_Cquat_by_v(qv0, xv)
        derCag = algebra.der_CquatT_by_v(qv0, xv)
        derCab = algebra.der_Ccrv_by_v(fv0, xv)
        derCba = algebra.der_CcrvT_by_v(fv0, xv)
        derCga_euler = algebra.der_Ceuler_by_v(ev0, xv)
        derCag_euler = algebra.der_Peuler_by_v(ev0, xv)

        A = np.array([1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6])
        er_ag = 10.
        er_ga = 10.
        er_ab = 10.
        er_ba = 10.
        er_ag_euler = 10.
        er_ga_euler = 10.

        for a in A:
            # perturbed
            qv = a * qv1 + (1. - a) * qv0
            fv = a * fv1 + (1. - a) * fv0
            ev = a * ev1 + (1. - a) * ev0
            dqv = qv - qv0
            dfv = fv - fv0
            dev = ev - ev0
            Cga = algebra.quat2rotation(qv)
            Cag = Cga.T
            Cab = algebra.crv2rotation(fv)
            Cba = Cab.T
            Cga_euler = algebra.euler2rot(ev)
            Cag_euler = Cga_euler.T

            dCag_num = np.dot(Cag - Cag0, xv)
            dCga_num = np.dot(Cga - Cga0, xv)
            dCag_an = np.dot(derCag, dqv)
            dCga_an = np.dot(derCga, dqv)
            er_ag_new = np.max(np.abs(dCag_num - dCag_an))
            er_ga_new = np.max(np.abs(dCga_num - dCga_an))

            dCab_num = np.dot(Cab - Cab0, xv)
            dCba_num = np.dot(Cba - Cba0, xv)
            dCab_an = np.dot(derCab, dfv)
            dCba_an = np.dot(derCba, dfv)
            er_ab_new = np.max(np.abs(dCab_num - dCab_an))
            er_ba_new = np.max(np.abs(dCba_num - dCba_an))

            dCag_num_euler = np.dot(Cag_euler - Cag0_euler, xv)
            dCga_num_euler = np.dot(Cga_euler - Cga0_euler, xv)
            dCag_an_euler = np.dot(derCag_euler, dev)
            dCga_an_euler = np.dot(derCga_euler, dev)
            er_ag_euler_new = np.max(np.abs(dCag_num_euler - dCag_an_euler))
            er_ga_euler_new = np.max(np.abs(dCga_num_euler - dCga_an_euler))

            assert er_ga_new < er_ga, 'der_Cquat_by_v error not converging to 0'
            assert er_ag_new < er_ag, 'der_CquatT_by_v error not converging to 0'
            assert er_ab_new < er_ab, 'der_Ccrv_by_v error not converging to 0'
            assert er_ba_new < er_ba, 'der_CcrvT_by_v error not converging to 0'
            assert er_ga_euler_new < er_ga_euler, 'der_Ceuler_by_v error not converging to 0'
            assert er_ag_euler_new < er_ag_euler, 'der_Peuler_by_v error not converging to 0'

            er_ag = er_ag_new
            er_ga = er_ga_new
            er_ab = er_ab_new
            er_ba = er_ba_new
            er_ag_euler = er_ag_euler_new
            er_ga_euler = er_ga_euler_new

        assert er_ga < A[-2], 'der_Cquat_by_v error too large'
        assert er_ag < A[-2], 'der_CquatT_by_v error too large'
        assert er_ab < A[-2], 'der_Ccrv_by_v error too large'
        assert er_ba < A[-2], 'der_CcrvT_by_v error too large'
        assert er_ag_euler < A[-2], 'der_Peuler_by_v error too large'
        assert er_ga_euler < A[-2], 'der_Ceuler_by_v error too large'
Esempio n. 16
0
def aero2struct_force_mapping(aero_forces,
                              struct2aero_mapping,
                              zeta,
                              pos_def,
                              psi_def,
                              master,
                              conn,
                              cag=np.eye(3),
                              aero_dict=None):
    r"""
    Maps the aerodynamic forces at the lattice to the structural nodes

    The aerodynamic forces from the UVLM are always in the inertial ``G`` frame of reference and have to be transformed
    to the body or local ``B`` frame of reference in which the structural forces are defined.

    Since the structural nodes and aerodynamic panels are coincident in a spanwise direction, the aerodynamic forces
    that correspond to a structural node are the summation of the ``M+1`` forces defined at the lattice at that
    spanwise location.

    .. math::
        \mathbf{f}_{struct}^B &= \sum\limits_{i=0}^{m+1}C^{BG}\mathbf{f}_{i,aero}^G \\
        \mathbf{m}_{struct}^B &= \sum\limits_{i=0}^{m+1}C^{BG}(\mathbf{m}_{i,aero}^G +
        \tilde{\boldsymbol{\zeta}}^G\mathbf{f}_{i, aero}^G)

    where :math:`\tilde{\boldsymbol{\zeta}}^G` is the skew-symmetric matrix of the vector between the lattice
    grid vertex and the structural node.

    It is possible to introduce efficiency and constant terms in the mapping of forces that are user-defined. For more
    info see :func:`~sharpy.aero.utils.mapping.efficiency_local_aero2struct_forces`.

    The efficiency and constant terms are introduced by means of the array ``airfoil_efficiency`` in the ``aero.h5``
    input file. If this variable has been defined, the function used to map the forces will be
    :func:`~sharpy.aero.utils.mapping.efficiency_local_aero2struct_forces`. Else, the standard formulation
    :func:`~sharpy.aero.utils.mapping.local_aero2struct_forces` will be used.

    Args:
        aero_forces (list): Aerodynamic forces from the UVLM in inertial frame of reference
        struct2aero_mapping (dict): Structural to aerodynamic node mapping
        zeta (list): Aerodynamic grid coordinates
        pos_def (np.ndarray): Vector of structural node displacements
        psi_def (np.ndarray): Vector of structural node rotations (CRVs)
        master: Unused
        conn (np.ndarray): Connectivities matrix
        cag (np.ndarray): Transformation matrix between inertial and body-attached reference ``A``
        aero_dict (dict): Dictionary containing the grid's information.

    Returns:
        np.ndarray: structural forces in an ``n_node x 6`` vector
    """

    n_node, _ = pos_def.shape
    n_elem, _, _ = psi_def.shape
    struct_forces = np.zeros((n_node, 6))

    nodes = []

    # load airfoil efficiency (if it exists); else set to one (to avoid multiple ifs in the loops)
    force_efficiency = None
    moment_efficiency = None
    struct2aero_force_function = local_aero2struct_forces

    if aero_dict is not None:
        try:
            airfoil_efficiency = aero_dict['airfoil_efficiency']
            # force efficiency dimensions [n_elem, n_node_elem, 2, [fx, fy, fz]] - all defined in B frame
            force_efficiency = np.zeros((n_elem, 3, 2, 3))
            force_efficiency[:, :, :, 1] = airfoil_efficiency[:, :, :, 0]
            force_efficiency[:, :, :, 2] = airfoil_efficiency[:, :, :, 1]

            # moment efficiency dimensions [n_elem, n_node_elem, 2, [mx, my, mz]] - all defined in B frame
            moment_efficiency = np.zeros_like(force_efficiency)
            moment_efficiency[:, :, :, 0] = airfoil_efficiency[:, :, :, 2]

            struct2aero_force_function = efficiency_local_aero2struct_forces

        except KeyError:
            pass

    for i_elem in range(n_elem):
        for i_local_node in range(3):

            i_global_node = conn[i_elem, i_local_node]
            if i_global_node in nodes:
                continue

            nodes.append(i_global_node)
            for mapping in struct2aero_mapping[i_global_node]:
                i_surf = mapping['i_surf']
                i_n = mapping['i_n']
                _, n_m, _ = aero_forces[i_surf].shape

                crv = psi_def[i_elem, i_local_node, :]
                cab = algebra.crv2rotation(crv)
                cbg = np.dot(cab.T, cag)

                for i_m in range(n_m):
                    chi_g = zeta[i_surf][:, i_m, i_n] - np.dot(cag.T, pos_def[i_global_node, :])
                    struct_forces[i_global_node, :] += struct2aero_force_function(aero_forces[i_surf][:, i_m, i_n],
                                                                                  chi_g,
                                                                                  cbg,
                                                                                  force_efficiency,
                                                                                  moment_efficiency,
                                                                                  i_elem,
                                                                                  i_local_node)

    return struct_forces
Esempio n. 17
0
    def test_rotation_matrices(self):
        """
        Checks routines and consistency of functions to generate rotation
        matrices.

        Note: test only includes triad <-> CRV <-> quaternions conversions
        """

        ### Verify that function build rotation matrix (not projection matrix)
        # set an easy rotation (x axis)
        a = np.pi / 6.
        nv = np.array([1, 0, 0])
        sa, ca = np.sin(a), np.cos(a)
        Cab_exp = np.array([
            [1, 0, 0],
            [0, ca, -sa],
            [0, sa, ca],
        ])
        ### rot from triad
        Cab_num = algebra.triad2rotation(Cab_exp[:, 0], Cab_exp[:, 1],
                                         Cab_exp[:, 2])
        assert np.linalg.norm(Cab_num - Cab_exp) < 1e-15, \
            'crv2rotation not producing the right result'
        ### rot from crv
        fv = a * nv
        Cab_num = algebra.crv2rotation(fv)
        assert np.linalg.norm(Cab_num - Cab_exp) < 1e-15, \
            'crv2rotation not producing the right result'
        ### rot from quat
        quat = algebra.crv2quat(fv)
        Cab_num = algebra.quat2rotation(quat)
        assert np.linalg.norm(Cab_num - Cab_exp) < 1e-15, \
            'quat2rotation not producing the right result'

        ### inverse relations
        # check crv2rotation and rotation2crv are biunivolcal in [-pi,pi]
        # check quat2rotation and rotation2quat are biunivocal in [-pi,pi]
        N = 100
        for nn in range(N):
            # def random rotation in [-pi,pi]
            a = np.pi * (2. * np.random.rand() - 1)
            nv = 2. * np.random.rand(3) - 1
            nv = nv / np.linalg.norm(nv)

            # inverse crv
            fv0 = a * nv
            Cab = algebra.crv2rotation(fv0)
            fv = algebra.rotation2crv(Cab)
            assert np.linalg.norm(fv - fv0) < 1e-12, \
                'rotation2crv not producing the right result'

            # triad2crv
            xa, ya, za = Cab[:, 0], Cab[:, 1], Cab[:, 2]
            assert np.linalg.norm(
                algebra.triad2crv(xa, ya, za) - fv0) < 1e-12, \
                'triad2crv not producing the right result'

            # inverse quat
            quat0 = np.zeros((4, ))
            quat0[0] = np.cos(.5 * a)
            quat0[1:] = np.sin(.5 * a) * nv
            quat = algebra.rotation2quat(algebra.quat2rotation(quat0))
            assert np.linalg.norm(quat - quat0) < 1e-12, \
                'rotation2quat not producing the right result'

        ### combined rotation
        # assume 3 FoR, G, A and B where:
        #   - G is the initial FoR
        #   - A derives from a 90 deg rotation about zG
        #   - B derives from a 90 deg rotation about yA
        crv_G_to_A = .5 * np.pi * np.array([0, 0, 1])
        crv_A_to_B = .5 * np.pi * np.array([0, 1, 0])
        Cga = algebra.crv2rotation(crv_G_to_A)
        Cab = algebra.crv2rotation(crv_A_to_B)

        # rotation G to B (i.e. projection B onto G)
        Cgb = np.dot(Cga, Cab)
        Cgb_exp = np.array([[0, -1, 0], [0, 0, 1], [-1, 0, 0]])
        assert np.linalg.norm(Cgb - Cgb_exp) < 1e-15, \
            'combined rotation not as expected!'
Esempio n. 18
0
def aero2struct_force_mapping(aero_forces,
                              struct2aero_mapping,
                              zeta,
                              pos_def,
                              psi_def,
                              master,
                              conn,
                              cag=np.eye(3)):

    n_node, _ = pos_def.shape
    n_elem, _, _ = psi_def.shape
    struct_forces = np.zeros((n_node, 6))

    nodes = []

    for i_elem in range(n_elem):
        for i_local_node in range(3):

            i_global_node = conn[i_elem, i_local_node]
            if i_global_node in nodes:
                continue

            nodes.append(i_global_node)
            for mapping in struct2aero_mapping[i_global_node]:
                i_surf = mapping['i_surf']
                i_n = mapping['i_n']
                _, n_m, _ = aero_forces[i_surf].shape

                # i_master_elem, master_elem_local_node = master[i_global_node, :]

                crv = psi_def[i_elem, i_local_node, :]
                cab = algebra.crv2rotation(crv)
                cbg = np.dot(cab.T, cag)

                for i_m in range(n_m):
                    chi_g = zeta[i_surf][:, i_m, i_n] - np.dot(
                        cag.T, pos_def[i_global_node, :])
                    struct_forces[i_global_node, 0:3] += np.dot(
                        cbg, aero_forces[i_surf][0:3, i_m, i_n])
                    struct_forces[i_global_node, 3:6] += np.dot(
                        cbg, aero_forces[i_surf][3:6, i_m, i_n])
                    struct_forces[i_global_node, 3:6] += np.dot(
                        cbg, np.cross(chi_g, aero_forces[i_surf][0:3, i_m,
                                                                 i_n]))

    # for i_global_node in range(n_node):
    # for mapping in struct2aero_mapping[i_global_node]:
    # i_surf = mapping['i_surf']
    # i_n = mapping['i_n']
    # _, n_m, _ = aero_forces[i_surf].shape

    # i_master_elem, master_elem_local_node = master[i_global_node, :]

    # crv = psi_def[i_master_elem, master_elem_local_node, :]
    # cab = algebra.crv2rotation(crv)
    # cbg = np.dot(cab.T, cag)

    # for i_m in range(n_m):
    # chi_g = zeta[i_surf][:, i_m, i_n] - np.dot(cag.T, pos_def[i_global_node, :])
    # struct_forces[i_global_node, 0:3] += np.dot(cbg, aero_forces[i_surf][0:3, i_m, i_n])
    # struct_forces[i_global_node, 3:6] += np.dot(cbg, aero_forces[i_surf][3:6, i_m, i_n])
    # struct_forces[i_global_node, 3:6] += np.dot(cbg, np.cross(chi_g, aero_forces[i_surf][0:3, i_m, i_n]))
    return struct_forces
Esempio n. 19
0
    def write_beam(self, it):
        it_filename = (self.filename + '%06u' % it)
        num_nodes = self.data.structure.num_node
        num_elem = self.data.structure.num_elem

        coords = np.zeros((num_nodes, 3))
        conn = np.zeros((num_elem, 3), dtype=int)
        node_id = np.zeros((num_nodes, ), dtype=int)
        elem_id = np.zeros((num_elem, ), dtype=int)
        coords_a_cell = np.zeros((num_elem, 3), dtype=int)
        local_x = np.zeros((num_nodes, 3))
        local_y = np.zeros((num_nodes, 3))
        local_z = np.zeros((num_nodes, 3))
        coords_a = np.zeros((num_nodes, 3))

        app_forces = np.zeros((num_nodes, 3))
        app_moment = np.zeros((num_nodes, 3))

        forces_constraints_nodes = np.zeros((num_nodes, 3))
        moments_constraints_nodes = np.zeros((num_nodes, 3))

        if self.data.structure.timestep_info[it].in_global_AFoR:
            tstep = self.data.structure.timestep_info[it]
        else:
            tstep = self.data.structure.timestep_info[it].copy()
            tstep.whole_structure_to_global_AFoR(self.data.structure)

        # aero2inertial rotation
        aero2inertial = tstep.cga()

        # coordinates of corners
        coords = tstep.glob_pos(include_rbm=self.settings['include_rbm'])

        # check if I can output gravity forces
        with_gravity = False
        try:
            gravity_forces = tstep.gravity_forces[:]
            gravity_forces_g = np.zeros_like(gravity_forces)
            with_gravity = True
        except AttributeError:
            pass

        # check if postproc dicts are present and count/prepare
        with_postproc_cell = False
        try:
            tstep.postproc_cell
            with_postproc_cell = True
        except AttributeError:
            pass
        with_postproc_node = False
        try:
            tstep.postproc_node
            with_postproc_node = True
        except AttributeError:
            pass

        # count number of arguments
        postproc_cell_keys = tstep.postproc_cell.keys()
        postproc_cell_vals = tstep.postproc_cell.values()
        postproc_cell_scalar = []
        postproc_cell_vector = []
        postproc_cell_6vector = []
        for k, v in tstep.postproc_cell.items():
            _, cols = v.shape
            if cols == 1:
                raise NotImplementedError(
                    'scalar cell types not supported in beamplot (Easy to implement)'
                )
                # postproc_cell_scalar.append(k)
            elif cols == 3:
                postproc_cell_vector.append(k)
            elif cols == 6:
                postproc_cell_6vector.append(k)
            else:
                raise AttributeError(
                    'Only scalar and 3-vector types supported in beamplot')
        # count number of arguments
        postproc_node_keys = tstep.postproc_node.keys()
        postproc_node_vals = tstep.postproc_node.values()
        postproc_node_scalar = []
        postproc_node_vector = []
        postproc_node_6vector = []
        for k, v in tstep.postproc_node.items():
            _, cols = v.shape
            if cols == 1:
                raise NotImplementedError(
                    'scalar node types not supported in beamplot (Easy to implement)'
                )
                # postproc_cell_scalar.append(k)
            elif cols == 3:
                postproc_node_vector.append(k)
            elif cols == 6:
                postproc_node_6vector.append(k)
            else:
                raise AttributeError(
                    'Only scalar and 3-vector types supported in beamplot')

        for i_node in range(num_nodes):
            i_elem = self.data.structure.node_master_elem[i_node, 0]
            i_local_node = self.data.structure.node_master_elem[i_node, 1]
            node_id[i_node] = i_node

            v1 = np.array([1., 0, 0])
            v2 = np.array([0., 1, 0])
            v3 = np.array([0., 0, 1])
            cab = algebra.crv2rotation(tstep.psi[i_elem, i_local_node, :])
            local_x[i_node, :] = np.dot(aero2inertial, np.dot(cab, v1))
            local_y[i_node, :] = np.dot(aero2inertial, np.dot(cab, v2))
            local_z[i_node, :] = np.dot(aero2inertial, np.dot(cab, v3))

            if i_local_node == 2:
                coords_a_cell[i_elem, :] = tstep.pos[i_node, :]
            coords_a[i_node, :] = tstep.pos[i_node, :]

            # applied forces
            cab = algebra.crv2rotation(tstep.psi[i_elem, i_local_node, :])
            app_forces[i_node, :] = np.dot(
                aero2inertial,
                np.dot(
                    cab, tstep.steady_applied_forces[i_node, 0:3] +
                    tstep.unsteady_applied_forces[i_node, 0:3]))
            app_moment[i_node, :] = np.dot(
                aero2inertial,
                np.dot(
                    cab, tstep.steady_applied_forces[i_node, 3:6] +
                    tstep.unsteady_applied_forces[i_node, 3:6]))
            forces_constraints_nodes[i_node, :] = np.dot(
                aero2inertial,
                np.dot(cab, tstep.forces_constraints_nodes[i_node, 0:3]))
            moments_constraints_nodes[i_node, :] = np.dot(
                aero2inertial,
                np.dot(cab, tstep.forces_constraints_nodes[i_node, 3:6]))

            if with_gravity:
                gravity_forces_g[i_node,
                                 0:3] = np.dot(aero2inertial,
                                               gravity_forces[i_node, 0:3])
                gravity_forces_g[i_node,
                                 3:6] = np.dot(aero2inertial,
                                               gravity_forces[i_node, 3:6])

        for i_elem in range(num_elem):
            conn[i_elem, :] = self.data.structure.elements[
                i_elem].reordered_global_connectivities
            elem_id[i_elem] = i_elem

        ug = tvtk.UnstructuredGrid(points=coords)
        ug.set_cells(tvtk.Line().cell_type, conn)
        ug.cell_data.scalars = elem_id
        ug.cell_data.scalars.name = 'elem_id'
        counter = 1
        if with_postproc_cell:
            for k in postproc_cell_vector:
                ug.cell_data.add_array(tstep.postproc_cell[k])
                ug.cell_data.get_array(counter).name = k + '_cell'
                counter += 1
            for k in postproc_cell_6vector:
                for i in range(0, 2):
                    ug.cell_data.add_array(tstep.postproc_cell[k][:, 3 * i:3 *
                                                                  (i + 1)])
                    ug.cell_data.get_array(
                        counter).name = k + '_' + str(i) + '_cell'
                    counter += 1
        ug.cell_data.add_array(coords_a_cell)
        ug.cell_data.get_array(counter).name = 'coords_a_elem'
        counter += 1

        ug.point_data.scalars = node_id
        ug.point_data.scalars.name = 'node_id'
        point_vector_counter = 1
        ug.point_data.add_array(local_x, 'vector')
        ug.point_data.get_array(point_vector_counter).name = 'local_x'
        point_vector_counter += 1
        ug.point_data.add_array(local_y, 'vector')
        ug.point_data.get_array(point_vector_counter).name = 'local_y'
        point_vector_counter += 1
        ug.point_data.add_array(local_z, 'vector')
        ug.point_data.get_array(point_vector_counter).name = 'local_z'
        point_vector_counter += 1
        ug.point_data.add_array(coords_a, 'vector')
        ug.point_data.get_array(point_vector_counter).name = 'coords_a'
        if self.settings['include_applied_forces']:
            point_vector_counter += 1
            ug.point_data.add_array(app_forces, 'vector')
            ug.point_data.get_array(point_vector_counter).name = 'app_forces'
            point_vector_counter += 1
            ug.point_data.add_array(forces_constraints_nodes, 'vector')
            ug.point_data.get_array(
                point_vector_counter).name = 'forces_constraints_nodes'
            if with_gravity:
                point_vector_counter += 1
                ug.point_data.add_array(gravity_forces_g[:, 0:3], 'vector')
                ug.point_data.get_array(
                    point_vector_counter).name = 'gravity_forces'

        if self.settings['include_applied_moments']:
            point_vector_counter += 1
            ug.point_data.add_array(app_moment, 'vector')
            ug.point_data.get_array(point_vector_counter).name = 'app_moments'
            point_vector_counter += 1
            ug.point_data.add_array(moments_constraints_nodes, 'vector')
            ug.point_data.get_array(
                point_vector_counter).name = 'moments_constraints_nodes'
            if with_gravity:
                point_vector_counter += 1
                ug.point_data.add_array(gravity_forces_g[:, 3:6], 'vector')
                ug.point_data.get_array(
                    point_vector_counter).name = 'gravity_moments'
        if with_postproc_node:
            for k in postproc_node_vector:
                point_vector_counter += 1
                ug.point_data.add_array(tstep.postproc_node[k])
                ug.point_data.get_array(
                    point_vector_counter).name = k + '_point'
            for k in postproc_node_6vector:
                for i in range(0, 2):
                    point_vector_counter += 1
                    ug.point_data.add_array(tstep.postproc_node[k][:, 3 * i:3 *
                                                                   (i + 1)])
                    ug.point_data.get_array(
                        point_vector_counter
                    ).name = k + '_' + str(i) + '_point'

        write_data(ug, it_filename)
Esempio n. 20
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
Esempio n. 21
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
Esempio n. 22
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