Source code for gemseo.core.mdofunctions.make_function
# 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, Charlie Vanaret
# OTHER AUTHORS - MACROSCOPIC CHANGES
"""A function computing some outputs of a discipline from some inputs."""
from __future__ import annotations
import logging
from numbers import Number
from typing import Callable
from typing import Iterable
from typing import Mapping
from typing import TYPE_CHECKING
from typing import Union
from numpy import empty
from numpy import ndarray
from gemseo.core.mdofunctions.mdo_function import MDOFunction
from gemseo.utils.data_conversion import concatenate_dict_of_arrays_to_array
from gemseo.utils.data_conversion import update_dict_of_arrays_from_array
if TYPE_CHECKING:
from gemseo.core.mdofunctions.function_generator import MDOFunctionGenerator
LOGGER = logging.getLogger(__name__)
OperandType = Union[ndarray, Number]
OperatorType = Callable[[OperandType, OperandType], OperandType]
[docs]class MakeFunction(MDOFunction):
"""A function computing some outputs of a discipline from some inputs."""
def __init__(
self,
input_names: Iterable[str],
output_names: Iterable[str],
default_inputs: Mapping[str, ndarray] | None,
mdo_function: MDOFunctionGenerator,
) -> None:
"""
Args:
input_names: The names of the inputs.
output_names: The names of the outputs.
default_inputs: The default values of the inputs
to overload the default values of the inputs of the discipline.
If None, do no overload them.
mdo_function: The generator of the :class:`.MDOFunction`
computing ``output_names`` from ``input_names``
based on a :class:`.MDODiscipline`.
"""
self.__input_names = input_names
self.__output_names = output_names
self.__mdo_function = mdo_function
self.__default_inputs = default_inputs
self.__input_indices = None
self.__output_indices = None
self.__output_size = 0
self.__input_size = 0
self.__jacobian = None
self.__discipline = self.__mdo_function.discipline
super().__init__(
self._func,
jac=self._func_jac,
name="_".join(self.__output_names),
args=self.__input_names,
outvars=self.__output_names,
)
def __compute_input_indices(self):
"""Compute the indices of the input variables in the Jacobian array."""
start = 0
self.__input_size = 0
self.__input_indices = {}
for name in self.__input_names:
jac = self.__discipline.jac[self.__output_names[0]][name]
self.__input_size += jac.shape[1]
self.__input_indices[name] = slice(start, self.__input_size)
start = self.__input_size
def __compute_output_indices(self):
"""Compute the indices of the input variables in the Jacobian array."""
start = 0
self.__output_size = 0
self.__output_indices = {}
for name in self.__output_names:
jac = self.__discipline.jac[name][self.__input_names[0]]
self.__output_size += jac.shape[0]
self.__output_indices[name] = slice(start, self.__output_size)
start = self.__output_size
@property
def _default_inputs(self) -> dict[str, ndarray]:
"""The default values of the inputs of the function at execution time.
They correspond to the default values of the discipline when calling this
property, and updated with :attr:`.__default_inputs` if not None.
"""
default_inputs = self.__discipline.default_inputs
if self.__default_inputs is not None:
default_inputs.update(self.__default_inputs)
return default_inputs
def _func(self, x_vect: ndarray) -> OperandType:
"""A function which executes a discipline for specific inputs and outputs.
Args:
x_vect: The input data of the function.
Returns:
The output data of the function.
"""
for input_name in self.__input_names:
if input_name not in self.__discipline.default_inputs:
raise ValueError(
"Discipline {} has no default input named {},"
"while input is required by MDOFunction.".format(
self.__discipline.name, input_name
)
)
self.__discipline.reset_statuses_for_run()
input_data = self.__compute_input_data(x_vect)
output_data = self.__discipline.execute(input_data)
output_data = concatenate_dict_of_arrays_to_array(
output_data, self.__output_names
)
if output_data.size == 1: # Then the function is scalar
return output_data[0]
return output_data
def _func_jac(self, x_vect: ndarray) -> ndarray:
"""A function which linearizes a discipline for specific inputs and outputs.
Args:
x_vect: The input data of the function.
Returns:
The Jacobian of the discipline for specific inputs and outputs.
"""
self.__discipline.linearize(self.__compute_input_data(x_vect))
if self.__jacobian is None:
self.__compute_input_indices()
self.__compute_output_indices()
if self.__output_size == 1:
self.__jacobian = empty(self.__input_size)
else:
self.__jacobian = empty((self.__output_size, self.__input_size))
if self.__output_size == 1:
output_name = self.__output_names[0]
for input_name in self.__input_names:
in_indices = self.__input_indices[input_name]
jac = self.__discipline.jac[output_name][input_name]
self.__jacobian[in_indices] = jac[0, :]
else:
for output_name in self.__output_names:
out_indices = self.__output_indices[output_name]
for input_name in self.__input_names:
in_indices = self.__input_indices[input_name]
jac = self.__discipline.jac[output_name][input_name]
self.__jacobian[out_indices, in_indices] = jac
return self.__jacobian
def __compute_input_data(
self,
x_vect: ndarray,
) -> dict[str, ndarray]:
"""Return the input data of the underlying discipline.
Args:
x_vect: The input vector of the function.
Returns:
The input data of the underlying discipline.
"""
return update_dict_of_arrays_from_array(
self._default_inputs, self.__input_names, x_vect, copy=False
)