Coverage for panelaero/VLM.py: 77%

117 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-04 09:17 +0000

1#!/usr/bin/env python2 

2# -*- coding: utf-8 -*- 

3import copy 

4import numpy as np 

5 

6 

7def calc_induced_velocities(aerogrid, Ma): 

8 # 

9 # l_2 

10 # 4 o---------o 3 

11 # | | 

12 # u --> b_1 | l k j | b_2 

13 # | | 

14 # 1 o---------o 2 

15 # y l_1 

16 # | 

17 # z.--- x 

18 

19 # define downwash location (3/4 chord and half span of the aero panel) 

20 P0 = aerogrid['offset_j'] 

21 # define vortex location points 

22 P1 = aerogrid['offset_P1'] 

23 P3 = aerogrid['offset_P3'] 

24 # P2 = mid-point between P1 and P3, not used 

25 # normal vector part in vertical direction 

26 n_hat_w = np.array(aerogrid['N'][:, 2], ndmin=2).T.repeat(aerogrid['n'], axis=1) 

27 # normal vector part in lateral direction 

28 n_hat_wl = np.array(aerogrid['N'][:, 1], ndmin=2).T.repeat(aerogrid['n'], axis=1) 

29 

30 # divide x coordinates with beta 

31 # See Hedman 1965. 

32 # However, Hedman divides by beta^2 ... why?? 

33 beta = (1 - (Ma ** 2.0)) ** 0.5 

34 P0[:, 0] = P0[:, 0] / beta 

35 P1[:, 0] = P1[:, 0] / beta 

36 P3[:, 0] = P3[:, 0] / beta 

37 

38 # See Katz & Plotkin, Chapter 10.4.5 

39 # get r1,r2,r0 

40 r1x = np.array(P0[:, 0], ndmin=2).T - np.array(P1[:, 0], ndmin=2) 

41 r1y = np.array(P0[:, 1], ndmin=2).T - np.array(P1[:, 1], ndmin=2) 

42 r1z = np.array(P0[:, 2], ndmin=2).T - np.array(P1[:, 2], ndmin=2) 

43 

44 r2x = np.array(P0[:, 0], ndmin=2).T - np.array(P3[:, 0], ndmin=2) 

45 r2y = np.array(P0[:, 1], ndmin=2).T - np.array(P3[:, 1], ndmin=2) 

46 r2z = np.array(P0[:, 2], ndmin=2).T - np.array(P3[:, 2], ndmin=2) 

47 

48 # Step 1 

49 r1Xr2_x = r1y * r2z - r1z * r2y 

50 r1Xr2_y = -r1x * r2z + r1z * r2x # Plus-Zeichen Abweichung zu Katz & Plotkin ?? 

51 r1Xr2_z = r1x * r2y - r1y * r2x 

52 mod_r1Xr2 = (r1Xr2_x ** 2.0 + r1Xr2_y ** 2.0 + r1Xr2_z ** 2.0) ** 0.5 

53 # Step 2 

54 r1 = (r1x ** 2.0 + r1y ** 2.0 + r1z ** 2.0) ** 0.5 

55 r2 = (r2x ** 2.0 + r2y ** 2.0 + r2z ** 2.0) ** 0.5 

56 # Step 4 

57 r0r1 = (P3[:, 0] - P1[:, 0]) * r1x + (P3[:, 1] - P1[:, 1]) * r1y + (P3[:, 2] - P1[:, 2]) * r1z 

58 r0r2 = (P3[:, 0] - P1[:, 0]) * r2x + (P3[:, 1] - P1[:, 1]) * r2y + (P3[:, 2] - P1[:, 2]) * r2z 

59 # Step 5 

60 gamma = np.ones((aerogrid['n'], aerogrid['n'])) 

61 D1_base = gamma / 4.0 / np.pi / mod_r1Xr2 ** 2.0 * (r0r1 / r1 - r0r2 / r2) 

62 D1_v = r1Xr2_y * D1_base 

63 D1_w = r1Xr2_z * D1_base 

64 # Step 3 

65 epsilon = 10e-6 

66 ind = np.where(r1 < epsilon)[0] 

