# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Contributors:
# INITIAL AUTHORS - initial API and implementation
# and/or initial documentation
# :author: Sobieski, Agte, and Sandusky
# OTHER AUTHORS - MACROSCOPIC CHANGES
# :author: Damien Guenot
# :author: Francois Gallard
# From NASA/TM-1998-208715
# Bi-Level Integrated System Synthesis (BLISS)
# Sobieski, Agte, and Sandusky
"""Structure discipline for the Sobieski's SSBJ use case."""
from __future__ import annotations
from typing import TYPE_CHECKING
from numpy import append
from numpy import array
from numpy import nan
from numpy import ones
from numpy import zeros
from gemseo.problems.mdo.sobieski.core.discipline import SobieskiDiscipline
from gemseo.problems.mdo.sobieski.core.utils import DEG_TO_RAD
if TYPE_CHECKING:
from numpy import ndarray
from gemseo.problems.mdo.sobieski.core.utils import SobieskiBase
[docs]
class SobieskiStructure(SobieskiDiscipline):
"""Structure discipline for the Sobieski's SSBJ use case."""
STRESS_LIMIT = 1.09
TWIST_UPPER_LIMIT = 1.04
TWIST_LOWER_LIMIT = 0.8
def __init__(self, sobieski_base: SobieskiBase) -> None: # noqa: D107
super().__init__(sobieski_base)
self.__ao_coeff_secthick = zeros(1, dtype=self.dtype)
self.__ai_coeff_secthick = zeros(1, dtype=self.dtype)
self.__aij_coeff_secthick = zeros((1, 1), dtype=self.dtype)
self.__ao_coeff_twist = zeros(1, dtype=self.dtype)
self.__ai_coeff_twist = zeros(4, dtype=self.dtype)
self.__aij_coeff_twist = zeros((4, 4), dtype=self.dtype)
self.__ao_coeff_stress = zeros(1, dtype=self.dtype)
self.__ai_coeff_stress = zeros(5, dtype=self.dtype)
self.__aij_coeff_stress = zeros((5, 5), dtype=self.dtype)
self.__flag1 = array([2, 4, 4, 3], dtype=self.dtype)
self.__bound1 = array([0.25, 0.25, 0.25, 0.25], dtype=self.dtype)
self.__flag_stress = array([4, 1, 4, 1, 1], dtype=self.dtype)
self.__flag_secthick = array([1], dtype=self.dtype)
self.__bound_secthick = array([0.008], dtype=self.dtype)
self.__s_initial_for_wing_twist = array(
[
self.x_initial,
self.half_span_initial,
self.aero_center_initial,
self.lift_initial,
],
dtype=self.dtype,
)
self.__s_initial_for_wing_weight = array([self.x_initial], dtype=self.dtype)
self.__s_initial_for_constraints = array(
[
self.tc_initial,
self.lift_initial,
self.x_initial,
self.half_span_initial,
self.aero_center_initial,
],
dtype=self.dtype,
)
self.__loc_ones = ones(5, dtype=self.dtype)
self.__ones_mat = ones(5, dtype=self.dtype)
self.__aero_center = None
self.__half_span = None
self.__dadimlift_dlift = None
def __compute_dadimcenter_dcenter(self) -> float:
"""Derive the adimensioned aerodynamic center wrt the aerodynamic center.
Returns:
The derivative of the adimensioned aerodynamic center
wrt the aerodynamic center.
"""
return self.base.derive_normalization(
self.aero_center_initial, self.__aero_center
)
def __compute_dcenter_dlambda(self, wing_taper_ratio: float) -> float:
"""Derive the aerodynamic center with respect to the wing taper ratio.
Args:
wing_taper_ratio: The wing taper ratio.
Returns:
The derivative of the aerodynamic center
with respect to the wing taper ratio.
"""
dadimcenter_dcenter = self.__compute_dadimcenter_dcenter()
return dadimcenter_dcenter * 1.0 / (3.0 * (1.0 + wing_taper_ratio) ** 2)
def __compute_wing_weight(
self,
tc_ratio: float,
aspect_ratio: float,
sweep: float,
wing_area: float,
wing_taper_ratio: float,
wingbox_area: float,
lift: float,
linearize: bool = False,
c_2: float | None = None,
) -> float:
"""Compute the weight of the wing.
Args:
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
sweep: The wing sweep.
wing_area: The wing surface area.
wing_taper_ratio: The wing taper ratio.
wingbox_area: The wingbox x-sectional area.
lift: The lift coefficient.
linearize: Whether to derive the polynomial approximation.
c_2: The maximum load factor.
If ``None``, use :meth:`.SobieskiBase.constants`.
Returns:
If ``linearize`` is ``True``,
the wing weight, the wing weight coefficient
and the value of the polynomial function.
Otherwise, the wing weight only.
"""
c_2 = c_2 or self.constants[2]
s_new = array([wingbox_area], dtype=self.dtype)
if linearize:
f_o, a_i, a_ij, s_shifted = self.base.derive_polynomial_approximation(
self.__s_initial_for_wing_weight,
s_new,
self.__flag_secthick,
self.__bound_secthick,
self.__ao_coeff_secthick,
self.__ai_coeff_secthick,
self.__aij_coeff_secthick,
)
else:
f_o = self.base.compute_polynomial_approximation(
self.__s_initial_for_wing_weight,
s_new,
self.__flag_secthick,
self.__bound_secthick,
self.__ao_coeff_secthick,
self.__ai_coeff_secthick,
self.__aij_coeff_secthick,
)
wing_weight_coeff = (
0.0051
* ((lift * c_2) ** 0.557)
* (wing_area**0.649)
* (aspect_ratio**0.5)
* (tc_ratio**-0.4)
* ((1 + wing_taper_ratio) ** 0.1)
* ((self.math.cos(sweep * DEG_TO_RAD)) ** -1.0)
* ((0.1875 * wing_area) ** 0.1)
)
wing_weight = wing_weight_coeff * f_o
if linearize:
return wing_weight, wing_weight_coeff, f_o, a_i, a_ij, s_shifted
return wing_weight
def __compute_fuelwing_weight(
self,
tc_ratio: float,
aspect_ratio: float,
wing_area: float,
) -> float:
"""Compute the fuel wing weight.
Args:
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
wing_area: The wing surface area.
Returns:
The fuel wing weight.
"""
thickness = self.base.compute_thickness(aspect_ratio, tc_ratio, wing_area)
return (5.0 * wing_area / 18.0) * (2.0 / 3.0 * thickness) * 42.5
def __compute_dfuelwing_dtoverc(
self,
aspect_ratio: float,
wing_area: float,
) -> float:
"""Derive the wing fuel weight.
Args:
aspect_ratio: The aspect ratio.
wing_area: The wing area.
Returns:
The derivative of the wing fuel weight.
"""
return 212.5 / 27.0 * wing_area ** (3.0 / 2.0) / self.math.sqrt(aspect_ratio)
@staticmethod
def __compute_dfuelwing_dar(
tc_ratio: float,
aspect_ratio: float,
wing_area: float,
) -> float:
"""Derive the wing fuel weight with respect to the aspect ratio.
Args:
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
wing_area: The wing surface.
Returns:
The derivative of the wing fuel weight with respect to the aspect ratio.
"""
dfuelwing_dar = 212.5 / 27.0 * wing_area ** (3.0 / 2.0)
dfuelwing_dar *= tc_ratio * -0.5 * aspect_ratio ** (-3.0 / 2.0)
return dfuelwing_dar
def __compute_dfuelwing_dsref(
self,
tc_ratio: float,
aspect_ratio: float,
wing_area: float,
) -> float:
"""Derive the wing fuel weight with respect to the reference surface.
Args:
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
wing_area: The wing surface area.
Returns:
The derivative of the fuel wing weight with respect to
the reference surface.
"""
return 637.5 / 54.0 * wing_area**0.5 * tc_ratio / self.math.sqrt(aspect_ratio)
def __compute_dhalfspan_dar(self, wing_area: float) -> float:
"""Derive the half-span with respect to the aspect ratio.
Args:
wing_area: The wing surface area.
Returns:
The derivative of the half-span with respect to the aspect ratio.
"""
dadimspan_dspan = self.__compute_dadimspan_dspan()
return dadimspan_dspan * wing_area / (8.0 * self.__half_span)
def __compute_dadimspan_dsref(self, aspect_ratio: float) -> float:
"""Derive the half-span with respect to the reference surface.
Args:
aspect_ratio: The aspect ratio.
Returns:
The derivative of the half-span with respect to the
reference surface.
"""
dadimspan_dspan = self.__compute_dadimspan_dspan()
return dadimspan_dspan * aspect_ratio / (8.0 * self.__half_span)
def __compute_dadimspan_dspan(self) -> float:
"""Derive the adimensioned half-span with respect to the half-span.
Returns:
The derivative of the adimensioned half-span with respect to the half-span.
"""
return self.base.derive_normalization(self.half_span_initial, self.__half_span)
def __compute_dadimx_dx(self, wingbox_sectional_area: float) -> float:
"""Derive the adimensional sectional area with respect to the sectional area.
Args:
wingbox_sectional_area: The wingbox x-sectional area.
Returns:
The derivative of the adimensional sectional area
with respect to the sectional area.
"""
return self.base.derive_normalization(self.x_initial, wingbox_sectional_area)
def __compute_dadimtaper_dtaper(self, tc_ratio: float) -> float:
"""Derive the adimensional taper ratio with respect to the taper ratio.
Args:
tc_ratio: The thickness-to-chord ratio.
Returns:
The derivative of the adimensional taper ratio
with respect to the taper ratio.
"""
return self.base.derive_normalization(self.tc_initial, tc_ratio)
def __compute_dadimlift_dlift(self, lift: float) -> float:
"""Derive the adimensioned lift with respect to the lift.
Args:
lift: The lift coefficient.
Returns:
The derivative of the adimensioned lift with respect to the lift.
"""
return self.base.derive_normalization(self.lift_initial, lift)
@staticmethod
def __compute_weight_ratio(
w_t: float,
w_f: float,
) -> float:
"""Computation the weight ratio from the Breguet formula.
Args:
w_t: The total aircraft mass.
w_f: The fuel mass.
Returns:
The weight ratio.
"""
return w_t / (w_t - w_f)
[docs]
def execute(
self,
x_shared: ndarray,
y_21: ndarray,
y_31: ndarray,
x_1: ndarray,
true_cstr: bool = False,
c_0: ndarray | None = None,
c_1: ndarray | None = None,
c_2: ndarray | None = None,
) -> tuple[ndarray, ndarray, ndarray, ndarray, ndarray]:
"""Compute the structural outputs and the structural constraints.
Args:
x_shared: The values of the shared design variables,
where ``x_shared[0]`` is the thickness-to-chord ratio,
``x_shared[1]`` is the altitude,
``x_shared[2]`` is the Mach number,
``x_shared[3]`` is the aspect ratio,
``x_shared[4]`` is the wing sweep and
``x_shared[5]`` is the wing surface area.
y_21: The lift coefficient.
y_31: The engine weight.
x_1: The wing taper ratio ``x_1[0]``
and the wingbox x-sectional area ``x_1[1]``.
true_cstr: If ``True``,
return the value of the constraint outputs.
Otherwise,
return the distance to the corresponding constraint thresholds.
c_0: The minimum fuel weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_1: The miscellaneous weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_2: The maximum load factor.
If ``None``, use :meth:`.SobieskiBase.constants`.
Returns:
The structural outputs and the structural constraints.
"""
return self._execute(
x_shared[0],
x_shared[3],
x_shared[4],
x_shared[5],
x_1[0],
x_1[1],
y_21[0],
y_31[0],
true_cstr=true_cstr,
c_0=c_0,
c_1=c_1,
c_2=c_2,
)
def _execute(
self,
tc_ratio: float,
aspect_ratio: float,
sweep: float,
wing_area: float,
taper_ratio: float,
wingbox_area: float,
lift: float,
engine_mass: float,
true_cstr: bool = False,
c_0: ndarray | None = None,
c_1: ndarray | None = None,
c_2: ndarray | None = None,
) -> tuple[ndarray, ndarray, ndarray, ndarray, ndarray]:
"""Compute the structural outputs and the structural constraints.
Args:
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
sweep: The wing sweep.
wing_area: The wing surface area.
taper_ratio: The wing taper ratio.
wingbox_area: The wingbox x-sectional area.
lift: The lift coefficient.
engine_mass: The mass of the engine.
true_cstr: If ``True``,
return the value of the constraint outputs.
Otherwise,
return the distance to the corresponding constraint thresholds.
c_0: The minimum fuel weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_1: The miscellaneous weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_2: The maximum load factor.
If ``None``, use :meth:`.SobieskiBase.constants`.
Returns:
The structural outputs and the structural constraints.
"""
c_0 = c_0 or self.constants[0]
c_1 = c_1 or self.constants[1]
c_2 = c_2 or self.constants[2]
y_12 = zeros(2, dtype=self.dtype)
y_14 = zeros(2, dtype=self.dtype)
self.__aero_center = self.base.compute_aero_center(taper_ratio)
self.__half_span = self.base.compute_half_span(aspect_ratio, wing_area)
y_1, y_11 = self.__poly_structure(
tc_ratio,
aspect_ratio,
sweep,
wing_area,
taper_ratio,
wingbox_area,
lift,
engine_mass,
c_0=c_0,
c_1=c_1,
c_2=c_2,
)
g_1 = self.__poly_structure_constraints(tc_ratio, wingbox_area, lift)
g_1[5] = y_1[2]
# Coupling variables
y_12[1] = y_1[2]
y_12[0] = y_1[0]
y_14[0] = y_1[0]
y_14[1] = y_1[1]
if not true_cstr:
g_1 = append(
g_1[0:5] - self.STRESS_LIMIT,
(g_1[5] - self.TWIST_UPPER_LIMIT, self.TWIST_LOWER_LIMIT - g_1[5]),
)
return y_1, y_11, y_12, y_14, g_1
def __initialize_jacobian(self) -> dict[str, dict[str, ndarray]]:
"""Initialize the Jacobian structure.
Returns:
The empty Jacobian structure.
"""
# Jacobian matrix as a dictionary
jacobian = {"y_1": {}, "g_1": {}, "y_12": {}, "y_14": {}, "y_11": {}}
n_y1 = 3
jacobian["y_1"]["x_shared"] = zeros((n_y1, 6), dtype=self.dtype)
jacobian["y_1"]["x_1"] = zeros((n_y1, 2), dtype=self.dtype)
jacobian["y_1"]["y_21"] = zeros((n_y1, 1), dtype=self.dtype)
jacobian["y_1"]["y_31"] = zeros((n_y1, 1), dtype=self.dtype)
jacobian["y_1"]["c_0"] = zeros((n_y1, 1), dtype=self.dtype)
jacobian["y_1"]["c_1"] = zeros((n_y1, 1), dtype=self.dtype)
jacobian["y_1"]["c_2"] = zeros((n_y1, 1), dtype=self.dtype)
n_y11 = 1
jacobian["y_11"]["x_shared"] = zeros((n_y11, 6), dtype=self.dtype)
jacobian["y_11"]["x_1"] = zeros((n_y11, 2), dtype=self.dtype)
jacobian["y_11"]["y_21"] = zeros((n_y11, 1), dtype=self.dtype)
jacobian["y_11"]["y_31"] = zeros((n_y11, 1), dtype=self.dtype)
jacobian["y_11"]["c_0"] = zeros((n_y11, 1), dtype=self.dtype)
jacobian["y_11"]["c_1"] = zeros((n_y11, 1), dtype=self.dtype)
jacobian["y_11"]["c_2"] = zeros((n_y11, 1), dtype=self.dtype)
n_y12 = 2
jacobian["y_12"]["x_shared"] = zeros((n_y12, 6), dtype=self.dtype)
jacobian["y_12"]["x_1"] = zeros((n_y12, 2), dtype=self.dtype)
jacobian["y_12"]["y_21"] = zeros((n_y12, 1), dtype=self.dtype)
jacobian["y_12"]["y_31"] = zeros((n_y12, 1), dtype=self.dtype)
jacobian["y_12"]["c_0"] = zeros((n_y12, 1), dtype=self.dtype)
jacobian["y_12"]["c_1"] = zeros((n_y12, 1), dtype=self.dtype)
jacobian["y_12"]["c_2"] = zeros((n_y12, 1), dtype=self.dtype)
for key, jac_val in jacobian["y_12"].items():
jacobian["y_14"][key] = jac_val
return jacobian
[docs]
def linearize(
self,
x_shared: ndarray,
y_21: ndarray,
y_31: ndarray,
x_1: ndarray,
true_cstr: bool = False,
c_0: float | None = None,
c_1: float | None = None,
c_2: float | None = None,
) -> dict[str, dict[str, ndarray]]:
"""Derive the structural outputs and the structural constraints.
Args:
x_shared: The values of the shared design variables,
where ``x_shared[0]`` is the thickness-to-chord ratio,
``x_shared[1]`` is the altitude,
``x_shared[2]`` is the Mach number,
``x_shared[3]`` is the aspect ratio,
``x_shared[4]`` is the wing sweep and
``x_shared[5]`` is the wing surface area.
y_21: The lift coefficient.
y_31: The engine weight.
x_1: The wing taper ratio ``x_1[0]``
and the wingbox x-sectional area ``x_1[1]``.
true_cstr: If ``True``,
return the value of the constraint outputs.
Otherwise,
return the distance to the corresponding constraint thresholds.
c_0: The minimum fuel weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_1: The miscellaneous weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_2: The maximum load factor.
If ``None``, use :meth:`.SobieskiBase.constants`.
Returns:
The derivatives of the structural outputs and the structural constraints.
"""
return self._linearize(
x_shared[0],
x_shared[3],
x_shared[4],
x_shared[5],
x_1[0],
x_1[1],
y_21[0],
y_31[0],
true_cstr=true_cstr,
c_0=c_0,
c_1=c_1,
c_2=c_2,
)
def _linearize(
self,
tc_ratio: float,
aspect_ratio: float,
sweep: float,
wing_area: float,
taper_ratio: float,
wingbox_area: float,
lift: float,
engine_mass: float,
true_cstr: bool = False,
c_0: float | None = None,
c_1: float | None = None,
c_2: float | None = None,
) -> dict[str, dict[str, ndarray]]:
"""Derive the structural outputs and the structural constraints.
Args:
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
sweep: The wing sweep.
wing_area: The wing surface area.
taper_ratio: The wing taper ratio.
wingbox_area: The wingbox x-sectional area.
lift: The lift coefficient.
engine_mass: The mass of the engine.
true_cstr: If ``True``,
return the value of the constraint outputs.
Otherwise,
return the distance to the corresponding constraint thresholds.
c_0: The minimum fuel weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_1: The miscellaneous weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_2: The maximum load factor.
If ``None``, use :meth:`.SobieskiBase.constants`.
Returns:
The derivatives of the structural outputs and the structural constraints.
"""
c_0 = c_0 or self.constants[0]
c_1 = c_1 or self.constants[1]
c_2 = c_2 or self.constants[2]
self.__aero_center = self.base.compute_aero_center(taper_ratio)
self.__half_span = self.base.compute_half_span(aspect_ratio, wing_area)
self.__dadimlift_dlift = self.__compute_dadimlift_dlift(lift)
# Jacobian matrix as a dictionary
jacobian = self.__initialize_jacobian()
jacobian = self.__derive_poly_structure(
jacobian,
tc_ratio,
aspect_ratio,
sweep,
wing_area,
taper_ratio,
wingbox_area,
lift,
engine_mass,
c_2=c_2,
)
y_1, _ = self.__poly_structure(
tc_ratio,
aspect_ratio,
sweep,
wing_area,
taper_ratio,
wingbox_area,
lift,
engine_mass,
c_0=c_0,
c_1=c_1,
c_2=c_2,
)
f = y_1[0] / (y_1[0] - y_1[1])
jacobian["y_1"]["c_0"][0, 0] = 1
jacobian["y_1"]["c_0"][1, 0] = 1
fp = (1 * (y_1[0] - y_1[1]) - y_1[0] * (1 - 1)) / (y_1[0] - y_1[1]) ** 2
jacobian["y_11"]["c_0"][:, 0] = fp / f
jacobian["y_1"]["c_1"][0, 0] = 1
fp = (1 * (y_1[0] - y_1[1]) - y_1[0] * (1 - 0)) / (y_1[0] - y_1[1]) ** 2
jacobian["y_11"]["c_1"][:, 0] = fp / f
dy10dc2 = (
self.__compute_wing_weight(
tc_ratio,
aspect_ratio,
sweep,
wing_area,
taper_ratio,
wingbox_area,
lift,
c_2=c_2,
)
* 0.557
/ c_2
)
jacobian["y_1"]["c_2"][0, 0] = dy10dc2
dy11dc2 = jacobian["y_1"]["c_2"][1, 0]
fp = (dy10dc2 * (y_1[0] - y_1[1]) - y_1[0] * (dy10dc2 - dy11dc2)) / (
y_1[0] - y_1[1]
) ** 2
jacobian["y_11"]["c_2"][:, 0] = fp / f
# Stress constraints
jacobian = self.__derive_constraints(
jacobian,
tc_ratio,
aspect_ratio,
wing_area,
taper_ratio,
wingbox_area,
lift,
true_cstr=true_cstr,
)
# Twist constraints
jacobian["g_1"]["x_1"][5, :] = jacobian["y_1"]["x_1"][2, :]
jacobian["g_1"]["x_shared"][5, :] = jacobian["y_1"]["x_shared"][2, :]
jacobian["g_1"]["y_21"][5, :] = jacobian["y_1"]["y_21"][2, :]
jacobian["g_1"]["y_31"][5, :] = jacobian["y_1"]["y_31"][2, :]
jacobian["g_1"]["c_0"][5, :] = jacobian["y_1"]["c_0"][2, :]
jacobian["g_1"]["c_1"][5, :] = jacobian["y_1"]["c_1"][2, :]
jacobian["g_1"]["c_2"][5, :] = jacobian["y_1"]["c_2"][2, :]
if not true_cstr:
jacobian["g_1"]["x_1"][6, :] = -jacobian["y_1"]["x_1"][2, :]
jacobian["g_1"]["x_shared"][6, :] = -jacobian["y_1"]["x_shared"][2, :]
jacobian["g_1"]["y_21"][6, :] = -jacobian["y_1"]["y_21"][2, :]
jacobian["g_1"]["y_31"][6, :] = -jacobian["y_1"]["y_31"][2, :]
jacobian["g_1"]["c_0"][6, :] = -jacobian["y_1"]["c_2"][2, :]
jacobian["g_1"]["c_1"][6, :] = -jacobian["y_1"]["c_2"][2, :]
jacobian["g_1"]["c_2"][6, :] = -jacobian["y_1"]["c_2"][2, :]
# Coupling variables
return self.__set_coupling_jacobian(jacobian)
@staticmethod
def __set_coupling_jacobian(
jacobian,
) -> dict[str, dict[str, ndarray]]:
"""Set Jacobian of the coupling variables."""
for der_v, jac_loc in jacobian["y_1"].items():
jacobian["y_12"][der_v][1, :] = jac_loc[2, :]
jacobian["y_12"][der_v][0, :] = jac_loc[0, :]
jacobian["y_14"][der_v] = jac_loc[0:2, :]
return jacobian
def __derive_poly_structure(
self,
jacobian: dict[str, dict[str, ndarray]],
tc_ratio: float,
aspect_ratio: float,
sweep: float,
wing_area: float,
taper_ratio: float,
wingbox_area: float,
lift: float,
engine_mass: float,
c_2: float | None = None,
):
"""Derive the structural variables.
Args:
jacobian: The Jacobian of the discipline.
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
sweep: The wing sweep.
wing_area: The wing surface area.
taper_ratio: The wing taper ratio.
wingbox_area: The wingbox x-sectional area.
lift: The lift coefficient.
c_2: The maximum load factor.
If ``None``, use :meth:`.SobieskiBase.constants`.
Returns:
The updated Jacobian of the discipline.
"""
c_2 = c_2 or self.constants[2]
# wing aero. center location
y_1, _ = self.__poly_structure(
tc_ratio,
aspect_ratio,
sweep,
wing_area,
taper_ratio,
wingbox_area,
lift,
engine_mass,
)
# dWf/d(t/c)
jacobian["y_1"]["x_shared"][1, 0] = self.__compute_dfuelwing_dtoverc(
aspect_ratio, wing_area
)
# dWf/d(AR)
jacobian["y_1"]["x_shared"][1, 3] = self.__compute_dfuelwing_dar(
tc_ratio, aspect_ratio, wing_area
)
# dWf/dsref
jacobian["y_1"]["x_shared"][1, 5] = self.__compute_dfuelwing_dsref(
tc_ratio, aspect_ratio, wing_area
)
# Derivation of wing twist = y_1[2]
s_1 = array(
[wingbox_area, self.__half_span, self.__aero_center, lift],
dtype=self.dtype,
)
_, a_i, a_ij, s_shifted = self.base.derive_polynomial_approximation(
self.__s_initial_for_wing_twist,
s_1,
self.__flag1,
self.__bound1,
self.__ao_coeff_twist,
self.__ai_coeff_twist,
self.__aij_coeff_twist,
)
dcenter_dlambda = self.__compute_dcenter_dlambda(taper_ratio)
jacobian["y_1"]["x_1"][2, 0] = dcenter_dlambda * (
a_i[2]
+ a_ij[2, 0] * s_shifted[0]
+ a_ij[2, 1] * s_shifted[1]
+ a_ij[2, 2] * s_shifted[2]
+ a_ij[2, 3] * s_shifted[3]
)
dadimx_dx = self.__compute_dadimx_dx(wingbox_area)
jacobian["y_1"]["x_1"][2, 1] = dadimx_dx * (
a_i[0]
+ a_ij[0, 0] * s_shifted[0]
+ a_ij[0, 1] * s_shifted[1]
+ a_ij[0, 2] * s_shifted[2]
+ a_ij[0, 3] * s_shifted[3]
)
dhalfspan_dar = self.__compute_dhalfspan_dar(wing_area)
jacobian["y_1"]["x_shared"][2, 3] = dhalfspan_dar * (
a_i[1]
+ a_ij[1, 0] * s_shifted[0]
+ a_ij[1, 1] * s_shifted[1]
+ a_ij[1, 2] * s_shifted[2]
+ a_ij[1, 3] * s_shifted[3]
)
dhalfspan_dsref = self.__compute_dadimspan_dsref(aspect_ratio)
jacobian["y_1"]["x_shared"][2, 5] = dhalfspan_dsref * (
a_i[1]
+ a_ij[1, 0] * s_shifted[0]
+ a_ij[1, 1] * s_shifted[1]
+ a_ij[1, 2] * s_shifted[2]
+ a_ij[1, 3] * s_shifted[3]
)
jacobian["y_1"]["y_21"][2, 0] = self.__dadimlift_dlift * (
a_i[3]
+ a_ij[0, 3] * s_shifted[0]
+ a_ij[1, 3] * s_shifted[1]
+ a_ij[2, 3] * s_shifted[2]
)
# Derivation of total weight = y_1[0] (requires derivation of wing weight)
# Calculation of wingbox X-sectional thickness
(
wing_w,
ww_coeff,
_,
a_i,
a_ij,
s_shifted,
) = self.__compute_wing_weight(
tc_ratio,
aspect_ratio,
sweep,
wing_area,
taper_ratio,
wingbox_area,
lift,
linearize=True,
c_2=c_2,
)
# dtotal_weight_d(t/c)
jacobian["y_1"]["x_shared"][0, 0] = (
-0.4 * tc_ratio ** (-1.0) * wing_w + jacobian["y_1"]["x_shared"][1, 0]
)
dy11_dz = jacobian["y_1"]["x_shared"]
# dtotal_weight_dAR
dy11_dz[0, 3] = 0.5 * aspect_ratio ** (-1.0) * wing_w
dy11_dz[0, 3] += jacobian["y_1"]["x_shared"][1, 3]
# d1total_weight_dsweep
dy11_dz[0, 4] = DEG_TO_RAD
dy11_dz[0, 4] *= self.math.sin(sweep * DEG_TO_RAD) * wing_w
dy11_dz[0, 4] /= self.math.cos(sweep * DEG_TO_RAD)
# dtotal_weight_dsref
dy11_dz[0, 5] = 0.749 * wing_area ** (-1.0) * wing_w
dy11_dz[0, 5] += jacobian["y_1"]["x_shared"][1, 5]
# dtotal_weight_d(lambda)
jacobian["y_1"]["x_1"][0, 0] = (
0.1 * (1 + taper_ratio) ** (-1.0) * wing_w + jacobian["y_1"]["x_1"][1, 0]
)
# dtotal_weight_dx
jacobian["y_1"]["x_1"][0, 1] = ww_coeff * dadimx_dx
jacobian["y_1"]["x_1"][0, 1] *= a_i[0] + a_ij[0, 0] * s_shifted[0]
jacobian["y_1"]["x_1"][0, 1] += jacobian["y_1"]["x_1"][1, 1]
# dtotal_weight_dLift
jacobian["y_1"]["y_21"][0, 0] = 0.557 * lift**-1 * wing_w
# dtotal_weight_dWe
jacobian["y_1"]["y_31"][0, 0] = 1.0
for out_v, jac_y_1 in jacobian["y_1"].items():
val = jac_y_1[0, :] / y_1[0]
val -= (jac_y_1[0, :] - jac_y_1[1, :]) / (y_1[0] - y_1[1])
jacobian["y_11"][out_v][0, :] = val
return jacobian
def __derive_constraints(
self,
jacobian: dict[str, dict[str, ndarray]],
tc_ratio: float,
aspect_ratio: float,
wing_area: float,
taper_ratio: float,
wingbox_area: float,
lift: float,
true_cstr: bool = False,
):
"""Derive the structural constraints from a polynomial approximation.
Args:
jacobian: The Jacobian of the discipline.
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
wing_area: The wing surface area.
taper_ratio: The wing taper ratio.
wingbox_area: The wingbox x-sectional area.
lift: The lift coefficient.
true_cstr: If ``True``,
return the value of the constraint outputs.
Otherwise,
return the distance to the corresponding constraint thresholds.
Returns:
The structural constraints from a polynomial approximation.
"""
n_g = 6 if true_cstr else 7
jacobian["g_1"]["x_shared"] = zeros((n_g, 6), dtype=self.dtype)
jacobian["g_1"]["x_1"] = zeros((n_g, 2), dtype=self.dtype)
jacobian["g_1"]["y_21"] = zeros((n_g, 1), dtype=self.dtype)
jacobian["g_1"]["y_31"] = zeros((n_g, 1), dtype=self.dtype)
jacobian["g_1"]["c_0"] = zeros((n_g, 1), dtype=self.dtype)
jacobian["g_1"]["c_1"] = zeros((n_g, 1), dtype=self.dtype)
jacobian["g_1"]["c_2"] = zeros((n_g, 1), dtype=self.dtype)
# Stress constraints
s_new = array(
[tc_ratio, lift, wingbox_area, self.__half_span, self.__aero_center],
dtype=self.dtype,
)
for i in range(5):
_, a_i, a_ij, s_shifted = self.base.derive_polynomial_approximation(
self.__s_initial_for_constraints,
s_new,
self.__flag_stress,
0.1 * self.__ones_mat + i * 0.05 * self.__ones_mat,
self.__ao_coeff_stress,
self.__ai_coeff_stress,
self.__aij_coeff_stress,
)
dg_dx_1, dg_dz, dg_dy_21, dg_dy_31 = self.__der_constraint(
taper_ratio,
wingbox_area,
tc_ratio,
aspect_ratio,
wing_area,
a_i,
a_ij,
s_shifted,
)
jacobian["g_1"]["x_1"][i, :] = dg_dx_1[:]
jacobian["g_1"]["x_shared"][i, :] = dg_dz[:]
jacobian["g_1"]["y_21"][i, :] = dg_dy_21[:]
jacobian["g_1"]["y_31"][i, :] = dg_dy_31[:]
return jacobian
def __poly_structure(
self,
tc_ratio: float,
aspect_ratio: float,
sweep: float,
wing_area: float,
taper_ratio: float,
wingbox_area: float,
lift: float,
engine_mass: float,
c_0: float | None = None,
c_1: float | None = None,
c_2: float | None = None,
) -> tuple[ndarray, ndarray]:
"""Compute the structural variables from a polynomial approximation.
Args:
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
sweep: The wing sweep.
wing_area: The wing surface area.
taper_ratio: The wing taper ratio.
wingbox_area: The wingbox x-sectional area.
lift: The lift coefficient.
engine_mass: The mass of the engine.
c_0: The minimum fuel weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_1: The miscellaneous weight.
If ``None``, use :meth:`.SobieskiBase.constants`.
c_2: The maximum load factor.
If ``None``, use :meth:`.SobieskiBase.constants`.
Returns:
The vector of the total aircraft mass, fuel mass and wing twist,
as well as the mass term in the Breguet range equation.
"""
c_0 = c_0 or self.constants[0]
c_1 = c_1 or self.constants[1]
c_2 = c_2 or self.constants[2]
y_1 = zeros(3, dtype=self.dtype)
# t = self.base.compute_thickness(x_shared) # wing thickness
# Calculation of wing twist
y_1[2] = self.__compute_wing_twist(wingbox_area, lift)
# Calculation of wingbox X-sectional thickness
wing_w = self.__compute_wing_weight(
tc_ratio,
aspect_ratio,
sweep,
wing_area,
taper_ratio,
wingbox_area,
lift,
c_2=c_2,
)
fuel_wing_weight = self.__compute_fuelwing_weight(
tc_ratio, aspect_ratio, wing_area
)
y_1_i1 = c_0 + fuel_wing_weight
y_1[1] = y_1_i1 # Fuel weight
y_1[0] = c_1 + wing_w + y_1_i1 + engine_mass
# This is the mass term in the Breguet range equation.
try:
logarithm = self.math.log(self.__compute_weight_ratio(y_1[0], y_1[1]))
except ValueError:
logarithm = nan
y_11 = array([logarithm], dtype=self.dtype)
return y_1, y_11
def __compute_wing_twist(
self,
wingbox_area: float,
lift: float,
) -> ndarray:
"""Compute the wing twist.
Args:
wingbox_area: The wingbox x-sectional area.
lift: The lift coefficient.
Returns:
The wing twist.
"""
# Compute the half span and the aerodynamic center
s_1 = array(
[wingbox_area, self.__half_span, self.__aero_center, lift],
dtype=self.dtype,
)
return self.base.compute_polynomial_approximation(
self.__s_initial_for_wing_twist,
s_1,
self.__flag1,
self.__bound1,
self.__ao_coeff_twist,
self.__ai_coeff_twist,
self.__aij_coeff_twist,
)
def __der_constraint(
self,
taper_ratio: float,
wingbox_area: float,
tc_ratio: float,
aspect_ratio: float,
wing_area: float,
a_i: ndarray,
a_ij: ndarray,
s_shifted: ndarray,
) -> tuple[ndarray, ndarray, ndarray, ndarray]:
"""Derive the structural constraints.
Args:
tc_ratio: The thickness-to-chord ratio.
aspect_ratio: The aspect ratio.
wing_area: The wing surface area.
a_i: The linear coefficients.
a_ij: The quadratic coefficients.
s_shifted: The normalized design variables.
Returns:
The derivatives of the structural constraints.
"""
dcenter_dlambda = self.__compute_dcenter_dlambda(taper_ratio)
dadimx_dx = self.__compute_dadimx_dx(wingbox_area)
dadimhalfspan_dar = self.__compute_dhalfspan_dar(wing_area)
dadimhalfspan_dsref = self.__compute_dadimspan_dsref(aspect_ratio)
dadimtaper_dtaper = self.__compute_dadimtaper_dtaper(tc_ratio)
dg_dz = zeros((1, 6), dtype=self.dtype)
dg_dx_1 = zeros((1, 2), dtype=self.dtype)
dg_dy_21 = zeros((1, 1), dtype=self.dtype)
dg_dy_31 = zeros((1, 1), dtype=self.dtype)
dg_dx_1[0, 0] = dcenter_dlambda * (
a_i[4]
+ a_ij[4, 0] * s_shifted[0]
+ a_ij[4, 1] * s_shifted[1]
+ a_ij[4, 2] * s_shifted[2]
+ a_ij[4, 3] * s_shifted[3]
+ a_ij[4, 4] * s_shifted[4]
)
# dsigma/dx
dg_dx_1[0, 1] = dadimx_dx * (
a_i[2]
+ a_ij[2, 0] * s_shifted[0]
+ a_ij[2, 1] * s_shifted[1]
+ a_ij[2, 2] * s_shifted[2]
+ a_ij[2, 3] * s_shifted[3]
+ a_ij[2, 4] * s_shifted[4]
)
# dsigma1/d(t/c)
dg_dz[0, 0] = dadimtaper_dtaper * (
a_i[0]
+ a_ij[0, 0] * s_shifted[0]
+ a_ij[0, 1] * s_shifted[1]
+ a_ij[0, 2] * s_shifted[2]
+ a_ij[0, 3] * s_shifted[3]
+ a_ij[0, 4] * s_shifted[4]
)
# dsigma1/dh
dg_dz[0, 1] = 0.0
# dsigma1/dM
dg_dz[0, 2] = 0.0
# dsigma1/dAR
dg_dz[0, 3] = dadimhalfspan_dar * (
a_i[3]
+ a_ij[3, 0] * s_shifted[0]
+ a_ij[3, 1] * s_shifted[1]
+ a_ij[3, 2] * s_shifted[2]
+ a_ij[3, 3] * s_shifted[3]
+ a_ij[3, 4] * s_shifted[4]
)
# dsigma1/dsweep
dg_dz[0, 4] = 0.0
# dsigma1/dsref
dg_dz[0, 5] = dadimhalfspan_dsref * (
a_i[3]
+ a_ij[3, 0] * s_shifted[0]
+ a_ij[3, 1] * s_shifted[1]
+ a_ij[3, 2] * s_shifted[2]
+ a_ij[3, 3] * s_shifted[3]
+ a_ij[3, 4] * s_shifted[4]
)
# dsigma1/dLift
dg_dy_21[0, 0] = self.__dadimlift_dlift * (
a_i[1]
+ a_ij[1, 0] * s_shifted[0]
+ a_ij[1, 1] * s_shifted[1]
+ a_ij[1, 2] * s_shifted[2]
+ a_ij[1, 3] * s_shifted[3]
+ a_ij[1, 4] * s_shifted[4]
)
# dsigma1/dWengine
dg_dy_31[0, 0] = 0.0
return dg_dx_1, dg_dz, dg_dy_21, dg_dy_31
def __poly_structure_constraints(
self,
tc_ratio: float,
wingbox_area: float,
lift: float,
) -> ndarray:
"""Compute the structural constraints from a polynomial approximation.
Args:
tc_ratio: The thickness-to-chord ratio.
wingbox_area: The wingbox x-sectional area.
lift: The lift coefficient.
Returns:
The structural constraints.
"""
g_1 = zeros(6, dtype=self.dtype)
s_new = array(
[tc_ratio, lift, wingbox_area, self.__half_span, self.__aero_center],
dtype=self.dtype,
)
for i in range(5):
g_1[i] = self.base.compute_polynomial_approximation(
self.__s_initial_for_constraints,
s_new,
self.__flag_stress,
0.1 * self.__loc_ones + i * 0.05 * self.__loc_ones,
self.__ao_coeff_stress,
self.__ai_coeff_stress,
self.__aij_coeff_stress,
)
return g_1