import typing
from more.api.exceptions.api_exception import NameNotFoundError, IndexNotFoundError, NotCompatibleError, \
NameNotUniqueError
from typing import NamedTuple
from more.api.exceptions.api_exception import NameNotFoundError, IndexNotFoundError, NotCompatibleError
from more.api.simulation.data_tables.data_table_setup import DataTableSetup
from more.api.utils.change_duplicate_name import change_duplicate_name
from more.api.utils.interface_registries import register_api_implementation
from more.transient_data_table import TransientDataTable
from more import log
from os import PathLike
import numpy as np
import numpy.typing as npt
log.init_logging()
logger = log.getLogger(__name__)
def _create_transient_data_table_setup(api) -> 'TransientDataTableSetup':
simulation_setup = api.create_simulation_setup()
return simulation_setup.create_data_table_setup(data_table_type_name='Transient data table')
[docs]
class VarAndData(NamedTuple):
""" Named tuple containing the data points and the data itself with shapes (n), (n, m) respectively
Shapes are not enforced
"""
var: npt.ArrayLike
data: npt.ArrayLike
[docs]
@register_api_implementation(core_class=TransientDataTable)
class TransientDataTableSetup(DataTableSetup):
""" API class for handling transient data tables.
In order to create a transient data table using the API perform the following:
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> simulation_setup = ApiGateway(proj=proj).create_simulation_setup()
>>> data_table_setup = simulation_setup \\
... .create_data_table_setup(data_table_type_name='Transient data table')
..
>>> isinstance(data_table_setup._data_table, TransientDataTable)
True
In order to get an API setup object for a transient data table that already exists:
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> simulation_setup = ApiGateway(proj=proj).create_simulation_setup()
..
>>> data_table_setup_1 = simulation_setup \\
... .create_data_table_setup(data_table_type_name='Transient data table').set_name(name='Example data table')
>>> data_table_setup = simulation_setup.get_data_table_setup(name='Example data table') # The data table must already exist
..
>>> data_table_setup._data_table == data_table_setup_1._data_table
True
The following examples assume that a TransientDataTableSetup class exists as created by one of the above methods.
"""
[docs]
def import_data(self, path: typing.Union[str, PathLike]) -> 'TransientDataTableSetup':
""" Imports data from a .mat file to the data table
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup_1 = _create_transient_data_table_setup(api)
>>> import numpy as np
>>> var_val, data_val = np.array([0, 0.3, 0.6]), np.array([[0, 1e-3, 2e-3], [1, 2, 3]])
>>> data_table_setup_1 = data_table_setup_1 \\
... .set_data(var=var_val, data=data_val)
>>> data_table_setup_2 = _create_transient_data_table_setup(api)
>>> import tempfile
>>> import os
>>> with tempfile.TemporaryDirectory(prefix='more_') as tmp_dir:
... data_path = os.path.join(tmp_dir, 'data.mat')
... data_table_setup_1 = data_table_setup_1.export_data(path=data_path)
... data_table_setup_2 = data_table_setup_2.import_data(path=data_path)
..
>>> np.allclose(data_table_setup_1._data_table.dataset.var, data_table_setup_2._data_table.dataset.var)
True
>>> np.allclose(data_table_setup_1._data_table.dataset.data, data_table_setup_2._data_table.dataset.data)
True
>>> data_table_setup_1.import_data(path='Non existent path.mat')
Traceback (most recent call last):
...
FileNotFoundError: [Errno 2] No such file or directory: 'Non existent path.mat'
Parameters
----------
path
A string of PathLike with the path to the desired export location
Returns
-------
self
This object
Raises
------
FileNotFoundError
Raised if the file with the given name was not found
"""
self.transient_data_table.import_data(path)
return self
[docs]
def export_data(self, path: typing.Union[str, PathLike]) -> 'TransientDataTableSetup':
""" Exports a data to a given location as a .mat file
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup = _create_transient_data_table_setup(api)
>>> import numpy as np
>>> var_val, data_val = np.array([0, 0.3, 0.6]), np.array([[0, 1e-3, 2e-3], [1, 2, 3]])
>>> data_table_setup = data_table_setup.set_data(var=var_val, data=data_val)
>>> import tempfile
>>> import os
>>> with tempfile.TemporaryDirectory(prefix='more_') as tmp_dir:
... data_path = os.path.join(tmp_dir, 'data.mat')
... data_table_setup.export_data(path=data_path)
<...>
Parameters
----------
path
A string of PathLike with the path to the desired export location
Returns
-------
self
This object
Raises
------
"""
self.transient_data_table.export_data(path)
return self
@property
def available_interpolation_method_names(self) -> typing.List[str]:
""" Returns the available interpolation method names
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup = _create_transient_data_table_setup(api)
>>> print(f'Available interpolation methods are: \"{data_table_setup.available_interpolation_method_names}\"')
Available interpolation methods are: "['hold', 'nearest']"
Returns
-------
methods: List[str]
The available interpolation methods
"""
return self.transient_data_table.dataset.get_compatible_interpolation_methods()
[docs]
def set_interpolation_method(self, method_name: str) -> 'TransientDataTableSetup':
""" Sets the interpolation method
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup = _create_transient_data_table_setup(api)
>>> import numpy as np
>>> var_val, data_val = np.array([0, 0.3, 0.6]), np.array([[0, 1e-3, 2e-3], [1, 2, 3]])
>>> data_table_setup = data_table_setup \\
... .set_data(var=var_val, data=data_val)
>>> data_table_setup = data_table_setup.set_interpolation_method(method_name='linear')
..
>>> data_table_setup._data_table.interpolation == 'linear'
True
>>> data_table_setup.set_interpolation_method(method_name='Non existent interpolation method')
Traceback (most recent call last):
...
more.api.exceptions.api_exception.NameNotFoundError: Interpolation method with name: "Non existent interpolation method" not found.
>>> data_table_setup.set_interpolation_method(method_name=None)
Traceback (most recent call last):
...
TypeError: The interpolation_method must be a string, but a type: "<class 'NoneType'>" was given
>>> data_table_setup.set_interpolation_method(method_name='bspline')
Traceback (most recent call last):
...
more.api.exceptions.api_exception.NotCompatibleError: Interpolation method: "bspline" is not compatible with the current data shape. Compatible types are: "['linear', 'hold', 'nearest']"
Parameters
----------
method_name
The name of the interpolation method
Returns
-------
self
This object
Raises
------
TypeError
Raised it the interpolation method name is not a string
NameNotFoundError
Raised if the given interpolation method name does not exist
NotCompatibleError
Raised if the interpolation method exists, but is not compatible with the data
"""
if not isinstance(method_name, str):
raise TypeError(f'The interpolation_method must be a string, but a type: \"{type(method_name)}\" was given')
if method_name not in list(self.transient_data_table.dataset.trait('interpolation').trait_type.values):
raise NameNotFoundError(f'Interpolation method with name: \"{method_name}\" not found.')
if method_name not in self.available_interpolation_method_names:
raise NotCompatibleError(f'Interpolation method: \"{method_name}\" is not compatible with the current data shape. Compatible types are: \"{self.available_interpolation_method_names}\"')
self._data_table.interpolation = method_name
return self
[docs]
def set_data(self, var: np.ndarray, data: np.ndarray) -> 'TransientDataTableSetup':
""" Sets the data table data
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup = _create_transient_data_table_setup(api)
>>> import numpy as np
>>> var_val, data_val = np.array([0, 0.3, 0.6]), np.array([[0, 1e-3, 2e-3], [1, 2, 3]])
>>> data_table_setup = data_table_setup \\
... .set_data(var=var_val, data=data_val)
..
>>> np.allclose(data_table_setup._data_table.dataset.var, var_val)
True
>>> np.allclose(data_table_setup._data_table.dataset.data, data_val)
True
>>> data_table_setup.set_data(var='not a numpy array', data=data_val)
Traceback (most recent call last):
...
TypeError: The "var" value must be a numpy array, but type: "<class 'str'>" was given
>>> data_table_setup.set_data(var=var_val, data='Not an array')
Traceback (most recent call last):
...
TypeError: The "data" value must be a numpy array, but type: "<class 'str'>" was given
>>> data_table_setup.set_data(var=var_val, data=np.array([[0, 1], [1e-3, 2]]))
Traceback (most recent call last):
...
ValueError: The "var" and "data" shapes should be respectively (n, ) and (m, n) but var.shape = (3,) and data.shape = (2, 2) were given
>>> data_table_setup.set_data(var=var_val, data=np.array([0, 1, 2]))
Traceback (most recent call last):
...
ValueError: The "var" and "data" shapes should be respectively (n, ) and (m, n) but var.shape = (3,) and data.shape = (3,) were given
Parameters
----------
var: np.ndarray
A 1D array of shape (n, ) representing the data points of the array, with 'n' being the number of rows in
the table. The table will grow or shrink to accomodate this size.
data: np.ndarray
A 2D array of shape (m, n) with 'n' being the number of rows, and 'm' being the number of data-carrying
columns in the table.
Returns
-------
self
This object
Raises
------
TypeError
Raised it either var or data are not numpy arrays
ValueError
Raised if the provided var or data shapes are not compatible
"""
if not isinstance(var, np.ndarray):
raise TypeError(f'The \"var\" value must be a numpy array, but type: \"{type(var)}\" was given')
if not isinstance(data, np.ndarray):
raise TypeError(f'The \"data\" value must be a numpy array, but type: \"{type(data)}\" was given')
if not len(var.shape) == 1 or not len(data.shape) == 2 or data.shape[1] != var.shape[0]:
raise ValueError(f'The \"var\" and \"data\" shapes should be respectively (n, ) and (m, n) but var.shape = {var.shape} and data.shape = {data.shape} were given')
self._data_table.set_data(var, data)
return self
[docs]
def set_column_name(self, index: int, new_name: str) -> 'TransientDataTableSetup':
""" Set the name of the column
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup = _create_transient_data_table_setup(api)
>>> import numpy as np
>>> var_val, data_val = np.array([0, 0.3, 0.6]), np.array([[0, 1e-3, 2e-3], [1, 2, 3]])
>>> data_table_setup = data_table_setup \\
... .set_data(var=var_val, data=data_val)
>>> data_table_setup = data_table_setup.set_column_name(index=1, new_name='Second column')
..
>>> data_table_setup._data_table.column_headers[2] == 'Second column'
True
>>> data_table_setup.set_column_name(index=3, new_name='Some name')
Traceback (most recent call last):
...
more.api.exceptions.api_exception.IndexNotFoundError: Index "3" is out of bounds, the highest possible index is: "1"
>>> data_table_setup.set_column_name(index=None, new_name='Some name')
Traceback (most recent call last):
...
TypeError: ...
>>> data_table_setup.set_column_name(index=2, new_name=None)
Traceback (most recent call last):
...
TypeError: ...
Parameters
----------
index
The index of the column whose name is to be changed
new_name
The new name to apply
Returns
-------
self
This object
Raises
------
IndexNotFoundError
Raised if the given index is out of range for the given data table
TypeError
Raised if one of the parameters has the wrong type
"""
if not isinstance(index, int):
raise TypeError(f'The index must be an integer, but type: \"{type(index)}\" was given')
if not isinstance(new_name, str):
raise TypeError(f'The new_name must be a string, but type: \"{type(index)}\" was given')
headers = self._data_table.column_headers[1:]
if index >= len(headers):
raise IndexNotFoundError(f'Index \"{index}\" is out of bounds, the highest possible index is: \"{len(headers) - 1}\"')
self._data_table.tabular.column_headers[index + 1] = new_name
return self
[docs]
def get_column_name(self, index: int) -> str:
""" Get the name of the column from the index
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup = _create_transient_data_table_setup(api)
>>> import numpy as np
>>> var_val, data_val = np.array([0, 0.3, 0.6]), np.array([[0, 1e-3, 2e-3], [1, 2, 3]])
>>> data_table_setup = data_table_setup \\
... .set_data(var=var_val, data=data_val)
>>> data_table_setup = data_table_setup.set_column_name(index=1, new_name='Second column')
>>> data_table_setup.get_column_name(index=1)
'Second column'
..
>>> data_table_setup.get_column_name(index=10)
Traceback (most recent call last):
...
more.api.exceptions.api_exception.IndexNotFoundError: Index "10" is out of bounds, the highest possible index is: "1"
>>> data_table_setup.get_column_name(index=None)
Traceback (most recent call last):
...
TypeError: ...
Parameters
----------
index
The index of the column for which teh name is requested
Returns
-------
column_name: str
the name of the column
Raises
------
TypeError
Raised if the index is not an integer
IndexNotFoundError
Raised if the given index is not in range
"""
if not isinstance(index, int):
raise TypeError(f'The index must be an integer, but type: \"{type(index)}\" was given')
headers = self._data_table.column_headers[1:]
if index >= len(headers):
raise IndexNotFoundError(f'Index \"{index}\" is out of bounds, the highest possible index is: \"{len(headers) - 1}\"')
return self._data_table.column_headers[index + 1]
[docs]
def get_column_index(self, name: str) -> int:
""" Get the index of a column with a given name
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup = _create_transient_data_table_setup(api)
>>> import numpy as np
>>> var_val, data_val = np.array([0, 0.3, 0.6]), np.array([[0, 1e-3, 2e-3], [1, 2, 3]])
>>> data_table_setup = data_table_setup \\
... .set_data(var=var_val, data=data_val)
>>> data_table_setup = data_table_setup.set_column_name(index=1, new_name='Second column')
>>> data_table_setup.get_column_index(name='Second column')
1
..
>>> data_table_setup.get_column_index(name='Non existent name')
Traceback (most recent call last):
...
more.api.exceptions.api_exception.NameNotFoundError: ...
>>> data_table_setup.get_column_index(name=None)
Traceback (most recent call last):
...
TypeError: A column name must be a string, but type: "<class 'NoneType'>" was given
>>> idx = data_table_setup.get_column_index(name='Second column')
>>> data_table_setup.get_column_name(index=idx)
'Second column'
Parameters
----------
name: str
The name of the column for which to returen the index
Returns
-------
column_index: int
The index of the column with the given name
Raises
------
NameNotFoundError
Raised if a column with such a name does not exist
TypeError
Raised if the given parameter is not a string
"""
if not isinstance(name, str):
raise TypeError(f'A column name must be a string, but type: \"{type(name)}\" was given')
headers = self._data_table.column_headers[1:]
if name not in headers:
raise NameNotFoundError(f'Name: \"{name}\" is not one of the column header names, available names are: \"{headers}\" ')
return self._data_table.column_headers.index(name) - 1
[docs]
def get_data(self) -> VarAndData:
""" Returns the data table data
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup = _create_transient_data_table_setup(api)
>>> import numpy as np
>>> var_val, data_val = np.array([0, 0.3, 0.6]), np.array([[0, 1e-3, 2e-3], [1, 2, 3]])
>>> data_table_setup = data_table_setup \\
... .set_data(var=var_val, data=data_val)
>>> var, data = data_table_setup.get_data()
..
>>> np.allclose(var, var_val)
True
>>> np.allclose(data, data_val)
True
Returns
-------
data tuple
Var and data in the form of a named tuple containing the data points and the data itself from the data table with shapes (n), (n, m) respectively
"""
return self._data_table.dataset.var, self._data_table.dataset.data
[docs]
def set_name(self, name: str, resolve_duplicate_name: bool = False) -> 'DataTableSetup':
""" Changes the name of the simresults
.. admonition:: Example
:class: note
..
>>> import more.project
>>> proj = more.project.Project()
>>> from more.api import ApiGateway
>>> api = ApiGateway(proj=proj)
>>> data_table_setup = _create_transient_data_table_setup(api)
>>> second_data_table_setup = _create_transient_data_table_setup(api)
>>> data_table_setup.set_name('new_name')
<more...>
>>> second_data_table_setup.set_name(name='new_name', resolve_duplicate_name=True).get_name()
'new_name 1'
..
>>> data_table_setup._data_table.name
'new_name'
Trying to set the name to a non-string value
>>> data_table_setup.set_name(name=None)
Traceback (most recent call last):
...
TypeError: ...
Setting a non-unique name
>>> second_data_table_setup.set_name('new_name')
Traceback (most recent call last):
...
more.api.exceptions.api_exception.NameNotUniqueError: ...
Parameters
----------
resolve_duplicate_name: bool
Whether to automatically assign a new name when the chosen one is already taken
name: str
The new name
Returns
-------
self
This object
Raises
------
TypeError
Raised if the given name is not a string
NameNotUniqueError
Raised if the given name is not unique
"""
pass
if not isinstance(name, str):
raise TypeError("The given name: \"{}\" is not a string, but a \"{}\"".format(name, type(name)))
created_names = [dt.name for dt in self._data_table.parent.elements]
if name in created_names:
if not resolve_duplicate_name:
raise NameNotUniqueError(f'The given name: \"{name}\" is already given in the list: \"{created_names}\"')
else:
name = change_duplicate_name(name=name, names_list=created_names)
self._data_table.name = name
return self
def get_name(self) -> str:
return self._data_table.name