1# -*- coding: utf-8 -*-
2"""
3Various types used in ObsPy.
4
5:copyright:
6    The ObsPy Development Team (devs@obspy.org)
7:license:
8    GNU Lesser General Public License, Version 3
9    (https://www.gnu.org/copyleft/lesser.html)
10"""
11from __future__ import (absolute_import, division, print_function,
12                        unicode_literals)
13from future.builtins import *  # NOQA
14
15from collections import OrderedDict
16
17try:
18    import __builtin__
19    list = __builtin__.list
20except ImportError:
21    pass
22
23
24class Enum(object):
25    """
26    Enumerated type (enum) implementation for Python.
27
28    :type enums: list of str
29    :type replace: dict, optional
30    :param replace: Dictionary of keys which are replaced by values.
31
32    .. rubric:: Example
33
34    >>> from obspy.core.util import Enum
35    >>> units = Enum(["m", "s", "m/s", "m/(s*s)", "m*s", "other"])
36
37    There are different ways to access the correct enum values:
38
39        >>> print(units.get('m/s'))
40        m/s
41        >>> print(units['S'])
42        s
43        >>> print(units.OTHER)
44        other
45        >>> print(units[3])
46        m/(s*s)
47        >>> units.xxx  # doctest: +ELLIPSIS
48        Traceback (most recent call last):
49        ...
50        AttributeError: 'xxx'
51
52    Changing enum values will not work:
53
54        >>> units.m = 5  # doctest: +ELLIPSIS
55        Traceback (most recent call last):
56        ...
57        NotImplementedError
58        >>> units['m'] = 'xxx'  # doctest: +ELLIPSIS
59        Traceback (most recent call last):
60        ...
61        NotImplementedError
62
63    Calling with a value will either return the mapped enum value or ``None``:
64
65        >>> print(units("M*s"))
66        m*s
67        >>> units('xxx')
68        >>> print(units(5))
69        other
70
71    The following enum allows replacing certain entries:
72
73        >>> units2 = Enum(["m", "s", "m/s", "m/(s*s)", "m*s", "other"],
74        ...               replace={'meter': 'm'})
75        >>> print(units2('m'))
76        m
77        >>> print(units2('meter'))
78        m
79    """
80    # marker needed for for usage within ABC classes
81    __isabstractmethod__ = False
82
83    def __init__(self, enums, replace={}):
84        self.__enums = OrderedDict((str(e).lower(), e) for e in enums)
85        self.__replace = replace
86
87    def __call__(self, enum):
88        try:
89            return self.get(enum)
90        except Exception:
91            return None
92
93    def get(self, key):
94        if isinstance(key, int):
95            return list(self.__enums.values())[key]
96        if key in self._Enum__replace:
97            return self._Enum__replace[key.lower()]
98        return self.__enums.__getitem__(key.lower())
99
100    def __getattr__(self, name):
101        try:
102            return self.get(name)
103        except KeyError:
104            raise AttributeError("'%s'" % (name, ))
105
106    def __setattr__(self, name, value):
107        if name == '_Enum__enums':
108            self.__dict__[name] = value
109            return
110        elif name == '_Enum__replace':
111            super(Enum, self).__setattr__(name, value)
112            return
113        raise NotImplementedError
114
115    __getitem__ = get
116    __setitem__ = __setattr__
117
118    def __contains__(self, value):
119        return value.lower() in self.__enums
120
121    def values(self):
122        return list(self.__enums.values())
123
124    def keys(self):
125        return list(self.__enums.keys())
126
127    def items(self):
128        return list(self.__enums.items())
129
130    def iteritems(self):
131        return iter(self.__enums.items())
132
133    def __str__(self):
134        """
135        >>> enum = Enum(["c", "a", "b"])
136        >>> print(enum)
137        Enum(["c", "a", "b"])
138        >>> enum = Enum(["not existing",
139        ...              "not reported",
140        ...              "earthquake",
141        ...              "controlled explosion",
142        ...              "experimental explosion",
143        ...              "industrial explosion"])
144        >>> print(enum)  # doctest: +NORMALIZE_WHITESPACE
145        Enum(["not existing", "not reported", ..., "experimental explosion",
146              "industrial explosion"])
147        """
148        return self.__repr__()
149
150    def __repr__(self):
151        """
152        >>> enum = Enum(["c", "a", "b"])
153        >>> print(repr(enum))
154        Enum(["c", "a", "b"])
155        >>> enum = Enum(["not existing",
156        ...              "not reported",
157        ...              "earthquake",
158        ...              "controlled explosion",
159        ...              "experimental explosion",
160        ...              "industrial explosion"])
161        >>> print(repr(enum))  # doctest: +NORMALIZE_WHITESPACE
162        Enum(["not existing", "not reported", ..., "experimental explosion",
163              "industrial explosion"])
164        """
165        def _repr_list_of_keys(keys):
166            return ", ".join('"{}"'.format(_i) for _i in keys)
167
168        keys = list(self.__enums.keys())
169        key_repr = _repr_list_of_keys(keys)
170        index = int(len(keys))
171        while len(key_repr) > 100:
172            if index == 0:
173                key_repr = "..."
174                break
175            index -= 1
176            key_repr = (_repr_list_of_keys(keys[:index]) + ", ..., " +
177                        _repr_list_of_keys(keys[-index:]))
178        return "Enum([{}])".format(key_repr)
179
180    def _repr_pretty_(self, p, cycle):
181        p.text(str(self))
182
183
184class CustomComplex(complex):
185    """
186    Helper class to inherit from and which stores a complex number that is
187    extendable.
188    """
189    def __new__(cls, *args):
190        return super(CustomComplex, cls).__new__(cls, *args)
191
192    def __init__(self, *args):
193        pass
194
195    def __iadd__(self, other):
196        new = self.__class__(complex(self) + other)
197        new.__dict__.update(**self.__dict__)
198        self = new
199
200    def __imul__(self, other):
201        new = self.__class__(complex(self) * other)
202        new.__dict__.update(**self.__dict__)
203        self = new
204
205
206class CustomFloat(float):
207    """
208    Helper class to inherit from and which stores a float number that is
209    extendable.
210    """
211    def __new__(cls, *args):
212        return super(CustomFloat, cls).__new__(cls, *args)
213
214    def __init__(self, *args):
215        pass
216
217    def __iadd__(self, other):
218        new = self.__class__(float(self) + other)
219        new.__dict__.update(**self.__dict__)
220        self = new
221
222    def __imul__(self, other):
223        new = self.__class__(float(self) * other)
224        new.__dict__.update(**self.__dict__)
225        self = new
226
227
228class FloatWithUncertainties(CustomFloat):
229    """
230    Helper class to inherit from and which stores a float with a given valid
231    range, upper/lower uncertainties and eventual additional attributes.
232    """
233    _minimum = float("-inf")
234    _maximum = float("inf")
235
236    def __new__(cls, value, **kwargs):
237        if not cls._minimum <= float(value) <= cls._maximum:
238            msg = "value %s out of bounds (%s, %s)"
239            msg = msg % (value, cls._minimum, cls._maximum)
240            raise ValueError(msg)
241        return super(FloatWithUncertainties, cls).__new__(cls, value)
242
243    def __init__(self, value, lower_uncertainty=None, upper_uncertainty=None,
244                 measurement_method=None):
245        # set uncertainties, if initialized with similar type
246        if isinstance(value, FloatWithUncertainties):
247            if lower_uncertainty is None:
248                lower_uncertainty = value.lower_uncertainty
249            if upper_uncertainty is None:
250                upper_uncertainty = value.upper_uncertainty
251        # set/override uncertainties, if explicitly specified
252        self.lower_uncertainty = lower_uncertainty
253        self.upper_uncertainty = upper_uncertainty
254        self.measurement_method = measurement_method
255
256
257class FloatWithUncertaintiesFixedUnit(FloatWithUncertainties):
258    """
259    Float value that has lower and upper uncertainties and a fixed unit
260    associated with it. Helper class to inherit from setting a custom value for
261    the fixed unit (set unit in derived class as class attribute).
262
263    :type value: float
264    :param value: Actual float value.
265    :type lower_uncertainty: float
266    :param lower_uncertainty: Lower uncertainty (aka minusError)
267    :type upper_uncertainty: float
268    :param upper_uncertainty: Upper uncertainty (aka plusError)
269    :type unit: str (read only)
270    :param unit: Unit for physical interpretation of the float value.
271    :type measurement_method: str
272    :param measurement_method: Method used in the measurement.
273    """
274    _unit = ""
275
276    def __init__(self, value, lower_uncertainty=None, upper_uncertainty=None,
277                 measurement_method=None):
278        super(FloatWithUncertaintiesFixedUnit, self).__init__(
279            value, lower_uncertainty=lower_uncertainty,
280            upper_uncertainty=upper_uncertainty,
281            measurement_method=measurement_method)
282
283    @property
284    def unit(self):
285        return self._unit
286
287    @unit.setter
288    def unit(self, value):
289        msg = "Unit is fixed for this object class."
290        raise ValueError(msg)
291
292
293class FloatWithUncertaintiesAndUnit(FloatWithUncertainties):
294    """
295    Float value that has lower and upper uncertainties and a unit associated
296    with it.
297
298    :type value: float
299    :param value: Actual float value.
300    :type lower_uncertainty: float
301    :param lower_uncertainty: Lower uncertainty (aka minusError)
302    :type upper_uncertainty: float
303    :param upper_uncertainty: Upper uncertainty (aka plusError)
304    :type unit: str
305    :param unit: Unit for physical interpretation of the float value.
306    :type measurement_method: str
307    :param measurement_method: Method used in the measurement.
308    """
309    def __init__(self, value, lower_uncertainty=None, upper_uncertainty=None,
310                 unit=None, measurement_method=None):
311        super(FloatWithUncertaintiesAndUnit, self).__init__(
312            value, lower_uncertainty=lower_uncertainty,
313            upper_uncertainty=upper_uncertainty,
314            measurement_method=measurement_method)
315        if unit is None and hasattr(value, "unit"):
316            unit = value.unit
317        self.unit = unit
318
319    @property
320    def unit(self):
321        return self._unit
322
323    @unit.setter
324    def unit(self, value):
325        self._unit = value
326
327
328class _ComplexUncertainty(complex):
329    """
330    Complex class which can accept a python None as an argument and map it to
331    a float value for storage.
332    """
333    _none = float("-inf")
334
335    @classmethod
336    def _encode(cls, arg):
337        if arg is None:
338            return cls._none
339        return arg
340
341    @classmethod
342    def _decode(cls, arg):
343        if arg == cls._none:
344            return None
345        return arg
346
347    def __new__(cls, *args):
348        cargs = [cls._encode(a) for a in args]
349        if len(args) < 1:
350            cargs.append(cls._none)
351        if len(args) < 2:
352            if args[0] is None:
353                cargs.append(cls._none)
354            else:
355                cargs.append(0)
356        return super(_ComplexUncertainty, cls).__new__(cls, *cargs)
357
358    @property
359    def real(self):
360        _real = super(_ComplexUncertainty, self).real
361        return self._decode(_real)
362
363    @property
364    def imag(self):
365        _imag = super(_ComplexUncertainty, self).imag
366        return self._decode(_imag)
367
368
369class ComplexWithUncertainties(CustomComplex):
370    """
371    Complex class which can store uncertainties.
372
373    Accepts FloatWithUncertainties and returns FloatWithUncertainties from
374    property methods.
375    """
376    _lower_uncertainty = None
377    _upper_uncertainty = None
378
379    @staticmethod
380    def _attr(obj, attr):
381        try:
382            return getattr(obj, attr)
383        except AttributeError:
384            return None
385
386    @staticmethod
387    def _uncertainty(value):
388        if isinstance(value, tuple) or isinstance(value, list):
389            u = _ComplexUncertainty(*value)
390        else:
391            u = _ComplexUncertainty(value)
392        if u.real is None and u.imag is None:
393            return None
394        return u
395
396    @property
397    def lower_uncertainty(self):
398        return self._lower_uncertainty
399
400    @lower_uncertainty.setter
401    def lower_uncertainty(self, value):
402        self._lower_uncertainty = self._uncertainty(value)
403
404    @property
405    def upper_uncertainty(self):
406        return self._upper_uncertainty
407
408    @upper_uncertainty.setter
409    def upper_uncertainty(self, value):
410        self._upper_uncertainty = self._uncertainty(value)
411
412    def __new__(cls, *args, **kwargs):
413        return super(ComplexWithUncertainties, cls).__new__(cls, *args)
414
415    def __init__(self, *args, **kwargs):
416        """
417        Complex type with optional keywords:
418
419        :type lower_uncertainty: complex
420        :param lower_uncertainty: Lower uncertainty (aka minusError)
421        :type upper_uncertainty: complex
422        :param upper_uncertainty: Upper uncertainty (aka plusError)
423        :type measurement_method_real: str
424        :param measurement_method_real: Method used in the measurement of real
425            part.
426        :type measurement_method_imag: str
427        :param measurement_method_imag: Method used in the measurement of
428            imaginary part.
429
430        """
431        real_upper = None
432        imag_upper = None
433        real_lower = None
434        imag_lower = None
435        if len(args) >= 1:
436            if isinstance(args[0], self.__class__):
437                self.upper_uncertainty = args[0].upper_uncertainty
438                self.lower_uncertainty = args[0].lower_uncertainty
439            elif isinstance(args[0], FloatWithUncertainties):
440                real_upper = args[0].upper_uncertainty
441                real_lower = args[0].lower_uncertainty
442        if len(args) >= 2 and isinstance(args[1], FloatWithUncertainties):
443            imag_upper = args[1].upper_uncertainty
444            imag_lower = args[1].lower_uncertainty
445        if self.upper_uncertainty is None:
446            self.upper_uncertainty = real_upper, imag_upper
447        if self.lower_uncertainty is None:
448            self.lower_uncertainty = real_lower, imag_lower
449        if "lower_uncertainty" in kwargs:
450            self.lower_uncertainty = kwargs['lower_uncertainty']
451        if "upper_uncertainty" in kwargs:
452            self.upper_uncertainty = kwargs['upper_uncertainty']
453        self.measurement_method_real = kwargs.get('measurement_method_real')
454        self.measurement_method_imag = kwargs.get('measurement_method_imag')
455
456    @property
457    def real(self):
458        _real = super(ComplexWithUncertainties, self).real
459        _lower = self._attr(self.lower_uncertainty, 'real')
460        _upper = self._attr(self.upper_uncertainty, 'real')
461        return FloatWithUncertainties(
462            _real, lower_uncertainty=_lower, upper_uncertainty=_upper,
463            measurement_method=self.measurement_method_real)
464
465    @property
466    def imag(self):
467        _imag = super(ComplexWithUncertainties, self).imag
468        _lower = self._attr(self.lower_uncertainty, 'imag')
469        _upper = self._attr(self.upper_uncertainty, 'imag')
470        return FloatWithUncertainties(
471            _imag, lower_uncertainty=_lower, upper_uncertainty=_upper,
472            measurement_method=self.measurement_method_imag)
473
474
475class ObsPyException(Exception):
476    pass
477
478
479class ObsPyReadingError(ObsPyException):
480    pass
481
482
483class ZeroSamplingRate(ObsPyException):
484    pass
485
486
487if __name__ == '__main__':
488    import doctest
489    doctest.testmod(exclude_empty=True)
490