# http://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------
'''
This module contains functions to plot instrument response transfer functions
in Bode plot style using Matplotlib.
Example
* :download:`test_response_plot.py </../../examples/test_response_plot.py>`
::
from pyrocko.plot import response
from pyrocko.example import get_example_data
get_example_data('test_response.resp')
resps, labels = response.load_response_information(
'test_response.resp', 'resp')
response.plot(
responses=resps, labels=labels, filename='test_response.png',
fmin=0.001, fmax=400., dpi=75.)
.. figure :: /static/test_response.png
:align: center
Example response plot
'''
from __future__ import absolute_import
import logging
import math
import numpy as num
from pyrocko import util
from pyrocko import guts
logger = logging.getLogger('plot.response')
def normalize_on_flat(f, tf, factor=10000.):
df = num.diff(num.log(f))
tap = 1.0 / (1.0 + factor * (num.diff(num.log(num.abs(tf)))/df)**2)
return tf / (num.sum(num.abs(tf[1:]) * tap) / num.sum(tap))
[docs]def draw(
response,
axes_amplitude=None, axes_phase=None,
fmin=0.01, fmax=100., nf=100,
normalize=False,
style={},
label=None):
'''
Draw instrument response in Bode plot style to given Matplotlib axes
:param response: instrument response as a
:py:class:`pyrocko.response.FrequencyResponse` object
:param axes_amplitude: :py:class:`matplotlib.axes.Axes` object to use when
drawing the amplitude response
:param axes_phase: :py:class:`matplotlib.axes.Axes` object to use when
drawing the phase response
:param fmin: minimum frequency [Hz]
:param fmax: maximum frequency [Hz]
:param nf: number of frequencies where to evaluate the response
:param style: :py:class:`dict` with keyword arguments to tune the line
style
:param label: string to be passed to the ``label`` argument of
:py:meth:`matplotlib.axes.Axes.plot`
'''
f = num.exp(num.linspace(num.log(fmin), num.log(fmax), nf))
resp_fmax = response.get_fmax()
if resp_fmax is not None:
if fmax > resp_fmax:
logger.warning(
'Maximum frequency above range supported by response. '
'Clipping to supported%s.' % (
' (%s)' % label if label else ''))
f = f[f <= resp_fmax]
if f.size == 0:
return
tf = response.evaluate(f)
ok = num.isfinite(tf)
if not num.all(ok):
logger.warning('NaN values present in evaluated response%s.' % (
' (%s)' % label if label else ''))
f = f[ok]
tf = tf[ok]
if normalize:
tf = normalize_on_flat(f, tf)
ta = num.abs(tf)
if axes_amplitude:
axes_amplitude.plot(f, ta, label=label, **style)
for checkpoint in response.checkpoints:
axes_amplitude.plot(
checkpoint.frequency, checkpoint.value, 'o',
color=style.get('color', 'black'))
if axes_phase:
dta = num.diff(num.log(ta))
iflat = num.nanargmin(num.abs(num.diff(dta)) + num.abs(dta[:-1]))
tp = num.unwrap(num.angle(tf))
ioff = int(num.round(tp[iflat] / (2.0*num.pi)))
tp -= ioff * 2.0 * num.pi
axes_phase.plot(f, tp/num.pi, label=label, **style)
else:
tp = [0.]
return (num.min(ta), num.max(ta)), (num.min(tp)/num.pi, num.max(tp)/num.pi)
[docs]def setup_axes(axes_amplitude=None, axes_phase=None):
'''
Configure axes in Bode plot style.
'''
if axes_amplitude is not None:
axes_amplitude.set_ylabel('Amplitude ratio')
axes_amplitude.set_xscale('log')
axes_amplitude.set_yscale('log')
axes_amplitude.grid(True)
axes_amplitude.axhline(1.0, lw=0.5, color='black')
if axes_phase is None:
axes_amplitude.set_xlabel('Frequency [Hz]')
axes_amplitude.set_xscale('log')
else:
axes_amplitude.xaxis.set_ticklabels([])
if axes_phase is not None:
axes_phase.set_ylabel('Phase [$\\pi$]')
axes_phase.set_xscale('log')
axes_phase.set_xlabel('Frequency [Hz]')
axes_phase.grid(True)
axes_phase.axhline(0.0, lw=0.5, color='black')
[docs]def plot(
responses,
filename=None,
dpi=100,
fmin=0.01, fmax=100., nf=100,
normalize=False,
fontsize=10.,
figsize=None,
styles=None,
labels=None):
'''
Draw instrument responses in Bode plot style.
:param responses: instrument responses as
:py:class:`pyrocko.response.FrequencyResponse` objects
:param fmin: minimum frequency [Hz]
:param fmax: maximum frequency [Hz]
:param nf: number of frequencies where to evaluate the response
:param normalize: if ``True`` normalize flat part of response to be ``1``
:param styles: :py:class:`list` of :py:class:`dict` objects with keyword
arguments to be passed to matplotlib's
:py:meth:`matplotlib.axes.Axes.plot` function when drawing the response
lines. Length must match number of responses.
:param filename: file name to pass to matplotlib's ``savefig`` function. If
``None``, the plot is shown with :py:func:`matplotlib.pyplot.show`.
:param fontsize: font size in points used in axis labels and legend
:param figsize: :py:class:`tuple`, ``(width, height)`` in inches
:param labels: :py:class:`list` of names to show in legend. Length must
correspond to number of responses.
'''
from matplotlib import pyplot as plt
from pyrocko.plot import mpl_init, mpl_margins, mpl_papersize
from pyrocko.plot import graph_colors, to01
mpl_init(fontsize=fontsize)
if figsize is None:
figsize = mpl_papersize('a4', 'portrait')
fig = plt.figure(figsize=figsize)
labelpos = mpl_margins(
fig, w=7., h=5., units=fontsize, nw=1, nh=2, hspace=2.)
axes_amplitude = fig.add_subplot(2, 1, 1)
labelpos(axes_amplitude, 2., 1.5)
axes_phase = fig.add_subplot(2, 1, 2)
labelpos(axes_phase, 2., 1.5)
setup_axes(axes_amplitude, axes_phase)
if styles is None:
styles = [
dict(color=to01(graph_colors[i % len(graph_colors)]))
for i in range(len(responses))]
else:
assert len(styles) == len(responses)
if labels is None:
labels = [None] * len(responses)
else:
assert len(labels) == len(responses)
a_ranges, p_ranges = [], []
have_labels = False
for style, resp, label in zip(styles, responses, labels):
a_range, p_range = draw(
response=resp,
axes_amplitude=axes_amplitude,
axes_phase=axes_phase,
fmin=fmin, fmax=fmax, nf=nf,
normalize=normalize,
style=style,
label=label)
if label is not None:
have_labels = True
a_ranges.append(a_range)
p_ranges.append(p_range)
if have_labels:
axes_amplitude.legend(loc='lower right', prop=dict(size=fontsize))
if a_ranges:
a_ranges = num.array(a_ranges)
p_ranges = num.array(p_ranges)
amin, amax = num.nanmin(a_ranges), num.nanmax(a_ranges)
pmin, pmax = num.nanmin(p_ranges), num.nanmax(p_ranges)
amin *= 0.5
amax *= 2.0
pmin -= 0.5
pmax += 0.5
pmin = math.floor(pmin)
pmax = math.ceil(pmax)
axes_amplitude.set_ylim(amin, amax)
axes_phase.set_ylim(pmin, pmax)
axes_amplitude.set_xlim(fmin, fmax)
axes_phase.set_xlim(fmin, fmax)
if filename is not None:
fig.savefig(filename, dpi=dpi)
else:
plt.show()
def tts(t):
if t is None:
return '?'
else:
return util.tts(t, format='%Y-%m-%d')
def load_response_information(
filename, format,
nslc_patterns=None, fake_input_units=None, stages=None):
from pyrocko import pz, trace
from pyrocko.io import resp as fresp
resps = []
labels = []
if format == 'sacpz':
if fake_input_units is not None:
raise Exception(
'cannot guess true input units from plain SAC PZ files')
zeros, poles, constant = pz.read_sac_zpk(filename)
resp = trace.PoleZeroResponse(
zeros=zeros, poles=poles, constant=constant)
resps.append(resp)
labels.append(filename)
elif format == 'pf':
if fake_input_units is not None:
raise Exception(
'Cannot guess input units from plain response files.')
resp = guts.load(filename=filename)
resps.append(resp)
labels.append(filename)
elif format in ('resp', 'evalresp'):
for resp in list(fresp.iload_filename(filename)):
if nslc_patterns is not None and not util.match_nslc(
nslc_patterns, resp.codes):
continue
units = ''
in_units = None
if resp.response.instrument_sensitivity:
s = resp.response.instrument_sensitivity
in_units = fake_input_units or s.input_units.name
if s.input_units and s.output_units:
units = ', %s -> %s' % (in_units, s.output_units.name)
if format == 'resp':
resps.append(resp.response.get_pyrocko_response(
'.'.join(resp.codes),
fake_input_units=fake_input_units,
stages=stages).expect_one())
else:
target = {
'M/S': 'vel',
'M': 'dis',
}[in_units]
if resp.end_date is not None:
time = (resp.start_date + resp.end_date)*0.5
else:
time = resp.start_date + 3600*24*10
r = trace.Evalresp(
respfile=filename,
nslc_id=resp.codes,
target=target,
time=time,
stages=stages)
resps.append(r)
labels.append('%s (%s.%s.%s.%s, %s - %s%s)' % (
(filename, ) + resp.codes +
(tts(resp.start_date), tts(resp.end_date), units)))
elif format == 'stationxml':
from pyrocko.fdsn import station as fs
sx = fs.load_xml(filename=filename)
for network in sx.network_list:
for station in network.station_list:
for channel in station.channel_list:
nslc = (
network.code,
station.code,
channel.location_code,
channel.code)
if nslc_patterns is not None and not util.match_nslc(
nslc_patterns, nslc):
continue
if not channel.response:
logger.warning(
'no response for channel %s.%s.%s.%s given.'
% nslc)
continue
units = ''
if channel.response.instrument_sensitivity:
s = channel.response.instrument_sensitivity
if s.input_units and s.output_units:
units = ', %s -> %s' % (
fake_input_units or s.input_units.name,
s.output_units.name)
resps.append(channel.response.get_pyrocko_response(
'.'.join(nslc),
fake_input_units=fake_input_units,
stages=stages).expect_one())
labels.append(
'%s (%s.%s.%s.%s, %s - %s%s)' % (
(filename, ) + nslc +
(tts(channel.start_date),
tts(channel.end_date),
units)))
return resps, labels
if __name__ == '__main__':
import sys
from optparse import OptionParser
util.setup_logging('pyrocko.plot.response.__main__', 'warning')
usage = 'python -m pyrocko.plot.response <filename> ... [options]'
description = '''Plot instrument responses (transfer functions).'''
allowed_formats = ['sacpz', 'resp', 'evalresp', 'stationxml', 'pf']
parser = OptionParser(
usage=usage,
description=description,
formatter=util.BetterHelpFormatter())
parser.add_option(
'--format',
dest='format',
default='sacpz',
choices=allowed_formats,
help='assume input files are of given FORMAT. Choices: %s' % (
', '.join(allowed_formats)))
parser.add_option(
'--fmin',
dest='fmin',
type='float',
default=0.01,
help='minimum frequency [Hz], default: %default')
parser.add_option(
'--fmax',
dest='fmax',
type='float',
default=100.,
help='maximum frequency [Hz], default: %default')
parser.add_option(
'--normalize',
dest='normalize',
action='store_true',
help='normalize response to be 1 on flat part')
parser.add_option(
'--save',
dest='filename',
help='save figure to file with name FILENAME')
parser.add_option(
'--dpi',
dest='dpi',
type='float',
default=100.,
help='DPI setting for pixel image output, default: %default')
parser.add_option(
'--patterns',
dest='nslc_patterns',
metavar='NET.STA.LOC.CHA,...',
help='show only responses of channels matching any of the given '
'patterns')
parser.add_option(
'--stages',
dest='stages',
metavar='START:STOP',
help='show only responses selected stages')
parser.add_option(
'--fake-input-units',
dest='fake_input_units',
choices=['M', 'M/S', 'M/S**2'],
metavar='UNITS',
help='show converted response for given input units, choices: '
'["M", "M/S", "M/S**2"]')
(options, args) = parser.parse_args(sys.argv[1:])
if len(args) == 0:
parser.print_help()
sys.exit(1)
fns = args
resps = []
labels = []
stages = None
if options.stages:
stages = tuple(int(x) for x in options.stages.split(':', 1))
stages = stages[0]-1, stages[1]
for fn in fns:
if options.nslc_patterns is not None:
nslc_patterns = options.nslc_patterns.split(',')
else:
nslc_patterns = None
resps_this, labels_this = load_response_information(
fn, options.format, nslc_patterns,
fake_input_units=options.fake_input_units,
stages=stages)
resps.extend(resps_this)
labels.extend(labels_this)
plot(
resps,
fmin=options.fmin, fmax=options.fmax, nf=200,
normalize=options.normalize,
labels=labels, filename=options.filename, dpi=options.dpi)