Source code for pterasoftware.movements.free_flight_movement

"""Contains the FreeFlightMovement class.

**Contains the following classes:**

FreeFlightMovement: A class used to contain a FreeFlightUnsteadyProblem's movement.

**Contains the following functions:**

None
"""

from __future__ import annotations

from typing import cast

from .. import _core, _parameter_validation, geometry
from . import free_flight_airplane_movement as free_flight_airplane_movement_mod
from . import (
    free_flight_operating_point_movement as free_flight_operating_point_movement_mod,
)


[docs] class FreeFlightMovement(_core.CoreMovement): """A class used to contain a FreeFlightUnsteadyProblem's movement. In free flight, airplane geometry is prescribed (flapping, CG oscillation, etc.) but OperatingPoints are dynamically determined by the solver as it integrates rigid body dynamics at each time step. FreeFlightMovement pre generates all Airplanes upfront and provides a FreeFlightOperatingPointMovement whose mutable operating_points list the solver populates during simulation. The simulation is divided into two phases. During the prescribed phase, the solver uses the operating conditions from the initial OperatingPoint. During the free flight phase, the solver integrates rigid body dynamics using MuJoCo and creates new OperatingPoints from the resulting state at each time step. **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 FreeFlightMovement'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 FreeFlightMovement'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 FreeFlightMovement's sub movement objects, its sub sub movement object(s), and its sub sub sub movement objects all represent no motion. """ __slots__ = ( "_prescribed_num_steps", "_free_num_steps", "_airplanes", ) def __init__( self, airplane_movements: list[ free_flight_airplane_movement_mod.FreeFlightAirplaneMovement ], operating_point_movement: free_flight_operating_point_movement_mod.FreeFlightOperatingPointMovement, delta_time: float | int, prescribed_num_steps: int, free_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. 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 FreeFlightAirplaneMovements associated with each of the FreeFlightUnsteadyProblem's Airplanes. :param operating_point_movement: A FreeFlightOperatingPointMovement holding the initial OperatingPoint. The solver populates its mutable operating_points list during simulation. :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 prescribed_num_steps: The number of prescribed flight time steps to simulate before the free flight time steps. It must be a positive int. :param free_num_steps: The number of free flight time steps to simulate after the prescribed time steps. 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 a FreeFlightAirplaneMovement, not just # a CoreAirplaneMovement. CoreMovement.__init__() validates at the Core # level, but FreeFlightMovement enforces the stricter type. for airplane_movement in airplane_movements: if not isinstance( airplane_movement, free_flight_airplane_movement_mod.FreeFlightAirplaneMovement, ): raise TypeError( "Every element in airplane_movements must be a " "FreeFlightAirplaneMovement." ) # Validate that operating_point_movement is a # FreeFlightOperatingPointMovement. if not isinstance( operating_point_movement, free_flight_operating_point_movement_mod.FreeFlightOperatingPointMovement, ): raise TypeError( "operating_point_movement must be a " "FreeFlightOperatingPointMovement." ) # Validate and store the phase step counts. prescribed_num_steps = _parameter_validation.int_in_range_return_int( prescribed_num_steps, "prescribed_num_steps", min_val=1, min_inclusive=True, ) free_num_steps = _parameter_validation.int_in_range_return_int( free_num_steps, "free_num_steps", min_val=1, min_inclusive=True, ) num_steps = prescribed_num_steps + free_num_steps # --- 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, ) # --- Store FreeFlightMovement only attributes --- self._prescribed_num_steps = prescribed_num_steps self._free_num_steps = free_num_steps # --- Batch generate Airplanes --- # Generate a list of lists of Airplanes that are the steps through each # FreeFlightAirplaneMovement. The first index identifies the # FreeFlightAirplaneMovement, and the second index identifies the time # step. airplanes_temp: list[list[geometry.airplane.Airplane]] = [] for airplane_movement in self.airplane_movements: airplanes_temp.append( airplane_movement.generate_airplanes( num_steps=self._num_steps, delta_time=self._delta_time ) ) # Validate that all Wings maintain their symmetry type across all time # steps. for airplane_movement_id, airplane_list in enumerate(airplanes_temp): # Get the base Airplane (first time step). base_airplane = airplane_list[0] # Store the symmetry types of the base Wings. base_wing_symmetry_types = [] for wing in base_airplane.wings: base_wing_symmetry_types.append(wing.symmetry_type) # Validate all subsequent time steps. for step_id, airplane in enumerate(airplane_list): # Check that Wings maintain their symmetry types. for wing_id, wing in enumerate(airplane.wings): base_symmetry_type = base_wing_symmetry_types[wing_id] if wing.symmetry_type != base_symmetry_type: raise ValueError( f"Wing {wing_id} in FreeFlightAirplaneMovement " f"{airplane_movement_id} changed from type " f"{base_symmetry_type} symmetry at time step 0 " f"to type {wing.symmetry_type} symmetry at time " f"step {step_id}. Wings cannot undergo motion " f"that changes their symmetry type. This happens " f"when a symmetric Wing moves such that its " f"symmetry plane is no longer coincident with " f"the wing axes' yz plane or vice versa." ) # Store as tuple of tuples to prevent external mutation. self._airplanes: tuple[tuple[geometry.airplane.Airplane, ...], ...] = tuple( tuple(airplane_list) for airplane_list in airplanes_temp ) # --- Immutable: read only properties --- @property def operating_point_movement( self, ) -> free_flight_operating_point_movement_mod.FreeFlightOperatingPointMovement: assert isinstance( self._operating_point_movement, free_flight_operating_point_movement_mod.FreeFlightOperatingPointMovement, ) return self._operating_point_movement @property def airplane_movements( self, ) -> tuple[free_flight_airplane_movement_mod.FreeFlightAirplaneMovement, ...]: return cast( tuple[free_flight_airplane_movement_mod.FreeFlightAirplaneMovement, ...], self._airplane_movements, ) @property def prescribed_num_steps(self) -> int: return self._prescribed_num_steps @property def free_num_steps(self) -> int: return self._free_num_steps @property def airplanes(self) -> tuple[tuple[geometry.airplane.Airplane, ...], ...]: return self._airplanes