# 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: Francois Gallard
# OTHER AUTHORS - MACROSCOPIC CHANGES
"""The disciplines of the Sobieski's SSBJ use case."""
from __future__ import annotations
import time
from numbers import Number
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from numpy import array
from gemseo.core.discipline import MDODiscipline
from gemseo.disciplines.remapping import RemappingDiscipline
from gemseo.problems.sobieski.core.problem import SobieskiProblem
from gemseo.problems.sobieski.core.utils import SobieskiBase
if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Mapping
[docs]
class SobieskiDiscipline(MDODiscipline):
"""Abstract base discipline for the Sobieski's SSBJ use case."""
dtype: SobieskiBase.DataType
"""The data type for the NumPy arrays."""
sobieski_problem: SobieskiProblem
"""The Sobieski's SSBJ use case defining the MDO problem, e.g. disciplines,
constraints, design space and reference optimum."""
GRAMMAR_DIRECTORY = Path(__file__).parent / "grammars"
_ATTR_NOT_TO_SERIALIZE = MDODiscipline._ATTR_NOT_TO_SERIALIZE.union(
[
"sobieski_problem",
],
)
def __init__(
self,
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
) -> None:
"""
Args:
dtype: The data type for the NumPy arrays, either "float64" or "complex128".
""" # noqa: D205 D212
super().__init__(auto_detect_grammar_files=True)
self.dtype = dtype
self.sobieski_problem = SobieskiProblem(dtype=dtype)
self.default_inputs = self.sobieski_problem.get_default_inputs(
self.get_input_data_names()
)
self.re_exec_policy = self.ReExecutionPolicy.DONE
def __setstate__(self, state: Mapping[str, Any]) -> None:
super().__setstate__(state)
self.sobieski_problem = SobieskiProblem(self.dtype)
[docs]
@classmethod
def create_with_physical_naming(
cls,
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
) -> RemappingDiscipline:
"""
Args:
dtype: The data type for the NumPy arrays, either "float64" or "complex128".
""" # noqa: D205 D212
raise NotImplementedError
[docs]
class SobieskiMission(SobieskiDiscipline):
"""Mission discipline of the Sobieski's SSBJ use case.
Compute the range with the Breguet formula.
"""
enable_delay: bool | float
"""If ``True``, wait one second before computation.
If a positive number, wait the corresponding number of seconds. If ``False``,
compute directly.
"""
def __init__(
self,
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
enable_delay: bool | float = False,
) -> None:
"""
Args:
enable_delay: If ``True``, wait one second before computation.
If a positive number, wait the corresponding number of seconds.
If ``False``, compute directly.
""" # noqa: D205 D212
super().__init__(dtype=dtype)
self.enable_delay = enable_delay
def _run(self) -> None:
if self.enable_delay:
if isinstance(self.enable_delay, Number):
time.sleep(self.enable_delay)
else:
time.sleep(1.0)
data_names = ["y_14", "y_24", "y_34", "x_shared"]
y_14, y_24, y_34, x_shared = self.get_inputs_by_name(data_names)
y_4 = self.sobieski_problem.mission.execute(x_shared, y_14, y_24, y_34)
self.store_local_data(y_4=y_4)
def _compute_jacobian(
self,
inputs: Iterable[str] | None = None,
outputs: Iterable[str] | None = None,
) -> None:
data_names = ["y_14", "y_24", "y_34", "x_shared"]
y_14, y_24, y_34, x_shared = self.get_inputs_by_name(data_names)
self.jac = self.sobieski_problem.mission.linearize(x_shared, y_14, y_24, y_34)
[docs]
@classmethod
def create_with_physical_naming(
cls,
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
enable_delay: bool | float = False,
) -> RemappingDiscipline:
"""
Args:
enable_delay: If ``True``, wait one second before computation.
If a positive number, wait the corresponding number of seconds.
If ``False``, compute directly.
""" # noqa: D205 D212
return RemappingDiscipline(
cls(dtype=dtype, enable_delay=enable_delay),
{
"t_w_4": ("y_14", 0),
"f_w": ("y_14", 1),
"altitude": ("x_shared", 1),
"mach": ("x_shared", 2),
"cl_cd": "y_24",
"sfc": "y_34",
},
{"range": "y_4"},
)
[docs]
class SobieskiStructure(SobieskiDiscipline):
"""Structure discipline of the Sobieski's SSBJ use case."""
def __init__( # noqa: D107
self,
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
) -> None:
super().__init__(dtype=dtype)
self.default_inputs["c_0"] = array([self.sobieski_problem.constants[0]])
self.default_inputs["c_1"] = array([self.sobieski_problem.constants[1]])
self.default_inputs["c_2"] = array([self.sobieski_problem.constants[2]])
def _run(self) -> None:
data_names = ["x_1", "y_21", "y_31", "x_shared", "c_0", "c_1", "c_2"]
x_1, y_21, y_31, x_shared, c_0, c_1, c_2 = self.get_inputs_by_name(data_names)
y_1, y_11, y_12, y_14, g_1 = self.sobieski_problem.structure.execute(
x_shared, y_21, y_31, x_1, c_0=c_0[0], c_1=c_1[0], c_2=c_2[0]
)
self.store_local_data(y_1=y_1, y_11=y_11, y_12=y_12, y_14=y_14, g_1=g_1)
def _compute_jacobian(
self,
inputs: Iterable[str] | None = None,
outputs: Iterable[str] | None = None,
) -> None:
data_names = ["x_1", "y_21", "y_31", "x_shared", "c_2"]
x_1, y_21, y_31, x_shared, c_2 = self.get_inputs_by_name(data_names)
self.jac = self.sobieski_problem.structure.linearize(
x_shared, y_21, y_31, x_1, c_2=c_2[0]
)
[docs]
@classmethod
def create_with_physical_naming( # noqa: D102
cls, dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT
) -> RemappingDiscipline:
return RemappingDiscipline(
cls(dtype=dtype),
{
"cl": "y_21",
"e_w": "y_31",
"t_c": ("x_shared", 0),
"ar": ("x_shared", 3),
"sweep": ("x_shared", 4),
"area": ("x_shared", 5),
"taper_ratio": ("x_1", 0),
"wingbox_area": ("x_1", 1),
"min_f_w": "c_0",
"m_w": "c_1",
"max_lf": "c_2",
},
{
"y_1": "y_1",
"y_11": "y_11",
"t_w_4": ("y_14", 0),
"t_w_2": ("y_12", 0),
"f_w": ("y_14", 1),
"twist": ("y_12", 1),
"stress": ("g_1", range(5)),
"twist_c": ("g_1", range(5, 7)),
},
)
[docs]
class SobieskiAerodynamics(SobieskiDiscipline):
"""Aerodynamics discipline for the Sobieski's SSBJ use case."""
def __init__( # noqa: D107
self,
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
) -> None:
super().__init__(dtype=dtype)
self.default_inputs["c_4"] = array([self.sobieski_problem.constants[4]])
def _run(self) -> None:
data_names = ["x_2", "y_12", "y_32", "x_shared", "c_4"]
x_2, y_12, y_32, x_shared, c_4 = self.get_inputs_by_name(data_names)
y_2, y_21, y_23, y_24, g_2 = self.sobieski_problem.aerodynamics.execute(
x_shared, y_12, y_32, x_2, c_4=c_4[0]
)
self.store_local_data(y_2=y_2, y_21=y_21, y_23=y_23, y_24=y_24, g_2=g_2)
def _compute_jacobian(
self,
inputs: Iterable[str] | None = None,
outputs: Iterable[str] | None = None,
) -> None:
data_names = ["x_2", "y_12", "y_32", "x_shared", "c_4"]
x_2, y_12, y_32, x_shared, c_4 = self.get_inputs_by_name(data_names)
self.jac = self.sobieski_problem.aerodynamics.linearize(
x_shared, y_12, y_32, x_2, c_4=c_4[0]
)
[docs]
@classmethod
def create_with_physical_naming( # noqa: D102
cls,
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
) -> RemappingDiscipline:
return RemappingDiscipline(
cls(dtype=dtype),
{
"esf": "y_32",
"t_w_2": ("y_12", 0),
"twist": ("y_12", 1),
"cf": "x_2",
"t_c": ("x_shared", 0),
"altitude": ("x_shared", 1),
"mach": ("x_shared", 2),
"sweep": ("x_shared", 4),
"area": ("x_shared", 5),
"min_cd": "c_4",
},
{
"y_2": "y_2",
"cl": "y_21",
"cd": "y_23",
"cl_cd": "y_24",
"dp_dx": "g_2",
},
)
[docs]
class SobieskiPropulsion(SobieskiDiscipline):
"""Propulsion discipline of the Sobieski's SSBJ use case."""
def __init__( # noqa: D107
self,
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
) -> None:
super().__init__(dtype=dtype)
self.default_inputs["c_3"] = array([self.sobieski_problem.constants[3]])
def _run(self) -> None:
data_names = ["x_3", "y_23", "x_shared", "c_3"]
x_3, y_23, x_shared, c_3 = self.get_inputs_by_name(data_names)
y_3, y_34, y_31, y_32, g_3 = self.sobieski_problem.propulsion.execute(
x_shared, y_23, x_3, c_3=c_3[0]
)
self.store_local_data(y_3=y_3, y_34=y_34, y_31=y_31, y_32=y_32, g_3=g_3)
def _compute_jacobian(
self,
inputs: Iterable[str] | None = None,
outputs: Iterable[str] | None = None,
) -> None:
data_names = ["x_3", "y_23", "x_shared", "c_3"]
x_3, y_23, x_shared, c_3 = self.get_inputs_by_name(data_names)
self.jac = self.sobieski_problem.propulsion.linearize(
x_shared, y_23, x_3, c_3=c_3[0]
)
[docs]
@classmethod
def create_with_physical_naming( # noqa: D102
cls,
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
) -> RemappingDiscipline:
return RemappingDiscipline(
cls(dtype=dtype),
{
"throttle": "x_3",
"cd": "y_23",
"altitude": ("x_shared", 1),
"mach": ("x_shared", 2),
"ref_weight": "c_3",
},
{
"y_3": "y_3",
"sfc": ("y_3", 0),
"e_w": ("y_3", 1),
"esf_c": ("g_3", range(2)),
"esf": "y_32",
"throttle_c": ("g_3", 2),
"temperature": ("g_3", 3),
},
)
[docs]
def create_disciplines(
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
) -> list[SobieskiDiscipline]:
"""Instantiate the structure, aerodynamics, propulsion and mission disciplines.
Args:
dtype: The NumPy type for data arrays, either "float64" or "complex128".
Returns:
The structure, aerodynamics, propulsion and mission disciplines.
"""
return [
SobieskiStructure(dtype),
SobieskiAerodynamics(dtype),
SobieskiPropulsion(dtype),
SobieskiMission(dtype),
]
[docs]
def create_disciplines_with_physical_naming(
dtype: SobieskiBase.DataType = SobieskiBase.DataType.FLOAT,
) -> list[RemappingDiscipline]:
"""Instantiate the structure, aerodynamics, propulsion and mission disciplines.
Use a physical naming for the input and output variables.
Args:
dtype: The NumPy type for data arrays, either "float64" or "complex128".
Returns:
The structure, aerodynamics, propulsion and mission disciplines.
"""
return [
SobieskiStructure.create_with_physical_naming(dtype),
SobieskiAerodynamics.create_with_physical_naming(dtype),
SobieskiPropulsion.create_with_physical_naming(dtype),
SobieskiMission.create_with_physical_naming(dtype),
]