Coverage for /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/hypervehicle/components/wing.py: 92%

160 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-25 22:58 +0000

1from __future__ import annotations 

2import numpy as np 

3from typing import Callable 

4from scipy.optimize import bisect 

5from hypervehicle.geometry.vector import Vector3 

6from hypervehicle.geometry.surface import CoonsPatch 

7from hypervehicle.geometry.path import Line, Bezier, Polyline 

8 

9from hypervehicle.components.component import Component 

10from hypervehicle.components.constants import WING_COMPONENT 

11from hypervehicle.geometry.geometry import ( 

12 OffsetPatchFunction, 

13 SubRangedPath, 

14 ElipsePath, 

15 ArcLengthParameterizedPath, 

16 TrailingEdgePath, 

17 OffsetPathFunction, 

18 GeometricMeanPathFunction, 

19 MeanLeadingEdgePatchFunction, 

20 MeanTrailingEdgePatch, 

21 RotatedPatch, 

22 FlatLeadingEdgePatchFunction, 

23) 

24 

25 

26class Wing(Component): 

27 componenttype = WING_COMPONENT 

28 

29 def __init__( 

30 self, 

31 A0: Vector3 = Vector3(0, 0, 0), 

32 A1: Vector3 = Vector3(0, 0, 0), 

33 TT: Vector3 = Vector3(0, 0, 0), 

34 B0: Vector3 = Vector3(0, 0, 0), 

35 Line_B0TT: Polyline = None, 

36 Line_B0TT_TYPE: str = "Bezier", 

37 t_B1: float = None, 

38 t_B2: float = None, 

39 top_tf: Callable[[float, float, float], Vector3] = None, 

40 bot_tf: Callable[[float, float, float], Vector3] = None, 

41 LE_wf: Callable[[float], Vector3] = None, 

42 LE_type: str = "custom", 

43 tail_option: str = "FLAP", 

44 flap_length: float = 0, 

45 flap_angle: float = 0, 

46 close_wing: bool = False, 

47 stl_resolution: int = 2, 

48 verbosity: int = 1, 

49 name: str = None, 

50 ) -> None: 

51 """Creates a new fin component. 

52 

53 Parameters 

54 ---------- 

55 A0 : Vector3 

56 Point p0 of the fin geometry. 

57 

58 A1 : Vector3 

59 Point p1 of the fin geometry. 

60 

61 TT : Vector3 

62 Point p2 of the fin geometry. 

63 

64 B0 : Vector3 

65 Point p3 of the fin geometry. 

66 

67 Line_B0TT : Polyline 

68 The thickness of the fin. 

69 

70 Line_B0TT_TYPE : str, optional 

71 The axial position angle of the placement of the fin. 

72 

73 t_B1 : float, optional 

74 The t value of the first discretisation point. The default 

75 is None. 

76 

77 t_B2 : float, optional 

78 The t value of the second discretisation point. The default 

79 is None. 

80 

81 top_tf : Callable 

82 The thickness function for the top surface of the wing. 

83 

84 bot_tf : Callable 

85 The thickness function for the top surface of the wing. 

86 

87 LE_wf : Callable, optional 

88 The thickness function for the leading edge of the wing. 

89 

90 LE_type : str, optional 

91 The type of LE to create, either "FLAT" or "custom". The 

92 default is "custom". 

93 

94 tail_option : str, optional 

95 The type of trailing edge to use, currently only "FLAP". The 

96 default is "FLAP". 

97 

98 flap_length : float, optional 

99 The length of the trailing edge flap. The default is 0. 

100 

101 flap_angle : float, optional 

102 The angle of the flap, specified in radians. The default is 0. 

103 

104 close_wing : bool, optional 

105 If the wing is not being mirrored, it is useful to set this 

106 to True, to close the STL object. The default is False. 

107 

108 stl_resolution : int, optional 

109 The stl resolution to use when creating the mesh for this 

110 component. The default is None. 

111 

112 verbosity : int, optional 

113 The verbosity of the component. The default is 1. 

114 

115 name : str, optional 

116 The name tag for the component. The default is None. 

117 """ 

118 # Check if a LE function was provided 

119 if LE_wf is None and LE_type == "custom": 

120 # Assign default LE function 

