"""Estimation of horizontal tail area."""
# This file is part of FAST-OAD_CS23 : A framework for rapid Overall Aircraft Design
# Copyright (C) 2022 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 numpy as np
import openmdao.api as om
from scipy.constants import g
from typing import Union, List, Optional, Tuple
# noinspection PyProtectedMember
from fastoad.module_management._bundle_loader import BundleLoader
import fastoad.api as oad
from fastoad.constants import EngineSetting
from stdatm import Atmosphere
from fastga.command.api import list_inputs, list_outputs
from .constants import SERVICE_HT_AREA, SUBMODEL_HT_AREA_LEGACY, SUBMODEL_HT_AREA_VOLUME_COEFF
_ANG_VEL = 12 * np.pi / 180 # 12 deg/s (typical for light aircraft)
oad.RegisterSubmodel.active_models[SERVICE_HT_AREA] = SUBMODEL_HT_AREA_LEGACY
[docs]@oad.RegisterSubmodel(SERVICE_HT_AREA, SUBMODEL_HT_AREA_LEGACY)
class UpdateHTArea(om.Group):
"""
Computes needed ht area to:
- have enough rotational power during take-off phase.
- have enough rotational power during landing phase.
"""
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO initialize
[docs] def initialize(self):
self.options.declare("propulsion_id", default="", types=str)
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO setup
[docs] def setup(self):
self.add_subsystem(
"aero_coeff_landing",
_ComputeAeroCoeff(landing=True),
promotes=self.get_io_names(_ComputeAeroCoeff(landing=True), iotypes="inputs"),
)
self.add_subsystem(
"aero_coeff_takeoff",
_ComputeAeroCoeff(),
promotes=self.get_io_names(_ComputeAeroCoeff(), iotypes="inputs"),
)
self.add_subsystem(
"ht_area",
_UpdateArea(propulsion_id=self.options["propulsion_id"]),
promotes=self.get_io_names(
_UpdateArea(propulsion_id=self.options["propulsion_id"]),
excludes=[
"landing:cl_htp",
"takeoff:cl_htp",
"low_speed:cl_alpha_htp_isolated",
],
),
)
self.add_subsystem(
"ht_area_constraints",
_ComputeHTPAreaConstraints(propulsion_id=self.options["propulsion_id"]),
promotes=self.get_io_names(
_ComputeHTPAreaConstraints(propulsion_id=self.options["propulsion_id"]),
excludes=[
"landing:cl_htp",
"takeoff:cl_htp",
"low_speed:cl_alpha_htp_isolated",
],
),
)
self.connect("aero_coeff_landing.cl_htp", "ht_area.landing:cl_htp")
self.connect("aero_coeff_takeoff.cl_htp", "ht_area.takeoff:cl_htp")
self.connect(
"aero_coeff_takeoff.cl_alpha_htp_isolated", "ht_area.low_speed:cl_alpha_htp_isolated"
)
self.connect("aero_coeff_landing.cl_htp", "ht_area_constraints.landing:cl_htp")
self.connect("aero_coeff_takeoff.cl_htp", "ht_area_constraints.takeoff:cl_htp")
self.connect(
"aero_coeff_takeoff.cl_alpha_htp_isolated",
"ht_area_constraints.low_speed:cl_alpha_htp_isolated",
)
[docs] @staticmethod
def get_io_names(
component: om.ExplicitComponent,
excludes: Optional[Union[str, List[str]]] = None,
iotypes: Optional[Union[str, Tuple[str, str]]] = ("inputs", "outputs"),
) -> List[str]:
list_names = []
if isinstance(iotypes, tuple):
list_names.extend(list_inputs(component))
list_names.extend(list_outputs(component))
else:
if iotypes == "inputs":
list_names.extend(list_inputs(component))
else:
list_names.extend(list_outputs(component))
if excludes is not None:
list_names = [x for x in list_names if x not in excludes]
return list_names
[docs]class HTPConstraints(om.ExplicitComponent):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._engine_wrapper = None
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO initialize
[docs] def initialize(self):
self.options.declare("propulsion_id", default="", types=str)
[docs] def takeoff_rotation(self, inputs):
n_engines = inputs["data:geometry:propulsion:engine:count"]
wing_area = inputs["data:geometry:wing:area"]
wing_mac = inputs["data:geometry:wing:MAC:length"]
lp_ht = inputs["data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"]
cg_range = inputs["settings:weight:aircraft:CG:range"]
takeoff_t_rate = inputs["data:mission:sizing:takeoff:thrust_rate"]
x_wing_aero_center = inputs["data:geometry:wing:MAC:at25percent:x"]
mtow = inputs["data:weight:aircraft:MTOW"]
x_cg_aft = inputs["data:weight:aircraft:CG:aft:x"]
x_lg = inputs["data:weight:airframe:landing_gear:main:CG:x"]
z_cg_aircraft = inputs["data:weight:aircraft_empty:CG:z"]
z_cg_engine = inputs["data:weight:propulsion:engine:CG:z"]
cl0_clean = inputs["data:aerodynamics:wing:low_speed:CL0_clean"]
cl_max_clean = inputs["data:aerodynamics:wing:low_speed:CL_max_clean"]
cl_max_takeoff = inputs["data:aerodynamics:aircraft:takeoff:CL_max"]
cl_flaps_takeoff = inputs["data:aerodynamics:flaps:takeoff:CL"]
tail_efficiency_factor = inputs["data:aerodynamics:horizontal_tail:efficiency"]
cl_htp_takeoff = inputs["takeoff:cl_htp"]
cm_takeoff = (
inputs["data:aerodynamics:wing:low_speed:CM0_clean"]
+ inputs["data:aerodynamics:flaps:takeoff:CM"]
)
cl_alpha_htp_isolated = inputs["low_speed:cl_alpha_htp_isolated"]
z_eng = z_cg_aircraft - z_cg_engine
# Conditions for calculation
atm = Atmosphere(0.0)
rho = atm.density
propulsion_model = self._engine_wrapper.get_model(inputs)
# Calculation of take-off minimum speed
weight = mtow * g
vs0 = np.sqrt(weight / (0.5 * rho * wing_area * cl_max_takeoff))
vs1 = np.sqrt(weight / (0.5 * rho * wing_area * cl_max_clean))
# Rotation speed requirement from FAR 23.51 (depends on number of engines)
if n_engines == 1:
v_r = vs1 * 1.0
else:
v_r = vs1 * 1.1
# Definition of max forward gravity center position
x_cg = x_cg_aft - cg_range * wing_mac
# Definition of horizontal tail global position
x_ht = x_wing_aero_center + lp_ht
# Calculation of wheel factor
flight_point = oad.FlightPoint(
mach=v_r / atm.speed_of_sound,
altitude=0.0,
engine_setting=EngineSetting.TAKEOFF,
thrust_rate=takeoff_t_rate,
)
propulsion_model.compute_flight_points(flight_point)
thrust = float(flight_point.thrust)
fact_wheel = (
(x_lg - x_cg - z_eng * thrust / weight) / wing_mac * (vs0 / v_r) ** 2
) # FIXME: not clear if vs0 or vs1 should be used in formula
# Compute aerodynamic coefficients for takeoff @ 0° aircraft angle
cl0_takeoff = cl0_clean + cl_flaps_takeoff
# Calculation of correction coefficient n_h and n_q
n_h = (
(x_ht - x_lg) / lp_ht * tail_efficiency_factor
) # tail_efficiency_factor: dynamic pressure reduction at
# tail (typical value)
n_q = 1 + cl_alpha_htp_isolated / cl_htp_takeoff * _ANG_VEL * (x_ht - x_lg) / v_r
# Calculation of volume coefficient based on Torenbeek formula
coeff_vol = (
cl_max_takeoff
/ (n_h * n_q * cl_htp_takeoff)
* (cm_takeoff / cl_max_takeoff - fact_wheel)
+ cl0_takeoff / cl_htp_takeoff * (x_lg - x_wing_aero_center) / wing_mac
)
# Calculation of equivalent area
area = coeff_vol * wing_area * wing_mac / lp_ht
return area
[docs] def landing(self, inputs):
propulsion_model = self._engine_wrapper.get_model(inputs)
wing_area = inputs["data:geometry:wing:area"]
x_wing_aero_center = inputs["data:geometry:wing:MAC:at25percent:x"]
lp_ht = inputs["data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"]
wing_mac = inputs["data:geometry:wing:MAC:length"]
mlw = inputs["data:weight:aircraft:MLW"]
cg_range = inputs["settings:weight:aircraft:CG:range"]
x_cg_aft = inputs["data:weight:aircraft:CG:aft:x"]
z_cg_aircraft = inputs["data:weight:aircraft_empty:CG:z"]
z_cg_engine = inputs["data:weight:propulsion:engine:CG:z"]
x_lg = inputs["data:weight:airframe:landing_gear:main:CG:x"]
cl0_clean = inputs["data:aerodynamics:wing:low_speed:CL0_clean"]
cl_max_landing = inputs["data:aerodynamics:aircraft:landing:CL_max"]
cl_flaps_landing = inputs["data:aerodynamics:flaps:landing:CL"]
tail_efficiency_factor = inputs["data:aerodynamics:horizontal_tail:efficiency"]
cl_htp_landing = inputs["landing:cl_htp"]
cm_landing = (
inputs["data:aerodynamics:wing:low_speed:CM0_clean"]
+ inputs["data:aerodynamics:flaps:landing:CM"]
)
cl_alpha_htp_isolated = inputs["low_speed:cl_alpha_htp_isolated"]
z_eng = z_cg_aircraft - z_cg_engine
# Conditions for calculation
atm = Atmosphere(0.0)
rho = atm.density
# Definition of max forward gravity center position
x_cg = x_cg_aft - cg_range * wing_mac
# Definition of horizontal tail global position
x_ht = x_wing_aero_center + lp_ht
# Calculation of take-off minimum speed
weight = mlw * g
vs0 = np.sqrt(weight / (0.5 * rho * wing_area * cl_max_landing))
# Rotation speed requirement from FAR 23.73
v_r = vs0 * 1.3
# Calculation of wheel factor
flight_point = oad.FlightPoint(
mach=v_r / atm.speed_of_sound,
altitude=0.0,
engine_setting=EngineSetting.IDLE,
thrust_rate=0.1,
) # FIXME: fixed thrust rate (should depend on wished descent rate)
propulsion_model.compute_flight_points(flight_point)
thrust = float(flight_point.thrust)
fact_wheel = (
(x_lg - x_cg - z_eng * thrust / weight) / wing_mac * (vs0 / v_r) ** 2
) # FIXME: not clear if vs0 or vs1 should be used in formula
# Evaluate aircraft overall angle (aoa)
cl0_landing = cl0_clean + cl_flaps_landing
# Calculation of correction coefficient n_h and n_q
n_h = (
(x_ht - x_lg) / lp_ht * tail_efficiency_factor
) # tail_efficiency_factor: dynamic pressure reduction at
# tail (typical value)
n_q = 1 + cl_alpha_htp_isolated / cl_htp_landing * _ANG_VEL * (x_ht - x_lg) / v_r
# Calculation of volume coefficient based on Torenbeek formula
coeff_vol = (
cl_max_landing
/ (n_h * n_q * cl_htp_landing)
* (cm_landing / cl_max_landing - fact_wheel)
+ cl0_landing / cl_htp_landing * (x_lg - x_wing_aero_center) / wing_mac
)
# Calculation of equivalent area
area = coeff_vol * wing_area * wing_mac / lp_ht
return area
class _UpdateArea(HTPConstraints):
"""Computes area of horizontal tail plane (internal function)."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._engine_wrapper = None
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO initialize
def initialize(self):
self.options.declare("propulsion_id", default="", types=str)
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO setup
def setup(self):
self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"])
self._engine_wrapper.setup(self)
self.add_input("settings:weight:aircraft:CG:range", val=0.3)
self.add_input("data:mission:sizing:takeoff:thrust_rate", val=np.nan)
self.add_input("data:geometry:wing:area", val=np.nan, units="m**2")
self.add_input("data:geometry:wing:MAC:at25percent:x", val=np.nan, units="m")
self.add_input(
"data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m"
)
self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m")
self.add_input("data:geometry:propulsion:nacelle:height", val=np.nan, units="m")
self.add_input("data:weight:aircraft:MTOW", val=np.nan, units="kg")
self.add_input("data:weight:aircraft:MLW", val=np.nan, units="kg")
self.add_input("data:weight:aircraft:CG:aft:x", val=np.nan, units="m")
self.add_input("data:weight:airframe:landing_gear:main:CG:x", val=np.nan, units="m")
self.add_input("data:weight:aircraft_empty:CG:z", val=np.nan, units="m")
self.add_input("data:weight:propulsion:engine:CG:z", val=np.nan, units="m")
self.add_input("data:aerodynamics:wing:low_speed:CL0_clean", val=np.nan)
self.add_input("data:aerodynamics:wing:low_speed:CM0_clean", val=np.nan)
self.add_input("data:aerodynamics:aircraft:landing:CL_max", val=np.nan)
self.add_input("data:aerodynamics:aircraft:takeoff:CL_max", val=np.nan)
self.add_input("data:aerodynamics:wing:low_speed:CL_max_clean", val=np.nan)
self.add_input("data:aerodynamics:flaps:landing:CL", val=np.nan)
self.add_input("data:aerodynamics:flaps:takeoff:CL", val=np.nan)
self.add_input("data:aerodynamics:flaps:landing:CM", val=np.nan)
self.add_input("data:aerodynamics:flaps:takeoff:CM", val=np.nan)
self.add_input(
"data:aerodynamics:horizontal_tail:low_speed:CL_alpha", val=np.nan, units="rad**-1"
)
self.add_input("data:aerodynamics:horizontal_tail:efficiency", val=np.nan)
self.add_input("landing:cl_htp", val=np.nan)
self.add_input("takeoff:cl_htp", val=np.nan)
self.add_input("low_speed:cl_alpha_htp_isolated", val=np.nan)
self.add_output("data:geometry:horizontal_tail:area", val=4.0, units="m**2")
self.declare_partials(
"*", "*", method="fd"
) # FIXME: write partial avoiding discrete parameters
# pylint: disable=missing-function-docstring, unused-argument
# Overriding OpenMDAO compute, not all arguments are used
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
# Sizing constraints for the horizontal tail (methods from Torenbeek).
# Limiting cases: Rotating power at takeoff/landing, with the most
# forward CG position. Returns maximum area.
# CASE1: TAKE-OFF ##########################################################################
# method extracted from Torenbeek 1982 p325
# Calculation of take-off minimum speed
area_1 = self.takeoff_rotation(inputs)
# CASE2: LANDING ###########################################################################
# method extracted from Torenbeek 1982 p325
# Calculation of equivalent area
area_2 = self.landing(inputs)
if max(area_1, area_2) < 0.0:
print("Warning: HTP area estimated negative (in ComputeHTArea) forced to 1m²!")
outputs["data:geometry:horizontal_tail:area"] = 1.0
else:
outputs["data:geometry:horizontal_tail:area"] = max(area_1, area_2)
class _ComputeHTPAreaConstraints(HTPConstraints):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._engine_wrapper = None
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO initialize
def initialize(self):
self.options.declare("propulsion_id", default="", types=str)
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO setup
def setup(self):
self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"])
self._engine_wrapper.setup(self)
self.add_input("settings:weight:aircraft:CG:range", val=0.3)
self.add_input("data:mission:sizing:takeoff:thrust_rate", val=np.nan)
self.add_input("data:geometry:wing:area", val=np.nan, units="m**2")
self.add_input("data:geometry:wing:MAC:at25percent:x", val=np.nan, units="m")
self.add_input(
"data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m"
)
self.add_input("data:geometry:horizontal_tail:area", val=np.nan, units="m**2")
self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m")
self.add_input("data:geometry:propulsion:nacelle:height", val=np.nan, units="m")
self.add_input("data:weight:aircraft:MTOW", val=np.nan, units="kg")
self.add_input("data:weight:aircraft:MLW", val=np.nan, units="kg")
self.add_input("data:weight:aircraft:CG:aft:x", val=np.nan, units="m")
self.add_input("data:weight:airframe:landing_gear:main:CG:x", val=np.nan, units="m")
self.add_input("data:weight:aircraft_empty:CG:z", val=np.nan, units="m")
self.add_input("data:weight:propulsion:engine:CG:z", val=np.nan, units="m")
self.add_input("data:aerodynamics:wing:low_speed:CL0_clean", val=np.nan)
self.add_input("data:aerodynamics:wing:low_speed:CM0_clean", val=np.nan)
self.add_input("data:aerodynamics:aircraft:landing:CL_max", val=np.nan)
self.add_input("data:aerodynamics:aircraft:takeoff:CL_max", val=np.nan)
self.add_input("data:aerodynamics:wing:low_speed:CL_max_clean", val=np.nan)
self.add_input("data:aerodynamics:flaps:landing:CL", val=np.nan)
self.add_input("data:aerodynamics:flaps:takeoff:CL", val=np.nan)
self.add_input("data:aerodynamics:flaps:landing:CM", val=np.nan)
self.add_input("data:aerodynamics:flaps:takeoff:CM", val=np.nan)
self.add_input(
"data:aerodynamics:horizontal_tail:low_speed:CL_alpha", val=np.nan, units="rad**-1"
)
self.add_input("data:aerodynamics:horizontal_tail:efficiency", val=np.nan)
self.add_input("landing:cl_htp", val=np.nan)
self.add_input("takeoff:cl_htp", val=np.nan)
self.add_input("low_speed:cl_alpha_htp_isolated", val=np.nan)
self.add_output("data:constraints:horizontal_tail:takeoff_rotation", units="m**2")
self.add_output("data:constraints:horizontal_tail:landing", units="m**2")
# pylint: disable=missing-function-docstring, unused-argument
# Overriding OpenMDAO compute, not all arguments are used
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
# Sizing constraints margin for the horizontal tail (methods from Torenbeek).
# Limiting cases: Rotating power at takeoff/landing, with the most
# forward CG position. Returns maximum area.
area_htp = inputs["data:geometry:horizontal_tail:area"]
# CASE1: TAKE-OFF ##########################################################################
# method extracted from Torenbeek 1982 p325
# Calculation of take-off minimum speed
area_diff_1 = area_htp - self.takeoff_rotation(inputs)
# CASE2: LANDING ###########################################################################
# method extracted from Torenbeek 1982 p325
# Calculation of equivalent area
area_diff_2 = area_htp - self.landing(inputs)
outputs["data:constraints:horizontal_tail:takeoff_rotation"] = area_diff_1
outputs["data:constraints:horizontal_tail:landing"] = area_diff_2
class _ComputeAeroCoeff(om.ExplicitComponent):
"""
Adapts aero-coefficients (reference surface is tail area for cl_ht)
"""
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO initialize
def initialize(self):
self.options.declare("landing", default=False, types=bool)
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO setup
def setup(self):
self.add_input("data:geometry:wing:area", val=np.nan, units="m**2")
self.add_input("data:geometry:horizontal_tail:area", val=2.0, units="m**2")
self.add_input("data:weight:aircraft:MLW", val=np.nan, units="kg")
self.add_input("data:weight:aircraft:MTOW", val=np.nan, units="kg")
self.add_input("data:aerodynamics:horizontal_tail:low_speed:CL0", val=np.nan)
self.add_input(
"data:aerodynamics:horizontal_tail:low_speed:CL_alpha_isolated",
val=np.nan,
units="rad**-1",
)
self.add_input(
"data:aerodynamics:horizontal_tail:low_speed:CL_alpha", val=np.nan, units="rad**-1"
)
self.add_input("data:aerodynamics:wing:low_speed:CL0_clean", val=np.nan)
self.add_input("data:aerodynamics:wing:low_speed:CL_alpha", val=np.nan, units="rad**-1")
self.add_input("data:aerodynamics:flaps:landing:CL", val=np.nan)
self.add_input("data:aerodynamics:flaps:takeoff:CL", val=np.nan)
self.add_input("data:aerodynamics:aircraft:landing:CL_max", val=np.nan)
self.add_input("data:aerodynamics:wing:low_speed:CL_max_clean", val=np.nan)
self.add_input("data:aerodynamics:elevator:low_speed:CL_delta", val=np.nan, units="rad**-1")
self.add_input("data:mission:sizing:landing:elevator_angle", val=np.nan, units="rad")
self.add_input("data:mission:sizing:takeoff:elevator_angle", val=np.nan, units="rad")
self.add_output("cl_htp")
self.add_output("cl_alpha_htp_isolated")
self.declare_partials("*", "*", method="fd")
# pylint: disable=missing-function-docstring, unused-argument
# Overriding OpenMDAO compute, not all arguments are used
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
wing_area = inputs["data:geometry:wing:area"]
ht_area = inputs["data:geometry:horizontal_tail:area"]
mlw = inputs["data:weight:aircraft:MLW"]
cl0_htp = inputs["data:aerodynamics:horizontal_tail:low_speed:CL0"]
cl_alpha_htp_isolated = inputs[
"data:aerodynamics:horizontal_tail:low_speed:CL_alpha_isolated"
]
cl_alpha_htp = inputs["data:aerodynamics:horizontal_tail:low_speed:CL_alpha"]
cl0_clean_wing = inputs["data:aerodynamics:wing:low_speed:CL0_clean"]
cl_alpha_wing = inputs["data:aerodynamics:wing:low_speed:CL_alpha"]
cl_flaps_landing = inputs["data:aerodynamics:flaps:landing:CL"]
cl_max_landing = inputs["data:aerodynamics:aircraft:landing:CL_max"]
cl_delta_elev = inputs["data:aerodynamics:elevator:low_speed:CL_delta"]
# Conditions for calculation
atm = Atmosphere(0.0)
rho = atm.density
# Calculate elevator max. additional lift
if self.options["landing"]:
elev_angle = inputs["data:mission:sizing:landing:elevator_angle"]
else:
elev_angle = inputs["data:mission:sizing:takeoff:elevator_angle"]
cl_elev = cl_delta_elev * elev_angle
# Define alpha angle depending on phase
if self.options["landing"]:
# Calculation of take-off minimum speed
weight = mlw * g
vs0 = np.sqrt(weight / (0.5 * rho * wing_area * cl_max_landing))
# Rotation speed correction
v_r = vs0 * 1.3
# Evaluate aircraft overall angle (aoa)
cl0_landing = cl0_clean_wing + cl_flaps_landing
cl_landing = weight / (0.5 * rho * v_r**2 * wing_area)
alpha = (cl_landing - cl0_landing) / cl_alpha_wing * 180 / np.pi
else:
# Define aircraft overall angle (aoa)
alpha = 0.0
# Interpolate cl/cm and define with ht reference surface
cl_htp = (cl0_htp + (alpha * np.pi / 180) * cl_alpha_htp + cl_elev) * wing_area / ht_area
# Define Cl_alpha with htp reference surface
cl_alpha_htp_isolated = cl_alpha_htp_isolated * wing_area / ht_area
outputs["cl_htp"] = cl_htp
outputs["cl_alpha_htp_isolated"] = cl_alpha_htp_isolated
@staticmethod
def _extrapolate(x, xp, yp) -> float:
"""
Extrapolate linearly out of range x-value
"""
if (x >= xp[0]) and (x <= xp[-1]):
result = float(np.interp(x, xp, yp))
elif x < xp[0]:
result = float(yp[0] + (x - xp[0]) * (yp[1] - yp[0]) / (xp[1] - xp[0]))
else:
result = float(yp[-1] + (x - xp[-1]) * (yp[-1] - yp[-2]) / (xp[-1] - xp[-2]))
if result is None:
result = np.array([np.nan])
return result
[docs]@oad.RegisterSubmodel(SERVICE_HT_AREA, SUBMODEL_HT_AREA_VOLUME_COEFF)
class UpdateHTAreaVolumeCoefficient(om.ExplicitComponent):
"""
Computation of the area of the horizontal tail with given volume coefficient. The formulas
and default values are obtained from :cite:`gudmundsson:2013`.
"""
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO initialize
[docs] def initialize(self):
self.options.declare("propulsion_id", default=None, types=str, allow_none=True)
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO setup
[docs] def setup(self):
self.add_input("data:geometry:wing:area", val=np.nan, units="m**2")
self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m")
self.add_input("data:geometry:horizontal_tail:volume_coefficient", val=0.7)
self.add_input(
"data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m"
)
self.add_output("data:geometry:horizontal_tail:area", val=4.0, units="m**2")
# pylint: disable=missing-function-docstring
# Overriding OpenMDAO setup_partials
[docs] def setup_partials(self):
self.declare_partials(of="*", wrt="*", method="exact")
# pylint: disable=missing-function-docstring, unused-argument
# Overriding OpenMDAO compute, not all arguments are used
[docs] def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
wing_area = inputs["data:geometry:wing:area"]
wing_mac = inputs["data:geometry:wing:MAC:length"]
lp_ht = inputs["data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"]
vc_ht = inputs["data:geometry:horizontal_tail:volume_coefficient"]
outputs["data:geometry:horizontal_tail:area"] = vc_ht * wing_area * wing_mac / lp_ht
# pylint: disable=missing-function-docstring, unused-argument
# Overriding OpenMDAO compute_partials, not all arguments are used
[docs] def compute_partials(self, inputs, partials, discrete_inputs=None):
wing_area = inputs["data:geometry:wing:area"]
wing_mac = inputs["data:geometry:wing:MAC:length"]
lp_ht = inputs["data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"]
vc_ht = inputs["data:geometry:horizontal_tail:volume_coefficient"]
partials["data:geometry:horizontal_tail:area", "data:geometry:wing:area"] = (
vc_ht * wing_mac / lp_ht
)
partials["data:geometry:horizontal_tail:area", "data:geometry:wing:MAC:length"] = (
vc_ht * wing_area / lp_ht
)
partials[
"data:geometry:horizontal_tail:area",
"data:geometry:horizontal_tail:volume_coefficient",
] = wing_area * wing_mac / lp_ht
partials[
"data:geometry:horizontal_tail:area",
"data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25",
] = -vc_ht * wing_area * wing_mac / lp_ht**2.0