Coverage for /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/hypervehicle/components/wing.py: 92%
160 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 02:51 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 02:51 +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
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)
26class Wing(Component):
27 componenttype = WING_COMPONENT
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.
53 Parameters
54 ----------
55 A0 : Vector3
56 Point p0 of the fin geometry.
58 A1 : Vector3
59 Point p1 of the fin geometry.
61 TT : Vector3
62 Point p2 of the fin geometry.
64 B0 : Vector3
65 Point p3 of the fin geometry.
67 Line_B0TT : Polyline
68 The thickness of the fin.
70 Line_B0TT_TYPE : str, optional
71 The axial position angle of the placement of the fin.
73 t_B1 : float, optional
74 The t value of the first discretisation point. The default
75 is None.
77 t_B2 : float, optional
78 The t value of the second discretisation point. The default
79 is None.
81 top_tf : Callable
82 The thickness function for the top surface of the wing.
84 bot_tf : Callable
85 The thickness function for the top surface of the wing.
87 LE_wf : Callable, optional
88 The thickness function for the leading edge of the wing.
90 LE_type : str, optional
91 The type of LE to create, either "FLAT" or "custom". The
92 default is "custom".
94 tail_option : str, optional
95 The type of trailing edge to use, currently only "FLAP". The
96 default is "FLAP".
98 flap_length : float, optional
99 The length of the trailing edge flap. The default is 0.
101 flap_angle : float, optional
102 The angle of the flap, specified in radians. The default is 0.
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.
108 stl_resolution : int, optional
109 The stl resolution to use when creating the mesh for this
110 component. The default is None.
112 verbosity : int, optional
113 The verbosity of the component. The default is 1.
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
123 LE_wf = leading_edge_width_function
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 }
144 super().__init__(
145 params=params, stl_resolution=stl_resolution, verbosity=verbosity, name=name
146 )
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"]
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"]
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"]
168 # Save other params
169 self.TE_top = None
170 self.TE_bot = None
171 self.TE_mean_line = None
173 def generate_patches(self):
174 # Create wing planform shape
175 self._create_planform_patches()
177 # Create leading edge patches
178 self._create_leading_edge()
180 # Create trailing edge patches
181 self._create_trailing_edge()
183 if "CLOSE_WING" in self.params and self.params["CLOSE_WING"]:
184 self._close_wing()
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 )
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 )
204 else:
205 raise Exception(
206 f"Option for 'Line_B0TT'={self.params['Line_B0TT_TYPE']} "
207 + "not supported."
208 )
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 )
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 )
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]
247 for i in range(2):
248 top_patch[i] = OffsetPatchFunction(
249 wing_patch_flipped[i], function=self.params["FUNC_TOP_THICKNESS"]
250 )
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 )
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
262 def _create_leading_edge(self):
263 # Add leading edge
264 if self.verbosity > 1:
265 print(" Adding Leading Edge to wing.")
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 )
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 )
285 else:
286 # Get mean line between upper and lower wing patches
287 mean_path = GeometricMeanPathFunction(top_edge_path, bot_edge_path)
289 LE_top_patch = [np.nan, np.nan, np.nan]
290 LE_bot_patch = [np.nan, np.nan, np.nan]
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 )
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 )
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]
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"]))
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"]))
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)
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 )
387 # Append to patch_dict
388 self.patches["TE_top_patch"] = TE_top_patch
389 self.patches["TE_bot_patch"] = TE_bot_patch
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))
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
402 if thickness_top == 0 or thickness_bot == 0:
403 raise Exception(
404 "Elliptical LE cannot be created when thickness converges to zero."
405 )
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 )
420 elipse_top = ArcLengthParameterizedPath(underlying_path=elipse_top)
421 elipse_bot = ArcLengthParameterizedPath(underlying_path=elipse_bot)
423 north = elipse_bot
424 west = elipse_top
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 )
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 )
456 TELE_patch = CoonsPatch(north=north, west=west, south=south, east=east)
458 # Append to patch_dict
459 self.patches["TELE_patch"] = TELE_patch
461 else:
462 raise Exception(
463 "Tail option = {} not supported.".format(self.params["TAIL_OPTION"])
464 )
466 def _close_wing(self):
467 # Add patch to close wing volume
468 if self.verbosity > 1:
469 print(" Closing interior side of wing.")
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)
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)
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)
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)
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))
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))
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 )
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 )
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)
547 # Now reverse the paths for correct orientation
548 elipse_bot = SubRangedPath(underlying_path=elipse_bot, t0=1.0, t1=0.0)
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 )
557 # Rotate patch
558 interior_ellip = RotatedPatch(
559 interior_ellip, np.deg2rad(-90), axis="z", point=TT_mid
560 )
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
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 )
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 )
585 self.patches["interior_flap_patch"] = interior_flap_patch