121 from hypervehicle.components.common import leading_edge_width_function 

122 

123 LE_wf = leading_edge_width_function 

124 

125 params = { 

126 "A0": A0, 

127 "A1": A1, 

128 "TT": TT, 

129 "B0": B0, 

130 "Line_B0TT": Line_B0TT, 

131 "Line_B0TT_TYPE": Line_B0TT_TYPE, 

132 "t_B1": t_B1, 

133 "t_B2": t_B2, 

134 "FUNC_TOP_THICKNESS": top_tf, 

135 "FUNC_BOT_THICKNESS": bot_tf, 

136 "FUNC_LEADING_EDGE_WIDTH": LE_wf, 

137 "LE_TYPE": LE_type.upper(), 

138 "TAIL_OPTION": tail_option, 

139 "FLAP_LENGTH": flap_length, 

140 "FLAP_ANGLE": flap_angle, 

141 "CLOSE_WING": close_wing, 

142 } 

143 

144 super().__init__( 

145 params=params, stl_resolution=stl_resolution, verbosity=verbosity, name=name 

146 ) 

147 

148 # Extract construction points for planform 

149 # TODO - avoid pre-defined params dict structure for flexibility 

150 self.A0 = params["A0"] 

151 self.A1 = params["A1"] 

152 self.TT = params["TT"] 

153 self.B0 = params["B0"] 

154 self.Line_B0TT = params["Line_B0TT"] 

155 

156 # Find Locations 

157 if self.params["t_B1"] == None: 

158 fun_B1 = lambda t: self.Line_B0TT(t).x - self.A1.x 

159 self.t_B1 = round(bisect(fun_B1, 0.0, 1.0), 6) 

160 else: 

161 self.t_B1 = self.params["t_B1"] 

162 

163 if self.params["t_B2"] == None: 

164 self.t_B2 = 0.5 * (1 + self.t_B1) 

165 else: 

166 self.t_B2 = self.params["t_B1"] 

167 

168 # Save other params 

169 self.TE_top = None 

170 self.TE_bot = None 

171 self.TE_mean_line = None 

172 

173 def generate_patches(self): 

174 # Create wing planform shape 

175 self._create_planform_patches() 

176 

177 # Create leading edge patches 

178 self._create_leading_edge() 

179 

180 # Create trailing edge patches 

181 self._create_trailing_edge() 

182 

183 if "CLOSE_WING" in self.params and self.params["CLOSE_WING"]: 

184 self._close_wing() 

185 

186 def _create_planform_patches(self): 

187 if self.params["Line_B0TT_TYPE"].lower() == "bezier": 

188 if self.verbosity > 1: 

189 print( 

190 " Constructing planform using Bezier Curve as Leading Edge shape." 

191 ) 

192 

193 B1 = self.Line_B0TT(self.t_B1) 

194 Line_B0B1 = SubRangedPath( 

195 underlying_path=self.Line_B0TT, t0=0.0, t1=self.t_B1 

196 ) 

197 Line_B1B2 = SubRangedPath( 

198 underlying_path=self.Line_B0TT, t0=self.t_B1, t1=self.t_B2 

199 ) 

200 Line_TTB2 = SubRangedPath( 

201 underlying_path=self.Line_B0TT, t0=1.0, t1=self.t_B2 

202 ) 

203 

204 else: 

205 raise Exception( 

206 f"Option for 'Line_B0TT'={self.params['Line_B0TT_TYPE']} " 

207 + "not supported." 

208 ) 

209 

210 wing_patch = [np.nan, np.nan] 

211 wing_patch_flipped = [np.nan, np.nan] 

212 wing_patch[0] = CoonsPatch( 

213 south=Line(p0=self.A0, p1=self.A1), 

214 north=Line_B0B1, 

215 west=Line(p0=self.A0, p1=self.B0), 

216 east=Line(p0=self.A1, p1=B1), 

217 ) 

218 wing_patch[1] = CoonsPatch( 

219 south=Line(p0=self.A1, p1=self.TT), 

220 north=Line_B1B2, 

221 west=Line(p0=self.A1, p1=B1), 

222 east=Line_TTB2, 

223 ) 

224 

225 # Need flipped planform for lower side to ensure vecors point in correct direction 

