Source code for gemseo.core.derivatives.jacobian_operator

# 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.
"""Abstraction of Jacobian as linear operators."""

from __future__ import annotations

import logging
from copy import copy
from typing import TYPE_CHECKING
from typing import Any
from typing import Generic
from typing import TypeVar

from docstring_inheritance import GoogleDocstringInheritanceMeta
from numpy import dtype
from numpy import eye
from scipy.sparse.linalg import LinearOperator

from gemseo.typing import SparseOrDenseRealArray
from gemseo.utils.compatibility.scipy import array_classes

if TYPE_CHECKING:
    from gemseo.typing import RealArray

_OperandT = TypeVar("_OperandT", "JacobianOperator", SparseOrDenseRealArray)

LOGGER = logging.getLogger(__name__)


[docs] class JacobianOperator(LinearOperator, metaclass=GoogleDocstringInheritanceMeta): # type: ignore[misc] # missing typing """The Jacobian of a discipline as linear operator.""" def __init__( self, dtype: dtype[Any], shape: tuple[int, ...], ) -> None: """ Args: dtype: The data type of the Jacobian. shape: The shape of the Jacobian. """ # noqa: D205 D212 D415 super().__init__(dtype, shape) def __add__(self, other: _OperandT) -> _BaseOperation[_OperandT]: if isinstance(other, array_classes): return _SumOperationWithArray(self, other) return _SumOperation(self, other) def __sub__(self, other: _OperandT) -> _BaseOperation[_OperandT]: if isinstance(other, array_classes): return _SubOperationWithArray(self, other) return _SubOperation(self, other) def __matmul__( self, other: _OperandT, ) -> _BaseComposedOperation[JacobianOperator, _OperandT]: if isinstance(other, array_classes): return _ComposedOperationOperatorArray(self, other) return _ComposedOperationOperatorOperator(self, other) def __rmatmul__( self, other: _OperandT, ) -> _BaseComposedOperation[_OperandT, JacobianOperator]: if isinstance(other, array_classes): return _ComposedOperationArrayOperator(other, self) return _ComposedOperationOperatorOperator(other, self) @property def real(self) -> _RealJacobianOperator: """The real casting of the Jacobian operator output.""" return _RealJacobianOperator(self)
[docs] def copy(self) -> JacobianOperator: """Create a shallow copy of the Jacobian operator. Returns: A shallow copy of the Jacobian operator. """ return copy(self)
@property def T(self) -> _AdjointJacobianOperator: # noqa: N802 """The transpose of the Jacobian operator. Returns: The transpose of the Jacobian operator. """ return _AdjointJacobianOperator(self)
[docs] def shift_identity(self) -> _SubOperation: """Subtract the identity to the Jacobian operator. Returns: The Jacobian operator shifted by minus the identity. """ return self - _IdentityOperator(self.shape[0])
[docs] def get_matrix_representation(self) -> RealArray: """Compute the matrix representation of the Jacobian. Returns: The matrix representation of the Jacobian. """ LOGGER.info( "The Jacobian is given as a linear operator. Performing the assembly " "required to apply it to the identity which is not performant." ) return self.dot(eye(self.shape[1])) # type: ignore[no-any-return]
class _RealJacobianOperator(JacobianOperator): """A jacobian operator that casts to real.""" def __init__(self, operator: JacobianOperator) -> None: """ Args: operator: The Jacobian operator to cast to real. """ # noqa: D205 D212 D415 super().__init__(operator.dtype, operator.shape) self.__operator = operator def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self.__operator.matvec(x).real # type: ignore[no-any-return] def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self.__operator.rmatvec(x).real # type: ignore[no-any-return] class _AdjointJacobianOperator(JacobianOperator): """A jacobian operator that handles adjoints.""" def __init__(self, operator: JacobianOperator) -> None: """ Args: operator: The Jacobian operator to take the adjoint of. """ # noqa: D205 D212 D415 super().__init__(operator.dtype, operator.shape[::-1]) self.__operator = operator def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self.__operator.rmatvec(x) # type: ignore[no-any-return] def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self.__operator.matvec(x) # type: ignore[no-any-return] class _IdentityOperator(JacobianOperator): """A jacobian operator that represents the identity operator.""" def __init__(self, size: int) -> None: """ Args: size: The size of the identity matrix. """ # noqa: D205 D212 D415 super().__init__(dtype(float), (size, size)) def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return x def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return x class _BaseOperation(JacobianOperator, Generic[_OperandT]): """A base class to handle operations on 2 jacobian operators.""" _operand_1: JacobianOperator """The first operand.""" _operand_2: _OperandT """The second operand.""" def __init__( self, operand_1: JacobianOperator, operand_2: _OperandT, ) -> None: """ Args: operand_1: The first operand. operand_2: The second operand. """ # noqa: D205 D212 D415 super().__init__(operand_1.dtype, operand_1.shape) self._operand_1 = operand_1 self._operand_2 = operand_2 class _SumOperation(_BaseOperation[JacobianOperator]): """A jacobian operator that handles the sum of 2 jacobian operators.""" def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.matvec(x) + self._operand_2.matvec(x) # type:ignore[no-any-return] def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.rmatvec(x) + self._operand_2.rmatvec(x) # type:ignore[no-any-return] class _SumOperationWithArray(_BaseOperation[SparseOrDenseRealArray]): """A jacobian operator that handles the sum operation with a standard jacobian.""" def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.matvec(x) + self._operand_2 @ x # type:ignore[no-any-return] def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.rmatvec(x) + self._operand_2.T @ x # type:ignore[no-any-return] class _SubOperation(_BaseOperation[JacobianOperator]): """A jacobian operator that handles the subtraction of 2 jacobian operators.""" def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.matvec(x) - self._operand_2.matvec(x) # type:ignore[no-any-return] def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.rmatvec(x) - self._operand_2.rmatvec(x) # type:ignore[no-any-return] class _SubOperationWithArray(_BaseOperation[SparseOrDenseRealArray]): """A jacobian operator that handles the subtraction with a standard jacobian.""" def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.matvec(x) - self._operand_2 @ x # type:ignore[no-any-return] def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.rmatvec(x) - self._operand_2.T @ x # type:ignore[no-any-return] # TODO: How to avoid duplication? (TypeAlias does not work) Operand1T = TypeVar("Operand1T", JacobianOperator, SparseOrDenseRealArray) Operand2T = TypeVar("Operand2T", JacobianOperator, SparseOrDenseRealArray) class _BaseComposedOperation(JacobianOperator, Generic[Operand1T, Operand2T]): """A base class for jacobian operators composition.""" _operand_1: Operand1T """The first operand.""" _operand_2: Operand2T """The second operand.""" def __init__( self, operand_1: Operand1T, operand_2: Operand2T, ) -> None: """ Args: operand_1: The first operand of the composition. operand_2: The second operand of the composition. """ # noqa: D205 D212 D415 super().__init__(operand_1.dtype, (operand_1.shape[0], operand_2.shape[1])) self._operand_1 = operand_1 self._operand_2 = operand_2 class _ComposedOperationArrayOperator( _BaseComposedOperation[SparseOrDenseRealArray, JacobianOperator] ): """A jacobian operator that handles a left composition with a standard jacobian.""" def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1 @ self._operand_2.matvec(x) # type:ignore[no-any-return] def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_2.rmatvec(self._operand_1.T @ x) # type:ignore[no-any-return] class _ComposedOperationOperatorOperator( _BaseComposedOperation[JacobianOperator, JacobianOperator] ): """A jacobian operator that compose another jacobian operator.""" def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.matvec(self._operand_2.matvec(x)) # type:ignore[no-any-return] def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_2.rmatvec(self._operand_1.rmatvec(x)) # type:ignore[no-any-return] class _ComposedOperationOperatorArray( _BaseComposedOperation[JacobianOperator, SparseOrDenseRealArray] ): """A jacobian operator that handles a right composition with a standard jacobian.""" def _matvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_1.matvec(self._operand_2 @ x) # type:ignore[no-any-return] def _rmatvec(self, x: RealArray) -> RealArray: """ Args: x: The vector to apply the transpose of ∂f/∂v to. """ # noqa: D205 D212 return self._operand_2.T @ self._operand_1.rmatvec(x) # type:ignore[no-any-return]