Source code for gemseo.mda.newton_raphson

# 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 - API and implementation and/or documentation
#        :author: Charlie Vanaret, Francois Gallard
#    OTHER AUTHORS   - MACROSCOPIC CHANGES
"""The Newton-Raphson algorithm for solving MDAs.

`Newton-Raphson <https://en.wikipedia.org/wiki/Newton%27s_method>`__
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING
from typing import ClassVar

from gemseo.mda.base_mda import _BaseMDAProcessFlow
from gemseo.mda.base_parallel_mda_solver import BaseParallelMDASolver
from gemseo.mda.newton_raphson_settings import MDANewtonRaphson_Settings

if TYPE_CHECKING:
    from collections.abc import Sequence
    from typing import Any

    from numpy.typing import NDArray

    from gemseo.core._process_flow.base_process_flow import BaseProcessFlow
    from gemseo.core.discipline import Discipline
    from gemseo.typing import StrKeyMapping


LOGGER = logging.getLogger(__name__)


class _ProcessFlow(_BaseMDAProcessFlow):
    """The process data and execution flow."""


[docs] class MDANewtonRaphson(BaseParallelMDASolver): r"""Newton solver for MDA. The `Newton-Raphson method <https://en.wikipedia.org/wiki/Newton%27s_method>`__ is an iterative method to solve general equations of the form, .. math:: F(x) = 0, \quad \text{where} \quad F: \mathbb{R}^n \rightarrow \mathbb{R}^n. Beginning with :math:`x_0 \in \mathbb{R}^n` the successive iterates are given by: .. math:: x_{k+1} = x_k - J_f(x_k)^{-1} f(x_k), where :math:`J_f(x_k)` denotes the Jacobian of :math:`f` at :math:`x_k`. """ Settings: ClassVar[type[MDANewtonRaphson_Settings]] = MDANewtonRaphson_Settings """The pydantic model for the settings.""" settings: MDANewtonRaphson_Settings """The settings of the MDA""" _process_flow_class: ClassVar[type[BaseProcessFlow]] = _ProcessFlow def __init__( self, disciplines: Sequence[Discipline], settings_model: MDANewtonRaphson_Settings | None = None, **settings: Any, ) -> None: """ Raises: ValueError: When there are no coupling variables, or when there are weakly coupled disciplines. In these cases, use MDAChain. """ # noqa:D205 D212 D415 super().__init__(disciplines, settings_model=settings_model, **settings) # We use all couplings to form the Newton matrix otherwise the effect of the # weak couplings are not taken into account in the coupling updates. if not sorted(self.coupling_structure.all_couplings): msg = "There is no couplings to compute. Please consider using MDAChain." raise ValueError(msg) strongly_coupled = self.coupling_structure.get_strongly_coupled_disciplines() if len(strongly_coupled) < len(self._disciplines): msg = ( "The MDANewtonRaphson has weakly coupled disciplines, " "which is not supported. Please consider using a MDAChain with " "the option inner_mda_name='MDANewtonRaphson'." ) raise ValueError(msg) self._set_resolved_variables(self.coupling_structure.strong_couplings) self._set_differentiated_ios() def _set_differentiated_ios(self) -> None: """Set the differentiated inputs and outputs for the Newton algorithm. Also ensures that :attr:`.JacobianAssembly.sizes` contains the sizes of all the coupling sizes needed for Newton. """ for discipline in self._disciplines: inputs_to_linearize = set(discipline.io.input_grammar).intersection( self._resolved_variable_names ) outputs_to_linearize = set(discipline.io.output_grammar).intersection( self._resolved_variable_names ) if ( set(discipline.io.residual_to_state_variable.values()) & set(self._resolved_variable_names) != set() ): outputs_to_linearize |= discipline.io.residual_to_state_variable.keys() # If outputs and inputs to linearize not empty, then linearize if inputs_to_linearize and outputs_to_linearize: discipline.add_differentiated_inputs(inputs_to_linearize) discipline.add_differentiated_outputs(outputs_to_linearize) def __compute_newton_step(self, input_data: StrKeyMapping) -> NDArray: """Compute the full Newton step without relaxation. The Newton's step is defined as :math:`-[∂R/∂Y(y)]^{-1} R(y)`, where R(y) is the vector of coupling residuals and Y is the vector of couplings. Args: input_data: The input data for the disciplines. Returns: The Newton step. """ self._linearize_disciplines(input_data) newton_step, is_converged = self.assembly.compute_newton_step( input_data, self._resolved_variable_names, self.settings.newton_linear_solver_name, matrix_type=self.matrix_type, residuals=self.get_current_resolved_residual_vector(), resolved_residual_names=self._resolved_residual_names, **self.settings.newton_linear_solver_settings.model_dump(), ) if not is_converged: LOGGER.warning( "The linear solver %s failed " "to converge during the Newton's step computation.", self.settings.newton_linear_solver_name, ) return newton_step def _iterate_once(self) -> bool: local_data_before_execution = self.io.data.copy() input_couplings = self.get_current_resolved_variables_vector() self._execute_disciplines_and_update_local_data() self._compute_residuals(local_data_before_execution) if self._check_stopping_criteria(): return False newton_step = self.__compute_newton_step(local_data_before_execution) updated_couplings = self._sequence_transformer.compute_transformed_iterate( input_couplings + newton_step, newton_step, ) self._update_local_data_from_array(updated_couplings) return True