226 Line_B0B1_flipped = SubRangedPath(underlying_path=Line_B0B1, t0=1.0, t1=0.0) 

227 Line_B1B2_flipped = SubRangedPath(underlying_path=Line_B1B2, t0=1.0, t1=0.0) 

228 wing_patch_flipped[0] = CoonsPatch( 

229 south=Line(p0=self.A1, p1=self.A0), 

230 north=Line_B0B1_flipped, 

231 east=Line(p0=self.A0, p1=self.B0), 

232 west=Line(p0=self.A1, p1=B1), 

233 ) 

234 wing_patch_flipped[1] = CoonsPatch( 

235 south=Line(p0=self.TT, p1=self.A1), 

236 north=Line_B1B2_flipped, 

237 east=Line(p0=self.A1, p1=B1), 

238 west=Line_TTB2, 

239 ) 

240 

241 # Create wing top & bottom surface 

242 if self.verbosity > 1: 

243 print(" Adding thickness to wing.") 

244 top_patch = [np.nan, np.nan] 

245 bot_patch = [np.nan, np.nan] 

246 

247 for i in range(2): 

248 top_patch[i] = OffsetPatchFunction( 

249 wing_patch_flipped[i], function=self.params["FUNC_TOP_THICKNESS"] 

250 ) 

251 

252 # flipped moves to top as z-positive points downwards 

253 bot_patch[i] = OffsetPatchFunction( 

254 wing_patch[i], function=self.params["FUNC_BOT_THICKNESS"] 

255 ) 

256 

257 self.patches["top_patch_0"] = top_patch[0] # top B0B1A1A0 

258 self.patches["top_patch_1"] = top_patch[1] # top B1B2TTA1 

259 self.patches["bot_patch_0"] = bot_patch[0] # bot B0B1A1A0 

260 self.patches["bot_patch_1"] = bot_patch[1] # bot B1B2TTA1 

261 

262 def _create_leading_edge(self): 

263 # Add leading edge 

264 if self.verbosity > 1: 

265 print(" Adding Leading Edge to wing.") 

266 

267 top_edge_path = OffsetPathFunction( 

268 self.Line_B0TT, self.params["FUNC_TOP_THICKNESS"] 

269 ) 

270 bot_edge_path = OffsetPathFunction( 

271 self.Line_B0TT, self.params["FUNC_BOT_THICKNESS"] 

272 ) 

273 

274 if "LE_TYPE" in self.params and self.params["LE_TYPE"] == "FLAT": 

275 self.patches["LE_patch0"] = FlatLeadingEdgePatchFunction( 

276 top_edge_path, bot_edge_path, 0, self.t_B1 

277 ) 

278 self.patches["LE_patch1"] = FlatLeadingEdgePatchFunction( 

279 top_edge_path, bot_edge_path, self.t_B1, self.t_B2 

280 ) 

281 self.patches["LE_patch2"] = FlatLeadingEdgePatchFunction( 

282 top_edge_path, bot_edge_path, self.t_B1, 1 

283 ) 

284 

285 else: 

286 # Get mean line between upper and lower wing patches 

287 mean_path = GeometricMeanPathFunction(top_edge_path, bot_edge_path) 

288 

289 LE_top_patch = [np.nan, np.nan, np.nan] 

290 LE_bot_patch = [np.nan, np.nan, np.nan] 

291 

292 # Eliptical LE 

293 LE_top_patch[0] = MeanLeadingEdgePatchFunction( 

294 mean_path, 

295 top_edge_path, 

296 LE_width_function=self.params["FUNC_LEADING_EDGE_WIDTH"], 

297 t0=0.0, 

298 t1=self.t_B1, 

299 side="top", 

300 ) 

301 LE_top_patch[1] = MeanLeadingEdgePatchFunction( 

302 mean_path, 

303 top_edge_path, 

304 LE_width_function=self.params["FUNC_LEADING_EDGE_WIDTH"], 

305 t0=self.t_B1, 

306 t1=self.t_B2, 

307 side="top", 

308 ) 

