Source code for pterasoftware.movements.aeroelastic_movement

"""Contains the AeroelasticMovement class.

**Contains the following classes:**

AeroelasticMovement: A class used to contain an AeroelasticUnsteadyProblem's movement.

**Contains the following functions:**

None
"""

from __future__ import annotations

from typing import cast

import numpy as np

from .. import _core, geometry
from .. import operating_point as operating_point_mod
from . import aeroelastic_airplane_movement as aeroelastic_airplane_movement_mod
from . import (
    aeroelastic_operating_point_movement as aeroelastic_operating_point_movement_mod,
)


[docs] class AeroelasticMovement(_core.CoreMovement): """A class used to contain an AeroelasticUnsteadyProblem's movement. In aeroelastic simulations, wing geometry is prescribed via oscillation parameters (flapping, CG oscillation, etc.) but the solver adds structural deformation at each time step based on aerodynamic loads. OperatingPoints are prescribed via the same oscillation parameters as OperatingPointMovement. AeroelasticMovement pre generates all OperatingPoints upfront (since they are prescribed) but does not pre generate Airplanes, because the deformed wing geometry at each time step depends on the solver's structural response calculation. **Contains the following methods:** lcm_period: The least common multiple of all motion periods, ensuring all motions complete an integer number of cycles when cycle averaging forces and moments. max_period: The longest period of motion of AeroelasticMovement's sub movement objects, the motion(s) of its sub sub movement object(s), and the motions of its sub sub sub movement objects. min_period: The shortest non zero period of motion of AeroelasticMovement's sub movement objects, the motion(s) of its sub sub movement object(s), and the motions of its sub sub sub movement objects. static: Flags if AeroelasticMovement's sub movement objects, its sub sub movement object(s), and its sub sub sub movement objects all represent no motion. generate_airplane_at_time_step: Creates the Airplane at a single time step, applying deformation from the solver's structural response. """ __slots__ = ("_operating_points",) def __init__( self, airplane_movements: list[ aeroelastic_airplane_movement_mod.AeroelasticAirplaneMovement ], operating_point_movement: aeroelastic_operating_point_movement_mod.AeroelasticOperatingPointMovement, delta_time: float | int, num_steps: int, max_wake_rows: int | None = None, ) -> None: """The initialization method. This method checks that all Wings maintain their symmetry type across all time steps (using the undeformed prescribed geometry). See the WingMovement class documentation for more details on this requirement. See the Wing class documentation for more information on symmetry types. :param airplane_movements: A list of the AeroelasticAirplaneMovements associated with each of the AeroelasticUnsteadyProblem's Airplanes. :param operating_point_movement: An AeroelasticOperatingPointMovement holding the oscillation parameters for prescribing OperatingPoints at each time step. :param delta_time: The time, in seconds, between each time step. It must be a positive number (int or float). It will be converted internally to a float. :param num_steps: The number of time steps to simulate. It must be a positive int. :param max_wake_rows: The maximum number of chordwise wake ring vortex rows per Wing. Must be a positive int if set. The default is None (no truncation). :return: None """ # Validate that every element is an AeroelasticAirplaneMovement, not # just a CoreAirplaneMovement. CoreMovement.__init__() validates at # the Core level, but AeroelasticMovement enforces the stricter type. for airplane_movement in airplane_movements: if not isinstance( airplane_movement, aeroelastic_airplane_movement_mod.AeroelasticAirplaneMovement, ): raise TypeError( "Every element in airplane_movements must be an " "AeroelasticAirplaneMovement." ) # Validate that operating_point_movement is an # AeroelasticOperatingPointMovement. if not isinstance( operating_point_movement, aeroelastic_operating_point_movement_mod.AeroelasticOperatingPointMovement, ): raise TypeError( "operating_point_movement must be an " "AeroelasticOperatingPointMovement." ) # --- Initialize CoreMovement --- super().__init__( airplane_movements=airplane_movements, operating_point_movement=operating_point_movement, delta_time=delta_time, num_steps=num_steps, max_wake_rows=max_wake_rows, ) # --- Batch generate OperatingPoints --- # OperatingPoints are prescribed in aeroelastic simulations, so # generate them all upfront. operating_points_list = operating_point_movement.generate_operating_points( num_steps=self._num_steps, delta_time=self._delta_time ) self._operating_points: tuple[operating_point_mod.OperatingPoint, ...] = tuple( operating_points_list ) # --- Immutable: read only properties --- @property def operating_point_movement( self, ) -> aeroelastic_operating_point_movement_mod.AeroelasticOperatingPointMovement: assert isinstance( self._operating_point_movement, aeroelastic_operating_point_movement_mod.AeroelasticOperatingPointMovement, ) return self._operating_point_movement @property def airplane_movements( self, ) -> tuple[aeroelastic_airplane_movement_mod.AeroelasticAirplaneMovement, ...]: return cast( tuple[aeroelastic_airplane_movement_mod.AeroelasticAirplaneMovement, ...], self._airplane_movements, ) @property def operating_points(self) -> tuple[operating_point_mod.OperatingPoint, ...]: return self._operating_points
[docs] def generate_airplane_at_time_step( self, airplane_movement_index: int, step: int, wing_deformation_angles_ixyz: list[np.ndarray | None] | None = None, ) -> geometry.airplane.Airplane: """Creates the Airplane at a single time step for a given AeroelasticAirplaneMovement, applying deformation from the solver's structural response. This is the method the aeroelastic solver calls at each time step to get the deformed Airplane geometry. :param airplane_movement_index: The index of the AeroelasticAirplaneMovement in this AeroelasticMovement's airplane_movements tuple. :param step: The time step index. Must be a non negative int. :param wing_deformation_angles_ixyz: A list of (N_wcs, 3) ndarrays of floats, one per Wing, where N_wcs is the number of WingCrossSections in that Wing. Each row is a (3,) deformation angle vector using an intrinsic xy'z" sequence. The units are in degrees. When None, no deformation is applied. The default is None. :return: The Airplane at this time step, with structural deformation applied if provided. """ return self.airplane_movements[ airplane_movement_index ].generate_airplane_at_time_step( step, self._delta_time, wing_deformation_angles_ixyz=wing_deformation_angles_ixyz, )