Source code for fastoad.models.geometry.profiles.profile

"""
Management of 2D wing profiles
"""
#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
#  FAST is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.

import operator
from collections import namedtuple
from typing import Sequence, Tuple

import numpy as np
import pandas as pd
from scipy.interpolate import interp1d

Coordinates2D = namedtuple("Coordinates2D", ["x", "y"])

X = "x"
Z = "z"
THICKNESS = "thickness"


[docs]class Profile: """Class for managing 2D wing profiles :param chord_length: :param x: :param y: """ # pylint: disable=invalid-name # X and Z are valid names in this context def __init__(self, chord_length: float = 0.0): self._rel_mean_line_and_thickness = pd.DataFrame(columns=[X, Z, THICKNESS]) """ Data of mean line and thickness, computed after inputs of :meth:`set_points`_. DataFrame keys are 'x', 'z' and 'thickness'. - 'x' and 'z' are relative to chord_length - 'thickness' is relative to max thickness (and given according to 'x') """ self.chord_length: float = chord_length """ in meters """ self._max_relative_thickness: float = 0.0 """ max thickness / chord length""" @property def thickness_ratio(self) -> float: """ thickness-to-chord ratio """ return self._max_relative_thickness @thickness_ratio.setter def thickness_ratio(self, value: float): # FIXME: mean line is modified accordingly to conform to legacy algorithm, but it # is questionable if self._max_relative_thickness != 0.0: coeff = value / self._max_relative_thickness self._rel_mean_line_and_thickness[Z] *= coeff self._max_relative_thickness = value
[docs] def set_points( self, x: Sequence, z: Sequence, keep_chord_length: bool = True, keep_relative_thickness: bool = True, ): """ Sets points of the 2D profile. Provided points are expected to be in order around the profile (clockwise or anti-clockwise). :param x: in meters :param z: in meters :param keep_relative_thickness: :param keep_chord_length: """ x = np.asarray(x) # Separate upper surface from lower surface (easier for computation # of thickness and mean line) upper, lower = self._create_upper_lower_sides(x, z) # Upper and lower sides are defined, we can compute mean line and thickness chord_length, max_thickness = self._compute_mean_line_and_thickness(upper, lower) if not keep_chord_length or self.chord_length == 0.0: self.chord_length = chord_length if not keep_relative_thickness or self.thickness_ratio == 0.0: self.thickness_ratio = max_thickness / chord_length
[docs] def get_mean_line(self) -> pd.DataFrame: """Point set of mean line of the profile. DataFrame keys are 'x' and 'z', given in meters. """ mean_line = self._rel_mean_line_and_thickness[[X, Z]] * self.chord_length return mean_line
[docs] def get_relative_thickness(self) -> pd.DataFrame: """Point set of relative thickness of the profile. DataFrame keys are 'x' and 'thickness' and are relative to chord_length. 'x' is from 0. to 1. """ return self._rel_mean_line_and_thickness[[X, THICKNESS]] * [1.0, self.thickness_ratio]
[docs] def get_upper_side(self) -> pd.DataFrame: """Point set of upper side of the profile. DataFrame keys are 'x' and 'z', given in meters. """ return self._get_side_points(operator.add)
[docs] def get_lower_side(self) -> pd.DataFrame: """Point set of lower side of the profile. DataFrame keys are 'x' and 'z', given in meters. """ return self._get_side_points(operator.sub)
[docs] def get_sides(self) -> pd.DataFrame: """Point set of the whole profile Points are given from trailing edge to trailing edge, starting by upper side. """ return pd.concat( [self.get_upper_side().sort_values(by=X, ascending=False), self.get_lower_side()[1:]] )
def _get_side_points(self, operator_) -> pd.DataFrame: """ Computes upper or lower side points. operator_ == operator.add() -> upper side operator_ == operator.sub() -> lower side """ mean_line = self._rel_mean_line_and_thickness[[X, Z]] half_thickness = pd.DataFrame().reindex_like(mean_line) half_thickness[X] = 0.0 half_thickness[Z] = ( self._rel_mean_line_and_thickness[THICKNESS] / 2.0 * self.thickness_ratio ) points = operator_(mean_line, half_thickness) * self.chord_length return points def _compute_mean_line_and_thickness( self, upper_side_points, lower_side_points ) -> Tuple[float, float]: """ Computes mean line and thickness from upper_side_points and lower_side_points. Fills self._rel_mean_line_and_thickness with relative values. Returns actual chord length and maximum thickness (in meters) """ x = ( lower_side_points[X] .append(upper_side_points[X]) .sort_values() .drop_duplicates() .reset_index(drop=True) ) interp_lower = interp1d(lower_side_points[X], lower_side_points[Z], kind="quadratic") interp_upper = interp1d(upper_side_points[X], upper_side_points[Z], kind="quadratic") z_sides = pd.DataFrame({"z_lower": interp_lower(x), "z_upper": interp_upper(x)}) z = z_sides.mean(axis=1) thickness = z_sides.diff(axis=1).iloc[:, -1] chord_length = np.max(x) - np.min(x) max_thickness = np.max(thickness) self._rel_mean_line_and_thickness = pd.DataFrame( {X: x / chord_length, Z: z / chord_length, THICKNESS: thickness / max_thickness} ) return chord_length, max_thickness @staticmethod def _create_upper_lower_sides(x: Sequence, z: Sequence) -> Tuple[pd.DataFrame, pd.DataFrame]: """ returns upper side points and lower side points using provided x and z """ # FIXME: leading and trailing edges are located roughly. i_leading_edge = np.argmin(x) i_trailing_edge = np.argmax(x) i1 = min(i_leading_edge, i_trailing_edge) i2 = max(i_leading_edge, i_trailing_edge) side1 = pd.DataFrame({X: x[i1 : i2 + 1], Z: z[i1 : i2 + 1]}) side2_1 = pd.DataFrame({X: x[i2:], Z: z[i2:]}) side2_2 = pd.DataFrame({X: x[: i1 + 1], Z: z[: i1 + 1]}) side2 = pd.concat((side2_1, side2_2)).reset_index(drop=True) side1.sort_values(by=X, inplace=True) side2.sort_values(by=X, inplace=True) # At this point, side2 and side1 have the same last point, but in in case of thick # trailing edge, it could lead to side2 having 2 points for the same X, which will be # harmful in next operations. # In that case, we simply have to remove last point of side2, as it actually belongs to # side1. if side2[X].iloc[-1] == side2[X].iloc[-2]: side2 = side2.iloc[:-1] if np.max(side1[Z]) > np.max(side2[Z]): upper_side_points = side1 lower_side_points = side2 else: upper_side_points = side2 lower_side_points = side1 upper_side_points.drop_duplicates(inplace=True) lower_side_points.drop_duplicates(inplace=True) upper_side_points.reset_index(drop=True, inplace=True) lower_side_points.reset_index(drop=True, inplace=True) return upper_side_points, lower_side_points