309 LE_top_patch[2] = MeanLeadingEdgePatchFunction( 

310 mean_path, 

311 top_edge_path, 

312 LE_width_function=self.params["FUNC_LEADING_EDGE_WIDTH"], 

313 t0=self.t_B2, 

314 t1=1.0, 

315 side="top", 

316 ) 

317 

318 LE_bot_patch[0] = MeanLeadingEdgePatchFunction( 

319 mean_path, 

320 bot_edge_path, 

321 LE_width_function=self.params["FUNC_LEADING_EDGE_WIDTH"], 

322 t0=0.0, 

323 t1=self.t_B1, 

324 side="bot", 

325 ) 

326 LE_bot_patch[1] = MeanLeadingEdgePatchFunction( 

327 mean_path, 

328 bot_edge_path, 

329 LE_width_function=self.params["FUNC_LEADING_EDGE_WIDTH"], 

330 t0=self.t_B1, 

331 t1=self.t_B2, 

332 side="bot", 

333 ) 

334 LE_bot_patch[2] = MeanLeadingEdgePatchFunction( 

335 mean_path, 

336 bot_edge_path, 

337 LE_width_function=self.params["FUNC_LEADING_EDGE_WIDTH"], 

338 t0=self.t_B2, 

339 t1=1.0, 

340 side="bot", 

341 ) 

342 

343 # Append to patch_dict 

344 self.patches["LE_top_patch_0"] = LE_top_patch[0] 

345 self.patches["LE_top_patch_1"] = LE_top_patch[1] 

346 self.patches["LE_top_patch_2"] = LE_top_patch[2] 

347 self.patches["LE_bot_patch_0"] = LE_bot_patch[0] 

348 self.patches["LE_bot_patch_1"] = LE_bot_patch[1] 

349 self.patches["LE_bot_patch_2"] = LE_bot_patch[2] 

350 

351 def _create_trailing_edge(self): 

352 # Add trailing Edge 

353 if self.verbosity > 1: 

354 print(" Adding Trailing Edge.") 

355 print(" Tail options - {}".format(self.params["TAIL_OPTION"])) 

356 

357 if self.params["TAIL_OPTION"] == "FLAP": 

358 if self.verbosity > 1: 

359 print(" Flap length = {}".format(self.params["FLAP_LENGTH"])) 

360 print(" Flap angle = {}".format(self.params["FLAP_ANGLE"])) 

361 

362 # Define top and bottom TE paths 

363 self.TE_top = TrailingEdgePath( 

364 self.A0, self.B0, thickness_function=self.params["FUNC_TOP_THICKNESS"] 

365 ) 

366 self.TE_bot = TrailingEdgePath( 

367 self.A0, self.B0, thickness_function=self.params["FUNC_BOT_THICKNESS"] 

368 ) 

369 self.TE_mean_line = GeometricMeanPathFunction(self.TE_top, self.TE_bot) 

370 

371 # Make top and bottom of flap 

372 TE_top_patch = MeanTrailingEdgePatch( 

373 self.TE_mean_line, 

374 TE_path=self.TE_top, 

375 flap_length=self.params["FLAP_LENGTH"], 

376 flap_angle=self.params["FLAP_ANGLE"], 

377 side="top", 

378 ) 

379 TE_bot_patch = MeanTrailingEdgePatch( 

380 self.TE_mean_line, 

381 TE_path=self.TE_bot, 

382 flap_length=self.params["FLAP_LENGTH"], 

383 flap_angle=self.params["FLAP_ANGLE"], 

384 side="bot", 

385 ) 

386 

387 # Append to patch_dict 

388 self.patches["TE_top_patch"] = TE_top_patch 

389 self.patches["TE_bot_patch"] = TE_bot_patch 

390 

391 # Create edge (connecting side to TE) 

392 if "LE_TYPE" in self.params and self.params["LE_TYPE"] == "FLAT": 

393 # Flat LE 

394 west = Line(p0=self.TE_top(1), p1=self.TE_mean_line(1)) 

395 north = Line(p0=self.TE_mean_line(1), p1=self.TE_bot(1)) 

396 

397 else: 

398 # Eiliptical LE 

399 thickness_top = self.TE_mean_line(1).z - self.TE_top(1).z 

400 thickness_bot = self.TE_mean_line(1).z - self.TE_bot(1).z 

401 

402 if thickness_top == 0 or thickness_bot == 0: 

