Source code for gemseo.problems.mdo.opt_as_mdo_scenario

# 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.
r"""Make a monodisciplinary optimization problem multidisciplinary.

The literature on problems to benchmark optimization algorithms is much richer
than the literature on problems to benchmark MDO algorithms.
This difference is even greater in the case of MDO under uncertainties.

Faced with this limitation,
the :class:`.OptAsMDOScenario` allows the user to
rewrite a monodisciplinary optimization problem into an MDO problem.

The original discipline

- must have as outputs one or more objectives,
- may have as outputs one or more constraints,
- may have as outputs one or more observables,,
- must have as inputs at least three design variables.

The MDO problem will include :math:`2+N` disciplines,
namely

- :math:`N` strongly coupled disciplines
  computing the values of some coupling variables
  from the values of the other coupling variables
  and the values of the design variables,
- one weakly coupled discipline,
  called *link discipline*,
  computing the values of the design variables in the original optimization problem
  from
  the values of the design and coupling variables in the MDO problem,
- the original discipline.

This scenario will also modify the design space passed as argument
by renaming
the first design variable as :math:`x_0`,
the second design variable as :math:`x_1`,
and so on.
Then,
:math:`x_0` will be the global design variable
and :math:`x_i` will be the local design variable
specific to the :math:`i`-th discipline.

In other words,
an optimization problem of the form

.. math::

   \min_{z\in Z} f(z_0,z_1,\ldots,z_N) \text{ s.t. } g(z_0,z_1,\ldots,z_N) \leq 0

will be transformed into an MDO problem of the form

.. math::

   \min_{x\in Z, y\in Y} F(x_0,x_1,\ldots,x_N,y_1,\ldots,y_N)
   \text{ s.t. } G(x_0,x_1,\ldots,x_N,y_1,\ldots,y_N) \leq 0

where
:math:`y_i=h_i(x_0,x_i,y_{-i})` is the coupling variable
outputted by the :math:`i`-th strongly coupled discipline
and where :math:`F = f \circ L` and :math:`G = f \circ L`,
with :math:`L` the link discipline.

The function :math:`h_i` is defined as

.. math::

   h_i(x_0,x_i,y_{-i})
   =a_i-D_{i,0}x_0-D_{i,i}x_i+\sum_{j=1\atop j\neq i}^N C_{i,j}y_j

where :math:`a_i`, :math:`D_{i,0}`, :math:`D_{i,i}`
and :math:`\left(C_{i,j}\right)_{j=1\atop j\neq i}`
are realizations of independent random variables
uniformly distributed between 0 and 1,
and the link discipline :math:`L` is defined as

.. math::

   z = L(x, y) = x + y - c(x)

where :math:`c` is the implicit function
such that :math:`c_i(x)=h_i(x_0,x_i,c_{-i}(x))` for all :math:`i\in\{1,\ldots,N\}`.

If the original discipline is analytically differentiable,
so are the objective and constraint functions of this MDO problem.

By default,
this scenario applies
the technique proposed by Amine Aziz-Alaoui in his doctoral thesis
to the case of linear coupling and link disciplines,
using the previous expressions of :math:`h_1,\ldots,h_N` and :math:`L`.
More advanced disciplines could be imagined,
using the arguments ``coupling_equations`` and ``link_discipline``.

:ref:`This example <sphx_glr_examples_mdo_plot_opt_as_mdo.py>`
from the documentation
illustrates this feature.
"""

from __future__ import annotations

from abc import abstractmethod
from typing import TYPE_CHECKING
from typing import Any
from typing import Callable

from numpy import array
from numpy import hstack
from numpy import ones

from gemseo.core.discipline.discipline import Discipline
from gemseo.problems.mdo.scalable.parametric.scalable_problem import ScalableProblem
from gemseo.scenarios.mdo_scenario import MDOScenario

if TYPE_CHECKING:
    from collections.abc import Iterable
    from collections.abc import Mapping
    from collections.abc import Sequence

    from gemseo.algos.design_space import DesignSpace
    from gemseo.formulations.base_formulation_settings import BaseFormulationSettings
    from gemseo.typing import RealArray


