# http://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------
import logging
import math
import hashlib
from collections import OrderedDict
import numpy as num
import matplotlib.pyplot as plt
from pyrocko import util, guts, plot
from pyrocko.squirrel import model, error
from . import base
from ...carpet import Carpet
guts_prefix = 'squirrel.ops'
logger = logging.getLogger('psq.ops.spectrogram')
class LRU:
def __init__(self, func, maxsize=128):
self.cache = OrderedDict()
self.func = func
self.maxsize = maxsize
def __call__(self, *args):
if args in self.cache:
self.cache.move_to_end(args)
return self.cache[args]
result = self.func(*args)
self.cache[args] = result
if len(self.cache) > self.maxsize:
self.cache.popitem(last=False)
return result
def is_pow2(x):
return round(math.log(x) / math.log(2.0), 7) % 1.0 == 0.0
def antialiasing_taper(n, corner=0.9):
h = num.ones(n)
f = num.arange(n) / (n-1)
mask = f > corner
h[mask] *= (0.5 + 0.5 * num.cos(
(f[mask] - corner) / (1.0 - corner) * num.pi))**2
# h[mask] = 0.0
return h
def cos_taper(x):
return 0.5 - 0.5 * num.cos(x)
[docs]
class Pow2Windowing(guts.Object):
nblock = guts.Int.T(default=1024)
nlevels = guts.Int.T(default=4)
weighting_exponent = guts.Int.T(default=2)
def __init__(self, **kwargs):
guts.Object.__init__(self, **kwargs)
if not is_pow2(self.nblock):
raise ValueError(
'Pow2Windowing: `nblock` must be a power of 2.')
self.nfrequencies = self.nblock // 2 + 1
self.taper_td = num.hanning(self.nblock)
self.taper_fd_2 = antialiasing_taper(self.nfrequencies // 2 + 1, 0.9)
self.taper_td_2_inv_center = 1.0 \
/ num.hanning(self.nblock // 2)[
self.nblock // 8: 3 * self.nblock // 8]
def hash(self):
return hashlib.sha256(b'%i,%i' % (self.nblock, self.nlevels))
def time_increment(self, deltat, ilevel=0):
if ilevel < 0:
ilevel = self.nlevels + ilevel
if not (0 <= ilevel < self.nlevels):
raise ValueError(
'Pow2Windowing: invalid `ilevel`: %i' % ilevel)
return deltat * (self.nblock // 2) * 2**ilevel
def iwindow_range(self, deltat, tmin, tmax, ilevel):
itmin = int(round(tmin/deltat))
itmax = int(round(tmax/deltat))
tinc = self.time_increment(deltat, ilevel)
itinc = int(round(tinc / deltat))
imin = (itmin + (itinc-1)) // itinc
imax = (itmax - 1) // itinc + 1
iadd = 2 if ilevel > 0 else 0
return num.arange(imin-iadd, imax+iadd)
def frequencies_and_weights(self, deltat):
eps1 = 1e-4
eps2 = 1e-7
nfrequencies = self.nfrequencies
nfrequencies_all \
= nfrequencies + nfrequencies // 2 * (self.nlevels - 1)
frequencies_all = num.zeros(nfrequencies_all)
ifmin = 0
for ilevel in range(self.nlevels-1, -1, -1):
if ilevel == self.nlevels-1:
ifmax = ifmin + nfrequencies
fmin_fill = 0.0
else:
ifmax = ifmin + nfrequencies // 2
df = 1.0 / (deltat*self.nblock * 2**ilevel)
fmin_fill = df * ((nfrequencies // 2) + 1)
fmax = (0.5 / deltat) / 2**ilevel
frequencies_all[ifmin:ifmax] = num.linspace(
fmin_fill, fmax, ifmax - ifmin)
ifmin = ifmax
log_frequencies_all = num.zeros_like(frequencies_all)
log_frequencies_all[1:] = num.log(frequencies_all[1:])
norm_log_frequencies_all = \
(log_frequencies_all[1:] - log_frequencies_all[1]) \
/ (log_frequencies_all[-1] - log_frequencies_all[1])
weights = num.zeros((self.nlevels, frequencies_all.size))
for ilevel in range(self.nlevels):
fmin = 1.0 / (deltat*self.nblock * 2**ilevel)
fmax = (0.5 / deltat) / 2**ilevel
fmask = num.logical_and(
fmin < frequencies_all, frequencies_all < fmax)
# weights[ilevel, fmask] = cos_taper(
# num.sqrt((frequencies_all[fmask] - fmin) / (fmax - fmin))
# * 2.0 * num.pi)** self.weighting_exponent
weights[ilevel, fmask] = cos_taper(
(log_frequencies_all[fmask] - num.log(fmin))
/ (num.log(fmax) - num.log(fmin)) * 2.0 * num.pi) \
** self.weighting_exponent
if ilevel == 0:
weights[ilevel, 1:] += cos_taper(
norm_log_frequencies_all*num.pi)**2 * eps1
if ilevel == self.nlevels - 1:
weights[ilevel, 1:] += cos_taper(
(1.0 - norm_log_frequencies_all)*num.pi)**2 * eps1
weights += eps2
weights /= num.sum(weights, axis=0)[num.newaxis, :]
return frequencies_all, weights
def make_block(ilevel, windowing, samples_td):
if num.all(samples_td == 0.0):
return (
num.full(samples_td.size // 2 + 1, num.nan),
num.zeros(samples_td.size // 4))
taper_td = windowing.taper_td
taper_fd_2 = windowing.taper_fd_2
taper_td_2_inv_center = windowing.taper_td_2_inv_center
nblock = windowing.nblock
dmean = num.mean(samples_td*taper_td)
samples_fd = num.fft.rfft((samples_td-dmean) * taper_td)
nf = samples_fd.size
samples_td_2 = num.fft.irfft(samples_fd[:nf//2 + 1] * taper_fd_2) / 2
samples_td_2_center = samples_td_2[nblock//8:3*nblock//8]
samples_td_2_center *= taper_td_2_inv_center
samples_td_2_center += dmean
samples_out = num.abs(samples_fd)
samples_out[samples_out == 0.0] = num.nan
# old_settings = num.seterr(divide='ignore')
num.log(samples_out, out=samples_out)
# num.seterr(**old_settings)
samples_out *= 2.0 # power-spectrum
samples_out += num.log(2**ilevel)
return (samples_out, samples_td_2_center)
[docs]
class SpectrogramGroup(guts.Object):
codes = model.CodesNSLCE.T()
tmin = guts.Timestamp.T()
tmax = guts.Timestamp.T()
deltat = guts.Float.T()
windowing = Pow2Windowing.T()
levels = guts.List.T(Carpet.T())
def get_multi_spectrogram(self, interpolation='cos'):
frequencies, weights = self.windowing.frequencies_and_weights(
self.deltat)
times = self.levels[0].times
spectrogram = num.zeros((frequencies.size, times.size))
nfcut = frequencies.size
# thalf0 = 0.5 * self.levels[0].deltat
thalf0 = 0.0
for ilevel, level in enumerate(self.levels):
index_f = num.floor(
frequencies[:nfcut]
/ level.component_axes['frequency'][1]).astype(int)
# thalf = 0.0625 * level.deltat
thalf = 0.0
if interpolation == 'nearest_neighbor' or ilevel == 0:
index_t = num.round((
(times + thalf0) - (level.tmin + thalf))
/ level.deltat).astype(int)
spectrogram[:nfcut, :] \
+= level.data[index_f][:, index_t] \
* weights[ilevel, :nfcut][:, num.newaxis]
elif interpolation == 'cos':
index_t = num.clip(
num.floor(
((times + thalf0) - (level.tmin + thalf))
/ level.deltat).astype(int),
0,
level.times.size-2)
w1 = cos_taper(
num.pi * ((times+thalf0) - (level.times[index_t]+thalf))
/ level.deltat)
w0 = 1.0 - w1
spectrogram[:nfcut, :] \
+= w0[num.newaxis, :] \
* level.data[index_f][:, index_t] \
* weights[ilevel, :nfcut][:, num.newaxis]
spectrogram[:nfcut, :] \
+= w1[num.newaxis, :] \
* level.data[index_f][:, index_t+1] \
* weights[ilevel, :nfcut][:, num.newaxis]
else:
raise ValueError(
'invalid interpolation method: %s' % interpolation)
nfcut -= self.windowing.nfrequencies // 2
return Carpet(
codes=self.codes,
tmin=self.levels[0].tmin,
deltat=self.levels[0].deltat,
component_axes={'frequency': frequencies},
data=spectrogram)
def get_frequency_range(self):
return (
1.0 / (self.deltat * self.windowing.nblock * 2**(
self.windowing.nlevels-1)),
0.5 / self.deltat)
def fill_template(self, path):
return path.format(
codes=self.codes.safe_str,
tmin=util.time_to_str(self.tmin, '%Y-%m-%dT%H-%M-%S'),
tmax=util.time_to_str(self.tmax, '%Y-%m-%dT%H-%M-%S'))
def plot_construction(self, path=None, interpolation='cos'):
fig = plt.figure(figsize=(60, 20))
fig.suptitle(self.fill_template('{codes} ({tmin} - {tmax})'))
axes_over = fig.add_subplot(3, 1, 2)
axes_over.set_xscale('log')
fslice = slice(2, None)
levels = [
level.crop(fslice=fslice)
for level in self.levels]
vmin = min(level.stats.p10 for level in levels)
vmax = max(level.stats.max for level in levels)
axes = None
for ilevel, level in enumerate(levels):
axes = fig.add_subplot(
3, self.windowing.nlevels + 1, ilevel+1,
sharex=axes)
level.mpl_draw(
axes,
vmin=vmin,
vmax=vmax,
component_axis='frequency')
axes.set_xlim(self.tmin, self.tmax)
axes.set_ylim(*self.get_frequency_range())
plot.mpl_time_axis(axes)
axes.set_yscale('log')
mean_amps = num.nanmean(level.data, axis=1)
axes_over.plot(level.component_axes['frequency'], mean_amps)
spectrogram = self.get_multi_spectrogram(interpolation=interpolation) \
.crop(fslice=fslice)
axes = fig.add_subplot(
3, self.windowing.nlevels + 1, self.windowing.nlevels + 1,
sharex=axes)
spectrogram.mpl_draw(
axes,
vmin=vmin,
vmax=vmax,
component_axis='frequency')
axes.set_xlim(self.tmin, self.tmax)
axes.set_ylim(*self.get_frequency_range())
plot.mpl_time_axis(axes)
axes.set_yscale('log')
mean_amps = num.nanmean(spectrogram.data, axis=1)
axes_over.plot(
spectrogram.component_axes['frequency'],
mean_amps, color='black', zorder=10)
frequencies, weights = self.windowing.frequencies_and_weights(
self.deltat)
axes_weights = fig.add_subplot(3, 1, 3)
axes_weights.set_xscale('log')
for ilevel in range(self.windowing.nlevels):
axes_weights.plot(
frequencies[fslice], weights[ilevel, fslice])
axes_weights.plot(
frequencies[fslice], num.sum(weights, axis=0)[fslice])
if path is not None:
fig.savefig(self.fill_template(path))
else:
plt.show()
[docs]
class MultiSpectrogramOperator(base.Operator):
codes_projection = base.basic_codes_projection_t(
'{i.network}.{i.station}.{i.location}.{i.channel}.{i.extra}MS')
windowing = Pow2Windowing.T(
default=Pow2Windowing.D())
quantity = model.QuantityType.T(default='velocity')
tinc_max = guts.Float.T(optional=True)
interpolation = guts.StringChoice.T(
choices=['cos', 'nearest_neighbor'], default='cos')
def __init__(self, *args, **kwargs):
base.Operator.__init__(self, *args, **kwargs)
self._caches = {}
self._tinc_max_effective = self.tinc_max
self._accessor_id = 'spectrogram_%s' % str(self._operator_id)
@property
def kind_requires(self):
if self.quantity == 'counts':
return ('waveform',)
else:
return ('waveform', 'response')
@property
def kind_provides(self):
return ('channel', 'carpet')
def make_block(self, deltat, kind_codes_id, ilevel, iwindow):
if ilevel == 0:
tmin_window = iwindow * self.windowing.time_increment(
deltat, ilevel)
tmax_window = tmin_window + self.windowing.nblock * deltat
trs = self._input.get_waveforms(
tmin=tmin_window,
tmax=tmax_window,
kind_codes_ids=[kind_codes_id],
want_incomplete=False,
accessor_id=self._accessor_id)
ntraces = len(trs)
assert ntraces <= 1
if ntraces:
tr = trs[0]
nblock = self.windowing.nblock
assert tr.ydata.size == nblock
return make_block(ilevel, self.windowing, tr.ydata)
else:
return make_block(
ilevel, self.windowing, num.zeros(self.windowing.nblock))
else:
blocks = [
self.get_block(deltat, kind_codes_id, ilevel - 1, iwin)
for iwin in range(iwindow*2-1, iwindow*2+4)]
nblock = self.windowing.nblock
aggregation = (
[blocks[0][1][nblock//8:]]
+ [block[1] for block in blocks[1:4]]
+ [blocks[-1][1][:-nblock//8]])
have_zero_block = any(
num.all(block[1] == 0.0) for block in blocks)
samples = num.concatenate(aggregation)
if have_zero_block:
samples[:] = 0.0
return make_block(ilevel, self.windowing, samples)
def get_block(self, deltat, kind_codes_id, ilevel, iwindow):
k1 = (deltat, kind_codes_id, ilevel)
if k1 not in self._caches:
def make_get_block_window(deltat, kind_codes_id, ilevel):
def get_block_window(iwindow):
return self.make_block(
deltat, kind_codes_id, ilevel, iwindow)
cachesize = int(
max(
4 * self._tinc_max_effective,
16 * self.windowing.time_increment(
deltat, self.windowing.nlevels-1))
/ (deltat*self.windowing.nblock))
logger.debug('Cache size: %i' % cachesize)
return LRU(get_block_window, maxsize=cachesize)
self._caches[k1] = make_get_block_window(
deltat, kind_codes_id, ilevel)
return self._caches[k1](iwindow)
def get_spectrogram_groups(
self,
obj=None,
tmin=None,
tmax=None,
codes=None,
nsamples_limit=None,
**kwargs):
if self._tinc_max_effective is None:
self._tinc_max_effective = tmax - tmin
if tmax - tmin > self._tinc_max_effective:
logger.warning(
'MultiSpectrogramOperator: Cache size may be too small. '
'Try setting or increasing `tinc_max` parameter.')
tmin, tmax, codes = model.get_selection_args(
model.CHANNEL, obj, tmin, tmax, None, codes)
if None in (tmin, tmax):
raise ValueError('No time span given.')
info = self._input.get_codes_info('waveform', codes=codes)
groups = []
for pattern, kind_codes_id, codes, deltat in info:
nsamples_required = int(round((tmax - tmin) / deltat))
if nsamples_limit is not None \
and nsamples_required > nsamples_limit:
continue
resp = None
if self.quantity != 'counts':
try:
resp = self._input \
.get_response(codes=codes, tmin=tmin, tmax=tmax) \
.get_effective(self.quantity)
except error.SquirrelError as e:
logger.debug(e)
continue
levels = []
for ilevel in range(self.windowing.nlevels):
iwindows = self.windowing.iwindow_range(
deltat, tmin, tmax, ilevel)
times = iwindows \
* self.windowing.time_increment(deltat, ilevel)
nwindows = iwindows.size
nfrequencies = self.windowing.nfrequencies
values = num.zeros(
(nfrequencies, nwindows))
for i in range(nwindows):
values[:, i] = self.get_block(
deltat, kind_codes_id, ilevel, iwindows[i])[0]
all_nan = not num.any(num.isfinite(values))
frequency_delta = 1.0 \
/ (deltat * self.windowing.nblock * 2**ilevel)
frequencies = num.arange(nfrequencies) * frequency_delta
if resp and not all_nan:
values[1:, :] -= 2.0 * num.log(num.abs(resp.evaluate(
frequencies[1:])))[:, num.newaxis]
deltat_carpet = self.windowing.time_increment(deltat, ilevel)
levels.append(Carpet(
codes=codes,
tmin=times[0]+deltat_carpet,
deltat=deltat_carpet,
component_axes={'frequency': frequencies},
data=values))
groups.append(SpectrogramGroup(
codes=codes,
tmin=tmin,
tmax=tmax,
deltat=deltat,
windowing=self.windowing,
levels=levels))
self.advance_accessor(self._accessor_id, 'waveform')
return groups
def get_carpets(
self,
codes=None,
tmin=None,
tmax=None,
show_construction=False,
nsamples_limit=None,
accessor_id=None):
# `accessor_id` is accepted for compatibility with other providers
# but not used: this operator manages its own accessor.
mappings, codes_want = self.get_mappings_and_matching_codes(codes)
mappings.sort(key=lambda mapping: mapping.in_codes[0])
carpets = []
for mapping in mappings:
(in_codes,) = mapping.in_codes
(out_codes,) = mapping.out_codes
groups = self.get_spectrogram_groups(
codes=in_codes,
tmin=tmin,
tmax=tmax,
nsamples_limit=nsamples_limit)
for group in groups:
if show_construction:
group.plot_construction(
interpolation=self.interpolation)
fslice = slice(2, None)
carpet = group.get_multi_spectrogram(
interpolation=self.interpolation).crop(fslice=fslice)
carpet.data = carpet.data.astype(num.float32)
carpet.codes = out_codes
if codes_want is None or carpet.codes in codes_want:
carpets.append(carpet)
return carpets
__all__ = [
'Pow2Windowing',
'SpectrogramGroup',
'MultiSpectrogramOperator',
]