403 raise Exception( 

404 "Elliptical LE cannot be created when thickness converges to zero." 

405 ) 

406 

407 elipse_top = ElipsePath( 

408 centre=self.TE_mean_line(1), 

409 thickness=thickness_top, 

410 LE_width=self.params["FUNC_LEADING_EDGE_WIDTH"](0.0), 

411 side="top", 

412 ) 

413 elipse_bot = ElipsePath( 

414 centre=self.TE_mean_line(1), 

415 thickness=thickness_bot, 

416 LE_width=self.params["FUNC_LEADING_EDGE_WIDTH"](0.0), 

417 side="bot", 

418 ) 

419 

420 elipse_top = ArcLengthParameterizedPath(underlying_path=elipse_top) 

421 elipse_bot = ArcLengthParameterizedPath(underlying_path=elipse_bot) 

422 

423 north = elipse_bot 

424 west = elipse_top 

425 

426 south = Line( 

427 p0=Vector3( 

428 x=self.TE_mean_line(1).x, 

429 y=self.TE_mean_line(1).y, 

430 z=self.TE_top(1).z, 

431 ), 

432 p1=Vector3( 

433 x=self.TE_mean_line(1).x 

434 - self.params["FLAP_LENGTH"] * np.cos(self.params["FLAP_ANGLE"]), 

435 y=self.TE_mean_line(1).y, 

436 z=self.TE_mean_line(1).z 

437 + self.params["FLAP_LENGTH"] * np.sin(self.params["FLAP_ANGLE"]), 

438 ), 

439 ) 

440 

441 east = Line( 

442 p0=Vector3( 

443 x=self.TE_mean_line(1).x 

444 - self.params["FLAP_LENGTH"] * np.cos(self.params["FLAP_ANGLE"]), 

445 y=self.TE_mean_line(1).y, 

446 z=self.TE_mean_line(1).z 

447 + self.params["FLAP_LENGTH"] * np.sin(self.params["FLAP_ANGLE"]), 

448 ), 

449 p1=Vector3( 

450 x=self.TE_mean_line(1).x, 

451 y=self.TE_mean_line(1).y, 

452 z=self.TE_bot(1).z, 

453 ), 

454 ) 

455 

456 TELE_patch = CoonsPatch(north=north, west=west, south=south, east=east) 

457 

458 # Append to patch_dict 

459 self.patches["TELE_patch"] = TELE_patch 

460 

461 else: 

462 raise Exception( 

463 "Tail option = {} not supported.".format(self.params["TAIL_OPTION"]) 

464 ) 

465 

466 def _close_wing(self): 

467 # Add patch to close wing volume 

468 if self.verbosity > 1: 

469 print(" Closing interior side of wing.") 

470 

471 TT_top = self.TT + self.params["FUNC_TOP_THICKNESS"]( 

472 x=self.TT.x, y=self.TT.y, z=self.TT.z 

473 ) 

474 TT_bot = self.TT + self.params["FUNC_BOT_THICKNESS"]( 

475 x=self.TT.x, y=self.TT.y, z=self.TT.z 

476 ) 

477 TT_mid = 0.5 * (TT_top + TT_bot) 

478 

479 interior_top_0 = Line(p0=self.TE_top(0), p1=self.patches["top_patch_0"](0, 0)) 

480 interior_top_1 = Line(p0=self.patches["top_patch_0"](0, 0), p1=TT_top) 

481 

482 interior_bot_0 = Line(p0=self.TE_bot(0), p1=self.patches["bot_patch_0"](1, 0)) 

483 interior_bot_1 = Line(p0=self.patches["bot_patch_0"](1, 0), p1=TT_bot) 

484 

485 mid_mid_point = Vector3( 

486 x=self.patches["bot_patch_0"](1, 0).x, 

487 y=self.patches["bot_patch_0"](1, 0).y, 

488 z=( 

489 self.patches["bot_patch_0"](1, 0).z 

490 + self.patches["top_patch_0"](0, 0).z 

491 ) 

492 / 2, 

493 ) 

494 interior_mid_0 = Line(p0=self.TE_mean_line(0), p1=mid_mid_point) 

495 interior_mid_1 = Line(p0=mid_mid_point, p1=TT_mid) 