[docs] class BaseLinkDiscipline(Discipline): r"""The base class for the link discipline :math:`c`. This discipline computes the values of the design variables :math:`z_0,z_1,\ldots,z_N` of the original optimization problem from the values of the design variables :math:`x_0,x_1,\ldots,x_N` and coupling variables :math:`y_0,y_1,\ldots,y_N` of the MDO problem. """ _differentiate_mda_analytically: Callable[[RealArray], RealArray] | None """The function differentiating the MDA analytically at a given design point. If ``None``, the discipline is not differentiable. """ _n_strongly_coupled_disciplines: int """The number of strongly coupled disciplines.""" _perform_mda_analytically: Callable[[RealArray], RealArray] """The function performing the MDA analytically at a given design point.""" _x_names: tuple[str, ...] """The names of the design variables in the MDO problem.""" _y_names: tuple[str, ...] """The names of the coupling variables in the MDO problem.""" _differentiate_mda_analytically: Callable[[RealArray], RealArray] | None """The function differentiating the MDA analytically at a given design point. If ``None``, the discipline is not differentiable. """ _n_strongly_coupled_disciplines: int """The number of strongly coupled disciplines.""" _original_x_names: list[str] """The names of the design variables in the original problem.""" _perform_mda_analytically: Callable[[RealArray], RealArray] """The function performing the MDA analytically at a given design point.""" _x_names: tuple[str, ...] """The names of the design variables in the MDO problem.""" _y_names: tuple[str, ...] """The names of the coupling variables in the MDO problem.""" def __init__( self, design_space: DesignSpace, perform_mda_analytically: Callable[[RealArray], RealArray], differentiate_mda_analytically: Callable[[RealArray], RealArray] | None = None, ) -> None: r""" Args: design_space: The design space of the original optimization problem. perform_mda_analytically: The function :math:`c` performing the MDA analytically at a given design point :math:`x=(x_0,x_1,\ldots,x_N)`. differentiate_mda_analytically: The function :math:`\nabla c` differentiating the MDA analytically at a given design point :math:`x=(x_0,x_1,\ldots,x_N)`. If ``None``, the discipline will not be differentiable. """ # noqa: D205 D212 super().__init__(name="L") n_strongly_coupled_disciplines = len(design_space) - 1 self._n_strongly_coupled_disciplines = n_strongly_coupled_disciplines # Names of the design variables and coupling variables in the MDO problem: original_x_names = design_space.variable_names for i, original_x_name in enumerate(original_x_names): design_space.rename_variable(original_x_name, f"x_{i}") self._x_names = tuple(design_space.variable_names) self._y_names = tuple( f"y_{i}" for i in range(1, n_strongly_coupled_disciplines + 1) ) self.input_grammar.update_from_names([*self._x_names, *self._y_names]) # Names of the design variables in the original optimization problem: self._original_x_names = original_x_names self.output_grammar.update_from_names(original_x_names) self._perform_mda_analytically = perform_mda_analytically self._differentiate_mda_analytically = differentiate_mda_analytically def _run(self, input_data: Mapping[str, RealArray]) -> dict[str, RealArray]: # The values of the design variables. x = tuple(input_data[name] for name in self._x_names) # The values of the coupling variables from a numerical MDA. approximated_y = tuple(input_data[name] for name in self._y_names) # The values of the coupling variables from the analytical MDA. expected_y = self._perform_mda_analytically(hstack(x)) return self._run_from_intermediate_data(x, approximated_y, expected_y) @abstractmethod def _run_from_intermediate_data( self, x: tuple[RealArray, ...], approximated_y: tuple[RealArray, ...], expected_y: RealArray, ) -> dict[str, RealArray]: r"""Compute the design variable values in the original optimization problem. Args: x: The values of the design variables :math:`x_0,x_1,\ldots,x_N` in the MDO problem. approximated_y: The values of the coupling variables :math:`y_1,\ldots,y_N` from a numerical MDA. expected_y: The values of the coupling variables :math:`c_1(x),\ldots,c_N(x)` from the analytical MDA. Returns: The values of the design variables :math:`z_0,z_1,\ldots,z_N` in the original optimization problem. """ def _compute_jacobian( self, input_names: Iterable[str] = (), output_names: Iterable[str] = (), ) -> None: if self._differentiate_mda_analytically is None: return self._init_jacobian(input_names, output_names) self.jac[self._original_x_names[0]][self._x_names[0]] = ones((1, 1)) # The derivatives of the coupling variables from the analytical MDA. input_data = self.io.get_input_data(with_namespaces=False) x = tuple(input_data[name] for name in self._x_names) d_expected_y_dx = self._differentiate_mda_analytically(hstack(x)) self._compute_jacobian_from_intermediate_data(x, d_expected_y_dx) @abstractmethod def _compute_jacobian_from_intermediate_data( self, x: tuple[RealArray, ...], d_expected_y_dx: RealArray ) -> None: r"""Compute the Jacobian :attr:`.jac`. Args: x: The values of the design variables :math:`x_0,x_1,\ldots,x_N` in the MDO problem. d_expected_y_dx: The derivatives of the coupling variables from the analytical MDA. """
[docs] class LinearLinkDiscipline(BaseLinkDiscipline): r"""A linear link discipline of the form :math:`L(x, y) = x + y - c(x)`. In more details, :math:`L_0(x, y) = x_0` and :math:`L_i(x, y) = x_i + y_i - c_i(x)` for :math:`i\in\{1,\ldots,N\}`. """ def _run_from_intermediate_data( self, x: tuple[RealArray, ...], approximated_y: tuple[RealArray, ...], expected_y: RealArray, ) -> dict[str, RealArray]: output_data = {self._original_x_names[0]: x[0]} for i in range(1, self._n_strongly_coupled_disciplines + 1): original_x_i_name = self._original_x_names[i] output_data[original_x_i_name] = ( x[i] + approximated_y[i - 1] - expected_y[i - 1] ) return output_data def _compute_jacobian_from_intermediate_data( self, x: tuple[RealArray, ...], d_expected_y_dx: RealArray ) -> None: for i in range(1, self._n_strongly_coupled_disciplines + 1): jac = self.jac[self._original_x_names[i]] jac[self._y_names[i - 1]] = ones((1, 1)) jac[self._x_names[i]] = ones((1, 1)) - d_expected_y_dx[i - 1, i] for j in range(self._n_strongly_coupled_disciplines + 1): if j != i: jac[self._x_names[j]] = -array([[d_expected_y_dx[i - 1, j]]])
[docs] class OptAsMDOScenario(MDOScenario): """A monodisciplinary optimization scenario made multidisciplinary.""" def __init__( self, discipline: Discipline, objective_name: str | Sequence[str], design_space: DesignSpace, name: str = "", maximize_objective: bool = False, formulation_settings_model: BaseFormulationSettings | None = None, coupling_equations: tuple[ Iterable[Discipline, ...], Callable[[RealArray], RealArray], Callable[[RealArray], RealArray], ] = (), link_discipline_class: type[BaseLinkDiscipline] = LinearLinkDiscipline, **formulation_settings: Any, ) -> None: r""" Args: discipline: The discipline computing the objective, constraints and observables from the design variables. design_space: The design space including the design variables :math:`z_0,z_1,\ldots,z_N` which will be replaced by :math:`x_0,x_1,\ldots,x_N` respectively in the MDO problem. coupling_equations: The material to evaluate and solve the coupling equations, namely the disciplines :math:`h_1,\ldots,h_N`, the function :math:`c` and the Jacobian function :math:`\nabla c(x)`. If empty, the :math:`i`-th discipline is linear. link_discipline_class: The class of the link discipline. .. note:: There is no naming convention for the input and output variables of ``discipline``. So, you can use :math:`a,b,c` in ``design_space`` instead of :math:`z_0,z_1,z_2`. """ # noqa: D205, D212, E501 disciplines = create_disciplines( discipline, design_space, coupling_equations, link_discipline_class ) super().__init__( disciplines, objective_name, design_space, name=name, maximize_objective=maximize_objective, formulation_settings_model=formulation_settings_model, **formulation_settings, )
[docs] def create_disciplines( discipline: Discipline, design_space: DesignSpace, coupling_equations: tuple[ Iterable[Discipline, ...], Callable[[RealArray], RealArray], Callable[[RealArray], RealArray], ], link_discipline_class: type[BaseLinkDiscipline], ) -> tuple[Discipline, BaseLinkDiscipline, Discipline]: r"""Create the disciplines to make an optimization problem multidisciplinary. Args: discipline: The discipline computing the objective, constraints and observables from the design variables. design_space: The design space including the design variables :math:`z_0,z_1,\ldots,z_N` which will be replaced by :math:`x_0,x_1,\ldots,x_N` respectively in the MDO problem. coupling_equations: The material to evaluate and solve the coupling equations, namely the disciplines :math:`h_1,\ldots,h_N`, the function :math:`c` and the Jacobian function :math:`\nabla c(x)`. If empty, the :math:`i`-th discipline is linear. link_discipline_class: The class of the link discipline. Returns: The original discipline, the link discipline, and the strongly coupled disciplines. Raises: ValueError: When the design space includes less than three variables or when a design variable is not scalar. """ n_variables = len(design_space) if n_variables < 3: msg = ( "The design space must have at least three scalar design variables; " f"got {n_variables}." ) raise ValueError(msg) if design_space.dimension != n_variables: msg = "The design space must include scalar variables only." raise ValueError(msg) if coupling_equations: strongly_coupled_disciplines, compute_y, differentiate_y = coupling_equations else: scalable_problem = ScalableProblem() strongly_coupled_disciplines = scalable_problem.scalable_disciplines for i, strongly_coupled_discipline in enumerate(strongly_coupled_disciplines): strongly_coupled_discipline.name = f"D{i + 1}" compute_y = scalable_problem.compute_y differentiate_y = scalable_problem.differentiate_y link_discipline = link_discipline_class(design_space, compute_y, differentiate_y) return discipline, link_discipline, *strongly_coupled_disciplines