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
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
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
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'