# 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 typing import TYPE_CHECKING
from typing import Any
from typing import ClassVar
from typing import Generic
from typing import TypeVar
from typing import Union
from typing import cast
from numpy import array as np_array
from numpy import concatenate
from gemseo.typing import NumberArray
from gemseo.utils.constants import READ_ONLY_EMPTY_DICT
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
ValueType = Union[int, float, complex, NumberArray]
T = TypeVar("T", bound="BaseGrammar")
[docs]
class BaseDataConverter(ABC, Generic[T]):
"""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: T
"""The grammar providing the data types used for the conversions."""
_NUMERIC_TYPES: ClassVar[tuple[type, ...]] = (int, float, complex)
"""The base types for numeric values."""
_IS_NUMERIC_TYPES: ClassVar[tuple[Any, ...]]
"""The types used for `is_numeric`."""
_IS_CONTINUOUS_TYPES: ClassVar[tuple[Any, ...]]
"""The types used for `is_continuous`."""
def __init__(self, grammar: T) -> 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,
) -> NumberArray:
"""Convert a data value to a NumPy array.
Args:
name: The data name.
value: The data value.
Returns:
The NumPy array.
"""
if isinstance(value, self._NUMERIC_TYPES):
return np_array([value])
return cast(NumberArray, value)
[docs]
def convert_array_to_value(self, name: str, array: NumberArray) -> 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]
@classmethod
def get_value_size(cls, 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, cls._NUMERIC_TYPES):
return 1
return cast(NumberArray, value).size
[docs]
def compute_names_to_slices(
self,
names: Iterable[str],
data: Data,
names_to_sizes: Mapping[str, int] = READ_ONLY_EMPTY_DICT,
) -> 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: NumberArray,
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,
) -> NumberArray:
"""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: NumberArray) -> 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]
def is_numeric(self, name: str) -> bool:
"""Check that a data item is numeric.
Args:
name: The name of the data item.
Returns:
Whether the data item is numeric.
"""
return self._has_type(name, self._IS_NUMERIC_TYPES)
[docs]
def is_continuous(self, name: str) -> bool:
"""Check that a data item has a type that can differentiate.
Args:
name: The name of the data item.
Returns:
Whether the data item can differentiate.
"""
return self._has_type(name, self._IS_CONTINUOUS_TYPES)
@abstractmethod
def _has_type(self, name: str, types: tuple[Any, ...]) -> bool:
"""Check the type of a data item against allowed types.
Args:
name: The name of the data item.
types: The allowed types.
Returns:
Whether the type of the data item is allowed.
"""