Source code for gemseo.core.data_converters.base

# 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.
"""Base class for converting data values to NumPy arrays and vice versa."""

from __future__ import annotations

from abc import ABC
from abc import abstractmethod
from types import MappingProxyType
from typing import TYPE_CHECKING
from typing import Final
from typing import Union

from numpy import array as np_array
from numpy import concatenate
from numpy import ndarray

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

    from gemseo.core.discipline_data import Data
    from gemseo.core.grammars.base_grammar import BaseGrammar

# The following is needed since a bool is a numbers.Number.
_NUMERIC_TYPES: Final[tuple[type]] = (int, float, complex)
"""The base types for numeric values."""

ValueType = Union[int, float, complex, ndarray]


[docs] class BaseDataConverter(ABC): """Base class for converting data values to NumPy arrays and vice versa. Typically, data are dictionary-like object that map names to values, such as :class:`.DisciplineData`. By default, a data converter can handle the conversion of a data value that is a standard number (``int``, ``float`` and ``complex``) or a 1D NumPy array. Other types could be handled in derived classes. A data converter can also be used to convert a data structure ``{data_name: data_value, ...}`` to a NumPy array and vice versa. In this class, a data structure is noted ``data``. For performance reasons, no checking or error handling is done when calling the methods of this class or of its derivatives. .. note:: The data converter uses a grammar, and in particular its mapping from data names to data types, to convert a NumPy array from/to a data value. .. warning:: Throughout this class, _NumPy array_ is equivalent to _1D numeric NumPy array_. """ _grammar: BaseGrammar """The grammar providing the data types used for the conversions.""" def __init__(self, grammar: BaseGrammar) -> None: """ Args: grammar: The grammar providing the data types used for the conversions. """ # noqa: D205 D212 D415 self._grammar = grammar
[docs] def convert_value_to_array( self, name: str, value: ValueType, ) -> ndarray: """Convert a data value to a NumPy array. Args: name: The data name. value: The data value. Returns: The NumPy array. """ if isinstance(value, _NUMERIC_TYPES): return np_array([value]) return value
[docs] def convert_array_to_value(self, name: str, array: ndarray) -> ValueType: """Convert a NumPy array to a data value. Args: name: The data name. array: The NumPy array to convert. Returns: The data value. """ return self._convert_array_to_value(name, array)
[docs] @staticmethod def get_value_size(name: str, value: ValueType) -> int: """Return the size of a data value. The size is typically what is returned by ``ndarray.size`` or ``len(list)``. The size of a number is 1. Args: name: The data name. value: The data value to get the size from. Returns: The size. """ if isinstance(value, _NUMERIC_TYPES): return 1 return value.size
[docs] def compute_names_to_slices( self, names: Iterable[str], data: Data, names_to_sizes: Mapping[str, int] = MappingProxyType({}), ) -> tuple[dict[str, slice], int]: """Compute a mapping from data names to data value slices. The slices are relative to a NumPy array concatenating the data values associated with these data names. Args: data: The data structure. names: The data names. names_to_sizes: The mapping from the data names to the data sizes. If empty, it will be computed. Returns: The mapping from the data names to the data slices of the expected concatenated NumPy array. and the size of this array. """ names_to_slices = {} get_size = self.get_value_size get_size_from_name = names_to_sizes.get start = 0 end = 0 for name in names: size = get_size_from_name(name) if size is None: size = get_size(name, data[name]) end = start + size names_to_slices[name] = slice(start, end) start = end return names_to_slices, end
[docs] def compute_names_to_sizes( self, names: Iterable[str], data: Data, ) -> dict[str, int]: """Compute a mapping from data names to data value sizes. .. seealso:: :meth:`.get_value_size`. Args: data: The data structure. names: The data names. Returns: The mapping from the data names to the data sizes. """ get_size = self.get_value_size return {name: get_size(name, data[name]) for name in names}
[docs] def convert_array_to_data( self, array: ndarray, names_to_slices: Mapping[str, slice], ) -> dict[str, ValueType]: """Convert a NumPy array to a data structure. .. seealso:: :meth:`.convert_array_to_value` Args: array: The NumPy array to slice. names_to_slices: The mapping from the data names to the array slices. Returns: The mapping from the data names to the array slices. """ to_value = self.convert_array_to_value return { name: to_value(name, array[slice_]) for name, slice_ in names_to_slices.items() }
[docs] def convert_data_to_array( self, names: Iterable[str], data: Data, ) -> ndarray: """Convert a part of a data structure to a NumPy array. .. seealso:: :meth:`.convert_value_to_array` Args: data: The data structure. names: The data names which values will be concatenated. Returns: The concatenated NumPy array. """ if not names: return np_array([]) to_array = self.convert_value_to_array return concatenate(tuple(to_array(name, data[name]) for name in names))
@abstractmethod def _convert_array_to_value(self, name: str, array: ndarray) -> ValueType: """Convert back a NumPy array to a data value. Args: name: The data name. array: The NumPy array to convert. Returns: The data value. """
[docs] @abstractmethod def is_numeric(self, name: str) -> bool: """Check that a data item can be converted to a NumPy array. Args: name: The name of the data item. Returns: Whether the data item can be converted to a NumPy array. """