Source code for pterasoftware.geometry.wing_cross_section

"""Contains the WingCrossSection class.

**Contains the following classes:**

WingCrossSection: A class used to contain wing cross sections of a Wing.

**Contains the following functions:**

None
"""

from __future__ import annotations

import copy
from collections.abc import Sequence

import numpy as np
import pyvista as pv

from .. import _parameter_validation, _transformations
from . import airfoil as airfoil_mod


[docs] class WingCrossSection: """A class used to contain the wing cross sections of a Wing. **Contains the following methods:** __deepcopy__: Creates a deep copy of this WingCrossSection. T_pas_Wcsp_Lpp_to_Wcs_Lp: Defines a property for the passive transformation matrix which maps in homogeneous coordinates from parent wing cross section axes, relative to the parent leading point, to wing cross section axes, relative to the leading point. Is None if the WingCrossSection hasn't been fully validated yet. T_pas_Wcs_Lp_to_Wcsp_Lpp: Defines a property for the passive transformation matrix which maps in homogeneous coordinates from wing cross section axes, relative to the leading point, to parent wing cross section axes, relative to the parent leading point. Is None if the WingCrossSection hasn't been fully validated yet. validated: A flag indicating if this WingCrossSection has been fully validated by its parent Wing. symmetry_type: The symmetry type inherited from the parent Wing. get_plottable_data: Returns plottable data for this WingCrossSection's Airfoil's outline and mean camber line. validate_root_constraints: Called by the parent Wing to validate constraints specific to root WingCrossSections. validate_mid_constraints: Called by the parent Wing to validate constraints specific to middle WingCrossSections. validate_tip_constraints: Called by the parent Wing to validate constraints specific to tip WingCrossSections. **Notes:** Immutable attributes (airfoil, num_spanwise_panels, chord, Lp_Wcsp_Lpp, angles_Wcsp_to_Wcs_ixyz, control_surface_hinge_point, control_surface_deflection, and spanwise_spacing) are set during initialization and cannot be modified afterward. The numpy arrays Lp_Wcsp_Lpp and angles_Wcsp_to_Wcs_ixyz are made read only to prevent in place mutation. Derived transformation matrices (T_pas_Wcsp_Lpp_to_Wcs_Lp and T_pas_Wcs_Lp_to_Wcsp_Lpp) are lazily evaluated and cached. The validated and symmetry_type attributes are set once by the parent Wing and cannot be modified after being set. The control_surface_symmetry_type attribute remains mutable as it may be modified by Airplane.process_wing_symmetry() for type 5 symmetry handling. The first WingCrossSection in a Wing's wing_cross_section list is known as the root WingCrossSection. The last is known as the tip WingCrossSection. Every WingCrossSection has its own wing cross section axes. For root WingCrossSections, their wing cross section axes are identical in position, orientation, and handedness to their Wing's wing axes. For all other WingCrossSections, their wing cross section axes are defined relative to the axes of the previous WingCrossSection. Locally, the x axis points from a cross section's leading point to its trailing point, the y axis points spanwise in the general direction of the next WingCrossSection, and the z axis points upwards. Things can get a little confusing with respect to WingCrossSections for Wings with symmetric or mirror_only set to True. For more details, look in the Wing class's docstring. Also, remember that WingCrossSections themselves aren't used by the solvers, they are merely one of the Wing attributes that tell the meshing function how we'd like to generate the Wing's Panels. **Citation:** Adapted from: geometry.WingXSec in AeroSandbox Author: Peter Sharpe Date of retrieval: 04/26/2020 """ __slots__ = ( "_airfoil", "_num_spanwise_panels", "_chord", "_Lp_Wcsp_Lpp", "_angles_Wcsp_to_Wcs_ixyz", "control_surface_symmetry_type", "_control_surface_hinge_point", "_control_surface_deflection", "_spanwise_spacing", "_validated", "_symmetry_type", "_T_pas_Wcsp_Lpp_to_Wcs_Lp", "_T_pas_Wcs_Lp_to_Wcsp_Lpp", ) def __init__( self, airfoil: airfoil_mod.Airfoil, num_spanwise_panels: int | None, chord: float | int = 1.0, Lp_Wcsp_Lpp: np.ndarray | Sequence[float | int] = (0.0, 0.0, 0.0), angles_Wcsp_to_Wcs_ixyz: np.ndarray | Sequence[float | int] = (0.0, 0.0, 0.0), control_surface_symmetry_type: str | None = None, control_surface_hinge_point: float = 0.75, control_surface_deflection: float | int = 0.0, spanwise_spacing: str | None = None, ) -> None: """The initialization method. :param airfoil: The Airfoil to be used at this WingCrossSection. :param num_spanwise_panels: The number of spanwise Panels to be used between this WingCrossSection and the next one. For tip WingCrossSections, it must be None. For all other WingCrossSections, it must be a positive integer. :param chord: The Wing's chord at this WingCrossSection. It must be greater than 0.0 and a number (int or float), and will be converted internally to a float. The units are in meters. The default value is 1.0. :param Lp_Wcsp_Lpp: An array-like object of 3 numbers (int or float) representing the position in meters of this WingCrossSection's leading edge in parent wing cross section axes, relative to the parent leading edge point. Can be a tuple, list, or ndarray. Values are converted to floats internally. If this is the root WingCrossSection, the parent wing cross section axes are the wing axes and the parent leading point is the Wing's leading edge root point. If not, the parent axes and point are those of the previous WingCrossSection. If this is the root WingCrossSection, it must be a zero vector. The second component must be non negative. For non root WingCrossSections, the second component must be strictly positive. The units are in meters. The default is (0.0, 0.0, 0.0). :param angles_Wcsp_to_Wcs_ixyz: An array-like object of 3 numbers (int or float) representing the angle vector of rotation angles that define the orientation of this WingCrossSection's axes relative to the parent wing cross section axes. Can be a tuple, list, or ndarray. Values are converted to floats internally. If this is a root WingCrossSection, these are the wing axes. If not, the parent axes are the previous WingCrossSection's axes. For the root WingCrossSection, this must be a zero vector. For other WingCrossSections, all angles must be in the range [-90, 90] degrees. Rotations are intrinsic, and proceed in the xy'z" order. The units are in degrees. The default is (0.0, 0.0, 0.0). :param control_surface_symmetry_type: Determines how control surfaces behave when the Wing has symmetry. Can be "symmetric", "asymmetric", or None. With "symmetric", mirrored control surfaces have the same deflection (like flaps). With "asymmetric", mirrored control surfaces have opposite deflections (like ailerons). The default is None. For Wings with type 4 or 5 symmetry, this parameter must be specified. For Wings with type 1, 2, or 3 symmetry, this parameter must be None. This validation is performed by the parent Airplane during Wing processing. :param control_surface_hinge_point: The location of the control surface hinge from the leading edge as a fraction of chord. It must be a float in the range (0.0, 1.0). The default is 0.75. :param control_surface_deflection: The control deflection in degrees. Deflection downwards is positive. It must be a number (int or float) in the range [-5.0, 5.0] degrees. It will be converted to a float internally. The default is 0.0 degrees. :param spanwise_spacing: For non tip WingCrossSections, this can be "cosine" or "uniform". I highly recommend using cosine spacing. For tip WingCrossSections it must be None. If the parent Wing has explode_into_strips=True, every non tip WingCrossSection must use "uniform" because the explosion distributes parent-relative offsets and twists uniformly across each WingCrossSection's intermediates. :return: None """ # Validate airfoil (immutable). if not isinstance(airfoil, airfoil_mod.Airfoil): raise TypeError("airfoil must be an Airfoil.") self._airfoil = airfoil # Perform a preliminary validation for num_spanwise_panels (immutable). The # parent Wing will later check that this is None if this WingCrossSection is # a tip WingCrossSection. if num_spanwise_panels is not None: num_spanwise_panels = _parameter_validation.int_in_range_return_int( num_spanwise_panels, "Non-None num_spanwise", min_val=1, min_inclusive=True, ) self._num_spanwise_panels = num_spanwise_panels # Validate chord (immutable). self._chord = _parameter_validation.number_in_range_return_float( chord, "chord", min_val=0.0, min_inclusive=False ) # Perform a preliminary validation for Lp_Wcsp_Lpp (immutable). The parent # Wing will later check that this is a zero vector if this WingCrossSection # is a root WingCrossSection. Lp_Wcsp_Lpp = _parameter_validation.threeD_number_vectorLike_return_float( Lp_Wcsp_Lpp, "Lp_Wcsp_Lpp" ) Lp_Wcsp_Lpp[1] = _parameter_validation.number_in_range_return_float( Lp_Wcsp_Lpp[1], "Lp_Wcsp_Lpp[1]", min_val=0.0, min_inclusive=True ) self._Lp_Wcsp_Lpp = Lp_Wcsp_Lpp self._Lp_Wcsp_Lpp.flags.writeable = False # Perform a preliminary validation for angles_Wcsp_to_Wcs_ixyz (immutable). # The parent Wing will later check that this is a zero vector if this # WingCrossSection is a root WingCrossSection. angles_Wcsp_to_Wcs_ixyz = ( _parameter_validation.threeD_number_vectorLike_return_float( angles_Wcsp_to_Wcs_ixyz, "angles_Wcsp_to_Wcs_ixyz" ) ) for angle_id, angle in enumerate(angles_Wcsp_to_Wcs_ixyz): angles_Wcsp_to_Wcs_ixyz[angle_id] = ( _parameter_validation.number_in_range_return_float( angle, f"angles_Wcsp_to_Wcs_ixyz[{angle_id}]", -90.0, True, 90.0, True, ) ) self._angles_Wcsp_to_Wcs_ixyz = angles_Wcsp_to_Wcs_ixyz self._angles_Wcsp_to_Wcs_ixyz.flags.writeable = False # Validate control surface symmetry type (mutable, may be modified by # Airplane.process_wing_symmetry for type 5 symmetry). if control_surface_symmetry_type is not None: control_surface_symmetry_type = _parameter_validation.str_return_str( control_surface_symmetry_type, "control_surface_symmetry_type" ) valid_control_surface_symmetry_types = ["symmetric", "asymmetric"] if ( control_surface_symmetry_type not in valid_control_surface_symmetry_types ): raise ValueError( f"control_surface_symmetry_type must be one of " f"{valid_control_surface_symmetry_types} or None." ) self.control_surface_symmetry_type = control_surface_symmetry_type # Validate control_surface_hinge_point and control_surface_deflection # (immutable). self._control_surface_hinge_point = ( _parameter_validation.number_in_range_return_float( control_surface_hinge_point, "control_surface_hinge_point", 0.0, False, 1.0, False, ) ) self._control_surface_deflection = ( _parameter_validation.number_in_range_return_float( control_surface_deflection, "control_surface_deflection", -5.0, True, 5.0, True, ) ) # Perform a preliminary validation for spanwise_spacing (immutable). The # parent Wing will later check that this is None if this WingCrossSection is # a tip WingCrossSection. if spanwise_spacing is not None: spanwise_spacing = _parameter_validation.str_return_str( spanwise_spacing, "spanwise_spacing" ) valid_non_none_spanwise_spacings = ["cosine", "uniform"] if spanwise_spacing not in valid_non_none_spanwise_spacings: raise ValueError( f"Values for non None spanwise_spacing must be one of " f"{valid_non_none_spanwise_spacings}." ) self._spanwise_spacing = spanwise_spacing # Define a flag for if this WingCrossSection has been fully validated # (set once). This will be set by the parent Wing after calling its # additional validation methods. self._validated: bool = False # Define a flag for this WingCrossSection's parent Wing's symmetry type # (set once). This will be set by its parent Wing immediately after it has # its own symmetry_type parameter set by its parent Airplane. self._symmetry_type: int | None = None # Initialize the caches for the properties derived from the immutable # attributes. self._T_pas_Wcsp_Lpp_to_Wcs_Lp: np.ndarray | None = None self._T_pas_Wcs_Lp_to_Wcsp_Lpp: np.ndarray | None = None # --- Deep copy method --- def __deepcopy__(self, memo: dict) -> WingCrossSection: """Creates a deep copy of this WingCrossSection. All attributes are copied. The Airfoil is deep copied to ensure independence. :param memo: A dict used by the copy module to track already copied objects and avoid infinite recursion. :return: A new WingCrossSection with copied attributes. """ # Create a new WingCrossSection instance without calling __init__ to avoid # redundant validation. new_wing_cross_section = object.__new__(WingCrossSection) # Store this WingCrossSection in memo to handle potential circular references. memo[id(self)] = new_wing_cross_section # Deep copy the Airfoil to ensure independence (immutable). new_wing_cross_section._airfoil = copy.deepcopy(self._airfoil, memo) # Copy simple immutable attributes (primitive types). new_wing_cross_section._num_spanwise_panels = self._num_spanwise_panels new_wing_cross_section._chord = self._chord new_wing_cross_section._control_surface_hinge_point = ( self._control_surface_hinge_point ) new_wing_cross_section._control_surface_deflection = ( self._control_surface_deflection ) new_wing_cross_section._spanwise_spacing = self._spanwise_spacing # Copy mutable attribute. new_wing_cross_section.control_surface_symmetry_type = ( self.control_surface_symmetry_type ) # Copy set once attributes directly to private fields to preserve their state. new_wing_cross_section._validated = self._validated new_wing_cross_section._symmetry_type = self._symmetry_type # Copy numpy arrays and make them read only. new_wing_cross_section._Lp_Wcsp_Lpp = self._Lp_Wcsp_Lpp.copy() new_wing_cross_section._Lp_Wcsp_Lpp.flags.writeable = False new_wing_cross_section._angles_Wcsp_to_Wcs_ixyz = ( self._angles_Wcsp_to_Wcs_ixyz.copy() ) new_wing_cross_section._angles_Wcsp_to_Wcs_ixyz.flags.writeable = False # Copy cached derived properties. This preserves computation from validation. # For those that are numpy arrays, make the copies read only. new_wing_cross_section._T_pas_Wcsp_Lpp_to_Wcs_Lp = ( self._T_pas_Wcsp_Lpp_to_Wcs_Lp.copy() if self._T_pas_Wcsp_Lpp_to_Wcs_Lp is not None else None ) if new_wing_cross_section._T_pas_Wcsp_Lpp_to_Wcs_Lp is not None: new_wing_cross_section._T_pas_Wcsp_Lpp_to_Wcs_Lp.flags.writeable = False new_wing_cross_section._T_pas_Wcs_Lp_to_Wcsp_Lpp = ( self._T_pas_Wcs_Lp_to_Wcsp_Lpp.copy() if self._T_pas_Wcs_Lp_to_Wcsp_Lpp is not None else None ) if new_wing_cross_section._T_pas_Wcs_Lp_to_Wcsp_Lpp is not None: new_wing_cross_section._T_pas_Wcs_Lp_to_Wcsp_Lpp.flags.writeable = False return new_wing_cross_section # --- Immutable: read only properties --- @property def airfoil(self) -> airfoil_mod.Airfoil: return self._airfoil @property def num_spanwise_panels(self) -> int | None: return self._num_spanwise_panels @property def chord(self) -> float: return self._chord @property def Lp_Wcsp_Lpp(self) -> np.ndarray: return self._Lp_Wcsp_Lpp @property def angles_Wcsp_to_Wcs_ixyz(self) -> np.ndarray: return self._angles_Wcsp_to_Wcs_ixyz @property def control_surface_hinge_point(self) -> float: return self._control_surface_hinge_point @property def control_surface_deflection(self) -> float: return self._control_surface_deflection @property def spanwise_spacing(self) -> str | None: return self._spanwise_spacing # --- Immutable derived: manual lazy caching --- @property def T_pas_Wcsp_Lpp_to_Wcs_Lp(self) -> np.ndarray | None: """Defines a property for the passive transformation matrix which maps in homogeneous coordinates from parent wing cross section axes, relative to the parent leading point, to wing cross section axes, relative to the leading point. Is None if the WingCrossSection hasn't been fully validated yet. :return: A (4,4) ndarray of floats representing the transformation matrix or None if self.validated=False. """ if not self.validated: return None if self._T_pas_Wcsp_Lpp_to_Wcs_Lp is None: # Step 1: Create T_trans_pas_Wcsp_Lpp_to_Wcsp_Lp, which maps in homogeneous # coordinates from parent wing cross section axes relative to the parent # leading point to parent wing cross section axes relative to the leading # point. This is the translation step. T_trans_pas_Wcsp_Lpp_to_Wcsp_Lp = _transformations.generate_trans_T( self._Lp_Wcsp_Lpp, passive=True ) # Step 2: Create T_rot_pas_Wcsp_to_Wcs, which maps in homogeneous # coordinates from parent wing cross section axes to wing cross section # axes. This is the rotation step. T_rot_pas_Wcsp_to_Wcs = _transformations.generate_rot_T( self._angles_Wcsp_to_Wcs_ixyz, passive=True, intrinsic=True, order="xyz" ) self._T_pas_Wcsp_Lpp_to_Wcs_Lp = _transformations.compose_T_pas( T_trans_pas_Wcsp_Lpp_to_Wcsp_Lp, T_rot_pas_Wcsp_to_Wcs ) self._T_pas_Wcsp_Lpp_to_Wcs_Lp.flags.writeable = False return self._T_pas_Wcsp_Lpp_to_Wcs_Lp @property def T_pas_Wcs_Lp_to_Wcsp_Lpp(self) -> np.ndarray | None: """Defines a property for the passive transformation matrix which maps in homogeneous coordinates from wing cross section axes, relative to the leading point, to parent wing cross section axes, relative to the parent leading point. Is None if the WingCrossSection hasn't been fully validated yet. :return: A (4,4) ndarray of floats representing the transformation matrix or None if self.validated=False. """ if not self.validated: return None if self._T_pas_Wcs_Lp_to_Wcsp_Lpp is None: _T_pas_Wcsp_Lpp_to_Wcs_Lp = self.T_pas_Wcsp_Lpp_to_Wcs_Lp assert _T_pas_Wcsp_Lpp_to_Wcs_Lp is not None self._T_pas_Wcs_Lp_to_Wcsp_Lpp = _transformations.invert_T_pas( _T_pas_Wcsp_Lpp_to_Wcs_Lp ) self._T_pas_Wcs_Lp_to_Wcsp_Lpp.flags.writeable = False return self._T_pas_Wcs_Lp_to_Wcsp_Lpp # --- Set once: properties with single assignment enforcement --- @property def validated(self) -> bool: """A flag indicating if this WingCrossSection has been fully validated by its parent Wing. :return: True if validated, False otherwise. """ return self._validated @validated.setter def validated(self, value: bool) -> None: if self._validated: raise AttributeError("validated can only be set once") self._validated = value @property def symmetry_type(self) -> int | None: """The symmetry type inherited from the parent Wing. :return: The symmetry type (1, 2, 3, or 4) or None if not yet set. """ return self._symmetry_type @symmetry_type.setter def symmetry_type(self, value: int) -> None: if self._symmetry_type is not None: raise AttributeError("symmetry_type can only be set once") self._symmetry_type = value # --- Other methods ---
[docs] def get_plottable_data( self, show: bool | np.bool_ = False, ) -> list[np.ndarray] | None: """Returns plottable data for this WingCrossSection's Airfoil's outline and mean camber line. :param show: Determines whether to display the plot. If True, the method displays the plot and returns None. If False, the method returns the data without displaying. Can be a bool or a numpy bool and will be converted internally to a bool. The default is False. :return: None if the WingCrossSection hasn't been validated, if its symmetry type hasn't been set, or if show is True. Otherwise, returns a list of two ndarrays. These ndarrays represent points on this WingCrossSection's Airfoil's outline and mean camber lines, respectively. The points are in wing cross section axes, relative to the leading point. The units are in meters. """ # Validate the input flag. show = _parameter_validation.boolLike_return_bool(show, "show") # If this WingCrossSection hasn't been fully validated, or its symmetry type # hasn't been set, return None. if self.symmetry_type is None or not self.validated: return None plottable_data = self.airfoil.get_plottable_data(show=False) assert plottable_data is not None [airfoilOutline_A_lp, airfoilMcl_A_lp] = plottable_data airfoilNonScaledOutline_Wcs_lp = np.column_stack( [ airfoilOutline_A_lp[:, 0], np.zeros_like(airfoilOutline_A_lp[:, 0]), airfoilOutline_A_lp[:, 1], ] ) airfoilNonScaledMcl_Wcs_lp = np.column_stack( [ airfoilMcl_A_lp[:, 0], np.zeros_like(airfoilMcl_A_lp[:, 0]), airfoilMcl_A_lp[:, 1], ] ) airfoilScalingMatrix = np.array( [ [self.chord, 0.0, 0.0, 0.0], [0.0, self.chord, 0.0, 0.0], [0.0, 0.0, self.chord, 0.0], [0.0, 0.0, 0.0, 1.0], ] ) airfoilOutline_Wcs_lp = _transformations.apply_T_to_vectors( airfoilScalingMatrix, airfoilNonScaledOutline_Wcs_lp, is_position=True ) airfoilMcl_Wcs_lp = _transformations.apply_T_to_vectors( airfoilScalingMatrix, airfoilNonScaledMcl_Wcs_lp, is_position=True ) if not show: return [airfoilOutline_Wcs_lp, airfoilMcl_Wcs_lp] plotter = pv.Plotter() _T_pas_Wcs_Lp_to_Wcsp_Lpp = self.T_pas_Wcs_Lp_to_Wcsp_Lpp assert _T_pas_Wcs_Lp_to_Wcsp_Lpp is not None airfoilOutline_Wcsp_lpp = _transformations.apply_T_to_vectors( _T_pas_Wcs_Lp_to_Wcsp_Lpp, airfoilOutline_Wcs_lp, is_position=True ) airfoilMcl_Wcsp_lpp = _transformations.apply_T_to_vectors( _T_pas_Wcs_Lp_to_Wcsp_Lpp, airfoilMcl_Wcs_lp, is_position=True ) if self.symmetry_type in (2, 3): UserMatrixAxesWcspLpp = _transformations.generate_reflect_T( np.array([0.0, 0.0, 0.0]), np.array([0.0, 1.0, 0.0]), passive=False ) airfoilOutline_WcspReflectY_lpp = _transformations.apply_T_to_vectors( UserMatrixAxesWcspLpp, airfoilOutline_Wcsp_lpp, is_position=True ) airfoilMcl_WcspReflectY_lpp = _transformations.apply_T_to_vectors( UserMatrixAxesWcspLpp, airfoilMcl_Wcsp_lpp, is_position=True ) else: UserMatrixAxesWcspLpp = np.eye(4, dtype=float) airfoilOutline_WcspReflectY_lpp = airfoilOutline_Wcsp_lpp airfoilMcl_WcspReflectY_lpp = airfoilMcl_Wcsp_lpp rot_T_act = _transformations.generate_rot_T( angles=self.angles_Wcsp_to_Wcs_ixyz, passive=False, intrinsic=True, order="xyz", ) trans_T_act = _transformations.generate_trans_T( translations=self.Lp_Wcsp_Lpp, passive=False, ) UserMatrixAxesWcsLp_WcspLpp = _transformations.compose_T_act( rot_T_act, trans_T_act, UserMatrixAxesWcspLpp, ) airfoilOutline_faces = np.hstack( [ airfoilOutline_WcspReflectY_lpp.shape[0], np.arange(airfoilOutline_WcspReflectY_lpp.shape[0]), ] ) airfoilOutline_mesh = pv.PolyData( airfoilOutline_WcspReflectY_lpp, faces=airfoilOutline_faces ) plotter.add_mesh(airfoilOutline_mesh) plotter.add_lines(airfoilMcl_WcspReflectY_lpp) if np.allclose(UserMatrixAxesWcsLp_WcspLpp, UserMatrixAxesWcspLpp): AxesWcsLpWcspLpp_Wcsp_lpp = pv.AxesAssembly( x_label="WcsX@Lp/WcspX@Lpp", y_label="WcsY@Lp/WcspY@Lpp", z_label="WcsZ@Lp/WcspZ@Lpp", # labels=None, label_color="black", show_labels=True, # label_position=(1, 1, 1), label_size=15, x_color="red", y_color="green", z_color="blue", # position=(0.0, 0.0, 0.0), # orientation=(0.0, 0.0, 0.0), # origin=(0.0, 0.0, 0.0), scale=(0.25, 0.25, 0.25), user_matrix=UserMatrixAxesWcsLp_WcspLpp, name="Wcs/Wcsp", shaft_type="cylinder", shaft_radius=0.025, shaft_length=(0.8, 0.8, 0.8), tip_type="cone", tip_radius=0.1, tip_length=(0.2, 0.2, 0.2), symmetric_bounds=False, ) plotter.add_actor(AxesWcsLpWcspLpp_Wcsp_lpp) else: AxesWcsLp_Wcsp_lpp = pv.AxesAssembly( x_label="WcsX@Lp", y_label="WcsY@Lp", z_label="WcsZ@Lp", # labels=None, label_color="black", show_labels=True, # label_position=(1, 1, 1), label_size=15, x_color="red", y_color="green", z_color="blue", # position=(0.0, 0.0, 0.0), # orientation=(0.0, 0.0, 0.0), # origin=(0.0, 0.0, 0.0), scale=(0.25, 0.25, 0.25), user_matrix=UserMatrixAxesWcsLp_WcspLpp, # user_matrix=self.T_pas_Wcs_Lp_to_Wcsp_Lpp, name="Wcs", shaft_type="cylinder", shaft_radius=0.025, shaft_length=(0.8, 0.8, 0.8), tip_type="cone", tip_radius=0.1, tip_length=(0.2, 0.2, 0.2), symmetric_bounds=False, ) AxesWcspLpp = pv.AxesAssembly( x_label="WcspX@Lpp", y_label="WcspY@Lpp", z_label="WcspZ@Lpp", # labels=None, label_color="black", show_labels=True, # label_position=(1, 1, 1), label_size=15, x_color="red", y_color="green", z_color="blue", # position=(0.0, 0.0, 0.0), # orientation=(0.0, 0.0, 0.0), # origin=(0.0, 0.0, 0.0), scale=(0.25, 0.25, 0.25), user_matrix=UserMatrixAxesWcspLpp, name="Wcsp", shaft_type="cylinder", shaft_radius=0.025, shaft_length=(0.8, 0.8, 0.8), tip_type="cone", tip_radius=0.1, tip_length=(0.2, 0.2, 0.2), symmetric_bounds=False, ) plotter.add_actor(AxesWcsLp_Wcsp_lpp) plotter.add_actor(AxesWcspLpp) plotter.enable_parallel_projection() # type: ignore[call-arg] plotter.show( cpos=(-1, -1, 1), full_screen=False, auto_close=False, ) return None
[docs] def validate_root_constraints(self) -> None: """Called by the parent Wing to validate constraints specific to root WingCrossSections. Root WingCrossSections must have Lp_Wcsp_Lpp and angles_Wcsp_to_Wcs_ixyz set to zero vectors. They also must have num_spanwise_panels not None (it's previously been checked to be None or a positive int). :return: None """ # These checks are sufficient because the types were already validated by the # initialization method. if not np.allclose(self.Lp_Wcsp_Lpp, np.array([0.0, 0.0, 0.0])): raise ValueError( "The root WingCrossSection's Lp_Wcsp_Lpp must be np.array([0.0, 0.0, " "0.0])." ) if not np.allclose(self.angles_Wcsp_to_Wcs_ixyz, np.array([0.0, 0.0, 0.0])): raise ValueError( "The root WingCrossSection's angles_Wcsp_to_Wcs_ixyz must be " "np.array([0.0, 0.0, 0.0])." ) if self.num_spanwise_panels is None: raise ValueError( "The root WingCrossSection cannot have num_spanwise_panels set to None." )
[docs] def validate_mid_constraints(self) -> None: """Called by the parent Wing to validate constraints specific to middle WingCrossSections. Middle WingCrossSections must have num_spanwise_panels not None (it's previously been checked to be None or a positive int). They must also have a strictly positive second component of Lp_Wcsp_Lpp. :return: None """ if self.Lp_Wcsp_Lpp[1] <= 0.0: raise ValueError( "Non root WingCrossSections must have a strictly positive second " "component of Lp_Wcsp_Lpp. To create a vertical surface, define " "the WingCrossSections with spanwise (y) offsets and rotate the " "Wing using angles_Gs_to_Wn_ixyz (e.g., (90.0, 0.0, 0.0) for a " "vertical tail). To create a winglet, rotate the previous " "WingCrossSection's axes using angles_Wcsp_to_Wcs_ixyz." ) if self.num_spanwise_panels is None: raise ValueError( "Middle WingCrossSections cannot have num_spanwise_panels set to None." )
[docs] def validate_tip_constraints(self) -> None: """Called by the parent Wing to validate constraints specific to tip WingCrossSections. Tip WingCrossSections must have num_spanwise_panels and spanwise_spacing set to None. They must also have a strictly positive second component of Lp_Wcsp_Lpp. :return: None """ if self.Lp_Wcsp_Lpp[1] <= 0.0: raise ValueError( "Non root WingCrossSections must have a strictly positive second " "component of Lp_Wcsp_Lpp. To create a vertical surface, define " "the WingCrossSections with spanwise (y) offsets and rotate the " "Wing using angles_Gs_to_Wn_ixyz (e.g., (90.0, 0.0, 0.0) for a " "vertical tail). To create a winglet, rotate the previous " "WingCrossSection's axes using angles_Wcsp_to_Wcs_ixyz." ) if self.num_spanwise_panels is not None: raise ValueError( "The tip WingCrossSection must have num_spanwise_panels=None." ) if self.spanwise_spacing is not None: raise ValueError( "The tip WingCrossSection must have spanwise_spacing=None." )