1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3"""
4Classes related to instrument responses.
5
6:copyright:
7    Lion Krischer (krischer@geophysik.uni-muenchen.de), 2013
8:license:
9    GNU Lesser General Public License, Version 3
10    (https://www.gnu.org/copyleft/lesser.html)
11"""
12from __future__ import (absolute_import, division, print_function,
13                        unicode_literals)
14from future.builtins import *  # NOQA
15
16import copy
17import ctypes as C  # NOQA
18from collections import defaultdict
19from copy import deepcopy
20import itertools
21from math import pi
22import warnings
23
24import numpy as np
25import scipy.interpolate
26
27from .. import compatibility
28from obspy.core.util.base import ComparingObject
29from obspy.core.util.deprecation_helpers import ObsPyDeprecationWarning
30from obspy.core.util.obspy_types import (ComplexWithUncertainties,
31                                         FloatWithUncertainties,
32                                         FloatWithUncertaintiesAndUnit,
33                                         ObsPyException,
34                                         ZeroSamplingRate)
35
36from .util import Angle, Frequency
37
38
39class ResponseStage(ComparingObject):
40    """
41    From the StationXML Definition:
42        This complex type represents channel response and covers SEED
43        blockettes 53 to 56.
44    """
45    def __init__(self, stage_sequence_number, stage_gain,
46                 stage_gain_frequency, input_units, output_units,
47                 resource_id=None, resource_id2=None, name=None,
48                 input_units_description=None,
49                 output_units_description=None, description=None,
50                 decimation_input_sample_rate=None, decimation_factor=None,
51                 decimation_offset=None, decimation_delay=None,
52                 decimation_correction=None):
53        """
54        :type stage_sequence_number: int
55        :param stage_sequence_number: Stage sequence number, greater or equal
56            to zero. This is used in all the response SEED blockettes.
57        :type stage_gain: float
58        :param stage_gain: Value of stage gain.
59        :type stage_gain_frequency: float
60        :param stage_gain_frequency: Frequency of stage gain.
61        :type input_units: str
62        :param input_units: The units of the data as input from the
63            perspective of data acquisition. After correcting data for this
64            response, these would be the resulting units.
65            Name of units, e.g. "M/S", "V", "PA".
66        :type output_units: str
67        :param output_units: The units of the data as output from the
68            perspective of data acquisition. These would be the units of the
69            data prior to correcting for this response.
70            Name of units, e.g. "M/S", "V", "PA".
71        :type resource_id: str
72        :param resource_id: This field contains a string that should serve as a
73            unique resource identifier. This identifier can be interpreted
74            differently depending on the data center/software that generated
75            the document. Also, we recommend to use something like
76            GENERATOR:Meaningful ID. As a common behavior equipment with the
77            same ID should contains the same information/be derived from the
78            same base instruments.
79        :type resource_id2: str
80        :param resource_id2: This field contains a string that should serve as
81            a unique resource identifier. Resource identifier of the subgroup
82            of the response stage that varies across different response stage
83            types (e.g. the poles and zeros part or the FIR part).
84        :type name: str
85        :param name: A name given to the filter stage.
86        :type input_units_description: str, optional
87        :param input_units_description: The units of the data as input from the
88            perspective of data acquisition. After correcting data for this
89            response, these would be the resulting units.
90            Description of units, e.g. "Velocity in meters per second",
91            "Volts", "Pascals".
92        :type output_units_description: str, optional
93        :param output_units_description: The units of the data as output from
94            the perspective of data acquisition. These would be the units of
95            the data prior to correcting for this response.
96            Description of units, e.g. "Velocity in meters per second",
97            "Volts", "Pascals".
98        :type description: str, optional
99        :param description: A short description of of the filter.
100        :type decimation_input_sample_rate:  float, optional
101        :param decimation_input_sample_rate: The sampling rate before the
102            decimation in samples per second.
103        :type decimation_factor: int, optional
104        :param decimation_factor: The applied decimation factor.
105        :type decimation_offset: int, optional
106        :param decimation_offset: The sample chosen for use. 0 denotes the
107            first sample, 1 the second, and so forth.
108        :type decimation_delay: float, optional
109        :param decimation_delay: The estimated pure delay from the decimation.
110        :type decimation_correction: float, optional
111        :param decimation_correction: The time shift applied to correct for the
112            delay at this stage.
113
114        .. note::
115            The stage gain (or stage sensitivity) is the gain at the stage of
116            the encapsulating response element and corresponds to SEED
117            blockette 58. In the SEED convention, stage 0 gain represents the
118            overall sensitivity of the channel.  In this schema, stage 0 gains
119            are allowed but are considered deprecated.  Overall sensitivity
120            should be specified in the InstrumentSensitivity element.
121        """
122        self.stage_sequence_number = stage_sequence_number
123        self.input_units = input_units
124        self.output_units = output_units
125        self.input_units_description = input_units_description
126        self.output_units_description = output_units_description
127        self.resource_id = resource_id
128        self.resource_id2 = resource_id2
129        self.stage_gain = stage_gain
130        self.stage_gain_frequency = stage_gain_frequency
131        self.name = name
132        self.description = description
133        self.decimation_input_sample_rate = \
134            Frequency(decimation_input_sample_rate) \
135            if decimation_input_sample_rate is not None else None
136        self.decimation_factor = decimation_factor
137        self.decimation_offset = decimation_offset
138        self.decimation_delay = \
139            FloatWithUncertaintiesAndUnit(decimation_delay) \
140            if decimation_delay is not None else None
141        self.decimation_correction = \
142            FloatWithUncertaintiesAndUnit(decimation_correction) \
143            if decimation_correction is not None else None
144
145    def __str__(self):
146        ret = (
147            "Response type: {response_type}, Stage Sequence Number: "
148            "{response_stage}\n"
149            "{name_desc}"
150            "{resource_id}"
151            "\tFrom {input_units}{input_desc} to {output_units}{output_desc}\n"
152            "\tStage gain: {gain}, defined at {gain_freq} Hz\n"
153            "{decimation}").format(
154            response_type=self.__class__.__name__,
155            response_stage=self.stage_sequence_number,
156            gain=self.stage_gain if self.stage_gain is not None else "UNKNOWN",
157            gain_freq=("%.2f" % self.stage_gain_frequency) if
158            self.stage_gain_frequency is not None else "UNKNOWN",
159            name_desc="\t%s %s\n" % (
160                self.name, "(%s)" % self.description
161                if self.description else "") if self.name else "",
162            resource_id=("\tResource Id: %s" % self.resource_id
163                         if self.resource_id else ""),
164            input_units=self.input_units if self.input_units else "UNKNOWN",
165            input_desc=(" (%s)" % self.input_units_description
166                        if self.input_units_description else ""),
167            output_units=self.output_units if self.output_units else "UNKNOWN",
168            output_desc=(" (%s)" % self.output_units_description
169                         if self.output_units_description else ""),
170            decimation=(
171                "\tDecimation:\n\t\tInput Sample Rate: %.2f Hz\n\t\t"
172                "Decimation Factor: %i\n\t\tDecimation Offset: %i\n\t\t"
173                "Decimation Delay: %.2f\n\t\tDecimation Correction: %.2f" % (
174                    self.decimation_input_sample_rate, self.decimation_factor,
175                    self.decimation_offset, self.decimation_delay,
176                    self.decimation_correction)
177                if self.decimation_input_sample_rate is not None else ""))
178        return ret.strip()
179
180    def _repr_pretty_(self, p, cycle):
181        p.text(str(self))
182
183
184class PolesZerosResponseStage(ResponseStage):
185    """
186    From the StationXML Definition:
187        Response: complex poles and zeros. Corresponds to SEED blockette 53.
188
189    The response stage is used for the analog stages of the filter system and
190    the description of infinite impulse response (IIR) digital filters.
191
192    Has all the arguments of the parent class
193    :class:`~obspy.core.inventory.response.ResponseStage` and the following:
194
195    :type pz_transfer_function_type: str
196    :param pz_transfer_function_type: A string describing the type of transfer
197        function. Can be one of:
198
199        * ``LAPLACE (RADIANS/SECOND)``
200        * ``LAPLACE (HERTZ)``
201        * ``DIGITAL (Z-TRANSFORM)``
202
203        The function tries to match inputs to one of three types if it can.
204    :type normalization_frequency: float
205    :param normalization_frequency: The frequency at which the normalization
206        factor is normalized.
207    :type zeros: list of complex
208    :param zeros: All zeros of the stage.
209    :type poles: list of complex
210    :param poles: All poles of the stage.
211    :type normalization_factor: float, optional
212    :param normalization_factor:
213    """
214    def __init__(self, stage_sequence_number, stage_gain,
215                 stage_gain_frequency, input_units, output_units,
216                 pz_transfer_function_type,
217                 normalization_frequency, zeros, poles,
218                 normalization_factor=1.0, resource_id=None, resource_id2=None,
219                 name=None, input_units_description=None,
220                 output_units_description=None, description=None,
221                 decimation_input_sample_rate=None, decimation_factor=None,
222                 decimation_offset=None, decimation_delay=None,
223                 decimation_correction=None):
224        # Set the Poles and Zeros specific attributes. Special cases are
225        # handled by properties.
226        self.pz_transfer_function_type = pz_transfer_function_type
227        self.normalization_frequency = normalization_frequency
228        self.normalization_factor = float(normalization_factor)
229        self.zeros = zeros
230        self.poles = poles
231        super(PolesZerosResponseStage, self).__init__(
232            stage_sequence_number=stage_sequence_number,
233            input_units=input_units,
234            output_units=output_units,
235            input_units_description=input_units_description,
236            output_units_description=output_units_description,
237            resource_id=resource_id, resource_id2=resource_id2,
238            stage_gain=stage_gain,
239            stage_gain_frequency=stage_gain_frequency, name=name,
240            description=description,
241            decimation_input_sample_rate=decimation_input_sample_rate,
242            decimation_factor=decimation_factor,
243            decimation_offset=decimation_offset,
244            decimation_delay=decimation_delay,
245            decimation_correction=decimation_correction)
246
247    def __str__(self):
248        ret = super(PolesZerosResponseStage, self).__str__()
249        ret += (
250            "\n"
251            "\tTransfer function type: {transfer_fct_type}\n"
252            "\tNormalization factor: {norm_fact:g}, "
253            "Normalization frequency: {norm_freq:.2f} Hz\n"
254            "\tPoles: {poles}\n"
255            "\tZeros: {zeros}").format(
256            transfer_fct_type=self.pz_transfer_function_type,
257            norm_fact=self.normalization_factor,
258            norm_freq=self.normalization_frequency,
259            poles=", ".join(map(str, self.poles)),
260            zeros=", ".join(map(str, self.zeros)))
261        return ret
262
263    def _repr_pretty_(self, p, cycle):
264        p.text(str(self))
265
266    @property
267    def zeros(self):
268        return self._zeros
269
270    @zeros.setter
271    def zeros(self, value):
272        value = list(value)
273        for i, x in enumerate(value):
274            if not isinstance(x, ComplexWithUncertainties):
275                value[i] = ComplexWithUncertainties(x)
276        self._zeros = value
277
278    @property
279    def poles(self):
280        return self._poles
281
282    @poles.setter
283    def poles(self, value):
284        value = list(value)
285        for i, x in enumerate(value):
286            if not isinstance(x, ComplexWithUncertainties):
287                value[i] = ComplexWithUncertainties(x)
288        self._poles = value
289
290    @property
291    def normalization_frequency(self):
292        return self._normalization_frequency
293
294    @normalization_frequency.setter
295    def normalization_frequency(self, value):
296        self._normalization_frequency = Frequency(value)
297
298    @property
299    def pz_transfer_function_type(self):
300        return self._pz_transfer_function_type
301
302    @pz_transfer_function_type.setter
303    def pz_transfer_function_type(self, value):
304        """
305        Setter for the transfer function type.
306
307        Rather permissive but should make it less awkward to use.
308        """
309        msg = ("'%s' is not a valid value for 'pz_transfer_function_type'. "
310               "Valid one are:\n"
311               "\tLAPLACE (RADIANS/SECOND)\n"
312               "\tLAPLACE (HERTZ)\n"
313               "\tDIGITAL (Z-TRANSFORM)") % value
314        value = value.lower()
315        if "laplace" in value:
316            if "radian" in value:
317                self._pz_transfer_function_type = "LAPLACE (RADIANS/SECOND)"
318            elif "hertz" in value or "hz" in value:
319                self._pz_transfer_function_type = "LAPLACE (HERTZ)"
320            else:
321                raise ValueError(msg)
322        elif "digital" in value:
323            self._pz_transfer_function_type = "DIGITAL (Z-TRANSFORM)"
324        else:
325            raise ValueError(msg)
326
327
328class CoefficientsTypeResponseStage(ResponseStage):
329    """
330    This response type can describe coefficients for FIR filters. Laplace
331    transforms and IIR filters can also be expressed using this type but should
332    rather be described using the PolesZerosResponseStage class. Effectively
333    corresponds to SEED blockette 54.
334
335    Has all the arguments of the parent class
336    :class:`~obspy.core.inventory.response.ResponseStage` and the following:
337
338    :type cf_transfer_function_type: str
339    :param cf_transfer_function_type: A string describing the type of transfer
340        function. Can be one of:
341
342        * ``ANALOG (RADIANS/SECOND)``
343        * ``ANALOG (HERTZ)``
344        * ``DIGITAL``
345
346        The function tries to match inputs to one of three types if it can.
347    :type numerator: list of
348        :class:`~obspy.core.util.obspy_types.CoefficientWithUncertainties`
349    :param numerator: Numerator of the coefficient response stage.
350    :type denominator: list of
351        :class:`~obspy.core.util.obspy_types.CoefficientWithUncertainties`
352    :param denominator: Denominator of the coefficient response stage.
353    """
354    def __init__(self, stage_sequence_number, stage_gain,
355                 stage_gain_frequency, input_units, output_units,
356                 cf_transfer_function_type, resource_id=None,
357                 resource_id2=None, name=None, numerator=None,
358                 denominator=None, input_units_description=None,
359                 output_units_description=None, description=None,
360                 decimation_input_sample_rate=None, decimation_factor=None,
361                 decimation_offset=None, decimation_delay=None,
362                 decimation_correction=None):
363        # Set the Coefficients type specific attributes. Special cases are
364        # handled by properties.
365        self.cf_transfer_function_type = cf_transfer_function_type
366        self.numerator = numerator
367        self.denominator = denominator
368        super(CoefficientsTypeResponseStage, self).__init__(
369            stage_sequence_number=stage_sequence_number,
370            input_units=input_units,
371            output_units=output_units,
372            input_units_description=input_units_description,
373            output_units_description=output_units_description,
374            resource_id=resource_id, resource_id2=resource_id2,
375            stage_gain=stage_gain,
376            stage_gain_frequency=stage_gain_frequency, name=name,
377            description=description,
378            decimation_input_sample_rate=decimation_input_sample_rate,
379            decimation_factor=decimation_factor,
380            decimation_offset=decimation_offset,
381            decimation_delay=decimation_delay,
382            decimation_correction=decimation_correction)
383
384    def __str__(self):
385        ret = super(CoefficientsTypeResponseStage, self).__str__()
386        ret += (
387            "\n"
388            "\tTransfer function type: {transfer_fct_type}\n"
389            "\tContains {num_count} numerators and {den_count} denominators")\
390            .format(
391                transfer_fct_type=self.cf_transfer_function_type,
392                num_count=len(self.numerator), den_count=len(self.denominator))
393        return ret
394
395    def _repr_pretty_(self, p, cycle):
396        p.text(str(self))
397
398    @property
399    def numerator(self):
400        return self._numerator
401
402    @numerator.setter
403    def numerator(self, value):
404        if value == []:
405            self._numerator = []
406            return
407        value = list(value) if isinstance(
408            value, compatibility.collections_abc.Iterable) else [value]
409        if any(getattr(x, 'unit', None) is not None for x in value):
410            msg = 'Setting Numerator/Denominator with a unit is deprecated.'
411            warnings.warn(msg, ObsPyDeprecationWarning)
412        for _i, x in enumerate(value):
413            if not isinstance(x, CoefficientWithUncertainties):
414                value[_i] = CoefficientWithUncertainties(x)
415        self._numerator = value
416
417    @property
418    def denominator(self):
419        return self._denominator
420
421    @denominator.setter
422    def denominator(self, value):
423        if value == []:
424            self._denominator = []
425            return
426        value = list(value) if isinstance(
427            value, compatibility.collections_abc.Iterable) else [value]
428        if any(getattr(x, 'unit', None) is not None for x in value):
429            msg = 'Setting Numerator/Denominator with a unit is deprecated.'
430            warnings.warn(msg, ObsPyDeprecationWarning)
431        for _i, x in enumerate(value):
432            if not isinstance(x, CoefficientWithUncertainties):
433                value[_i] = CoefficientWithUncertainties(x)
434        self._denominator = value
435
436    @property
437    def cf_transfer_function_type(self):
438        return self._cf_transfer_function_type
439
440    @cf_transfer_function_type.setter
441    def cf_transfer_function_type(self, value):
442        """
443        Setter for the transfer function type.
444
445        Rather permissive but should make it less awkward to use.
446        """
447        msg = ("'%s' is not a valid value for 'cf_transfer_function_type'. "
448               "Valid one are:\n"
449               "\tANALOG (RADIANS/SECOND)\n"
450               "\tANALOG (HERTZ)\n"
451               "\tDIGITAL") % value
452        value = value.lower()
453        if "analog" in value:
454            if "rad" in value:
455                self._cf_transfer_function_type = "ANALOG (RADIANS/SECOND)"
456            elif "hertz" in value or "hz" in value:
457                self._cf_transfer_function_type = "ANALOG (HERTZ)"
458            else:
459                raise ValueError(msg)
460        elif "digital" in value:
461            self._cf_transfer_function_type = "DIGITAL"
462        else:
463            raise ValueError(msg)
464
465
466class ResponseListResponseStage(ResponseStage):
467    """
468    This response type gives a list of frequency, amplitude and phase value
469    pairs. Effectively corresponds to SEED blockette 55.
470
471    Has all the arguments of the parent class
472    :class:`~obspy.core.inventory.response.ResponseStage` and the following:
473
474    :type response_list_elements: list of
475        :class:`~obspy.core.inventory.response.ResponseListElement`
476    :param response_list_elements: A list of single discrete frequency,
477        amplitude and phase response values.
478    """
479    def __init__(self, stage_sequence_number, stage_gain,
480                 stage_gain_frequency, input_units, output_units,
481                 resource_id=None, resource_id2=None, name=None,
482                 response_list_elements=None,
483                 input_units_description=None, output_units_description=None,
484                 description=None, decimation_input_sample_rate=None,
485                 decimation_factor=None, decimation_offset=None,
486                 decimation_delay=None, decimation_correction=None):
487        self.response_list_elements = response_list_elements or []
488        super(ResponseListResponseStage, self).__init__(
489            stage_sequence_number=stage_sequence_number,
490            input_units=input_units,
491            output_units=output_units,
492            input_units_description=input_units_description,
493            output_units_description=output_units_description,
494            resource_id=resource_id, resource_id2=resource_id2,
495            stage_gain=stage_gain,
496            stage_gain_frequency=stage_gain_frequency, name=name,
497            description=description,
498            decimation_input_sample_rate=decimation_input_sample_rate,
499            decimation_factor=decimation_factor,
500            decimation_offset=decimation_offset,
501            decimation_delay=decimation_delay,
502            decimation_correction=decimation_correction)
503
504
505class ResponseListElement(ComparingObject):
506    """
507    Describes the amplitude and phase response value for a discrete frequency
508    value.
509    """
510    def __init__(self, frequency, amplitude, phase):
511        """
512        :type frequency: float
513        :param frequency: The frequency for which the response is valid.
514        :type amplitude: float
515        :param amplitude: The value for the amplitude response at this
516            frequency.
517        :type phase: float
518        :param phase: The value for the phase response at this frequency.
519        """
520        self.frequency = frequency
521        self.amplitude = amplitude
522        self.phase = phase
523
524    @property
525    def frequency(self):
526        return self._frequency
527
528    @frequency.setter
529    def frequency(self, value):
530        if not isinstance(value, Frequency):
531            value = Frequency(value)
532        self._frequency = value
533
534    @property
535    def amplitude(self):
536        return self._amplitude
537
538    @amplitude.setter
539    def amplitude(self, value):
540        if not isinstance(value, FloatWithUncertaintiesAndUnit):
541            value = FloatWithUncertaintiesAndUnit(value)
542        self._amplitude = value
543
544    @property
545    def phase(self):
546        return self._phase
547
548    @phase.setter
549    def phase(self, value):
550        if not isinstance(value, Angle):
551            value = Angle(value)
552        self._phase = value
553
554
555class FIRResponseStage(ResponseStage):
556    """
557    From the StationXML Definition:
558        Response: FIR filter. Corresponds to SEED blockette 61. FIR filters are
559        also commonly documented using the CoefficientsType element.
560
561    Has all the arguments of the parent class
562    :class:`~obspy.core.inventory.response.ResponseStage` and the following:
563
564    :type symmetry: str
565    :param symmetry: A string describing the symmetry. Can be one of:
566
567            * ``NONE``
568            * ``EVEN``
569            * ``ODD``
570
571    :type coefficients: list of floats
572    :param coefficients: List of FIR coefficients.
573    """
574    def __init__(self, stage_sequence_number, stage_gain,
575                 stage_gain_frequency, input_units, output_units,
576                 symmetry="NONE", resource_id=None, resource_id2=None,
577                 name=None,
578                 coefficients=None, input_units_description=None,
579                 output_units_description=None, description=None,
580                 decimation_input_sample_rate=None, decimation_factor=None,
581                 decimation_offset=None, decimation_delay=None,
582                 decimation_correction=None):
583        self._symmetry = symmetry
584        self.coefficients = coefficients or []
585        super(FIRResponseStage, self).__init__(
586            stage_sequence_number=stage_sequence_number,
587            input_units=input_units,
588            output_units=output_units,
589            input_units_description=input_units_description,
590            output_units_description=output_units_description,
591            resource_id=resource_id, resource_id2=resource_id2,
592            stage_gain=stage_gain,
593            stage_gain_frequency=stage_gain_frequency, name=name,
594            description=description,
595            decimation_input_sample_rate=decimation_input_sample_rate,
596            decimation_factor=decimation_factor,
597            decimation_offset=decimation_offset,
598            decimation_delay=decimation_delay,
599            decimation_correction=decimation_correction)
600
601    @property
602    def symmetry(self):
603        return self._symmetry
604
605    @symmetry.setter
606    def symmetry(self, value):
607        value = str(value).upper()
608        allowed = ("NONE", "EVEN", "ODD")
609        if value not in allowed:
610            msg = ("Value '%s' for FIR Response symmetry not allowed. "
611                   "Possible values are: '%s'")
612            msg = msg % (value, "', '".join(allowed))
613            raise ValueError(msg)
614        self._symmetry = value
615
616    @property
617    def coefficients(self):
618        return self._coefficients
619
620    @coefficients.setter
621    def coefficients(self, value):
622        new_values = []
623        for x in value:
624            if not isinstance(x, FilterCoefficient):
625                x = FilterCoefficient(x)
626            new_values.append(x)
627        self._coefficients = new_values
628
629
630class PolynomialResponseStage(ResponseStage):
631    """
632    From the StationXML Definition:
633        Response: expressed as a polynomial (allows non-linear sensors to be
634        described). Corresponds to SEED blockette 62. Can be used to describe a
635        stage of acquisition or a complete system.
636
637    Has all the arguments of the parent class
638    :class:`~obspy.core.inventory.response.ResponseStage` and the following:
639
640    :type approximation_type: str
641    :param approximation_type: Approximation type. Currently restricted to
642        'MACLAURIN' by StationXML definition.
643    :type frequency_lower_bound: float
644    :param frequency_lower_bound: Lower frequency bound.
645    :type frequency_upper_bound: float
646    :param frequency_upper_bound: Upper frequency bound.
647    :type approximation_lower_bound: float
648    :param approximation_lower_bound: Lower bound of approximation.
649    :type approximation_upper_bound: float
650    :param approximation_upper_bound: Upper bound of approximation.
651    :type maximum_error: float
652    :param maximum_error: Maximum error.
653    :type coefficients: list of floats
654    :param coefficients: List of polynomial coefficients.
655    """
656    def __init__(self, stage_sequence_number, stage_gain,
657                 stage_gain_frequency, input_units, output_units,
658                 frequency_lower_bound,
659                 frequency_upper_bound, approximation_lower_bound,
660                 approximation_upper_bound, maximum_error, coefficients,
661                 approximation_type='MACLAURIN', resource_id=None,
662                 resource_id2=None, name=None,
663                 input_units_description=None,
664                 output_units_description=None, description=None,
665                 decimation_input_sample_rate=None, decimation_factor=None,
666                 decimation_offset=None, decimation_delay=None,
667                 decimation_correction=None):
668        # XXX remove stage_gain and stage_gain_frequency completely, since
669        # changes in StationXML 1.1?
670        self._approximation_type = approximation_type
671        self.frequency_lower_bound = frequency_lower_bound
672        self.frequency_upper_bound = frequency_upper_bound
673        self.approximation_lower_bound = approximation_lower_bound
674        self.approximation_upper_bound = approximation_upper_bound
675        self.maximum_error = maximum_error
676        self.coefficients = coefficients
677        # XXX StationXML 1.1 does not allow stage gain in Polynomial response
678        # stages. Maybe we should we warn here.. but this could get very
679        # verbose when reading StationXML 1.0 files, so maybe not
680        super(PolynomialResponseStage, self).__init__(
681            stage_sequence_number=stage_sequence_number,
682            input_units=input_units,
683            output_units=output_units,
684            input_units_description=input_units_description,
685            output_units_description=output_units_description,
686            resource_id=resource_id, resource_id2=resource_id2,
687            stage_gain=stage_gain,
688            stage_gain_frequency=stage_gain_frequency, name=name,
689            description=description,
690            decimation_input_sample_rate=decimation_input_sample_rate,
691            decimation_factor=decimation_factor,
692            decimation_offset=decimation_offset,
693            decimation_delay=decimation_delay,
694            decimation_correction=decimation_correction)
695
696    @property
697    def approximation_type(self):
698        return self._approximation_type
699
700    @approximation_type.setter
701    def approximation_type(self, value):
702        value = str(value).upper()
703        allowed = ("MACLAURIN",)
704        if value not in allowed:
705            msg = ("Value '%s' for polynomial response approximation type not "
706                   "allowed. Possible values are: '%s'")
707            msg = msg % (value, "', '".join(allowed))
708            raise ValueError(msg)
709        self._approximation_type = value
710
711    @property
712    def coefficients(self):
713        return self._coefficients
714
715    @coefficients.setter
716    def coefficients(self, value):
717        new_values = []
718        for x in value:
719            if not isinstance(x, CoefficientWithUncertainties):
720                x = CoefficientWithUncertainties(x)
721            new_values.append(x)
722        self._coefficients = new_values
723
724    def __str__(self):
725        ret = super(PolynomialResponseStage, self).__str__()
726        ret += (
727            "\n"
728            "\tPolynomial approximation type: {approximation_type}\n"
729            "\tFrequency lower bound: {lower_freq_bound}\n"
730            "\tFrequency upper bound: {upper_freq_bound}\n"
731            "\tApproximation lower bound: {lower_approx_bound}\n"
732            "\tApproximation upper bound: {upper_approx_bound}\n"
733            "\tMaximum error: {max_error}\n"
734            "\tNumber of coefficients: {coeff_count}".format(
735                approximation_type=self._approximation_type,
736                lower_freq_bound=self.frequency_lower_bound,
737                upper_freq_bound=self.frequency_upper_bound,
738                lower_approx_bound=self.approximation_lower_bound,
739                upper_approx_bound=self.approximation_upper_bound,
740                max_error=self.maximum_error,
741                coeff_count=len(self.coefficients)))
742        return ret
743
744
745class Response(ComparingObject):
746    """
747    The root response object.
748    """
749    def __init__(self, resource_id=None, instrument_sensitivity=None,
750                 instrument_polynomial=None, response_stages=None):
751        """
752        :type resource_id: str
753        :param resource_id: This field contains a string that should serve as a
754            unique resource identifier. This identifier can be interpreted
755            differently depending on the data center/software that generated
756            the document. Also, we recommend to use something like
757            GENERATOR:Meaningful ID. As a common behavior equipment with the
758            same ID should contains the same information/be derived from the
759            same base instruments.
760        :type instrument_sensitivity:
761            :class:`~obspy.core.inventory.response.InstrumentSensitivity`
762        :param instrument_sensitivity: The total sensitivity for the given
763            channel, representing the complete acquisition system expressed as
764            a scalar.
765        :type instrument_polynomial:
766            :class:`~obspy.core.inventory.response.InstrumentPolynomial`
767        :param instrument_polynomial: The total sensitivity for the given
768            channel, representing the complete acquisition system expressed as
769            a polynomial.
770        :type response_stages: list of
771            :class:`~obspy.core.inventory.response.ResponseStage` objects
772        :param response_stages: A list of the response stages. Covers SEED
773            blockettes 53 to 56.
774        """
775        self.resource_id = resource_id
776        self.instrument_sensitivity = instrument_sensitivity
777        self.instrument_polynomial = instrument_polynomial
778        if response_stages is None:
779            self.response_stages = []
780        elif hasattr(response_stages, "__iter__"):
781            self.response_stages = response_stages
782        else:
783            msg = "response_stages must be an iterable."
784            raise ValueError(msg)
785
786    def _attempt_to_fix_units(self):
787        """
788        Internal helper function that will add units to gain only stages based
789        on the units of surrounding stages.
790
791        Should be called when parsing from file formats that don't have units
792        for identity stages.
793        """
794        previous_output_units = None
795        previous_output_units_description = None
796
797        # Potentially set the input units of the first stage to the units of
798        # the overall sensitivity and the output units of the second stage.
799        if self.response_stages and self.response_stages[0] and \
800                hasattr(self, "instrument_sensitivity"):
801            s = self.instrument_sensitivity
802
803            if s:
804                if self.response_stages[0].input_units is None:
805                    self.response_stages[0].input_units = s.input_units
806                if self.response_stages[0].input_units_description is None:
807                    self.response_stages[0].input_units_description = \
808                        s.input_units_description
809
810            if len(self.response_stages) >= 2 and self.response_stages[1]:
811                if self.response_stages[0].output_units is None:
812                    self.response_stages[0].output_units = \
813                        self.response_stages[1].input_units
814                if self.response_stages[0].output_units_description is None:
815                    self.response_stages[0].output_units_description = \
816                        self.response_stages[1].input_units_description
817
818        # Front to back.
819        for r in self.response_stages:
820            # Only for identity/stage only.
821            if type(r) is ResponseStage:
822                if not r.input_units and not r.output_units and \
823                        previous_output_units:
824                    r.input_units = previous_output_units
825                    r.output_units = previous_output_units
826                if not r.input_units_description and \
827                        not r.output_units_description \
828                        and previous_output_units_description:
829                    r.input_units_description = \
830                        previous_output_units_description
831                    r.output_units_description = \
832                        previous_output_units_description
833
834            previous_output_units = r.output_units
835            previous_output_units_description = r.output_units_description
836
837        # Back to front.
838        previous_input_units = None
839        previous_input_units_description = None
840        for r in reversed(self.response_stages):
841            # Only for identity/stage only.
842            if type(r) is ResponseStage:
843                if not r.input_units and not r.output_units and \
844                        previous_input_units:
845                    r.input_units = previous_input_units
846                    r.output_units = previous_input_units
847                if not r.input_units_description and \
848                        not r.output_units_description \
849                        and previous_input_units_description:
850                    r.input_units_description = \
851                        previous_input_units_description
852                    r.output_units_description = \
853                        previous_input_units_description
854
855            previous_input_units = r.input_units
856            previous_input_units_description = r.input_units_description
857
858    def get_sampling_rates(self):
859        """
860        Computes the input and output sampling rates of each stage.
861
862        For well defined files this will just read the decimation attributes
863        of each stage. For others it will attempt to infer missing values
864        from the surrounding stages.
865
866        :returns: A nested dictionary detailing the sampling rates of each
867            response stage.
868        :rtype: dict
869
870        >>> import obspy
871        >>> inv = obspy.read_inventory("AU.MEEK.xml")  # doctest: +SKIP
872        >>> inv[0][0][0].response.get_sampling_rates()  # doctest: +SKIP
873        {1: {'decimation_factor': 1,
874             'input_sampling_rate': 600.0,
875             'output_sampling_rate': 600.0},
876         2: {'decimation_factor': 1,
877             'input_sampling_rate': 600.0,
878             'output_sampling_rate': 600.0},
879         3: {'decimation_factor': 1,
880             'input_sampling_rate': 600.0,
881             'output_sampling_rate': 600.0},
882         4: {'decimation_factor': 3,
883             'input_sampling_rate': 600.0,
884             'output_sampling_rate': 200.0},
885         5: {'decimation_factor': 10,
886             'input_sampling_rate': 200.0,
887             'output_sampling_rate': 20.0}}
888        """
889        # Get all stages, but skip stage 0.
890        stages = [_i.stage_sequence_number for _i in self.response_stages
891                  if _i.stage_sequence_number]
892        if not stages:
893            return {}
894
895        if list(range(1, len(stages) + 1)) != stages:
896            raise ValueError("Can only determine sampling rates if response "
897                             "stages are in order.")
898
899        # First fill in all the set values.
900        sampling_rates = {}
901        for stage in self.response_stages:
902            input_sr = None
903            output_sr = None
904            factor = None
905            if stage.decimation_input_sample_rate:
906                input_sr = stage.decimation_input_sample_rate
907                if stage.decimation_factor:
908                    factor = stage.decimation_factor
909                    output_sr = input_sr / float(factor)
910            sampling_rates[stage.stage_sequence_number] = {
911                "input_sampling_rate": input_sr,
912                "output_sampling_rate": output_sr,
913                "decimation_factor": factor}
914
915        # Nothing might be set - just return in that case.
916        if set(itertools.chain.from_iterable(v.values()
917               for v in sampling_rates.values())) == {None}:
918            return sampling_rates
919
920        # Find the first set input sampling rate. The output sampling rate
921        # cannot be set without it. Set all prior input and output sampling
922        # rates to it.
923        for i in stages:
924            sr = sampling_rates[i]["input_sampling_rate"]
925            if sr:
926                for j in range(1, i):
927                    sampling_rates[j]["input_sampling_rate"] = sr
928                    sampling_rates[j]["output_sampling_rate"] = sr
929                    sampling_rates[j]["decimation_factor"] = 1
930                break
931
932        # This should guarantee that the input and output sampling rate of the
933        # the first stage are set.
934        output_sr = sampling_rates[1]["output_sampling_rate"]
935        if not output_sr:  # pragma: no cover
936            raise NotImplementedError
937
938        for i in stages:
939            si = sampling_rates[i]
940            if not si["input_sampling_rate"]:
941                si["input_sampling_rate"] = output_sr
942            if not si["output_sampling_rate"]:
943                if not si["decimation_factor"]:
944                    si["output_sampling_rate"] = si["input_sampling_rate"]
945                    si["decimation_factor"] = 1
946                else:
947                    si["output_sampling_rate"] = si["input_sampling_rate"] / \
948                        float(si["decimation_factor"])
949            if not si["decimation_factor"]:
950                si["decimation_factor"] = int(round(
951                    si["input_sampling_rate"] / si["output_sampling_rate"]))
952            output_sr = si["output_sampling_rate"]
953
954        def is_close(a, b):
955            return abs(a - b) < 1e-5
956
957        # Final consistency checks.
958        sr = sampling_rates[stages[0]]["input_sampling_rate"]
959        for i in stages:
960            si = sampling_rates[i]
961            if not is_close(si["input_sampling_rate"], sr):  # pragma: no cover
962                msg = ("Input sampling rate of stage %i is inconsistent "
963                       "with the previous stages' output sampling rate")
964                warnings.warn(msg % i)
965
966            if not is_close(
967                    si["input_sampling_rate"] / si["output_sampling_rate"],
968                    si["decimation_factor"]):  # pragma: no cover
969                msg = ("Decimation factor in stage %i is inconsistent with "
970                       "input and output sampling rates.")
971                warnings.warn(msg % i)
972            sr = si["output_sampling_rate"]
973
974        return sampling_rates
975
976    def recalculate_overall_sensitivity(self, frequency=None):
977        """
978        Recalculates the overall sensitivity.
979
980        :param frequency: Choose frequency at which to calculate the
981            sensitivity. If not given it will be chosen automatically.
982        """
983        if not hasattr(self, "instrument_sensitivity"):
984            msg = "Could not find an instrument sensitivity - will not " \
985                  "recalculate the overall sensitivity."
986            raise ValueError(msg)
987
988        if not self.instrument_sensitivity.input_units:
989            msg = "Could not determine input units - will not " \
990                  "recalculate the overall sensitivity."
991            raise ValueError(msg)
992
993        i_u = self.instrument_sensitivity.input_units
994
995        unit_map = {
996            "DISP": ["M"],
997            "VEL": ["M/S", "M/SEC"],
998            "ACC": ["M/S**2", "M/(S**2)", "M/SEC**2", "M/(SEC**2)",
999                    "M/S/S"]}
1000        unit = None
1001        for key, value in unit_map.items():
1002            if i_u and i_u.upper() in value:
1003                unit = key
1004        if not unit:
1005            msg = ("ObsPy does not know how to map unit '%s' to "
1006                   "displacement, velocity, or acceleration - overall "
1007                   "sensitivity will not be recalculated.") % i_u
1008            raise ValueError(msg)
1009
1010        # Determine frequency if not given.
1011        if frequency is None:
1012            # lookup normalization frequency of sensor's first stage it should
1013            # be in the flat part of the response
1014            stage_one = self.response_stages[0]
1015            try:
1016                frequency = stage_one.normalization_frequency
1017            except AttributeError:
1018                pass
1019            for stage in self.response_stages[::-1]:
1020                # determine sampling rate
1021                try:
1022                    sampling_rate = (stage.decimation_input_sample_rate /
1023                                     stage.decimation_factor)
1024                    break
1025                except Exception:
1026                    continue
1027            else:
1028                sampling_rate = None
1029            if sampling_rate:
1030                # if sensor's normalization frequency is above 0.5 * nyquist,
1031                # use that instead (e.g. to avoid computing an overall
1032                # sensitivity above nyquist)
1033                nyquist = sampling_rate / 2.0
1034                if frequency:
1035                    frequency = min(frequency, nyquist / 2.0)
1036                else:
1037                    frequency = nyquist / 2.0
1038
1039        if frequency is None:
1040            msg = ("Could not automatically determine a suitable frequency "
1041                   "at which to calculate the sensitivity. The overall "
1042                   "sensitivity will not be recalculated.")
1043            raise ValueError(msg)
1044
1045        freq, gain = self._get_overall_sensitivity_and_gain(
1046            output=unit, frequency=float(frequency))
1047
1048        self.instrument_sensitivity.value = gain
1049        self.instrument_sensitivity.frequency = freq
1050
1051    def _get_overall_sensitivity_and_gain(
1052            self, frequency=None, output='VEL'):
1053        """
1054        Get the overall sensitivity and gain from stages 1 to N.
1055
1056        Returns the overall sensitivity frequency and gain, which can be
1057        used to create stage 0.
1058
1059        :type output: str
1060        :param output: Output units. One of:
1061
1062            ``"DISP"``
1063                displacement, output unit is meters
1064            ``"VEL"``
1065                velocity, output unit is meters/second
1066            ``"ACC"``
1067                acceleration, output unit is meters/second**2
1068
1069        :type frequency: float
1070        :param frequency: Frequency to calculate overall sensitivity for in
1071            Hertz. Defaults to normalization frequency of stage 1.
1072        :rtype: :tuple: ( float, float )
1073        :returns: frequency and gain at frequency.
1074        """
1075        if frequency is None:
1076            # XXX is this safe enough, or should we lookup the stage sequence
1077            # XXX number explicitly?
1078            frequency = self.response_stages[0].normalization_frequency
1079        response_at_frequency = self._call_eval_resp_for_frequencies(
1080            frequencies=[frequency], output=output,
1081            hide_sensitivity_mismatch_warning=True)[0][0]
1082        overall_sensitivity = abs(response_at_frequency)
1083        return frequency, overall_sensitivity
1084
1085    def _call_eval_resp_for_frequencies(
1086            self, frequencies, output="VEL", start_stage=None,
1087            end_stage=None, hide_sensitivity_mismatch_warning=False):
1088        """
1089        Returns frequency response for given frequencies using evalresp.
1090
1091        Also returns the overall sensitivity frequency and its gain.
1092
1093        :type frequencies: list of float
1094        :param frequencies: Discrete frequencies to calculate response for.
1095        :type output: str
1096        :param output: Output units. One of:
1097
1098            ``"DISP"``
1099                displacement, output unit is meters
1100            ``"VEL"``
1101                velocity, output unit is meters/second
1102            ``"ACC"``
1103                acceleration, output unit is meters/second**2
1104
1105        :type start_stage: int, optional
1106        :param start_stage: Stage sequence number of first stage that will be
1107            used (disregarding all earlier stages).
1108        :type end_stage: int, optional
1109        :param end_stage: Stage sequence number of last stage that will be
1110            used (disregarding all later stages).
1111        :type hide_sensitivity_mismatch_warning: bool
1112        :param hide_sensitivity_mismatch_warning: Hide the evalresp warning
1113            that computed and reported sensitivities don't match.
1114        :rtype: :tuple: ( :class:`numpy.ndarray`, chan )
1115        :returns: frequency response at requested frequencies
1116        """
1117        if not self.response_stages:
1118            msg = ("Can not use evalresp on response with no response "
1119                   "stages.")
1120            raise ObsPyException(msg)
1121
1122        import obspy.signal.evrespwrapper as ew
1123        from obspy.signal.headers import clibevresp
1124
1125        out_units = output.upper()
1126        if out_units not in ("DISP", "VEL", "ACC"):
1127            msg = ("requested output is '%s' but must be one of 'DISP', 'VEL' "
1128                   "or 'ACC'") % output
1129            raise ValueError(msg)
1130
1131        frequencies = np.asarray(frequencies)
1132
1133        # Whacky. Evalresp uses a global variable and uses that to scale the
1134        # response if it encounters any unit that is not SI.
1135        scale_factor = [1.0]
1136
1137        def get_unit_mapping(key):
1138            try:
1139                key = key.upper()
1140            except Exception:
1141                pass
1142            units_mapping = {
1143                "M": ew.ENUM_UNITS["DIS"],
1144                "NM": ew.ENUM_UNITS["DIS"],
1145                "CM": ew.ENUM_UNITS["DIS"],
1146                "MM": ew.ENUM_UNITS["DIS"],
1147                "M/S": ew.ENUM_UNITS["VEL"],
1148                "M/SEC": ew.ENUM_UNITS["VEL"],
1149                "NM/S": ew.ENUM_UNITS["VEL"],
1150                "NM/SEC": ew.ENUM_UNITS["VEL"],
1151                "CM/S": ew.ENUM_UNITS["VEL"],
1152                "CM/SEC": ew.ENUM_UNITS["VEL"],
1153                "MM/S": ew.ENUM_UNITS["VEL"],
1154                "MM/SEC": ew.ENUM_UNITS["VEL"],
1155                "M/S**2": ew.ENUM_UNITS["ACC"],
1156                "M/(S**2)": ew.ENUM_UNITS["ACC"],
1157                "M/SEC**2": ew.ENUM_UNITS["ACC"],
1158                "M/(SEC**2)": ew.ENUM_UNITS["ACC"],
1159                "M/S/S": ew.ENUM_UNITS["ACC"],
1160                "NM/S**2": ew.ENUM_UNITS["ACC"],
1161                "NM/(S**2)": ew.ENUM_UNITS["ACC"],
1162                "NM/SEC**2": ew.ENUM_UNITS["ACC"],
1163                "NM/(SEC**2)": ew.ENUM_UNITS["ACC"],
1164                "CM/S**2": ew.ENUM_UNITS["ACC"],
1165                "CM/(S**2)": ew.ENUM_UNITS["ACC"],
1166                "CM/SEC**2": ew.ENUM_UNITS["ACC"],
1167                "CM/(SEC**2)": ew.ENUM_UNITS["ACC"],
1168                "MM/S**2": ew.ENUM_UNITS["ACC"],
1169                "MM/(S**2)": ew.ENUM_UNITS["ACC"],
1170                "MM/SEC**2": ew.ENUM_UNITS["ACC"],
1171                "MM/(SEC**2)": ew.ENUM_UNITS["ACC"],
1172                # Evalresp internally treats strain as displacement.
1173                "M/M": ew.ENUM_UNITS["DIS"],
1174                "M**3/M**3": ew.ENUM_UNITS["DIS"],
1175                "V": ew.ENUM_UNITS["VOLTS"],
1176                "VOLT": ew.ENUM_UNITS["VOLTS"],
1177                "VOLTS": ew.ENUM_UNITS["VOLTS"],
1178                # This is weird, but evalresp appears to do the same.
1179                "V/M": ew.ENUM_UNITS["VOLTS"],
1180                "COUNT": ew.ENUM_UNITS["COUNTS"],
1181                "COUNTS": ew.ENUM_UNITS["COUNTS"],
1182                "T": ew.ENUM_UNITS["TESLA"],
1183                "PA": ew.ENUM_UNITS["PRESSURE"],
1184                "PASCAL": ew.ENUM_UNITS["PRESSURE"],
1185                "PASCALS": ew.ENUM_UNITS["PRESSURE"],
1186                "MBAR": ew.ENUM_UNITS["PRESSURE"]}
1187            if key not in units_mapping:
1188                if key is not None:
1189                    msg = ("The unit '%s' is not known to ObsPy. It will be "
1190                           "assumed to be displacement for the calculations. "
1191                           "This mostly does the right thing but please "
1192                           "proceed with caution.") % key
1193                    warnings.warn(msg)
1194                value = ew.ENUM_UNITS["DIS"]
1195            else:
1196                value = units_mapping[key]
1197
1198            # Scale factor with the same logic as evalresp.
1199            if key in ["CM/S**2", "CM/S", "CM/SEC", "CM"]:
1200                scale_factor[0] = 1.0E2
1201            elif key in ["MM/S**2", "MM/S", "MM/SEC", "MM"]:
1202                scale_factor[0] = 1.0E3
1203            elif key in ["NM/S**2", "NM/S", "NM/SEC", "NM"]:
1204                scale_factor[0] = 1.0E9
1205
1206            return value
1207
1208        all_stages = defaultdict(list)
1209
1210        for stage in self.response_stages:
1211            # optionally select only stages as requested by user
1212            if start_stage is not None:
1213                if stage.stage_sequence_number < start_stage:
1214                    continue
1215            if end_stage is not None:
1216                if stage.stage_sequence_number > end_stage:
1217                    continue
1218            all_stages[stage.stage_sequence_number].append(stage)
1219
1220        stage_lengths = set(map(len, all_stages.values()))
1221        if len(stage_lengths) != 1 or stage_lengths.pop() != 1:
1222            msg = "Each stage can only appear once."
1223            raise ValueError(msg)
1224
1225        stage_list = sorted(all_stages.keys())
1226
1227        stage_objects = []
1228
1229        # Attempt to fix some potentially faulty responses here.
1230        if 1 in all_stages and all_stages[1] and (
1231                not all_stages[1][0].input_units or
1232                not all_stages[1][0].output_units):
1233            # Make a copy to not modify the original
1234            all_stages[1][0] = copy.deepcopy(all_stages[1][0])
1235            # Some stages 1 are just the sensitivity and as thus don't store
1236            # input and output units in for example StationXML. In these cases
1237            # try to guess it from the overall sensitivity or stage 2.
1238            if not all_stages[1][0].input_units:
1239                if self.instrument_sensitivity.input_units:
1240                    all_stages[1][0].input_units = \
1241                        self.instrument_sensitivity.input_units
1242                    msg = "Set the input units of stage 1 to the overall " \
1243                        "input units."
1244                    warnings.warn(msg)
1245            if not all_stages[1][0].output_units:
1246                if max(all_stages.keys()) == 1 and \
1247                        self.instrument_sensitivity.output_units:
1248                    all_stages[1][0].output_units = \
1249                        self.instrument_sensitivity.output_units
1250                    msg = "Set the output units of stage 1 to the overall " \
1251                        "output units."
1252                    warnings.warn(msg)
1253                if 2 in all_stages and all_stages[2] and \
1254                        all_stages[2][0].input_units:
1255                    all_stages[1][0].output_units = \
1256                        all_stages[2][0].input_units
1257                    msg = "Set the output units of stage 1 to the input " \
1258                        "units of stage 2."
1259                    warnings.warn(msg)
1260
1261        for stage_number in stage_list:
1262            st = ew.Stage()
1263            st.sequence_no = stage_number
1264
1265            stage_blkts = []
1266
1267            blockette = all_stages[stage_number][0]
1268
1269            # Write the input and output units.
1270            st.input_units = get_unit_mapping(blockette.input_units)
1271            st.output_units = get_unit_mapping(blockette.output_units)
1272
1273            if isinstance(blockette, PolesZerosResponseStage):
1274                blkt = ew.Blkt()
1275                # Map the transfer function type.
1276                transfer_fct_mapping = {
1277                    "LAPLACE (RADIANS/SECOND)": "LAPLACE_PZ",
1278                    "LAPLACE (HERTZ)": "ANALOG_PZ",
1279                    "DIGITAL (Z-TRANSFORM)": "IIR_PZ"}
1280                blkt.type = ew.ENUM_FILT_TYPES[transfer_fct_mapping[
1281                    blockette.pz_transfer_function_type]]
1282
1283                # The blockette is a pole zero blockette.
1284                pz = blkt.blkt_info.pole_zero
1285
1286                pz.nzeros = len(blockette.zeros)
1287                pz.npoles = len(blockette.poles)
1288                pz.a0 = blockette.normalization_factor
1289                pz.a0_freq = blockette.normalization_frequency
1290
1291                # XXX: Find a better way to do this.
1292                poles = (ew.ComplexNumber * len(blockette.poles))()
1293                for i, value in enumerate(blockette.poles):
1294                    poles[i].real = value.real
1295                    poles[i].imag = value.imag
1296
1297                zeros = (ew.ComplexNumber * len(blockette.zeros))()
1298                for i, value in enumerate(blockette.zeros):
1299                    zeros[i].real = value.real
1300                    zeros[i].imag = value.imag
1301
1302                pz.poles = C.cast(C.pointer(poles),
1303                                  C.POINTER(ew.ComplexNumber))
1304                pz.zeros = C.cast(C.pointer(zeros),
1305                                  C.POINTER(ew.ComplexNumber))
1306            elif isinstance(blockette, CoefficientsTypeResponseStage):
1307                blkt = ew.Blkt()
1308                # This type can have either an FIR or an IIR response. If
1309                # the number of denominators is 0, it is a FIR. Otherwise
1310                # an IIR.
1311
1312                # FIR
1313                if len(blockette.denominator) == 0:
1314                    if blockette.cf_transfer_function_type.lower() \
1315                            != "digital":
1316                        msg = ("When no denominators are given it must "
1317                               "be a digital FIR filter.")
1318                        raise ValueError(msg)
1319                    # Set the type to an asymmetric FIR blockette.
1320                    blkt.type = ew.ENUM_FILT_TYPES["FIR_ASYM"]
1321                    fir = blkt.blkt_info.fir
1322                    fir.h0 = 1.0
1323                    fir.ncoeffs = len(blockette.numerator)
1324                    # XXX: Find a better way to do this.
1325                    coeffs = (C.c_double * len(blockette.numerator))()
1326                    for i, value in enumerate(blockette.numerator):
1327                        coeffs[i] = float(value)
1328                    fir.coeffs = C.cast(C.pointer(coeffs),
1329                                        C.POINTER(C.c_double))
1330                # IIR
1331                else:
1332                    blkt.type = ew.ENUM_FILT_TYPES["IIR_COEFFS"]
1333                    coeff = blkt.blkt_info.coeff
1334
1335                    coeff.h0 = 1.0
1336                    coeff.nnumer = len(blockette.numerator)
1337                    coeff.ndenom = len(blockette.denominator)
1338
1339                    # XXX: Find a better way to do this.
1340                    coeffs = (C.c_double * len(blockette.numerator))()
1341                    for i, value in enumerate(blockette.numerator):
1342                        coeffs[i] = float(value)
1343                    coeff.numer = C.cast(C.pointer(coeffs),
1344                                         C.POINTER(C.c_double))
1345                    coeffs = (C.c_double * len(blockette.denominator))()
1346                    for i, value in enumerate(blockette.denominator):
1347                        coeffs[i] = float(value)
1348                    coeff.denom = C.cast(C.pointer(coeffs),
1349                                         C.POINTER(C.c_double))
1350            elif isinstance(blockette, ResponseListResponseStage):
1351                blkt = ew.Blkt()
1352                blkt.type = ew.ENUM_FILT_TYPES["LIST"]
1353
1354                # Get values as numpy arrays.
1355                f = np.array([float(_i.frequency)
1356                              for _i in blockette.response_list_elements],
1357                             dtype=np.float64)
1358                amp = np.array([float(_i.amplitude)
1359                                for _i in blockette.response_list_elements],
1360                               dtype=np.float64)
1361                phase = np.array([
1362                    float(_i.phase)
1363                    for _i in blockette.response_list_elements],
1364                    dtype=np.float64)
1365
1366                # Sanity check.
1367                min_f = frequencies[frequencies > 0].min()
1368                max_f = frequencies.max()
1369
1370                min_f_avail = min(f)
1371                max_f_avail = max(f)
1372
1373                # Allow interpolation for at most two samples.
1374                _d = np.abs(np.diff(f))
1375                _d = _d[_d > 0].min() * 2
1376                min_f_avail -= _d
1377                max_f_avail += _d
1378
1379                if min_f < min_f_avail or max_f > max_f_avail:
1380                    msg = (
1381                        "Cannot calculate the response as it contains a "
1382                        "response list stage with frequencies only from "
1383                        "%.4f - %.4f Hz. You are requesting a response from "
1384                        "%.4f - %.4f Hz.")
1385                    raise ValueError(msg % (min_f_avail, max_f_avail, min_f,
1386                                            max_f))
1387
1388                amp = scipy.interpolate.InterpolatedUnivariateSpline(
1389                    f, amp, k=3)(frequencies)
1390                phase = scipy.interpolate.InterpolatedUnivariateSpline(
1391                    f, phase, k=3)(frequencies)
1392
1393                # Set static offset to zero.
1394                amp[amp == 0] = 0
1395                phase[phase == 0] = 0
1396
1397                rl = blkt.blkt_info.list
1398                rl.nresp = len(frequencies)
1399
1400                _freq_c = (C.c_double * rl.nresp)()
1401                _amp_c = (C.c_double * rl.nresp)()
1402                _phase_c = (C.c_double * rl.nresp)()
1403
1404                _freq_c[:] = frequencies[:]
1405                _amp_c[:] = amp[:]
1406                _phase_c[:] = phase[:]
1407
1408                rl.freq = C.cast(C.pointer(_freq_c),
1409                                 C.POINTER(C.c_double))
1410                rl.amp = C.cast(C.pointer(_amp_c),
1411                                C.POINTER(C.c_double))
1412                rl.phase = C.cast(C.pointer(_phase_c),
1413                                  C.POINTER(C.c_double))
1414
1415            elif isinstance(blockette, FIRResponseStage):
1416                blkt = ew.Blkt()
1417
1418                if blockette.symmetry == "NONE":
1419                    blkt.type = ew.ENUM_FILT_TYPES["FIR_ASYM"]
1420                if blockette.symmetry == "ODD":
1421                    blkt.type = ew.ENUM_FILT_TYPES["FIR_SYM_1"]
1422                if blockette.symmetry == "EVEN":
1423                    blkt.type = ew.ENUM_FILT_TYPES["FIR_SYM_2"]
1424
1425                # The blockette is a fir blockette
1426                fir = blkt.blkt_info.fir
1427                fir.h0 = 1.0
1428                fir.ncoeffs = len(blockette.coefficients)
1429
1430                # XXX: Find a better way to do this.
1431                coeffs = (C.c_double * len(blockette.coefficients))()
1432                for i, value in enumerate(blockette.coefficients):
1433                    coeffs[i] = float(value)
1434                fir.coeffs = C.cast(C.pointer(coeffs),
1435                                    C.POINTER(C.c_double))
1436            elif isinstance(blockette, PolynomialResponseStage):
1437                msg = ("PolynomialResponseStage not yet implemented. "
1438                       "Please contact the developers.")
1439                raise NotImplementedError(msg)
1440            else:
1441                # Otherwise it could be a gain only stage.
1442                if blockette.stage_gain is not None and \
1443                        blockette.stage_gain_frequency is not None:
1444                    blkt = None
1445                else:
1446                    msg = "Type: %s." % str(type(blockette))
1447                    raise NotImplementedError(msg)
1448
1449            if blkt is not None:
1450                stage_blkts.append(blkt)
1451
1452            # Evalresp requires FIR and IIR blockettes to have decimation
1453            # values. Set the "unit decimation" values in case they are not
1454            # set.
1455            #
1456            # Only set it if there is a stage gain - otherwise evalresp
1457            # complains again.
1458            if isinstance(blockette, PolesZerosResponseStage) and \
1459                    blockette.stage_gain and \
1460                    None in set([
1461                        blockette.decimation_correction,
1462                        blockette.decimation_delay,
1463                        blockette.decimation_factor,
1464                        blockette.decimation_input_sample_rate,
1465                        blockette.decimation_offset]):
1466                # Don't modify the original object.
1467                blockette = copy.deepcopy(blockette)
1468                blockette.decimation_correction = 0.0
1469                blockette.decimation_delay = 0.0
1470                blockette.decimation_factor = 1
1471                blockette.decimation_offset = 0
1472                sr = self.get_sampling_rates()
1473                if sr and blockette.stage_sequence_number in sr and \
1474                        sr[blockette.stage_sequence_number][
1475                            "input_sampling_rate"]:
1476                    blockette.decimation_input_sample_rate = \
1477                        self.get_sampling_rates()[
1478                            blockette.stage_sequence_number][
1479                            "input_sampling_rate"]
1480                # This branch get's large called for responses that only have a
1481                # a single stage.
1482                else:
1483                    blockette.decimation_input_sample_rate = 1.0
1484
1485            # Parse the decimation if is given.
1486            decimation_values = set([
1487                blockette.decimation_correction,
1488                blockette.decimation_delay, blockette.decimation_factor,
1489                blockette.decimation_input_sample_rate,
1490                blockette.decimation_offset])
1491            if None in decimation_values:
1492                if len(decimation_values) != 1:
1493                    msg = ("If a decimation is given, all values must "
1494                           "be specified.")
1495                    raise ValueError(msg)
1496            else:
1497                blkt = ew.Blkt()
1498                blkt.type = ew.ENUM_FILT_TYPES["DECIMATION"]
1499                decimation_blkt = blkt.blkt_info.decimation
1500
1501                # Evalresp does the same!
1502                if blockette.decimation_input_sample_rate == 0:
1503                    decimation_blkt.sample_int = 0.0
1504                else:
1505                    decimation_blkt.sample_int = \
1506                        1.0 / blockette.decimation_input_sample_rate
1507
1508                decimation_blkt.deci_fact = blockette.decimation_factor
1509                decimation_blkt.deci_offset = blockette.decimation_offset
1510                decimation_blkt.estim_delay = blockette.decimation_delay
1511                decimation_blkt.applied_corr = \
1512                    blockette.decimation_correction
1513                stage_blkts.append(blkt)
1514
1515            # Add the gain if it is available.
1516            if blockette.stage_gain is not None and \
1517                    blockette.stage_gain_frequency is not None:
1518                blkt = ew.Blkt()
1519                blkt.type = ew.ENUM_FILT_TYPES["GAIN"]
1520                gain_blkt = blkt.blkt_info.gain
1521                gain_blkt.gain = blockette.stage_gain
1522                gain_blkt.gain_freq = blockette.stage_gain_frequency
1523                stage_blkts.append(blkt)
1524
1525            if not stage_blkts:
1526                msg = "At least one blockette is needed for the stage."
1527                raise ValueError(msg)
1528
1529            # Attach the blockette chain to the stage.
1530            st.first_blkt = C.pointer(stage_blkts[0])
1531            for _i in range(1, len(stage_blkts)):
1532                stage_blkts[_i - 1].next_blkt = C.pointer(stage_blkts[_i])
1533
1534            stage_objects.append(st)
1535
1536        # Attach the instrument sensitivity as stage 0 at the end.
1537        st = ew.Stage()
1538        st.sequence_no = 0
1539        st.input_units = 0
1540        st.output_units = 0
1541        blkt = ew.Blkt()
1542        blkt.type = ew.ENUM_FILT_TYPES["GAIN"]
1543        gain_blkt = blkt.blkt_info.gain
1544        gain_blkt.gain = self.instrument_sensitivity.value
1545        # Set the sensitivity frequency - use 1.0 if not given. This is also
1546        # what evalresp does.
1547        gain_blkt.gain_freq = self.instrument_sensitivity.frequency \
1548            if self.instrument_sensitivity.frequency else 0.0
1549        st.first_blkt = C.pointer(blkt)
1550        stage_objects.append(st)
1551
1552        chan = ew.Channel()
1553        if not stage_objects:
1554            msg = "At least one stage is needed."
1555            raise ValueError(msg)
1556
1557        # Attach the stage chain to the channel.
1558        chan.first_stage = C.pointer(stage_objects[0])
1559        for _i in range(1, len(stage_objects)):
1560            stage_objects[_i - 1].next_stage = C.pointer(stage_objects[_i])
1561
1562        chan.nstages = len(stage_objects)
1563
1564        # Evalresp will take care of setting it to the overall sensitivity.
1565        chan.sensit = 0.0
1566        chan.sensfreq = 0.0
1567
1568        output = np.empty(len(frequencies), dtype=np.complex128)
1569        out_units = C.c_char_p(out_units.encode('ascii', 'strict'))
1570
1571        # Set global variables
1572        if self.resource_id:
1573            clibevresp.curr_file.value = self.resource_id.encode('utf-8')
1574        else:
1575            clibevresp.curr_file.value = None
1576
1577        try:
1578            rc = clibevresp._obspy_check_channel(C.byref(chan))
1579            if rc:
1580                e, m = ew.ENUM_ERROR_CODES[rc]
1581                raise e('check_channel: ' + m)
1582            rc = clibevresp._obspy_norm_resp(
1583                C.byref(chan), -1, 0,
1584                1 if hide_sensitivity_mismatch_warning else 0)
1585            if rc:
1586                e, m = ew.ENUM_ERROR_CODES[rc]
1587                raise e('norm_resp: ' + m)
1588
1589            rc = clibevresp._obspy_calc_resp(C.byref(chan), frequencies,
1590                                             len(frequencies),
1591                                             output, out_units, -1, 0, 0)
1592            if rc:
1593                e, m = ew.ENUM_ERROR_CODES[rc]
1594                raise e('calc_resp: ' + m)
1595
1596            # XXX: Check if this is really not needed.
1597            # output *= scale_factor[0]
1598
1599        finally:
1600            clibevresp.curr_file.value = None
1601
1602        return output, chan
1603
1604    def get_evalresp_response_for_frequencies(
1605            self, frequencies, output="VEL", start_stage=None, end_stage=None):
1606        """
1607        Returns frequency response for given frequencies using evalresp.
1608
1609        :type frequencies: list of float
1610        :param frequencies: Discrete frequencies to calculate response for.
1611        :type output: str
1612        :param output: Output units. One of:
1613
1614            ``"DISP"``
1615                displacement, output unit is meters
1616            ``"VEL"``
1617                velocity, output unit is meters/second
1618            ``"ACC"``
1619                acceleration, output unit is meters/second**2
1620
1621        :type start_stage: int, optional
1622        :param start_stage: Stage sequence number of first stage that will be
1623            used (disregarding all earlier stages).
1624        :type end_stage: int, optional
1625        :param end_stage: Stage sequence number of last stage that will be
1626            used (disregarding all later stages).
1627        :rtype: :class:`numpy.ndarray`
1628        :returns: frequency response at requested frequencies
1629        """
1630        output, chan = self._call_eval_resp_for_frequencies(
1631            frequencies, output=output, start_stage=start_stage,
1632            end_stage=end_stage)
1633        return output
1634
1635    def get_evalresp_response(self, t_samp, nfft, output="VEL",
1636                              start_stage=None, end_stage=None):
1637        """
1638        Returns frequency response and corresponding frequencies using
1639        evalresp.
1640
1641        :type t_samp: float
1642        :param t_samp: time resolution (inverse frequency resolution)
1643        :type nfft: int
1644        :param nfft: Number of FFT points to use
1645        :type output: str
1646        :param output: Output units. One of:
1647
1648            ``"DISP"``
1649                displacement, output unit is meters
1650            ``"VEL"``
1651                velocity, output unit is meters/second
1652            ``"ACC"``
1653                acceleration, output unit is meters/second**2
1654
1655        :type start_stage: int, optional
1656        :param start_stage: Stage sequence number of first stage that will be
1657            used (disregarding all earlier stages).
1658        :type end_stage: int, optional
1659        :param end_stage: Stage sequence number of last stage that will be
1660            used (disregarding all later stages).
1661        :rtype: tuple of two arrays
1662        :returns: frequency response and corresponding frequencies
1663        """
1664        # Calculate the output frequencies.
1665        fy = 1 / (t_samp * 2.0)
1666        # start at zero to get zero for offset/ DC of fft
1667        # numpy 1.9 introduced a dtype kwarg
1668        try:
1669            freqs = np.linspace(0, fy, int(nfft // 2) + 1, dtype=np.float64)
1670        except Exception:
1671            freqs = np.linspace(0, fy, int(nfft // 2) + 1).astype(np.float64)
1672
1673        response = self.get_evalresp_response_for_frequencies(
1674            freqs, output=output, start_stage=start_stage, end_stage=end_stage)
1675        return response, freqs
1676
1677    def __str__(self):
1678        i_s = self.instrument_sensitivity
1679        if i_s:
1680            input_units = i_s.input_units \
1681                if i_s.input_units else "UNKNOWN"
1682            input_units_description = i_s.input_units_description \
1683                if i_s.input_units_description else ""
1684            output_units = i_s.output_units \
1685                if i_s.output_units else "UNKNOWN"
1686            output_units_description = i_s.output_units_description \
1687                if i_s.output_units_description else ""
1688            sensitivity = ("%g" % i_s.value) if i_s.value else "UNKNOWN"
1689            freq = ("%.3f" % i_s.frequency) if i_s.frequency else "UNKNOWN"
1690        else:
1691            input_units = "UNKNOWN"
1692            input_units_description = ""
1693            output_units = "UNKNOWN"
1694            output_units_description = ""
1695            sensitivity = "UNKNOWN"
1696            freq = "UNKNOWN"
1697
1698        ret = (
1699            "Channel Response\n"
1700            "\tFrom {input_units} ({input_units_description}) to "
1701            "{output_units} ({output_units_description})\n"
1702            "\tOverall Sensitivity: {sensitivity} defined at {freq} Hz\n"
1703            "\t{stages} stages:\n{stage_desc}").format(
1704            input_units=input_units,
1705            input_units_description=input_units_description,
1706            output_units=output_units,
1707            output_units_description=output_units_description,
1708            sensitivity=sensitivity,
1709            freq=freq,
1710            stages=len(self.response_stages),
1711            stage_desc="\n".join(
1712                ["\t\tStage %i: %s from %s to %s,"
1713                 " gain: %s" % (
1714                     i.stage_sequence_number, i.__class__.__name__,
1715                     i.input_units, i.output_units,
1716                     ("%g" % i.stage_gain) if i.stage_gain else "UNKNOWN")
1717                 for i in self.response_stages]))
1718        return ret
1719
1720    def _repr_pretty_(self, p, cycle):
1721        p.text(str(self))
1722
1723    def plot(self, min_freq, output="VEL", start_stage=None,
1724             end_stage=None, label=None, axes=None, sampling_rate=None,
1725             unwrap_phase=False, plot_degrees=False, show=True, outfile=None):
1726        """
1727        Show bode plot of instrument response.
1728
1729        :type min_freq: float
1730        :param min_freq: Lowest frequency to plot.
1731        :type output: str
1732        :param output: Output units. One of:
1733
1734                ``"DISP"``
1735                    displacement
1736                ``"VEL"``
1737                    velocity
1738                ``"ACC"``
1739                    acceleration
1740
1741        :type start_stage: int, optional
1742        :param start_stage: Stage sequence number of first stage that will be
1743            used (disregarding all earlier stages).
1744        :type end_stage: int, optional
1745        :param end_stage: Stage sequence number of last stage that will be
1746            used (disregarding all later stages).
1747        :type label: str
1748        :param label: Label string for legend.
1749        :type axes: list of 2 :class:`matplotlib.axes.Axes`
1750        :param axes: List/tuple of two axes instances to plot the
1751            amplitude/phase spectrum into. If not specified, a new figure is
1752            opened.
1753        :type sampling_rate: float
1754        :param sampling_rate: Manually specify sampling rate of time series.
1755            If not given it is attempted to determine it from the information
1756            in the individual response stages.  Does not influence the spectra
1757            calculation, if it is not known, just provide the highest frequency
1758            that should be plotted times two.
1759        :type unwrap_phase: bool
1760        :param unwrap_phase: Set optional phase unwrapping using NumPy.
1761        :type plot_degrees: bool
1762        :param plot_degrees: if ``True`` plot bode in degrees
1763        :type show: bool
1764        :param show: Whether to show the figure after plotting or not. Can be
1765            used to do further customization of the plot before showing it.
1766        :type outfile: str
1767        :param outfile: Output file path to directly save the resulting image
1768            (e.g. ``"/tmp/image.png"``). Overrides the ``show`` option, image
1769            will not be displayed interactively. The given path/filename is
1770            also used to automatically determine the output format. Supported
1771            file formats depend on your matplotlib backend.  Most backends
1772            support png, pdf, ps, eps and svg. Defaults to ``None``.
1773
1774        .. rubric:: Basic Usage
1775
1776        >>> from obspy import read_inventory
1777        >>> resp = read_inventory()[0][0][0].response
1778        >>> resp.plot(0.001, output="VEL")  # doctest: +SKIP
1779
1780        .. plot::
1781
1782            from obspy import read_inventory
1783            resp = read_inventory()[0][0][0].response
1784            resp.plot(0.001, output="VEL")
1785        """
1786        import matplotlib.pyplot as plt
1787        from matplotlib.transforms import blended_transform_factory
1788
1789        # detect sampling rate from response stages
1790        if sampling_rate is None:
1791            for stage in self.response_stages[::-1]:
1792                if (stage.decimation_input_sample_rate is not None and
1793                        stage.decimation_factor is not None):
1794                    sampling_rate = (stage.decimation_input_sample_rate /
1795                                     stage.decimation_factor)
1796                    break
1797            else:
1798                msg = ("Failed to autodetect sampling rate of channel from "
1799                       "response stages. Please manually specify parameter "
1800                       "`sampling_rate`")
1801                raise Exception(msg)
1802        if sampling_rate == 0:
1803            msg = "Can not plot response for channel with sampling rate `0`."
1804            raise ZeroSamplingRate(msg)
1805
1806        t_samp = 1.0 / sampling_rate
1807        nyquist = sampling_rate / 2.0
1808        nfft = int(sampling_rate / min_freq)
1809
1810        cpx_response, freq = self.get_evalresp_response(
1811            t_samp=t_samp, nfft=nfft, output=output, start_stage=start_stage,
1812            end_stage=end_stage)
1813
1814        if axes is not None:
1815            ax1, ax2 = axes
1816            fig = ax1.figure
1817        else:
1818            fig = plt.figure()
1819            ax1 = fig.add_subplot(211)
1820            ax2 = fig.add_subplot(212, sharex=ax1)
1821
1822        label_kwarg = {}
1823        if label is not None:
1824            label_kwarg['label'] = label
1825
1826        # plot amplitude response
1827        lw = 1.5
1828        lines = ax1.loglog(freq, abs(cpx_response), lw=lw, **label_kwarg)
1829        color = lines[0].get_color()
1830        if self.instrument_sensitivity:
1831            trans_above = blended_transform_factory(ax1.transData,
1832                                                    ax1.transAxes)
1833            trans_right = blended_transform_factory(ax1.transAxes,
1834                                                    ax1.transData)
1835            arrowprops = dict(
1836                arrowstyle="wedge,tail_width=1.4,shrink_factor=0.8", fc=color)
1837            bbox = dict(boxstyle="round", fc="w")
1838            if self.instrument_sensitivity.frequency:
1839                ax1.annotate("%.1g" % self.instrument_sensitivity.frequency,
1840                             (self.instrument_sensitivity.frequency, 1.0),
1841                             xytext=(self.instrument_sensitivity.frequency,
1842                                     1.1),
1843                             xycoords=trans_above, textcoords=trans_above,
1844                             ha="center", va="bottom",
1845                             arrowprops=arrowprops, bbox=bbox)
1846            if self.instrument_sensitivity.value:
1847                ax1.annotate("%.1e" % self.instrument_sensitivity.value,
1848                             (1.0, self.instrument_sensitivity.value),
1849                             xytext=(1.05, self.instrument_sensitivity.value),
1850                             xycoords=trans_right, textcoords=trans_right,
1851                             ha="left", va="center",
1852                             arrowprops=arrowprops, bbox=bbox)
1853
1854        # plot phase response
1855        phase = np.angle(cpx_response, deg=plot_degrees)
1856        if unwrap_phase and not plot_degrees:
1857            phase = np.unwrap(phase)
1858        ax2.semilogx(freq, phase, color=color, lw=lw)
1859
1860        # plot nyquist frequency
1861        for ax in (ax1, ax2):
1862            ax.axvline(nyquist, ls="--", color=color, lw=lw)
1863
1864        # only do adjustments if we initialized the figure in here
1865        if not axes:
1866            _adjust_bode_plot_figure(fig, show=False,
1867                                     plot_degrees=plot_degrees)
1868
1869        if outfile:
1870            fig.savefig(outfile)
1871        else:
1872            if show:
1873                plt.show()
1874
1875        return fig
1876
1877    def get_paz(self):
1878        """
1879        Get Poles and Zeros stage.
1880
1881        Prints a warning if more than one poles and zeros stage is found.
1882        Raises if no poles and zeros stage is found.
1883
1884        :rtype: :class:`PolesZerosResponseStage`
1885        :returns: Poles and Zeros response stage.
1886        """
1887        paz = [deepcopy(stage) for stage in self.response_stages
1888               if isinstance(stage, PolesZerosResponseStage)]
1889        if len(paz) == 0:
1890            msg = "No PolesZerosResponseStage found."
1891            raise Exception(msg)
1892        elif len(paz) > 1:
1893            msg = ("More than one PolesZerosResponseStage encountered. "
1894                   "Returning first one found.")
1895            warnings.warn(msg)
1896        return paz[0]
1897
1898    def get_sacpz(self):
1899        """
1900        Returns SACPZ ASCII text representation of Response.
1901
1902        :rtype: str
1903        :returns: Textual SACPZ representation of response.
1904        """
1905        # extract paz
1906        paz = self.get_paz()
1907        return paz_to_sacpz_string(paz, self.instrument_sensitivity)
1908
1909    @classmethod
1910    def from_paz(cls, zeros, poles, stage_gain,
1911                 stage_gain_frequency=1.0, input_units='M/S',
1912                 output_units='VOLTS', normalization_frequency=1.0,
1913                 pz_transfer_function_type='LAPLACE (RADIANS/SECOND)',
1914                 normalization_factor=1.0):
1915        """
1916        Convert poles and zeros lists into a single-stage response.
1917
1918        Takes in lists of complex poles and zeros and returns a Response with
1919        those values defining its only stage. Most of the optional parameters
1920        defined here are from
1921        :class:`~obspy.core.inventory.response.PolesZerosResponseStage`.
1922
1923        :type zeros: list of complex
1924        :param zeros: All zeros of the response to be defined.
1925        :type poles: list of complex
1926        :param poles: All poles of the response to be defined.
1927        :type stage_gain: float
1928        :param stage_gain: The gain value of the response [sensitivity]
1929        :returns: new Response instance with given P-Z values
1930        """
1931        pzstage = PolesZerosResponseStage(
1932            stage_sequence_number=1, stage_gain=stage_gain,
1933            stage_gain_frequency=stage_gain_frequency,
1934            input_units=input_units, output_units=output_units,
1935            pz_transfer_function_type=pz_transfer_function_type,
1936            normalization_frequency=normalization_frequency,
1937            zeros=zeros, poles=poles,
1938            normalization_factor=normalization_factor)
1939        sens = InstrumentSensitivity(value=stage_gain,
1940                                     frequency=stage_gain_frequency,
1941                                     input_units=input_units,
1942                                     output_units=output_units)
1943        resp = cls(instrument_sensitivity=sens, response_stages=[pzstage])
1944        resp.recalculate_overall_sensitivity()
1945        return resp
1946
1947
1948def paz_to_sacpz_string(paz, instrument_sensitivity):
1949    """
1950    Returns SACPZ ASCII text representation of Response.
1951
1952    :type paz: :class:`PolesZerosResponseStage`
1953    :param paz: Poles and Zeros response information
1954    :type instrument_sensitivity: :class:`InstrumentSensitivity`
1955    :param paz: Overall instrument sensitivity of response
1956    :rtype: str
1957    :returns: Textual SACPZ representation of poles and zeros response stage.
1958    """
1959    # assemble output string
1960    out = []
1961    out.append("ZEROS %i" % len(paz.zeros))
1962    for c in paz.zeros:
1963        out.append(" %+.6e %+.6e" % (c.real, c.imag))
1964    out.append("POLES %i" % len(paz.poles))
1965    for c in paz.poles:
1966        out.append(" %+.6e %+.6e" % (c.real, c.imag))
1967    constant = paz.normalization_factor * instrument_sensitivity.value
1968    out.append("CONSTANT %.6e" % constant)
1969    return "\n".join(out)
1970
1971
1972class InstrumentSensitivity(ComparingObject):
1973    """
1974    From the StationXML Definition:
1975        The total sensitivity for a channel, representing the complete
1976        acquisition system expressed as a scalar. Equivalent to SEED stage 0
1977        gain with (blockette 58) with the ability to specify a frequency range.
1978
1979    Sensitivity and frequency ranges. The FrequencyRangeGroup is an optional
1980    construct that defines a pass band in Hertz (FrequencyStart and
1981    FrequencyEnd) in which the SensitivityValue is valid within the number of
1982    decibels specified in FrequencyDBVariation.
1983    """
1984    def __init__(self, value, frequency, input_units,
1985                 output_units, input_units_description=None,
1986                 output_units_description=None, frequency_range_start=None,
1987                 frequency_range_end=None, frequency_range_db_variation=None):
1988        """
1989        :type value: float
1990        :param value: Complex type for sensitivity and frequency ranges.
1991            This complex type can be used to represent both overall
1992            sensitivities and individual stage gains. The FrequencyRangeGroup
1993            is an optional construct that defines a pass band in Hertz (
1994            FrequencyStart and FrequencyEnd) in which the SensitivityValue is
1995            valid within the number of decibels specified in
1996            FrequencyDBVariation.
1997        :type frequency: float
1998        :param frequency: Complex type for sensitivity and frequency
1999            ranges.  This complex type can be used to represent both overall
2000            sensitivities and individual stage gains. The FrequencyRangeGroup
2001            is an optional construct that defines a pass band in Hertz (
2002            FrequencyStart and FrequencyEnd) in which the SensitivityValue is
2003            valid within the number of decibels specified in
2004            FrequencyDBVariation.
2005        :param input_units: string
2006        :param input_units: The units of the data as input from the
2007            perspective of data acquisition. After correcting data for this
2008            response, these would be the resulting units.
2009            Name of units, e.g. "M/S", "V", "PA".
2010        :param input_units_description: string, optional
2011        :param input_units_description: The units of the data as input from the
2012            perspective of data acquisition. After correcting data for this
2013            response, these would be the resulting units.
2014            Description of units, e.g. "Velocity in meters per second",
2015            "Volts", "Pascals".
2016        :param output_units: string
2017        :param output_units: The units of the data as output from the
2018            perspective of data acquisition. These would be the units of the
2019            data prior to correcting for this response.
2020            Name of units, e.g. "M/S", "V", "PA".
2021        :type output_units_description: str, optional
2022        :param output_units_description: The units of the data as output from
2023            the perspective of data acquisition. These would be the units of
2024            the data prior to correcting for this response.
2025            Description of units, e.g. "Velocity in meters per second",
2026            "Volts", "Pascals".
2027        :type frequency_range_start: float, optional
2028        :param frequency_range_start: Start of the frequency range for which
2029            the SensitivityValue is valid within the dB variation specified.
2030        :type frequency_range_end: float, optional
2031        :param frequency_range_end: End of the frequency range for which the
2032            SensitivityValue is valid within the dB variation specified.
2033        :type frequency_range_db_variation: float, optional
2034        :param frequency_range_db_variation: Variation in decibels within the
2035            specified range.
2036        """
2037        self.value = value
2038        self.frequency = frequency
2039        self.input_units = input_units
2040        self.input_units_description = input_units_description
2041        self.output_units = output_units
2042        self.output_units_description = output_units_description
2043        self.frequency_range_start = frequency_range_start
2044        self.frequency_range_end = frequency_range_end
2045        self.frequency_range_db_variation = frequency_range_db_variation
2046
2047    def __str__(self):
2048        ret = ("Instrument Sensitivity:\n"
2049               "\tValue: {value}\n"
2050               "\tFrequency: {frequency}\n"
2051               "\tInput units: {input_units}\n"
2052               "\tInput units description: {input_units_description}\n"
2053               "\tOutput units: {output_units}\n"
2054               "\tOutput units description: {output_units_description}\n")
2055        ret = ret.format(**self.__dict__)
2056        return ret
2057
2058    def _repr_pretty_(self, p, cycle):
2059        p.text(str(self))
2060
2061
2062# XXX duplicated code, PolynomialResponseStage could probably be implemented by
2063# XXX inheriting from both InstrumentPolynomial and ResponseStage
2064class InstrumentPolynomial(ComparingObject):
2065    """
2066    From the StationXML Definition:
2067        The total sensitivity for a channel, representing the complete
2068        acquisition system expressed as a polynomial. Equivalent to SEED stage
2069        0 polynomial (blockette 62).
2070    """
2071    def __init__(self, input_units, output_units,
2072                 frequency_lower_bound,
2073                 frequency_upper_bound, approximation_lower_bound,
2074                 approximation_upper_bound, maximum_error, coefficients,
2075                 approximation_type='MACLAURIN', resource_id=None, name=None,
2076                 input_units_description=None,
2077                 output_units_description=None, description=None):
2078        """
2079        :type approximation_type: str
2080        :param approximation_type: Approximation type. Currently restricted to
2081            'MACLAURIN' by StationXML definition.
2082        :type frequency_lower_bound: float
2083        :param frequency_lower_bound: Lower frequency bound.
2084        :type frequency_upper_bound: float
2085        :param frequency_upper_bound: Upper frequency bound.
2086        :type approximation_lower_bound: float
2087        :param approximation_lower_bound: Lower bound of approximation.
2088        :type approximation_upper_bound: float
2089        :param approximation_upper_bound: Upper bound of approximation.
2090        :type maximum_error: float
2091        :param maximum_error: Maximum error.
2092        :type coefficients: list of floats
2093        :param coefficients: List of polynomial coefficients.
2094        :param input_units: string
2095        :param input_units: The units of the data as input from the
2096            perspective of data acquisition. After correcting data for this
2097            response, these would be the resulting units.
2098            Name of units, e.g. "M/S", "V", "PA".
2099        :param output_units: string
2100        :param output_units: The units of the data as output from the
2101            perspective of data acquisition. These would be the units of the
2102            data prior to correcting for this response.
2103            Name of units, e.g. "M/S", "V", "PA".
2104        :type resource_id: str
2105        :param resource_id: This field contains a string that should serve as a
2106            unique resource identifier. This identifier can be interpreted
2107            differently depending on the data center/software that generated
2108            the document. Also, we recommend to use something like
2109            GENERATOR:Meaningful ID. As a common behavior equipment with the
2110            same ID should contains the same information/be derived from the
2111            same base instruments.
2112        :type name: str
2113        :param name: A name given to the filter stage.
2114        :param input_units_description: string, optional
2115        :param input_units_description: The units of the data as input from the
2116            perspective of data acquisition. After correcting data for this
2117            response, these would be the resulting units.
2118            Description of units, e.g. "Velocity in meters per second",
2119            "Volts", "Pascals".
2120        :type output_units_description: str, optional
2121        :param output_units_description: The units of the data as output from
2122            the perspective of data acquisition. These would be the units of
2123            the data prior to correcting for this response.
2124            Description of units, e.g. "Velocity in meters per second",
2125            "Volts", "Pascals".
2126        :type description: str, optional
2127        :param description: A short description of of the filter.
2128        """
2129        self.input_units = input_units
2130        self.output_units = output_units
2131        self.input_units_description = input_units_description
2132        self.output_units_description = output_units_description
2133        self.resource_id = resource_id
2134        self.name = name
2135        self.description = description
2136        self._approximation_type = approximation_type
2137        self.frequency_lower_bound = frequency_lower_bound
2138        self.frequency_upper_bound = frequency_upper_bound
2139        self.approximation_lower_bound = approximation_lower_bound
2140        self.approximation_upper_bound = approximation_upper_bound
2141        self.maximum_error = maximum_error
2142        self.coefficients = coefficients
2143
2144    @property
2145    def approximation_type(self):
2146        return self._approximation_type
2147
2148    @approximation_type.setter
2149    def approximation_type(self, value):
2150        value = str(value).upper()
2151        allowed = ("MACLAURIN",)
2152        if value not in allowed:
2153            msg = ("Value '%s' for polynomial response approximation type not "
2154                   "allowed. Possible values are: '%s'")
2155            msg = msg % (value, "', '".join(allowed))
2156            raise ValueError(msg)
2157        self._approximation_type = value
2158
2159
2160class FilterCoefficient(FloatWithUncertainties):
2161    """
2162    A filter coefficient.
2163    """
2164    def __init__(self, value, number=None):
2165        """
2166        :type value: float
2167        :param value: The actual value of the coefficient
2168        :type number: int, optional
2169        :param number: Number to indicate the position of the coefficient.
2170        """
2171        super(FilterCoefficient, self).__init__(value)
2172        self.number = number
2173
2174    @property
2175    def number(self):
2176        return self._number
2177
2178    @number.setter
2179    def number(self, value):
2180        if value is not None:
2181            value = int(value)
2182        self._number = value
2183
2184
2185class CoefficientWithUncertainties(FloatWithUncertainties):
2186    """
2187    A coefficient with optional uncertainties.
2188    """
2189    def __init__(self, value, number=None, lower_uncertainty=None,
2190                 upper_uncertainty=None):
2191        """
2192        :type value: float
2193        :param value: The actual value of the coefficient
2194        :type number: int, optional
2195        :param number: Number to indicate the position of the coefficient.
2196        :type lower_uncertainty: float
2197        :param lower_uncertainty: Lower uncertainty (aka minusError)
2198        :type upper_uncertainty: float
2199        :param upper_uncertainty: Upper uncertainty (aka plusError)
2200        """
2201        super(CoefficientWithUncertainties, self).__init__(
2202            value, lower_uncertainty=lower_uncertainty,
2203            upper_uncertainty=upper_uncertainty)
2204        self.number = number
2205
2206    @property
2207    def number(self):
2208        return self._number
2209
2210    @number.setter
2211    def number(self, value):
2212        if value is not None:
2213            value = int(value)
2214        self._number = value
2215
2216
2217def _adjust_bode_plot_figure(fig, plot_degrees=False, grid=True, show=True):
2218    """
2219    Helper function to do final adjustments to Bode plot figure.
2220    """
2221    import matplotlib.pyplot as plt
2222    # make more room in between subplots for the ylabel of right plot
2223    fig.subplots_adjust(hspace=0.02, top=0.87, right=0.82)
2224    ax1, ax2 = fig.axes[:2]
2225    # workaround for older matplotlib versions
2226    try:
2227        ax1.legend(loc="lower center", ncol=3, fontsize='small')
2228    except TypeError:
2229        leg_ = ax1.legend(loc="lower center", ncol=3)
2230        leg_.prop.set_size("small")
2231    plt.setp(ax1.get_xticklabels(), visible=False)
2232    ax1.set_ylabel('Amplitude')
2233    minmax1 = ax1.get_ylim()
2234    ax1.set_ylim(top=minmax1[1] * 5)
2235    ax1.grid(True)
2236    ax2.set_xlabel('Frequency [Hz]')
2237    if plot_degrees:
2238        # degrees bode plot
2239        ax2.set_ylabel('Phase [degrees]')
2240        ax2.set_yticks(np.arange(-180, 180, 30))
2241        ax2.set_yticklabels(np.arange(-180, 180, 30))
2242        ax2.set_ylim(-180, 180)
2243    else:
2244        # radian bode plot
2245        plt.setp(ax2.get_yticklabels()[-1], visible=False)
2246        ax2.set_ylabel('Phase [rad]')
2247        minmax2 = ax2.yaxis.get_data_interval()
2248        yticks2 = np.arange(minmax2[0] - minmax2[0] % (pi / 2),
2249                            minmax2[1] - minmax2[1] % (pi / 2) + pi, pi / 2)
2250        ax2.set_yticks(yticks2)
2251        ax2.set_yticklabels([_pitick2latex(x) for x in yticks2])
2252    ax2.grid(True)
2253
2254    if show:
2255        plt.show()
2256
2257
2258def _pitick2latex(x):
2259    """
2260    Helper function to convert a float that is a multiple of pi/2
2261    to a latex string.
2262    """
2263    # safety check, if no multiple of pi/2 return normal representation
2264    if x % (pi / 2) != 0:
2265        return "%#.3g" % x
2266    string = "$"
2267    if x < 0:
2268        string += "-"
2269    if x / pi % 1 == 0:
2270        x = abs(int(x / pi))
2271        if x == 0:
2272            return "$0$"
2273        elif x == 1:
2274            x = ""
2275        string += r"%s\pi$" % x
2276    else:
2277        x = abs(int(2 * x / pi))
2278        if x == 1:
2279            x = ""
2280        string += r"\frac{%s\pi}{2}$" % x
2281    return string
2282
2283
2284if __name__ == '__main__':
2285    import doctest
2286    doctest.testmod(exclude_empty=True)
2287