496 

497 # Create vertical edges 

498 mid_vert_top = Line(p1=mid_mid_point, p0=self.patches["top_patch_0"](0, 0)) 

499 mid_vert_bot = Line(p0=mid_mid_point, p1=self.patches["bot_patch_0"](1, 0)) 

500 

501 # Vertical edges at TE ('back') 

502 back_top = Line(p0=self.TE_top(0), p1=self.TE_mean_line(0)) 

503 back_bot = Line(p0=self.TE_mean_line(0), p1=self.TE_bot(0)) 

504 

505 # Create patches 

506 interior_top_patch0 = CoonsPatch( 

507 north=interior_mid_0, 

508 east=mid_vert_top, 

509 south=interior_top_0, 

510 west=back_top, 

511 ) 

512 interior_top_patch1 = CoonsPatch( 

513 north=interior_mid_1, 

514 east=Line(p0=TT_top, p1=TT_mid), 

515 south=interior_top_1, 

516 west=mid_vert_top, 

517 ) 

518 

519 interior_bot_patch0 = CoonsPatch( 

520 north=interior_bot_0, 

521 east=mid_vert_bot, 

522 south=interior_mid_0, 

523 west=back_bot, 

524 ) 

525 interior_bot_patch1 = CoonsPatch( 

526 north=interior_bot_1, 

527 east=Line(p0=TT_mid, p1=TT_bot), 

528 south=interior_mid_1, 

529 west=mid_vert_bot, 

530 ) 

531 

532 elipse_top = ElipsePath( 

533 centre=TT_mid, 

534 thickness=TT_mid.z - TT_top.z, 

535 LE_width=self.params["FUNC_LEADING_EDGE_WIDTH"](1), 

536 side="top", 

537 ) 

538 elipse_bot = ElipsePath( 

539 centre=TT_mid, 

540 thickness=TT_mid.z - TT_bot.z, 

541 LE_width=self.params["FUNC_LEADING_EDGE_WIDTH"](1), 

542 side="bot", 

543 ) 

544 elipse_top = ArcLengthParameterizedPath(underlying_path=elipse_top) 

545 elipse_bot = ArcLengthParameterizedPath(underlying_path=elipse_bot) 

546 

547 # Now reverse the paths for correct orientation 

548 elipse_bot = SubRangedPath(underlying_path=elipse_bot, t0=1.0, t1=0.0) 

549 

550 interior_ellip = CoonsPatch( 

551 north=elipse_bot, 

552 east=elipse_top, 

553 south=Line(p0=TT_mid, p1=TT_top), 

554 west=Line(p0=TT_mid, p1=TT_bot), 

555 ) 

556 

557 # Rotate patch 

558 interior_ellip = RotatedPatch( 

559 interior_ellip, np.deg2rad(-90), axis="z", point=TT_mid 

560 ) 

561 

562 # Append to patch_dict 

563 self.patches["wing_close_top_patch0"] = interior_top_patch0 

564 self.patches["wing_close_top_patch1"] = interior_top_patch1 

565 self.patches["wing_close_bot_patch0"] = interior_bot_patch0 

566 self.patches["wing_close_bot_patch1"] = interior_bot_patch1 

567 self.patches["interior_ellip"] = interior_ellip 

568 

569 # Interior TE 

570 TE_point = Vector3( 

571 x=self.TE_mean_line(0).x 

572 - self.params["FLAP_LENGTH"] * np.cos(self.params["FLAP_ANGLE"]), 

573 y=self.TE_mean_line(0).y, 

574 z=self.TE_mean_line(0).z 

575 + self.params["FLAP_LENGTH"] * np.sin(self.params["FLAP_ANGLE"]), 

576 ) 

577 

578 interior_flap_patch = CoonsPatch( 

579 north=Line(p0=self.TE_top(0), p1=TE_point), 

580 east=Line(p0=self.TE_bot(0), p1=TE_point), 

581 south=Line(p0=self.TE_mean_line(0), p1=self.TE_bot(0)), 

582 west=Line(p0=self.TE_mean_line(0), p1=self.TE_top(0)), 

583 ) 

584 

585 self.patches["interior_flap_patch"] = interior_flap_patch