Source code for csdmpy.dimensions

# -*- coding: utf-8 -*-
"""The Dimension object: attributes and methods."""
import json
import warnings
from copy import deepcopy

from csdmpy.dimensions.labeled import LabeledDimension
from csdmpy.dimensions.linear import LinearDimension
from csdmpy.dimensions.monotonic import MonotonicDimension
from csdmpy.units import frequency_ratio
from csdmpy.utils import _axis_label
from csdmpy.utils import _get_dictionary
from csdmpy.utils import attribute_error
from csdmpy.utils import validate

__author__ = "Deepansh J. Srivastava"
__email__ = "srivastava.89@osu.edu"
__all__ = ["Dimension"]


functional_dimension = ["linear"]


[docs]class Dimension: r""" Create an instance of the Dimension class. An instance of this class describes a dimension of a multi-dimensional system. In version 1.0 of the CSD model, there are three subtypes of the Dimension class: - :ref:`linearDimension_uml`, - :ref:`monotonicDimension_uml`, and - :ref:`labeledDimension_uml`. **Creating an instance of a dimension object** There are two ways of creating a new instance of a Dimension class. *From a python dictionary containing valid keywords.* .. doctest:: >>> from csdmpy import Dimension >>> dimension_dictionary = { ... 'type': 'linear', ... 'description': 'test', ... 'increment': '5 G', ... 'count': 10, ... 'coordinates_offset': '10 mT', ... 'origin_offset': '10 T' ... } >>> x = Dimension(dimension_dictionary) Here, `dimension_dictionary` is the python dictionary. *From valid keyword arguments.* .. doctest:: >>> x = Dimension(type = 'linear', ... description = 'test', ... increment = '5 G', ... count = 10, ... coordinates_offset = '10 mT', ... origin_offset = '10 T') """ __slots__ = ("subtype",) _immutable_objects_ = () def __init__(self, *args, **kwargs): """Initialize an instance of Dimension object.""" default = { "type": None, # valid for all dimension subtypes "description": "", # valid for all dimension subtypes "count": None, # valid for linear dimension subtype "increment": None, # valid for linear dimension subtype "labels": None, # valid for labled dimension subtype "coordinates": None, # valid for monotonic dimension subtype "coordinates_offset": None, # valid for linear dimension subtype "origin_offset": None, # valid for linear dimension subtype "complex_fft": False, # valid for linear dimension subtype "period": None, # valid for monotonic and linear dimension subtypes "quantity_name": None, # valid for monotonic and linear dimension subtypes "label": "", # valid for all dimension subtypes "application": {}, # valid for all dimension subtypes "reciprocal": { # valid for all monotonic and linear subtypes "increment": None, # valid for all monotonic and linear subtypes "coordinates_offset": None, # valid for all monotonic and linear subtypes "origin_offset": None, # valid for all monotonic and linear subtypes "period": None, # valid for all monotonic and linear subtypes "quantity_name": None, # valid for all monotonic and linear subtypes "label": "", # valid for all monotonic and linear subtypes "description": "", # valid for all monotonic and linear subtypes "application": {}, # valid for all monotonic and linear subtypes }, } default_keys = default.keys() input_dict = _get_dictionary(*args, **kwargs) input_keys = input_dict.keys() if "type" not in input_keys: raise KeyError("Missing a required 'type' key from the dimension object.") if "reciprocal" in input_keys: input_subkeys = input_dict["reciprocal"].keys() for key in input_keys: if key in default_keys: if key == "reciprocal": for subkey in input_subkeys: default[key][subkey] = input_dict[key][subkey] else: default[key] = input_dict[key] _valid_types = ["monotonic", "linear", "labeled"] type_ = default["type"] message = ( f"'{type_}' is an invalid value for the dimension type. " "The allowed values are 'monotonic', 'linear' and 'labeled'." ) if default["type"] not in _valid_types: raise ValueError(message) if default["type"] == "labeled" and default["labels"] is None: raise KeyError("LabeledDimension is missing a required `labels` key.") if default["type"] == "labeled": self.subtype = LabeledDimension(**default) if default["type"] == "monotonic" and default["coordinates"] is None: raise KeyError( "Missing a required `coordinates` key from the MonotonicDimension " "object." ) if default["type"] == "monotonic": self.subtype = MonotonicDimension(values=default["coordinates"], **default) if default["type"] == "linear": self.subtype = self._linear(default) def _linear(self, default): """Create and assign a linear dimension.""" missing_key = ["increment", "count"] for item in missing_key: if default[item] is None: raise KeyError( f"Missing a required `{item}` key from the " "LinearDimension object." ) validate(default["count"], "count", int) return LinearDimension(**default) def __repr__(self): properties = ", ".join([f"{k}={v}" for k, v in self.to_dict().items()]) return f"Dimension({properties})" def __str__(self): properties = ", ".join([f"{k}={v}" for k, v in self.to_dict().items()]) return f"Dimension({properties})" # ======================================================================= # # Dimension Attributes # # ======================================================================= # @property def absolute_coordinates(self): r""" Absolute coordinates, :math:`\bf X_k^{\rm{abs}}`, along the dimension. This attribute is only *valid* for quantitative dimensions, that is, `linear` and `monotonic` dimensions. The absolute coordinates are given as .. math:: \mathbf{X}_k^\mathrm{abs} = \mathbf{X}_k + o_k \mathbf{1} where :math:`\mathbf{X}_k` are the coordinates along the dimension and :math:`o_k` is the :attr:`~csdmpy.dimensions.Dimension.origin_offset`. For example, consider .. doctest:: >>> print(x.origin_offset) 10.0 T >>> print(x.coordinates[:5]) [100. 105. 110. 115. 120.] G then the absolute coordinates are .. doctest:: >>> print(x.absolute_coordinates[:5]) [100100. 100105. 100110. 100115. 100120.] G For `linear` dimensions, the order of the `absolute_coordinates` further depend on the value of the :attr:`~csdmpy.dimensions.Dimension.complex_fft` attributes. For examples, when the value of the `complex_fft` attribute is True, the absolute coordinates are .. doctest:: >>> x.complex_fft = True >>> print(x.absolute_coordinates[:5]) [100075. 100080. 100085. 100090. 100095.] G .. testsetup:: >>> x.complex_fft = False Returns: A Quantity array of absolute coordinates for quantitative dimensions, `i.e` `linear` and `monotonic`. Raises: AttributeError: For labeled dimensions. """ if self.subtype != "labeled": return (self.coordinates + self.origin_offset).to(self.subtype._unit) raise AttributeError(attribute_error(self.subtype, "absolute_coordinates")) @property def application(self): """ Application metadata dictionary of the dimension object. .. doctest:: >>> print(x.application) {} The application attribute is where an application can place its own metadata as a python dictionary object containing application specific metadata, using a reverse domain name notation string as the attribute key, for example, .. doctest:: >>> x.application = { ... "com.example.myApp" : { ... "myApp_key": "myApp_metadata" ... } ... } >>> print(x.application) {'com.example.myApp': {'myApp_key': 'myApp_metadata'}} Returns: A python dictionary containing dimension application metadata. """ return self.subtype._application @application.setter def application(self, value): self.subtype.application = value @property def axis_label(self): r""" Formatted string for displaying label along the dimension axis. This attribute is not a part of the original core scientific dataset model, however, it is a convenient supplementary attribute that provides a formated string ready for labeling dimension axes. For quantitative dimensions, this attributes returns a string, `label / unit`, if the `label` is a non-empty string, otherwise, `quantity_name / unit`. Here :attr:`~csdmpy.dimensions.Dimension.quantity_name` and :attr:`~csdmpy.dimensions.Dimension.label` are the attributes of the :ref:`dim_api` instances, and `unit` is the unit associated with the coordinates along the dimension. For examples, .. doctest:: >>> x.label 'field strength' >>> x.axis_label 'field strength / (G)' For `labeled` dimensions, this attribute returns `label`. Returns: A formated string of label. Raises: AttributeError: When assigned a value. """ if hasattr(self.subtype, "quantity_name"): if self.label.strip() == "": label = self.quantity_name else: label = self.label return _axis_label(label, self.subtype._unit) else: return self.label @property def coordinates(self): r""" Coordinates, :math:`{\bf X}_k`, along the dimension. Example: >>> print(x.coordinates) [100. 105. 110. 115. 120. 125. 130. 135. 140. 145.] G For `linear` dimensions, the order of the `coordinates` also depend on the value of the :attr:`~csdmpy.dimensions.Dimension.complex_fft` attributes. For examples, when the value of the `complex_fft` attribute is True, the coordinates are .. doctest:: >>> x.complex_fft = True >>> print(x.coordinates) [ 75. 80. 85. 90. 95. 100. 105. 110. 115. 120.] G .. testsetup:: >>> x.complex_fft = False Returns: A Quantity array of coordinates for quantitative dimensions, `i.e.` `linear` and `monotonic`. Returns: A Numpy array for labeled dimensions. Raises: AttributeError: For dimensions with subtype `linear`. """ n = self.subtype._count if self.type == "monotonic": return self.subtype._coordinates[:n] if self.type == "linear": unit = self.subtype._unit equivalent_fn = self.subtype._equivalencies coordinates = self.subtype._coordinates[:n] + self.coordinates_offset if equivalent_fn is None: return coordinates.to(self.subtype._unit) return coordinates.to(unit, equivalent_fn(self.origin_offset)) if self.type == "labeled": return self.subtype.labels[:n] @coordinates.setter def coordinates(self, value): if self.type == "monotonic": self.subtype.values = value if self.type == "labeled": self.subtype.labels = value if self.type == "linear": raise AttributeError( ( "The attribute cannot be modifed for dimensions with subtype `linear`. " "Use `count`, `increment` or `coordinates_offset` attributes to update " "the coordinate along a linear dimension." ) ) @property def data_structure(self): r""" Json serialized string describing the Dimension class instance. This supplementary attribute is useful for a quick preview of the dimension object. The attribute cannot be modified. .. doctest:: >>> print(x.data_structure) { "type": "linear", "description": "This is a test", "count": 10, "increment": "5.0 G", "coordinates_offset": "10.0 mT", "origin_offset": "10.0 T", "quantity_name": "magnetic flux density", "label": "field strength" } Returns: A json serialized string of the dimension object. Raises: AttributeError: When modified. """ return json.dumps(self.to_dict(), ensure_ascii=False, sort_keys=False, indent=2) @property def description(self): """ Brief description of the dimension object. The default value is an empty string, ''. The attribute may be modified, for example, .. doctest:: >>> print(x.description) This is a test >>> x.description = 'This is a test dimension.' Returns: A string of UTF-8 allows characters describing the dimension. Raises: TypeError: When the assigned value is not a string. """ return self.subtype.description @description.setter def description(self, value): self.subtype.description = value @property def complex_fft(self): r""" Boolean specifying if the coordinates along the dimension are the output of a complex fft. This attribute is only `valid` for the Dimension instances with `linear` subtype. The value of this attribute is a boolean specifying if the coordinates along the dimension are evaluated as the output of a complex fast Fourier transform (FFT) routine. For example, consider the following Dimension object, .. doctest:: >>> test = Dimension( ... type='linear', ... increment = '1', ... count = 10 ... ) >>> test.complex_fft False >>> print(test.coordinates) [0. 1. 2. 3. 4. 5. 6. 7. 8. 9.] >>> test.complex_fft = True >>> print(test.coordinates) [-5. -4. -3. -2. -1. 0. 1. 2. 3. 4.] Returns: A Boolean. Raises: TypeError: When the assigned value is not a boolean. """ return self.subtype.complex_fft @complex_fft.setter def complex_fft(self, value): self.subtype.complex_fft = value @property def increment(self): r""" Increment along a `linear` dimension. The attribute is only `valid` for Dimension instances with the subtype `linear`. When assigning a value, the dimensionality of the value must be consistent with the dimensionality of other members specifying the dimension. Example: >>> print(x.increment) 5.0 G >>> x.increment = "0.1 G" >>> print(x.coordinates) [100. 100.1 100.2 100.3 100.4 100.5 100.6 100.7 100.8 100.9] G Returns: A Quantity instance with the increment along the dimension. Raises: AttributeError: For dimension with subtypes other than `linear`. TypeError: When the assigned value is not a string containing a quantity or a Quantity object. """ # .. note:: The sampling interval along a grid dimension and the # respective reciprocal grid dimension follow the Nyquist–Shannon # sampling theorem. Therefore, updating the ``increment`` # will automatically trigger an update on its reciprocal # counterpart. return self.subtype.increment @increment.setter def increment(self, value): self.subtype.increment = value @property def coordinates_offset(self): r""" Offset corresponding to the zero of the indexes array, :math:`\mathbf{J}_k`. When assigning a value, the dimensionality of the value must be consistent with the dimensionality of the other members specifying the dimension. Example: >>> print(x.coordinates_offset) 10.0 mT >>> x.coordinates_offset = "0 T" >>> print(x.coordinates) [ 0. 5. 10. 15. 20. 25. 30. 35. 40. 45.] G The attribute is `invalid` for `labeled` dimensions. Returns: A Quantity instance with the coordinates offset. Raises: AttributeError: For `labeled` dimensions. TypeError: When the assigned value is not a string containing a quantity or a Quantity object. """ if self.type == "linear": return self.subtype.coordinates_offset raise AttributeError(attribute_error(self.subtype, "coordinates_offset")) @coordinates_offset.setter def coordinates_offset(self, value): self.subtype.coordinates_offset = value @property def label(self): r""" Label associated with the dimension. Example: >>> print(x.label) field strength >>> x.label = 'magnetic field strength' Returns: A string containing the label. Raises: TypeError: When the assigned value is not a string. """ return self.subtype.label @label.setter def label(self, label=""): self.subtype.label = label @property def count(self): r""" Number of coordinates, :math:`N_k \ge 1`, along the dimension. Example: >>> print(x.count) 10 >>> x.count = 5 Returns: An Integer specifying the number of coordinates along the dimension. Raises: TypeError: When the assigned value is not an integer. """ return self.subtype._count @count.setter def count(self, value): value = validate(value, "count", int) if self.type in functional_dimension: self.subtype._count = value self.subtype._get_coordinates() return if value > self.count: raise ValueError( ( f"Cannot set count, {value}, more than the number of " f"coordinates, {self.count}, for monotonic and labeled" " dimensions." ) ) if value < self.count: warnings.warn( f"The number of coordinates, {self.count}, are truncated " f"to {value}." ) self.subtype._count = value @property def origin_offset(self): r""" Origin offset, :math:`o_k`, along the dimension. When assigning a value, the dimensionality of the value must be consistent with the dimensionality of other members specifying the dimension. Example: >>> print(x.origin_offset) 10.0 T >>> x.origin_offset = "1e5 G" The origin offset only affect the absolute_coordinates along the dimension. This attribute is `invalid` for `labeled` dimensions. Returns: A Quantity instance with the origin offset. Raises: AttributeError: For `labeled` dimensions. TypeError: When the assigned value is not a string containing a quantity or a Quantity object. """ return self.subtype.origin_offset @origin_offset.setter def origin_offset(self, value): self.subtype.origin_offset = value @property def period(self): r""" Period of the dimension. The default value of the period is infinity, i.e., the dimension is non-periodic. Example: >>> print(x.period) inf G >>> x.period = '1 T' To assign a dimension as non-periodic, one of the following may be used, .. doctest:: >>> x.period = '1/0 T' >>> x.period = 'infinity µT' >>> x.period = '∞ G' .. Attention:: The physical quantity of the period must be consistent with other physical quantities specifying the dimension. Returns: A Quantity instance with the period of the dimension. Raises: AttributeError: For `labeled` dimensions. TypeError: When the assigned value is not a string containing a quantity or a Quantity object. """ return self.subtype.period @period.setter def period(self, value=None): self.subtype.period = value @property def quantity_name(self): r""" Quantity name associated with the physical quantities specifying the dimension. The attribute is `invalid` for the labeled dimension. .. doctest:: >>> print(x.quantity_name) magnetic flux density Returns: A string with the `quantity name`. Raises: AttributeError: For `labeled` dimensions. NotImplementedError: When assigning a value. """ return self.subtype.quantity_name @quantity_name.setter def quantity_name(self, value): self.subtype.quantity_name = value @property def type(self): r""" The dimension subtype. There are three *valid* subtypes of Dimension class. The valid literals are given by the :ref:`dimObjectSubtype_uml` enumeration. .. doctest:: >>> print(x.type) linear Returns: A string with a valid dimension subtype. Raises: AttributeError: When the attribute is modified. """ return self.subtype.__class__._type @property def labels(self): r""" Ordered list of labels along the `Labeled` dimension. Consider the following labeled dimension, .. doctest:: >>> x2 = Dimension( ... type='labeled', ... labels=['Cu', 'Ag', 'Au'] ... ) then the lables along the labeled dimension are .. doctest:: >>> print(x2.labels) ['Cu' 'Ag' 'Au'] .. note:: For Labeled dimension, the :attr:`~csdmpy.dimensions.Dimension.coordinates` attribute is an alias of :attr:`~csdmpy.dimensions.Dimension.labels` attribute. For example, .. doctest:: >>> np.all(x2.coordinates == x2.labels) True In the above example, ``x2`` is an instance of the :ref:`dim_api` class with `labeled` subtype. Returns: A Numpy array with labels along the dimension. Raises: AttributeError: For dimensions with subtype other than `labeled`. """ return self.coordinates @labels.setter def labels(self, array): self.subtype.labels = array self.subtype._get_coordinates(array) @property def reciprocal(self): r""" An instance of the ReciprocalVariable class. The attributes of ReciprocalVariable class are: - coordinates_offset - origin_offset - period - quantity_name - label where the definition of each attribute is the same as the corresponding attribute from the Dimension instance. """ return self.subtype.reciprocal # ======================================================================= # # Dimension Methods # # ======================================================================= #
[docs] def to_dict(self): r""" Return Dimension object as a python dictionary. Example: >>> x.to_dict() {'type': 'linear', 'description': 'This is a test', 'count': 10, 'increment': '5.0 G', 'coordinates_offset': '10.0 mT', 'origin_offset': '10.0 T', 'quantity_name': 'magnetic flux density', 'label': 'field strength'} """ return self.subtype.to_dict()
[docs] def is_quantitative(self): r""" Return True if the dependent variable is quantitative. Example: >>> x.is_quantitative() True """ return self.subtype._is_quantitative()
[docs] def to(self, unit="", equivalencies=None): r""" Convert the coordinates along the dimension to the unit, `unit`. This method is a wrapper of the `to` method from the `Quantity <http://docs.astropy.org/en/stable/api/\ astropy.units.Quantity.html#astropy.units.Quantity.to>`_ class and is only `valid` for physical dimensions. Example: >>> print(x.coordinates) [100. 105. 110. 115. 120. 125. 130. 135. 140. 145.] G >>> x.to('mT') >>> print(x.coordinates) [10. 10.5 11. 11.5 12. 12.5 13. 13.5 14. 14.5] mT Args: `unit` : A string containing a unit with the same dimensionality as the coordinates along the dimension. Raises: AttributeError: For `labeled` dimensions. """ if equivalencies == "nmr_frequency_ratio": self.subtype._to(unit, frequency_ratio) else: self.subtype._to(unit, equivalencies)
def copy(self): """Return a copy of the Dimension object.""" return deepcopy(self)