67 D1_v[ind] = 0.0 

68 D1_w[ind] = 0.0 

69 

70 ind = np.where(r2 < epsilon)[0] 

71 D1_v[ind] = 0.0 

72 D1_w[ind] = 0.0 

73 

74 ind = np.where(mod_r1Xr2 < epsilon) 

75 D1_v[ind] = 0.0 

76 D1_w[ind] = 0.0 

77 

78 # get final D1 matrix 

79 # D1 matrix contains the perpendicular component of induced velocities at all panels. 

80 # For wing panels, it's the z component of induced velocities (D1_w) while for 

81 # winglets, it's the y component of induced velocities (D1_v) 

82 D1 = D1_w * n_hat_w + D1_v * n_hat_wl 

83 

84 # See Katz & Plotkin, Chapter 10.4.7 

85 # induced velocity due to inner semi-infinite vortex line 

86 d2 = (r1y ** 2.0 + r1z ** 2.0) ** 0.5 

87 cosBB1 = 1.0 

88 cosBB2 = -r1x / r1 

89 cosGamma = r1y / d2 

90 sinGamma = -r1z / d2 

91 

92 D2_base = -(1.0 / (4.0 * np.pi)) * (cosBB1 - cosBB2) / d2 

93 D2_v = sinGamma * D2_base 

94 D2_w = cosGamma * D2_base 

95 

96 ind = np.where((r1 < epsilon) + (d2 < epsilon))[0] 

97 D2_v[ind] = 0.0 

98 D2_w[ind] = 0.0 

99 

100 D2 = D2_w * n_hat_w + D2_v * n_hat_wl 

101 

102 # induced velocity due to outer semi-infinite vortex line 

103 d3 = (r2y ** 2.0 + r2z ** 2.0) ** 0.5 

104 cosBB1 = r2x / r2 

105 cosBB2 = -1.0 

106 cosGamma = -r2y / d3 

107 sinGamma = r2z / d3 

108 

109 D3_base = -(1.0 / (4.0 * np.pi)) * (cosBB1 - cosBB2) / d3 

110 D3_v = sinGamma * D3_base 

111 D3_w = cosGamma * D3_base 

112 

113 ind = np.where((r2 < epsilon) + (d3 < epsilon))[0] 

114 D3_v[ind] = 0.0 

115 D3_w[ind] = 0.0 

116 

117 D3 = D3_w * n_hat_w + D3_v * n_hat_wl 

118 

119 return D1, D2, D3 

120 

121 

122def mirror_aerogrid_xz(aerogrid): 

123 tmp = copy.deepcopy(aerogrid) 

124 # mirror y-coord 

125 tmp['offset_j'][:, 1] = -tmp['offset_j'][:, 1] 

126 tmp['offset_k'][:, 1] = -tmp['offset_k'][:, 1] 

127 tmp['offset_l'][:, 1] = -tmp['offset_l'][:, 1] 

128 tmp['offset_P1'][:, 1] = -tmp['offset_P1'][:, 1] 

129 tmp['offset_P3'][:, 1] = -tmp['offset_P3'][:, 1] 

130 tmp['N'][:, 1] = -aerogrid['N'][:, 1] 

131 tmp['N'][:, 2] = -aerogrid['N'][:, 2] 

132 # assemble both grids 

133 aerogrid_xzsym = {'offset_j': np.vstack((aerogrid['offset_j'], tmp['offset_j'])), 

134 'offset_k': np.vstack((aerogrid['offset_k'], tmp['offset_l'])), 

135 'offset_l': np.vstack((aerogrid['offset_k'], tmp['offset_l'])), 

136 'offset_P1': np.vstack((aerogrid['offset_P1'], tmp['offset_P1'])), 

137 'offset_P3': np.vstack((aerogrid['offset_P3'], tmp['offset_P3'])), 

138 'N': np.vstack((aerogrid['N'], tmp['N'])), 

139 'A': np.hstack((aerogrid['A'], tmp['A'])), 

140 'l': np.hstack((aerogrid['l'], tmp['l'])), 

141 'n': aerogrid['n'] * 2, 

142 } 

