Source code for sidpy.sid.dimension

# -*- coding: utf-8 -*-
"""
Objects that represent dimensions or axes of scientific datasets

Created on Thu Jul  7 21:14:25 2020

@author: Gerd Duscher, Suhas Somnath
"""

from __future__ import division, print_function, unicode_literals, \
    absolute_import
from warnings import warn
import sys
import numpy as np
from enum import Enum
from sidpy.base.string_utils import validate_single_string_arg
import copy

__all__ = ['Dimension', 'DimensionType']

if sys.version_info.major == 3:
    unicode = str


class DimensionType(Enum):
    """
    Physical type of Dimension object. This information will be used for
    visualization and processing purposes.
    """
    UNKNOWN = -1
    SPATIAL = 1
    RECIPROCAL = 2
    SPECTRAL = 3
    TEMPORAL = 4
    CHANNEL = 5


class Dimension(np.ndarray):
    """
    """

    def __new__(cls, values, name='none', quantity='generic', units='generic',
                dimension_type=DimensionType.UNKNOWN, *args, **kwargs):
        """
        Parameters
        ----------
        name : str or unicode
            Name of the dimension. For example 'X'
        quantity : str or unicode
            Quantity for this dimension. For example: 'Length'
        units : str or unicode
            Units for this dimension. For example: 'um'
        values : array-like or int
            Values over which this dimension was varied. A linearly increasing
            set of values will be generated if an integer is provided instead
            of an array.
        dimension_type : str or sidpy.sid.dimension.DimensionType
            For example: 'spectral', 'spatial', 'reciprocal', 'channel', or 'UNKNOWN',
            'time', 'frame', 'reciprocal'
            This will determine how the data are visualized. 'spatial' are
            image dimensions. 'spectral' indicate spectroscopy data dimensions.

        Attributes
        ----------
        self.name : str
            Name of the dimension
        self.quantity : str
            Physical quantity. E.g. - current
        self.units : str
            Physical units. E.g. - amperes
        self.dimension_type : enum
            Type of dimension. E.g. - Spectral, Spatial, etc.
        self.values : array-like
            Values over which this dimension was varied
        """
        if isinstance(values, int):
            if values < 1:
                raise TypeError("When specifying the size of a Dimension, "
                                "values should at be integers > 1")
            values = np.arange(values)
        elif len(np.array(values)) < 1:
            raise TypeError("When specifying values over which a parameter is "
                            "varied, values should not be an empty array")
        if np.array(values).ndim != 1:
            raise ValueError('Dimension can only be 1 dimensional')
        new_dim = np.asarray(values, dtype=float).view(cls)
        new_dim._name = validate_single_string_arg(name, 'name')
        new_dim.quantity = quantity
        new_dim.units = units
        new_dim.dimension_type = dimension_type
        return new_dim

    def __array_finalize__(self, obj):
        # see InfoArray.__array_finalize__ for comments
        if obj is None:
            return
        self._name = validate_single_string_arg(getattr(obj, '_name', 'generic'), 'name')
        self.quantity = getattr(obj, '_quantity', 'generic')
        self.units = getattr(obj, '_units', 'generic')
        self.dimension_type = getattr(obj, '_dimension_type', 'UNKNOWN')

    def __array_wrap__(self, out_arr, context=None):
        # just call the parent
        super(Dimension, self).__array_wrap__(self, out_arr, context)
        # return correct values
        return out_arr

    def __repr__(self):
        return '{}:  {} ({}) of size {}'.format(self.name, self.quantity, self.units, self.shape)

    def __str__(self):
        return '{}:  {} ({}) of size {}'.format(self.name, self.quantity, self.units, self.shape)

    # def __copy__(self):
    #     new_dim = Dimension(np.array(self), name=self.name, quantity=self.quantity, units=self.units)
    #     new_dim.dimension_type = self.dimension_type
    #     return new_dim

    def __copy__(self):
        # Create a new instance of Dimension
        new_instance = Dimension(
            copy.copy(self.values),
            copy.copy(self.name),
            copy.copy(self.quantity),
            copy.copy(self.units),
            copy.copy(self.dimension_type)
        )

        return new_instance

    def __deepcopy__(self, memo):
        # For now this is what chatGPT came up with and it does not break any tests

        # Create a new instance of Dimension
        new_instance = Dimension(
            copy.deepcopy(self.values, memo),
            copy.deepcopy(self.name, memo),
            copy.deepcopy(self.quantity, memo),
            copy.deepcopy(self.units, memo),
            copy.deepcopy(self.dimension_type, memo)
        )

        return new_instance

    # TODO: Implement equality

    # TODO: Find out how to get rid of this
[docs] def copy(self): # Not sure why __copy__() would not be called by itself new_dim = self.__copy__() return new_dim
@property def info(self): return '{} - {} ({}): {}'.format(self.name, self.quantity, self.units, self.values) @property def name(self): return self._name @name.setter def name(self, value): raise AttributeError("Cannot change the name of the dimension. " "If the dimension is associated with the dataset, please try " "dataset.rename_dimension") # # self._name = validate_single_string_arg(value, 'name') @property def quantity(self): return self._quantity @quantity.setter def quantity(self, value): self._quantity = validate_single_string_arg(value, 'quantity') @property def units(self): return self._units @units.setter def units(self, value): self._units = validate_single_string_arg(value, 'units') @property def dimension_type(self): return self._dimension_type @dimension_type.setter def dimension_type(self, value): if isinstance(value, DimensionType): self._dimension_type = value else: dimension_type = validate_single_string_arg(value, 'dimension_type') if dimension_type.upper() in [member.name for member in DimensionType]: self._dimension_type = DimensionType[dimension_type.upper()] elif dimension_type.lower() in ['frame', 'time', 'stack']: self._dimension_type = DimensionType.TEMPORAL else: self._dimension_type = DimensionType.UNKNOWN warn('Supported dimension types for plotting are only: {}' ''.format([member.name for member in DimensionType])) warn('Setting DimensionType to UNKNOWN') @property def values(self): return np.array(self) # @values.setter # def values(self, value): # isinstance(np.ndarray) def __eq__(self, other): if not isinstance(other, Dimension): return False if self.name != other.name: return False if self.units != other.units: return False if self.quantity != other.quantity: return False if len(self.values) != len(other): return False if not (np.array(self) == np.array(other)).all(): return False if not (self.values == other.values).all(): return False return True