import numpy
from matplotlib import pyplot
from mspasspy.ccore.seismic import (
Seismogram,
TimeSeries,
TimeSeriesEnsemble,
SeismogramEnsemble,
)
from mspasspy.algorithms.basic import ExtractComponent
from mspasspy.algorithms.window import scale as alg_scale
[docs]def wtva_raw(section, t0, dt, ranges=None, scale=1.0, color="k", normalize=False):
"""
Plot a numpy 2D array (matrix) in a wiggle trace, variable area
format as used in paper seismic section displays. This is the low
level function that displays a ensemble of scalar data cast into
a matrix with the rows as time and the columns as just trace number.
This function is called by the ensemble plotters. For scalar data
it is called one but for 3C data 3 windows are created with this
function called 3 times.
This function was taken directly from the fatiando a terra
0.5 package. The current web site is https://www.fatiando.org/
and apparently the code we use here has been depricated by that
group. This function is unaltered from the original found on
github for the 0.5 version. Only the documentation was changed
to clarify this ancestory. We also changed the name from
seismic_wiggle to wtva_raw to mesh more cleanly with mspass as
the name better reflects the functions use.
Additional changes:
Pavlis Aug 27, 2020
Added a test that if color was a None only the wiggles will be drawn
(i.e. no shading)
Parameters:
* section : 2D array
matrix of traces (first dimension time, second dimension traces)
* t0 : float
time of first sample (added for mspass - needed for passive data)
* dt : float
sample rate in seconds
* ranges : (x1, x2)
min and max horizontal values (default trace number)
* scale : float
scale factor multiplied by the section values before plotting
* color : tuple of strings
Color for filling the wiggle, positive and negative lobes. If None
draw only wiggle traces
* normalize :
True to normalizes all trace in the section using global max/min
data will be in the range (-0.5, 0.5) zero centered
.. warning::
Slow for more than 200 traces, in this case decimate your
data or use ``image_raw``.
"""
npts, ntraces = section.shape # time/traces
if ntraces < 1:
raise IndexError("Nothing to plot")
if npts < 1:
raise IndexError("Nothing to plot")
t = numpy.linspace(t0, t0 + dt * npts, npts)
amp = 1.0 # normalization factor
gmin = 0.0 # global minimum
toffset = 0.0 # offset in time to make 0 centered
if normalize:
gmax = section.max()
gmin = section.min()
amp = gmax - gmin
toffset = 0.5
pyplot.ylim(max(t), 0)
if ranges is None:
ranges = (0, ntraces)
x0, x1 = ranges
# horizontal increment
dx = (x1 - x0) / ntraces
pyplot.xlim(x0 - dx / 2.0, x1 + dx / 2.0)
for i, trace in enumerate(section.transpose()):
tr = (((trace - gmin) / amp) - toffset) * scale * dx
x = x0 + i * dx # x position for this trace
pyplot.plot(x + tr, t, "k")
if color != None:
pyplot.fill_betweenx(t, x + tr, x, tr > 0, color=color)
[docs]def image_raw(
section, t0, dt, ranges=None, cmap=pyplot.cm.gray, aspect=None, vmin=None, vmax=None
):
"""
Plot a numpy 2D array (matrix) in an image format. This type of
plot has two uses: (1) large numbers of spatially coherent signals
(as in a conventional seismic reflection) are more clearly displayed
this way than as wiggles, and (2) this function can be called
immediately following a call to wtva_raw to produce an section
with variable area wiggle trace data shown on top of the an image
representation of the data data (note for dense data it is usually
necessary to decimate the data on the column axis to make a
clean plot - functions to make such plots need a decimate parameter).
This is the low level function to make such plots. It is
directly comparable to wtva_raw.
This function was taken directly from the fatiando a terra
0.5 package. The current web site is https://www.fatiando.org/
and apparently the code we use here has been depricated by that
group. This function is unaltered from the original found on
github for the 0.5 version. Only the documentation was changed
to clarify this ancestory. We also changed the name from
seismic_image to image_raw to mesh more cleanly with mspass as
the name better reflects the functions use.
Parameters:
* section : 2D array
matrix of traces (first dimension time, second dimension traces)
* t0 : float
time of first sample (added for mspass - needed for passive data)
* dt : float
sample rate in seconds
* ranges : (x1, x2)
min and max horizontal coordinate values (default trace number)
* cmap : colormap
color map to be used. (see pyplot.cm module)
* aspect : float
matplotlib imshow aspect parameter, ratio between axes
* vmin, vmax : float
min and max values for imshow
"""
npts, maxtraces = section.shape # time/traces
if maxtraces < 1:
raise IndexError("Nothing to plot")
if npts < 1:
raise IndexError("Nothing to plot")
t = numpy.linspace(t0, t0 + dt * npts, npts)
data = section
if ranges is None:
ranges = (0, maxtraces - 1)
x0, x1 = ranges
extent = (x0 - 0.5, x1 + 0.5, t[npts - 1], t[0])
if aspect is None: # guarantee a rectangular picture
aspect = numpy.round((x1 - x0) / numpy.max(t))
if aspect <= 0.0:
aspect = 1.0
aspect -= aspect * 0.2
if vmin is None and vmax is None:
scale = numpy.abs([section.max(), section.min()]).max()
vmin = -scale
vmax = scale
pyplot.imshow(
data,
aspect=aspect,
cmap=cmap,
origin="upper",
extent=extent,
vmin=vmin,
vmax=vmax,
)
[docs]def tse2nparray(ens):
nseis = len(ens.member)
tmax = 0.0
tmin = 0.0
dt = 0.0
for i in range(nseis):
if i == 0:
tmin = ens.member[i].t0
tmax = ens.member[i].endtime()
dt = ens.member[i].dt
else:
tmin = min(tmin, ens.member[i].t0)
tmax = max(tmax, ens.member[i].endtime())
# check for irregular sample rates. Test uses a fractional
# tolerance that is a frozen constant here
delta_dt = abs(dt - ens.member[i].dt)
if delta_dt / dt > 0.01:
raise RuntimeError(
"tse2dmatrix: Irregular sample rates - cannot convert"
)
# A naive user might pass an ensemble of data with absolute times
# spanning years. The calculation here uses a threshold on size
# as a sanity check to avoid absurd malloc requests
n = nseis
m = int((tmax - tmin) / dt + 1)
Mmax = 10000000 # size limit hard wired
if m > Mmax:
raise RuntimeError(
"tse2dmatix: irrational computed time range - you are probably incorrectly using data with large range of absolute times"
)
work = numpy.zeros(shape=[m, n])
# The algorithm used here is horribly inefficient if there are large
# differences in start and end times but this approach is safer
for j in range(n):
tjstart = ens.member[j].t0
tjend = ens.member[j].endtime()
for i in range(m):
t = tmin + i * dt
if (t >= tjstart) and (t <= tjend):
k = ens.member[j].sample_number(t)
work[i, j] = ens.member[j].data[k]
return [tmin, dt, work]
[docs]def seis2nparray(d):
tmin = d.t0
dt = d.dt
m = d.npts
n = 3
work = numpy.array(d.data)
return [tmin, dt, work]
[docs]def ts2nparray(d):
tmin = d.t0
dt = d.dt
m = d.npts
work = numpy.array(d.data)
return [tmin, dt, work]
[docs]def wtvaplot(
d, ranges=None, scale=1.0, fill_color="k", normalize=False, cmap=None, title=None
):
"""
Wiggle trace variable area plotter for mspass ensemble objects.
"""
# We have to handle 3C ensembles specially to make 3 separate
# windows. this logic is potentially confusing. the else
# block handles all but 3C ensembles - common read structure
# makes a single plot call work for all 3 cases
figure_handles = []
if isinstance(d, SeismogramEnsemble):
# We always plot 3C data as 3 windows. We extract each
# component and then call this function with a trivial
# recursion - only call itself once and only once
title3c = title
for i in range(3):
pyplot.figure(i)
dcomp = ExtractComponent(d, i)
if title != None:
title3c = "%s:%d" % (title, i)
try:
[t0, dt, section] = tse2nparray(dcomp)
wtva_raw(section, t0, dt, ranges, scale, fill_color, normalize)
if cmap != None:
image_raw(section, t0, dt, ranges, cmap)
if title3c != None:
pyplot.title(title3c)
figure_handles.append(pyplot.gcf())
except RuntimeError as err:
print(err)
return None
else:
try:
# need to force these into the scope of this block
plotdata = []
if isinstance(d, TimeSeriesEnsemble):
plotdata = tse2nparray(d)
elif isinstance(d, Seismogram):
plotdata = seis2nparray(d)
elif isinstance(d, TimeSeries):
plotdata = ts2nparray(d)
else:
raise RuntimeError(
"wtvaplot - data received is not one supported by mspass"
)
t0 = plotdata[0]
dt = plotdata[1]
section = plotdata[2]
wtva_raw(section, t0, dt, ranges, scale, fill_color, normalize)
if cmap != None:
image_raw(section, t0, dt, ranges, cmap)
if title != None:
pyplot.title(title)
figure_handles.append(pyplot.gcf())
except RuntimeError as err:
print(err)
return None
return figure_handles
[docs]def imageplot(
d, ranges=None, cmap=pyplot.cm.gray, aspect=None, vmin=None, vmax=None, title=None
):
"""
Image plotter for mspass ensemble objects.
"""
# We have to handle 3C ensembles specially to make 3 separate
# windows. this logic is potentially confusing. the else
# block handles all but 3C ensembles - common read structure
# makes a single plot call work for all 3 cases
figure_handles = []
if isinstance(d, SeismogramEnsemble):
# We always plot 3C data as 3 windows. We extract each
# component and then call this function with a trivial
# recursion - only call itself once and only once
title3c = title
for i in range(3):
pyplot.figure(i)
dcomp = ExtractComponent(d, i)
if title != None:
title3c = "%s:%d" % (title, i)
try:
[t0, dt, section] = tse2nparray(dcomp)
image_raw(section, t0, dt, ranges, cmap, aspect, vmin, vmax)
if title3c != None:
pyplot.title(title3c)
figure_handles.append(pyplot.gcf())
except RuntimeError as err:
print(err)
return None
else:
try:
# need to force these into the scope of this block
plotdata = []
if isinstance(d, TimeSeriesEnsemble):
plotdata = tse2nparray(d)
elif isinstance(d, Seismogram):
plotdata = seis2nparray(d)
elif isinstance(d, TimeSeries):
plotdata = ts2nparray(d)
else:
raise RuntimeError(
"wtvaplot - data received is not one supported by mspass"
)
t0 = plotdata[0]
dt = plotdata[1]
section = plotdata[2]
image_raw(section, t0, dt, ranges, cmap, aspect, vmin, vmax)
if title != None:
pyplot.title(title)
figure_handles.append(pyplot.gcf())
except RuntimeError as err:
print(err)
return None
return figure_handles
[docs]class SectionPlotter:
"""
This class was designed to plot data in a seismic reflection style
display. That means time is the y axis but runs in a reverse direction
and traces are equally spaced along the x axis. The data can be plotted
as simple wiggle traces, wiggle trace variable area, an image format,
and a wiggletrace variable area plot overlaying an image plot.
The object itself only defines the style of plot to be produced along
with other assorted plot parameters. The plot method can be called on
any mspass data object to produce a graphic display. TimeSeries
data produce a frame with one seismogram plotted. Seismogram data
will produce a one frame display with the 3 components arranged in
component order from left to right. TimeSeriesEnsembles produce
conventional reflection style sections with the data displayed from
left to right in whatever order the ensemble is sorted to.
SeismogramEnsembles are the only type that create multiple windows.
That is, each component is displayed in a different window. The
display can be understood as a conversion of a SeismogramEnsemble to
a set of 3 TimeSeriesEnsembles that are displayed in 3 windows.
The component can be deduced only from the title string with
has :0, :1, and :2 appended to it to denote components 0, 1, and 2
respectively.
The plots produced by this object are simple ones for display only.
The concept is that fancier tools for gui interaction like a trace
editor be created as children of this base class.
"""
def __init__(self, scale=1.0, normalize=False, title=None):
"""
The constructor initializes all the internal variables that define
the possible behaviors of the plotting engine. The constructor
currently does no sanity check on the compatibility of the parameters.
The intent is overall behavior should be altered from the default
only through the change_style method. The public attributes
(scale,normalize, and title) can be changed without hard. Those
defined as private should be altered only indirectly through the
change_style method.
:param scale: Set the amount by which the data should be scaled
before plotting. Note the assumption is the data amplitude is
of order 1. Use normalize if that assumption is bad.
:param normalize: boolean used to determine if data need to be
autoscaled. If true a section scaling is produced from the minimum
and maximum of the input data. Relative amplitudes between
each scalar signal will be preserved. To change scaling use
a mspass amplitude scaling function before calling the plotter.
:param title: is a string written verbatim at the top of each
plot. For 3C ensembles the string :0, :1, and :2 is added to
title for each of the 3 windows drawn for each of the 3 components
of the parent ensemble.
"""
# these are public because they can set independently
self.scale = scale
self.title = title
self.normalize = normalize
# these have some interdependencies and are best altered only
# through the change_style method. This may be unnecessary
# as it may duplicate the call to change_style at the end, BUT
# if the default changes these initial values do not need to be
# changed
self._style = "wtvaimg"
self._use_variable_area = True
self._fill_color = "k" # black in matplotlib
self._color_background = True
self._color_map = "seismic"
# these are options to raw codes adapted from fatiando a terra
# that are currently ignored. Convert to args for __init__ if
# it proves useful to have them in the api
self._ranges = None
self._aspect = None
self._vmin = None
self._vmax = None
# use change_style to simply default style
self.change_style("wtvaimg")
[docs] def change_style(self, newstyle, fill_color="k", color_map="seismic"):
"""
Use this method to change the plot style. Options are described
below. Some parameter combinations are illegal and will result in
a RuntimeError if used. The illegal combos are listed below for
each style.
:param newstyle: This is the primary switch for this method and
is used to define the style of plot that will be produced. The
allowed values for this string are listed below along with
restrictions (when they exist) on related parameters.
wtva - wiggle trace variable area. That means a wiggle traced with
the positive quadrants colored by the color defined by the fill_color
parameter. If fill_color is passed as None a RuntimeError will
be thrown.
img - data are displayed as an image with the color_map parameter
defining the matplotlib color map used to map amplitudes into
a given color. Note as with wtva the data are presumed to be
scaled to be of order 1. color_map must not be None, which it
can be if the plot was changed from a previous style like wtva,
or a RuntimeError exception will be thrown.
wtvaimg is like wtva but the wtva plot is drawn on top of an
image plot. Since this is the union of wtva and img a RuntimeError
exception will be throw if either color_map or fill_color are null.
wt will produce a standard line graph of each input as a
black line (that feature is currently frozen). color_map and
fill_color are ignored for this style so no exceptions should
occur when the method is called with this value of newstyle.
"""
if newstyle == "wtva":
if fill_color == None:
raise RuntimeError(
"SectionPlotter.change_style: wtva style requires fill_color to be a valid color code - received a None"
)
self._style = "wtva"
self._color_background = False
# force this when set this way
self._color_map = None
self._fill_color = fill_color
self._use_variable_area = True
elif newstyle == "wtvaimg":
if color_map == None:
raise RuntimeError(
"SectionPlotter.change_style: wtvaimg style requires a color_map definition - received a None"
)
if fill_color == None:
raise RuntimeError(
"SectionPlotter.change_style: wtvaimg style requires a fill_color definition - received a None"
)
self._style = "wtvaimg"
self._color_background = True
self._fill_color = fill_color
self._color_map = color_map
self._use_variable_area = True
elif newstyle == "img":
if color_map == None:
raise RuntimeError(
"SectionPlotter.change_style: img style requires a color_map definition - received a None"
)
self._style = "img"
self._color_background = True
self._fill_color = None
self._use_variable_area = False
self._color_map = color_map
elif newstyle == "wt":
self._style = "wt"
self._color_background = False
self._color_map = None
self._fill_color = None
self._use_variable_area = False
else:
raise RuntimeError(
"SectionPlotter.change_style: unknown style type=" + newstyle
)
[docs] def plot(self, d):
"""
Call this method to plot any data using the current style setup and any
details defined by public attributes.
:param d: is the data to be plotted. It can be any of the following:
TimeSeries, Seismogram, TimeSeriesEnsemble, or SeismogramEnsemble.
If d is any other type the method will throw a RuntimeError exception.
:Returns: an array of one or 3 (only for SeismogramEnsemble dat)
matplotlib.pylot.gcf() plot handle(s).
:rtype: The plot handle is what matplotlib.pyplot calls a gcf. It
is the return of pyplot.gcf() and can be used to alter some properties
of the figure. See matplotlib documentation.
"""
# these are all handled by the same function with argument combinations defined by
# change_style determining the behavior.
if self._style == "wtva" or self._style == "wtvaimg" or self._style == "wt":
handle = wtvaplot(
d,
self._ranges,
self.scale,
self._fill_color,
self.normalize,
self._color_map,
self.title,
)
return handle
elif self._style == "img":
handle = imageplot(
d,
self._ranges,
self._color_map,
self._aspect,
self._vmin,
self._vmax,
self.title,
)
return handle
else:
raise RuntimeError(
"SectionPlotter.plot: internal style definition="
+ self._style
+ " which is illegal. Run change_style method"
)
[docs]class SeismicPlotter:
"""
SeismicPlotter is used to plot mspass data objects in the
seismology convention with time as the x axis. Use SectionPlotter
to plot data in the seismic reflection convention with time on the
y axis and oriented backward (increasing downward).
Four types of plots are supported: wiggle trace, wiggle trace variable
area (i.e. filled on positive peaks), image plots, and colored wiggle
trace variable area (wtva overlays an image display).
Ensembles of Seismogram objects (SeismogramEnsemble object) are plotted in
three windows that will appear as Figure 0, Figure 1, and Figure 2.
All other object are displayed in a single window. Default display
order for ensembles is fill in ensemble order from the bottom up.
Use the topdown() method to switch to reverse order.
This is a simple plotter that just generates a basic graphic.
The design idea is to use this as a base class for more advanced
graphics uses like a trace editor or generic picker. The
plot object must be created before any plots can be generated.
Any optional changes to the plot behavior, most commonly calls
to the change_style method, should be done before calling plot.
"""
def __init__(self, scale=1.0, normalize=False, title=None):
"""
Constructor for this object. It mostly sets defaults but
has a few common optional parameters to set at construction
time. Note style is intentionally not a constructor
parameter because of parameter interdependence. The default
plot style is "wtva". Use change_style to use change plot
style.
:param scale: optional scale factor to apply to data before plotting
(default assumes data have been scaled to amplitude of order 1)
:param normalize: Default assumes the data have been scaled with
one of the mspass amplitude scaling functions to be of order one
(or the scale parameter). Set True if the data are to be
normalized internally by the plotter.
:param title: optional title to put on graphics. Note for
SeismogramEnsembles each of the three windows will be tagged
with a variant of this title string adding ":n" where n is
the component number (0,1,2).
"""
# these are public because they can set independently
self.scale = scale
self.title = title
self.normalize = normalize
# these have some interdependencies and are best altered only
# through the change_style method. This may be unnecessary
# as it may duplicate the call to change_style at the end, BUT
# if the default changes these initial values do not need to be
# changed
self._style = "wtva"
self._fill_color = "k" # black in matplotlib
self._color_map = "seismic"
# these are options to raw codes adapted from fatiando a terra
# that are currently ignored. Convert to args for __init__ if
# it proves useful to have them in the api
self._ranges = None
self._aspect = None
self._vmin = None
self._vmax = None
# These are added for SeismicPlotter and not in SectionPlotter
self._plot_topdown = False
# Internal constants
# Should be smaller than 1/screem horizontal pixel maximum size
self._RANGE_RATIO_TEST = 0.0001
self._default_single_ts_aspect = 0.25
# use change_style to simply default style
self.change_style("wtvaimg")
[docs] def change_style(self, newstyle, fill_color="k", color_map="seismic"):
"""
Use this method to change the plot style. Options are described
below. Some parameter combinations are illegal and will result in
a RuntimeError if used. The illegal combos are listed below for
each style.
:param newstyle: This is the primary switch for this method and
is used to define the style of plot that will be produced. The
allowed values for this string are listed below along with
restrictions (when they exist) on related parameters.
wtva - wiggle trace variable area. That means a wiggle traced with
the positive quadrants colored by the color defined by the fill_color
parameter. If fill_color is passed as None a RuntimeError will
be thrown.
img - data are displayed as an image with the color_map parameter
defining the matplotlib color map used to map amplitudes into
a given color. Note as with wtva the data are presumed to be
scaled to be of order 1. color_map must not be None, which it
can be if the plot was changed from a previous style like wtva,
or a RuntimeError exception will be thrown.
wtvaimg is like wtva but the wtva plot is drawn on top of an
image plot. Since this is the union of wtva and img a RuntimeError
exception will be throw if either color_map or fill_color are null.
wt will produce a standard line graph of each input as a
black line (that feature is currently frozen). color_map and
fill_color are ignored for this style so no exceptions should
occur when the method is called with this value of newstyle.
"""
if newstyle == "wtva":
if fill_color == None:
raise RuntimeError(
"SectionPlotter.change_style: wtva style requires fill_color to be a valid color code - received a None"
)
self._style = "wtva"
# force this when set this way
self._color_map = None
self._fill_color = fill_color
elif newstyle == "wtvaimg":
if color_map == None:
raise RuntimeError(
"SectionPlotter.change_style: wtvaimg style requires a color_map definition - received a None"
)
if fill_color == None:
raise RuntimeError(
"SectionPlotter.change_style: wtvaimg style requires a fill_color definition - received a None"
)
self._style = "wtvaimg"
self._fill_color = fill_color
self._color_map = color_map
elif newstyle == "img":
if color_map == None:
raise RuntimeError(
"SectionPlotter.change_style: img style requires a color_map definition - received a None"
)
self._style = "img"
self._fill_color = None
self._color_map = color_map
elif newstyle == "wt":
self._style = "wt"
self._color_map = None
self._fill_color = None
else:
raise RuntimeError(
"SectionPlotter.change_style: unknown style type=" + newstyle
)
# These two method should perhaps be implemented as decorators
[docs] def topdown(self):
"""
Switch to mode of plotting ensembles from the top downward
from default of bottum upward.
"""
self._plot_topdown = True
[docs] def bottomup(self):
"""
Restore (or force) default ensemble plot order of bottup upward.
"""
self._plot_topdown = False
[docs] def plot(self, d):
# make copy always to prevent unintentional scaling of input data
if self.normalize:
d2plot = self._deepcopy(d)
self._normalize(d2plot)
else:
d2plot = d # always a shallow copy in python
if self._style == "wtva":
self._wtva(d2plot, fill=True)
elif self._style == "wt":
self._wtva(d2plot, fill=False)
elif self._style == "wtvaimg":
self._wtva(d2plot, fill=True)
self._imageplot(d2plot)
elif self._style == "img":
self._imageplot(d2plot)
else:
raise RuntimeError(
"SeismicPlotter.plot: internal style definition="
+ self._style
+ " is invalid\nThis should not happen"
)
def _deepcopy(self, d):
"""
Private helper method for immediately above. Necessary because
copy.deepcopy doesn't work with our pybind11 wrappers. There may be a
fix, but for now we have to use copy constructors specific to each
object type.
"""
if isinstance(d, TimeSeries):
return TimeSeries(d)
elif isinstance(d, Seismogram):
return Seismogram(d)
elif isinstance(d, TimeSeriesEnsemble):
return TimeSeriesEnsemble(d)
elif isinstance(d, SeismogramEnsemble):
return SeismogramEnsemble(d)
else:
raise RuntimeError(
"SeismicPlotter._deepcopy: received and unsupported data type=",
type(d),
)
[docs] def set_topdown(self):
"""
Call this method to have datat plotted from top down. Default plots
ensemble member 0 at the bottom of the plot with successive members
plotted in units of 1.0 above the last. When this is called the
order is reversed.
"""
self._plot_topdown = True
# These are private methods used internally
def _normalize(self, d):
"""
Normalizes data (d) using scale function. for ensembles that
is peak normalization to level self.scale by section. For
Seismograms each component will be normalized independently.
For TimeSeries the peak is adjusted to self.scale.
:param d: input data to be scanned (must be one of mspass supported
data objects or will throw an exception)
:param perf: clip level as used in seismic unix displays.
"""
# These are place holders for now. Requires some new code in ccore
# to sort absolute values and return perf level - should use faster
# max value when perf is 100%
if isinstance(d, SeismogramEnsemble):
alg_scale(d, scale_by_section=True, level=self.scale)
elif isinstance(d, TimeSeriesEnsemble):
alg_scale(d, scale_by_section=True, level=self.scale)
elif isinstance(d, TimeSeries):
alg_scale(d, level=self.scale)
elif isinstance(d, Seismogram):
alg_scale(d, level=self.scale)
else:
raise RuntimeError(
"SeismicPlotter._normalize: Received unsupported data type=", type(d)
)
def _add_3C_titles(self):
"""
Private method to add titles with pyplot.title to 3C ensemble data.
This algorithm is a bit fragile as it depends upon use of
figure tagged with integer component number.
"""
if self.title != None:
for k in range(3):
pyplot.figure(k)
title3c = "%s:%d" % (self.title, k)
pyplot.title(title3c)
def _wtva(self, d, fill=True):
"""
Private method for making all forms of wiggle trace plots. The
fill boolean determines if shading is applied. When used the shading
is assumed properly defined by self._fill_color. In this implementation
this function does little more than determine the type of the input
data d and call the appropriate private method for that data type.
"""
base_error = "SeismicPlotter._imageplot (Error): "
if isinstance(d, SeismogramEnsemble):
if len(d.member) <= 0:
raise IndexError(base_error + "ensemble container is empty")
self._wtva_SeismogramEnsemble(d, fill)
self._add_3C_titles()
elif isinstance(d, TimeSeriesEnsemble):
if len(d.member) <= 0:
raise IndexError(base_error + "ensemble container is empty")
self._wtva_TimeSeriesEnsemble(d, fill)
if self.title != None:
pyplot.title(self.title)
elif isinstance(d, TimeSeries):
if d.npts <= 0:
raise IndexError(base_error + "data vector is empty. Nothing to plot")
self._wtva_TimeSeries(d, fill)
if self.title != None:
pyplot.title(self.title)
elif isinstance(d, Seismogram):
if d.npts <= 0:
raise IndexError(base_error + "data vector is empty. Nothing to plot")
self._wtva_Seismogram(d, fill)
if self.title != None:
pyplot.title(self.title)
else:
raise RuntimeError(base_error + "Received unsupported data type=", type(d))
def _imageplot(self, d):
"""
Private method for making all forms of image plots.
"""
base_error = "SeismicPlotter._imageplot (Error): "
if isinstance(d, SeismogramEnsemble):
if len(d.member) <= 0:
raise IndexError(base_error + "ensemble container is empty")
self._imageplot_SeismogramEnsemble(d)
self._add_3C_titles()
elif isinstance(d, TimeSeriesEnsemble):
if len(d.member) <= 0:
raise IndexError(base_error + "ensemble container is empty")
self._imageplot_TimeSeriesEnsemble(d)
if self.title != None:
pyplot.title(self.title)
elif isinstance(d, TimeSeries):
if d.npts <= 0:
raise IndexError(base_error + "data vector is empty. Nothing to plot")
self._imageplot_TimeSeries(d)
if self.title != None:
pyplot.title(self.title)
elif isinstance(d, Seismogram):
if d.npts <= 0:
raise IndexError(base_error + "data vector is empty. Nothing to plot")
self._imageplot_Seismogram(d)
if self.title != None:
pyplot.title(self.title)
else:
raise RuntimeError(base_error + "Received unsupported data type=", type(d))
def _wtva_TimeSeries(self, d, fill):
# this plot reduces to a simple call to plot defining a time axis from
# a single trace - pretty much like the obspy plot but with optional
# shading. It assumes d is a TimeSeries.
t = numpy.linspace(d.t0, d.t0 + d.dt * d.npts, d.npts)
# We don't need a gain factor here - will be needed for an image overlay through
pyplot.plot(t, d.data, "k")
if fill:
# Necessary because fill_between doesn't support the pybind11
# wrapped vector directly - need to convert to numpy array
y = numpy.array(d.data)
pyplot.fill_between(
t, 0, y, where=y > 0.0, interpolate=True, color=self._fill_color
)
return pyplot.gcf()
def _wtva_Seismogram(self, d, fill):
# this could be implemented by converting d to an ensemble
ens = TimeSeriesEnsemble()
for k in range(3):
dcomp = ExtractComponent(d, k)
ens.member.append(dcomp)
self._wtva_TimeSeriesEnsemble(ens, fill)
def _get_ensemble_size(self, d):
# Unlike SectionPlotter we allow irregular start times and don't
# need to do the baggage of zero padding - will be needed for image
# plots though. So, step one is to scan for the time range. We
# also search for longest time interval and throw and exception if
# the ratio of that time interval to data time interval span is too
# small.
moderror = "SeismicPlotter._get_ensemble_size (Error): "
ndata = len(d.member)
if ndata <= 0:
raise RuntimeError("Trying to plot an empty ensemble")
t0 = []
endtimes = []
nlive = 0
for i in range(ndata):
if d.member[i].live:
t0.append(d.member[i].t0)
endtimes.append(d.member[i].endtime())
nlive += 1
if nlive == 0:
raise RuntimeError(
moderror + "All members of ensemble are marked dead - nothing to plot"
)
tmin = min(t0)
tmax = max(endtimes)
if (tmax - tmin) <= 0:
raise RuntimeError(moderror + "time span of data is zero")
maxdt = 0.0
# this is no doubt a faster vector way to do this calculation, but
# this loop is guaranteed by screen limits to never be huge
for i in range(len(t0)):
dt = endtimes[i] - t0[i]
maxdt = max(maxdt, dt)
if maxdt / (tmax - tmin) < self._RANGE_RATIO_TEST:
raise RuntimeError(
moderror
+ "data appear to be mix of data with absolute times. Computed time range is absurd - convert data to relative time"
)
return (ndata, tmin, tmax)
def _wtva_TimeSeriesEnsemble(self, d, fill):
(ndata, tmin, tmax) = self._get_ensemble_size(d)
pyplot.xlim(tmin, tmax)
# This plotting engine always equally spaces traces horizontally so
# unlike SectionPlotter we just compute the range as the number of
# intervals + 1 for padding
pyplot.ylim(-0.5, float(ndata) - 0.5)
for i in range(ndata):
# skip data marked dead - this will leave a hole in plot. We could
# plot a line but this is proably better unless proven otherwise
if d.member[i].dead():
continue
# Make a copy - fast method with numpy
y = numpy.array(d.member[i].data)
# Fast and easy way to add offset with overloaded operator -= and +=
if self._plot_topdown:
offset = ndata - i - 1
y += offset
else:
y += i
offset = i
t0 = d.member[i].t0
endtime = d.member[i].endtime()
npts = d.member[i].npts
t = numpy.linspace(t0, endtime, npts)
pyplot.plot(t, y, "k")
if fill:
pyplot.fill_between(
t,
offset,
y,
where=y > offset,
interpolate=True,
color=self._fill_color,
)
return pyplot.gcf()
def _wtva_SeismogramEnsemble(self, d, fill):
# implement by call to ExtractComponent and calling TimeSeriesEnsemble method 3 times
# should return a list of 3 gcf handles
figure_handles = []
for k in range(3):
dcomp = ExtractComponent(d, k)
# figure_title='Component %d' % k
# pyplot.figure(figure_title)
pyplot.figure(k)
handle = self._wtva(dcomp)
figure_handles.append(handle)
# pyplot.show()
return figure_handles
def _imageplot_TimeSeriesEnsemble(self, d):
(ndata, tmin, tmax) = self._get_ensemble_size(d)
extent = (tmin, tmax, -0.5, float(ndata) - 0.5)
# left off here - below is copy from SectionPlotter
# need a different algorithm to support mixed sample
# rates. Probably should search for shortest dt and
# define the matrix from that
# to handle mixed sample rate data use the shortest dt and
# use a crude boxcar resampling for data with larger dt
# Boxcar resampling is implict in use of time method which gets the
# nearest sample
dt = 10000000.0
for i in range(ndata):
dt = min(dt, d.member[i].dt)
# compute the number of points for the time axis
nt = int((tmax - tmin) / dt) + 1
# WARNING - this assumes size of nt is limited by error checking
# in get_ensemble_size called above. Prone to malloc error if nt
# is huge - easy to do with absolute time data segments
work = numpy.zeros(shape=[ndata, nt])
for i in range(ndata):
# skip data marked dead
if d.member[i].dead():
continue
t0 = d.member[i].t0
endtime = d.member[i].endtime()
npts = d.member[i].npts
if self._plot_topdown:
iwork = ndata - i - 1
else:
iwork = i
if t0 == tmin:
t = tmin
j = 0
else: # above logic guarantees < not possible so this is > block
t = t0
# often one less than needed but minimal inefficiency
j = int((t0 - tmin) / dt)
# tmax test shouldn't be necessary but small cost for safety
while t <= endtime and t <= tmax:
k = d.member[i].sample_number(t)
if k > 0 and k < npts:
work[iwork, j] = d.member[i].data[k]
t += dt
j += 1
if self._aspect is None: # guarantee a rectangular picture
aspect = (tmax - tmin) / ndata
# avoid long, skinny plots that would be the norm from
# above calculation
if aspect > 10.0:
aspect = 0.8
else:
aspect = self._aspect
if self._vmin is None and self._vmax is None:
scale = numpy.abs([work.max(), work.min()]).max()
vmin = -scale
vmax = scale
# imshow handles topdown or updown order with this parameter
if self._plot_topdown:
origin_position = "upper"
extent = (tmin, tmax, float(ndata) - 0.5, -0.5)
else:
origin_position = "lower"
pyplot.imshow(
work,
aspect="auto",
cmap=self._color_map,
origin=origin_position,
extent=extent,
vmin=vmin,
vmax=vmax,
)
def _imageplot_SeismogramEnsemble(self, d):
# implement by call to ExtractComponent and calling TimeSeriesEnsemble method 3 times
# should return a list of 3 gcf handles
figure_handles = []
for k in range(3):
dcomp = ExtractComponent(d, k)
pyplot.figure(k)
figure = self._imageplot(dcomp)
figure_handles.append(figure)
# pyplot.show()
return figure_handles
def _imageplot_Seismogram(self, d):
# this could be implemented by converting d to an ensemble
ens = TimeSeriesEnsemble()
for k in range(3):
dcomp = ExtractComponent(d, k)
ens.member.append(dcomp)
self._imageplot_TimeSeriesEnsemble(ens)
def _imageplot_TimeSeries(self, d):
# this plot reduces to a simple call to plot defining a time axis from
# a single trace - pretty much like the obspy plot but with optional
# shading. It assumes d is a TimeSeries.
t = numpy.linspace(d.t0, d.endtime(), d.npts)
# somewhat inefficient, but TimeSeries size in this context
# would always be small enough to be irrelevant
work = numpy.zeros(shape=[1, d.npts])
extent = (d.t0, d.endtime(), -1.0, 1.0)
for j in range(d.npts):
work[0, j] = d.data[j]
if self._aspect == None:
aspect = self._default_single_ts_aspect
if self._vmin is None and self._vmax is None:
scale = numpy.abs([work.max(), work.min()]).max()
vmin = -scale
vmax = scale
pyplot.imshow(
work,
aspect="auto",
cmap=self._color_map,
extent=extent,
vmin=vmin,
vmax=vmax,
)
return pyplot.gcf()