143 return aerogrid_xzsym 

144 

145 

146def calc_Ajj(aerogrid, Ma): 

147 D1, D2, D3 = calc_induced_velocities(aerogrid, Ma) 

148 # define area, chord length and spann of each panel 

149 A = aerogrid['A'] 

150 chord = aerogrid['l'] 

151 span = A / chord 

152 # total D 

153 D = D1 + D2 + D3 

154 Ajj = D * 0.5 * A / span 

155 Bjj = (D2 + D3) * 0.5 * A / span 

156 return Ajj, Bjj 

157 

158 

159def calc_Qjj(aerogrid, Ma, xz_symmetry=False): 

160 ''' 

161 Symmetry about xz-plane: 

162 Only the right hand side is give. The (missing) left hand side is created virtually using mirror_aerogrid_xz(). 

163 The AIC matrix is calculated for the whole system and can be partitioned into terms: 

164 AIC = |RR|LR| 

165 |RL|LL| 

166 with 

167 RR - influence of right side onto right side, 

168 LL - influence of left side onto left side, 

169 RL and LR - influence from left side onto right side. Due to symmetry, both term are identical. 

170 Then, for symmetric motions: AIC_sym = RR - LR 

171 And, for asymmetric motions: AIC_asym = RR + LR ??? -> to be checked !!! 

172 ''' 

173 if xz_symmetry: 

174 n = aerogrid['n'] 

175 aerogrid = mirror_aerogrid_xz(aerogrid) 

176 

177 # The function calc_Ajj() is Mach number dependent, which involves a scaling of the aerogrid in x-direction. 

178 # To make sure that the geometrical scaling has no effect on the following calculations, a 'fresh' a copy of the aerogrid, 

179 # created with copy.deepcopy(), is handed over. 

180 Ajj, Bjj = calc_Ajj(aerogrid=copy.deepcopy(aerogrid), Ma=Ma) 

181 Qjj = -np.linalg.inv(Ajj) 

182 if xz_symmetry: 

183 return Qjj[0:n, 0:n] - Qjj[n:2 * n, 0:n], Bjj[0:n, 0:n] - Bjj[n:2 * n, 0:n] 

184 return Qjj, Bjj 

185 

186 

187def calc_Qjjs(aerogrid, Ma, xz_symmetry=False): 

188 Qjj = np.zeros((len(Ma), aerogrid['n'], aerogrid['n'])) # dim: Ma,n,n 

189 Bjj = np.zeros((len(Ma), aerogrid['n'], aerogrid['n'])) # dim: Ma,n,n 

190 for i, i_Ma in enumerate(Ma): 

191 Qjj[i, :, :], Bjj[i, :, :] = calc_Qjj(aerogrid, i_Ma, xz_symmetry) 

192 return Qjj, Bjj 

193 

194 

195def calc_Gamma(aerogrid, Ma, xz_symmetry=False): 

196 if xz_symmetry: 

197 n = aerogrid['n'] 

198 aerogrid = mirror_aerogrid_xz(aerogrid) 

199 D1, D2, D3 = calc_induced_velocities(aerogrid, Ma) 

200 # total D 

201 Gamma = -np.linalg.inv((D1 + D2 + D3)) 

202 Q_ind = D2 + D3 

203 

204 if xz_symmetry: 

205 return Gamma[0:n, 0:n] - Gamma[n:2 * n, 0:n], Q_ind[0:n, 0:n] - Q_ind[n:2 * n, 0:n] 

206 return Gamma, Q_ind 

207 

208 

209def calc_Gammas(aerogrid, Ma, xz_symmetry=False): 

210 Gamma = np.zeros((len(Ma), aerogrid['n'], aerogrid['n'])) # dim: Ma,n,n 

211 Q_ind = np.zeros((len(Ma), aerogrid['n'], aerogrid['n'])) # dim: Ma,n,n 

212 for i, i_Ma in enumerate(Ma): 

213 Gamma[i, :, :], Q_ind[i, :, :] = calc_Gamma(aerogrid, i_Ma, xz_symmetry) 

214 return Gamma, Q_ind