Source code for pterasoftware.aeroelastic_unsteady_ring_vortex_lattice_method

"""Contains the AeroelasticUnsteadyRingVortexLatticeMethodSolver class.

**Contains the following classes:**

AeroelasticUnsteadyRingVortexLatticeMethodSolver: A subclass of
CoupledUnsteadyRingVortexLatticeMethodSolver that solves AeroelasticUnsteadyProblems,
extending the coupled solver with Strip Leading Edge Point (SLEP) functionality for
computing aerodynamic moments about the strip leading edge so that wing deformations can
be coupled with aerodynamic loads.

**Contains the following functions:**

None
"""

from __future__ import annotations

from typing import cast

import numpy as np

from . import _functions, problems
from ._coupled_unsteady_ring_vortex_lattice_method import (
    CoupledUnsteadyRingVortexLatticeMethodSolver,
)


[docs] class AeroelasticUnsteadyRingVortexLatticeMethodSolver( CoupledUnsteadyRingVortexLatticeMethodSolver ): """A subclass of CoupledUnsteadyRingVortexLatticeMethodSolver that adds SLEP (Strip Leading Edge Point) functionality for aeroelastic simulations. This solver extends the coupled solver with moment calculations about each panel's strip leading edge point, which is important for analyzing wing loading and deformation characteristics relative to the wing root. **Key additions over parent CoupledUnsteadyRingVortexLatticeMethodSolver:** initializes and maintains SLEP index mapping and position arrays, overrides ``_reinitialize_step_arrays_hook`` to reset SLEP arrays each step, overrides ``_load_calculation_moment_processing_hook`` to compute SLEP moments, and computes bound vortex positions relative to strip leading edge points. """ __slots__ = ( "slep_point_indices", "stackCblvpr_GP1_Slep", "stackCblvpf_GP1_Slep", "stackCblvpl_GP1_Slep", "stackCblvpb_GP1_Slep", "stackCpp_GP1_Slep", "stackFlpp_GP1_CgP1", "moments_GP1_Slep", "stack_leading_edge_points", ) def __init__( self, aeroelastic_unsteady_problem: problems.AeroelasticUnsteadyProblem, ) -> None: """Initialize the solver for an AeroelasticUnsteadyProblem. Sets up the solver infrastructure and initializes SLEP (Strip Leading Edge Point) related attributes. :param aeroelastic_unsteady_problem: The AeroelasticUnsteadyProblem to be solved. :return: None """ if not isinstance( aeroelastic_unsteady_problem, problems.AeroelasticUnsteadyProblem ): raise TypeError( "aeroelastic_unsteady_problem must be an " "AeroelasticUnsteadyProblem." ) super().__init__(aeroelastic_unsteady_problem) first_steady_problem: problems.SteadyProblem = self._get_steady_problem_at(0) # Initialize SLEP (Strip Leading Edge Point) information. For each airplane # and wing, we track the panel index where each new spanwise strip begins. # This allows efficient computation of moments about the strip leading edge # (wing root to tip). panel_count = 0 slep_point_indices_list: list[int] = [] for airplane in first_steady_problem.airplanes: for wing in airplane.wings: for wing_cross_section in wing.wing_cross_sections: # Record the first panel index for this wing cross-section # (start of strip). slep_point_indices_list.append(panel_count) if wing_cross_section.num_spanwise_panels is not None: panel_count += wing_cross_section.num_spanwise_panels self.slep_point_indices: np.ndarray = np.array( slep_point_indices_list, dtype=int ) # The current time step's center bound LineVortex points for the right, # front, left, and back legs (in the first Airplane's geometry axes, # relative to the local strip leading edge point). self.stackCblvpr_GP1_Slep: np.ndarray = np.empty(0, dtype=float) self.stackCblvpf_GP1_Slep: np.ndarray = np.empty(0, dtype=float) self.stackCblvpl_GP1_Slep: np.ndarray = np.empty(0, dtype=float) self.stackCblvpb_GP1_Slep: np.ndarray = np.empty(0, dtype=float) # The colocation panel points and the front left panel point (in the first # Airplane's geometry axes, relative to the local strip leading edge point # and the first Airplane's CG respectively). self.stackCpp_GP1_Slep: np.ndarray = np.empty(0, dtype=float) self.stackFlpp_GP1_CgP1: np.ndarray = np.empty(0, dtype=float) self.moments_GP1_Slep: np.ndarray = np.empty(0, dtype=float) self.stack_leading_edge_points: np.ndarray = np.empty(0, dtype=float) @property def _aeroelastic_unsteady_problem(self) -> problems.AeroelasticUnsteadyProblem: """The solver's AeroelasticUnsteadyProblem, narrowed from the inherited unsteady_problem. The inherited unsteady_problem slot is typed as the base CoreUnsteadyProblem so the parent solver can hold any coupled problem. This solver's constructor only accepts an AeroelasticUnsteadyProblem, so the cast here is safe. :return: This solver's AeroelasticUnsteadyProblem. """ return cast(problems.AeroelasticUnsteadyProblem, self.unsteady_problem) def _reinitialize_step_arrays_hook(self) -> None: """Reinitialize SLEP arrays at the start of each time step. :return: None """ self.stackCblvpr_GP1_Slep = np.zeros((self.num_panels, 3), dtype=float) self.stackCblvpf_GP1_Slep = np.zeros((self.num_panels, 3), dtype=float) self.stackCblvpl_GP1_Slep = np.zeros((self.num_panels, 3), dtype=float) self.stackCblvpb_GP1_Slep = np.zeros((self.num_panels, 3), dtype=float) self.stackCpp_GP1_Slep = np.zeros((self.num_panels, 3), dtype=float) self.moments_GP1_Slep = np.zeros((self.num_panels, 3), dtype=float) self.stackFlpp_GP1_CgP1 = np.zeros((self.num_panels, 3), dtype=float) def _load_calculation_moment_processing_hook( self, rightLegForces_GP1, frontLegForces_GP1, leftLegForces_GP1, backLegForces_GP1, unsteady_forces_GP1, ) -> np.ndarray: """Override parent to compute moments about both center-of-gravity and SLEP. This hook extends the parent class's moment calculation by additionally computing moments about each panel's Strip Leading Edge Point (SLEP). This is used for analyzing wing loading and deformation characteristics relative to the wing root. The method first calls the parent's implementation to get CG-based moments, then updates bound vortex positions relative to SLEP points, recalculates all moment contributions in the SLEP frame, and stores the SLEP moments in self.moments_GP1_Slep. :return: moments_GP1_CgP1, a (N,3) ndarray of floats representing the moments (in the first Airplane's geometry axes, relative to the first Airplane's CG) on every Panel at the current time step. SLEP moments are stored separately in self.moments_GP1_Slep. """ moments_GP1_CgP1 = super()._load_calculation_moment_processing_hook( rightLegForces_GP1, frontLegForces_GP1, leftLegForces_GP1, backLegForces_GP1, unsteady_forces_GP1, ) self._update_bound_vortex_positions_relative_to_slep_points() rightLegMoments_GP1_Slep = _functions.numba_1d_explicit_cross( self.stackCblvpr_GP1_Slep, rightLegForces_GP1 ) frontLegMoments_GP1_Slep = _functions.numba_1d_explicit_cross( self.stackCblvpf_GP1_Slep, frontLegForces_GP1 ) leftLegMoments_GP1_Slep = _functions.numba_1d_explicit_cross( self.stackCblvpl_GP1_Slep, leftLegForces_GP1 ) backLegMoments_GP1_Slep = _functions.numba_1d_explicit_cross( self.stackCblvpb_GP1_Slep, backLegForces_GP1 ) # The unsteady moment is calculated at the collocation point because the # unsteady force acts on the bound RingVortex, whose center is at the # collocation point, not at the Panel's centroid. unsteady_moments_GP1_Slep = _functions.numba_1d_explicit_cross( self.stackCpp_GP1_Slep, unsteady_forces_GP1 ) self.moments_GP1_Slep = ( rightLegMoments_GP1_Slep + frontLegMoments_GP1_Slep + leftLegMoments_GP1_Slep + backLegMoments_GP1_Slep + unsteady_moments_GP1_Slep ) return moments_GP1_CgP1 def _update_bound_vortex_positions_relative_to_slep_points(self) -> None: """Transform bound RingVortex leg center positions from CG-relative to SLEP- relative. For each panel, this method: 1. Gets the front-left panel point (leading edge) from each panel 2. Maps panels to their corresponding strip's leading edge point using slep_point_indices 3. Subtracts the SLEP position from all vortex leg center positions 4. Subtracts the SLEP position from collocation points This prepares positions for computing moments about the strip leading edge, which is important for analyzing local wing loading and deformations. :return: None """ for panel_num, panel in enumerate(self.panels): self.stackFlpp_GP1_CgP1[panel_num] = panel.Flpp_GP1_CgP1 slep_points = self.stackFlpp_GP1_CgP1[self.slep_point_indices] slep_map = ( np.searchsorted( self.slep_point_indices, np.arange(self.num_panels), side="right" ) - 1 ) self.stack_leading_edge_points = np.array([slep_points[i] for i in slep_map]) self.stackCblvpr_GP1_Slep = ( self.stackCblvpr_GP1_CgP1 - self.stack_leading_edge_points ) self.stackCblvpf_GP1_Slep = ( self.stackCblvpf_GP1_CgP1 - self.stack_leading_edge_points ) self.stackCblvpl_GP1_Slep = ( self.stackCblvpl_GP1_CgP1 - self.stack_leading_edge_points ) self.stackCblvpb_GP1_Slep = ( self.stackCblvpb_GP1_CgP1 - self.stack_leading_edge_points ) # Find the collocation point positions relative to the SLEP points. self.stackCpp_GP1_Slep = self.stackCpp_GP1_CgP1 - self.stack_leading_edge_points