diff --git a/PyLoT.py b/PyLoT.py index a6d902d8..8ce799bb 100755 --- a/PyLoT.py +++ b/PyLoT.py @@ -1773,7 +1773,7 @@ class MainWindow(QMainWindow): def getStime(self): if self.get_data(): - return full_range(self.get_data().getWFData())[0] + return full_range(self.get_data().get_wf_data())[0] def addActions(self, target, actions): for action in actions: @@ -1983,7 +1983,7 @@ class MainWindow(QMainWindow): tstart = None tstop = None - self.data.setWFData(self.fnames, + self.data.set_wf_data(self.fnames, self.fnames_comp, checkRotated=True, metadata=self.metadata, @@ -2035,7 +2035,7 @@ class MainWindow(QMainWindow): def get_npts_to_plot(self): if not hasattr(self.data, 'wfdata'): return 0 - return sum(trace.stats.npts for trace in self.data.getWFData()) + return sum(trace.stats.npts for trace in self.data.get_wf_data()) def connectWFplotEvents(self): ''' @@ -2248,14 +2248,14 @@ class MainWindow(QMainWindow): zne_text = {'Z': 'vertical', 'N': 'north-south', 'E': 'east-west'} comp = self.getComponent() title = 'section: {0} components'.format(zne_text[comp]) - wfst = self.get_data().getWFData() + wfst = self.get_data().get_wf_data() wfsyn = self.get_data().getAltWFdata() if self.filterActionP.isChecked() and filter: self.filterWaveformData(plot=False, phase='P') elif self.filterActionS.isChecked() and filter: self.filterWaveformData(plot=False, phase='S') - # wfst = self.get_data().getWFData().select(component=comp) - # wfst += self.get_data().getWFData().select(component=alter_comp) + # wfst = self.get_data().get_wf_data().select(component=comp) + # wfst += self.get_data().get_wf_data().select(component=alter_comp) plotWidget = self.getPlotWidget() self.adjustPlotHeight() if get_bool(settings.value('large_dataset')) == True: @@ -2270,7 +2270,7 @@ class MainWindow(QMainWindow): def adjustPlotHeight(self): if self.pg: return - height_need = len(self.data.getWFData()) * self.height_factor + height_need = len(self.data.get_wf_data()) * self.height_factor plotWidget = self.getPlotWidget() if self.tabs.widget(0).frameSize().height() < height_need: plotWidget.setMinimumHeight(height_need) @@ -2290,24 +2290,24 @@ class MainWindow(QMainWindow): self.plotWaveformDataThread() def pushFilterWF(self, param_args): - self.get_data().filterWFData(param_args) + self.get_data().filter_wf_data(param_args) def filterP(self): self.filterActionS.setChecked(False) if self.filterActionP.isChecked(): self.filterWaveformData(phase='P') else: - self.resetWFData() + self.reset_wf_data() def filterS(self): self.filterActionP.setChecked(False) if self.filterActionS.isChecked(): self.filterWaveformData(phase='S') else: - self.resetWFData() + self.reset_wf_data() - def resetWFData(self): - self.get_data().resetWFData() + def reset_wf_data(self): + self.get_data().reset_wf_data() self.plotWaveformDataThread() def filterWaveformData(self, plot=True, phase=None): @@ -2326,11 +2326,11 @@ class MainWindow(QMainWindow): kwargs = self.getFilterOptions()[phase].parseFilterOptions() self.pushFilterWF(kwargs) else: - self.get_data().resetWFData() + self.get_data().reset_wf_data() elif self.filterActionP.isChecked() or self.filterActionS.isChecked(): self.adjustFilterOptions() else: - self.get_data().resetWFData() + self.get_data().reset_wf_data() if plot: self.plotWaveformDataThread(filter=False) @@ -2531,10 +2531,10 @@ class MainWindow(QMainWindow): show_comp_data=self.dataPlot.comp_checkbox.isChecked()) if self.filterActionP.isChecked(): pickDlg.currentPhase = "P" - pickDlg.filterWFData() + pickDlg.filter_wf_data() elif self.filterActionS.isChecked(): pickDlg.currentPhase = "S" - pickDlg.filterWFData() + pickDlg.filter_wf_data() pickDlg.nextStation.setChecked(self.nextStation) pickDlg.nextStation.stateChanged.connect(self.toggle_next_station) if pickDlg.exec_(): @@ -3478,7 +3478,7 @@ class MainWindow(QMainWindow): if not self.metadata: return None - wf_copy = self.get_data().getWFData().copy() + wf_copy = self.get_data().get_wf_data().copy() wf_select = Stream() # restitute only picked traces diff --git a/autoPyLoT.py b/autoPyLoT.py index 506025c5..cc86a786 100755 --- a/autoPyLoT.py +++ b/autoPyLoT.py @@ -243,7 +243,7 @@ def autoPyLoT(input_dict=None, parameter=None, inputfile=None, fnames=None, even pylot_event = Event(eventpath) # event should be path to event directory data.setEvtData(pylot_event) if fnames == 'None': - data.setWFData(glob.glob(os.path.join(datapath, event_datapath, '*'))) + data.set_wf_data(glob.glob(os.path.join(datapath, event_datapath, '*'))) # the following is necessary because within # multiple event processing no event ID is provided # in autopylot.in @@ -258,7 +258,7 @@ def autoPyLoT(input_dict=None, parameter=None, inputfile=None, fnames=None, even now.minute) parameter.setParam(eventID=eventID) else: - data.setWFData(fnames) + data.set_wf_data(fnames) eventpath = events[0] # now = datetime.datetime.now() @@ -268,7 +268,7 @@ def autoPyLoT(input_dict=None, parameter=None, inputfile=None, fnames=None, even # now.hour, # now.minute) parameter.setParam(eventID=eventid) - wfdat = data.getWFData() # all available streams + wfdat = data.get_wf_data() # all available streams if not station == 'all': wfdat = wfdat.select(station=station) if not wfdat: diff --git a/pylot/core/io/data.py b/pylot/core/io/data.py index 1172568e..26a2e72a 100644 --- a/pylot/core/io/data.py +++ b/pylot/core/io/data.py @@ -9,11 +9,13 @@ from obspy import UTCDateTime from pylot.core.io.event import EventData from pylot.core.io.waveformdata import WaveformData +from pylot.core.util.dataprocessing import Metadata @dataclass class Data: event_data: EventData = field(default_factory=EventData) waveform_data: WaveformData = field(default_factory=WaveformData) + metadata: Metadata = field(default_factory=Metadata) _parent: Union[None, 'QtWidgets.QWidget'] = None def __init__(self, parent=None, evtdata=None): @@ -52,10 +54,17 @@ class Data: self.waveform_data.dirty = True def set_wf_data(self, fnames: List[str], fnames_alt: List[str] = None, check_rotated=False, metadata=None, tstart=0, tstop=0): - return self.waveform_data.set_wf_data(fnames, fnames_alt, check_rotated, metadata, tstart, tstop) + return self.waveform_data.load_waveforms(fnames, fnames_alt, check_rotated, metadata, tstart, tstop) def reset_wf_data(self): - self.waveform_data.reset_wf_data() + self.waveform_data.reset() + + def get_wf_data(self): + return self.waveform_data.wfdata + + def rotate_wf_data(self): + self.waveform_data.rotate_zne(self.metadata) + class GenericDataStructure(object): """ diff --git a/pylot/core/io/utils.py b/pylot/core/io/utils.py new file mode 100644 index 00000000..109ee8a2 --- /dev/null +++ b/pylot/core/io/utils.py @@ -0,0 +1,13 @@ +import os +from typing import List + + +def validate_filenames(filenames: List[str]) -> List[str]: + """ + validate a list of filenames for file abundance + :param filenames: list of possible filenames + :type filenames: List[str] + :return: list of valid filenames + :rtype: List[str] + """ + return [fn for fn in filenames if os.path.isfile(fn)] \ No newline at end of file diff --git a/pylot/core/io/waveformdata.py b/pylot/core/io/waveformdata.py index 7ecfff42..c2b0078c 100644 --- a/pylot/core/io/waveformdata.py +++ b/pylot/core/io/waveformdata.py @@ -1,14 +1,13 @@ import logging -import os from dataclasses import dataclass, field from typing import Union, List -import numpy as np from obspy import Stream, read from obspy.io.sac import SacIOError -from obspy.signal.rotate import rotate2zne -from pylot.core.util.utils import full_range, get_stations +from pylot.core.io.utils import validate_filenames +from pylot.core.util.dataprocessing import Metadata +from pylot.core.util.utils import get_stations, check_for_nan, check4rotated @dataclass @@ -18,26 +17,39 @@ class WaveformData: wf_alt: Stream = field(default_factory=Stream) dirty: bool = False - def set_wf_data(self, fnames: List[str], fnames_alt: List[str] = None, check_rotated=False, metadata=None, tstart=0, tstop=0): - self.clear_data() - fnames = self.check_fname_exists(fnames) - fnames_alt = self.check_fname_exists(fnames_alt) + def load_waveforms(self, fnames: List[str], fnames_alt: List[str] = None, check_rotated=False, metadata=None, tstart=0, tstop=0): + fn_list = validate_filenames(fnames) + if not fn_list: + logging.warning('No valid filenames given for loading waveforms') + else: + self.clear() + self.add_waveforms(fn_list) - if fnames: - self.append_wf_data(fnames) - if fnames_alt: - self.append_wf_data(fnames_alt, alternative=True) - self.wfdata, _ = self.check_for_gaps_and_merge(self.wfdata) - self.check_for_nan(self.wfdata) - if check_rotated and metadata: - self.wfdata = self.check4rotated(self.wfdata, metadata, verbosity=0) - self.trim_station_components(self.wfdata, trim_start=True, trim_end=False) - self.wforiginal = self.wfdata.copy() - self.dirty = False - return True - return False + if fnames_alt is None: + pass + else: + alt_fn_list = validate_filenames(fnames_alt) + if not alt_fn_list: + logging.warning('No valid alternative filenames given for loading waveforms') + else: + self.add_waveforms(alt_fn_list, alternative=True) - def append_wf_data(self, fnames: List[str], alternative: bool = False): + if not fn_list and not alt_fn_list: + logging.error('No filenames or alternative filenames given for loading waveforms') + return False + + self.merge() + self.replace_nan() + if not check_rotated or not metadata: + pass + else: + self.rotate_zne() + self.trim_station_traces() + self.wforiginal = self.wfdata.copy() + self.dirty = False + return True + + def add_waveforms(self, fnames: List[str], alternative: bool = False): data_stream = self.wf_alt if alternative else self.wfdata warnmsg = '' for fname in set(fnames): @@ -55,189 +67,57 @@ class WaveformData: warnmsg += f'{fname}\n{se}\n' if warnmsg: - print(f'WARNING in appendWFData: unable to read waveform data\n{warnmsg}') + print(f'WARNING in add_waveforms: unable to read waveform data\n{warnmsg}') - def clear_data(self): + def clear(self): self.wfdata = Stream() self.wforiginal = None self.wf_alt = Stream() - def reset_wf_data(self): + def reset(self): + """ + Resets the waveform data to its original state. + """ if self.wforiginal: self.wfdata = self.wforiginal.copy() else: self.wfdata = Stream() self.dirty = False - def check_fname_exists(self, filenames: List[str]) -> List[str]: - return [fn for fn in filenames if os.path.isfile(fn)] - - def check_for_gaps_and_merge(self, stream): + def merge(self): """ check for gaps in Stream and merge if gaps are found - :param stream: stream of seismic data - :type stream: `~obspy.core.stream.Stream` - :return: data stream, gaps returned from obspy get_gaps - :rtype: `~obspy.core.stream.Stream` """ - gaps = stream.get_gaps() + gaps = self.wfdata.get_gaps() if gaps: merged = ['{}.{}.{}.{}'.format(*gap[:4]) for gap in gaps] - stream.merge(method=1) - print('Merged the following stations because of gaps:') - for merged_station in merged: - print(merged_station) + self.wfdata.merge(method=1) + logging.info('Merged the following stations because of gaps:') + for station in merged: + logging.info(station) - return stream, gaps - - def check_for_nan(self, stream): + def replace_nan(self): """ - Replace all NaNs in data with nan_value (in place) - :param stream: stream of seismic data - :type stream: `~obspy.core.stream.Stream` - :param nan_value: value which all NaNs are set to - :type nan_value: float, int - :return: None + Replace all NaNs in data with 0. (in place) """ - if not stream: - return - for trace in stream: - np.nan_to_num(trace.data, copy=False, nan=0.) + self.wfdata = check_for_nan(self.wfdata) - - def check4rotated(self, stream, metadata=None, verbosity=1): + def rotate_zne(self, metadata: Metadata = None): """ - Check all traces in data. If a trace is not in ZNE rotation (last symbol of channel code is numeric) and the trace + Check all traces in stream for rotation. If a trace is not in ZNE rotation (last symbol of channel code is numeric) and the trace is in the metadata with azimuth and dip, rotate it to classical ZNE orientation. Rotating the traces requires them to be of the same length, so, all traces will be trimmed to a common length as a side effect. - :param stream: stream object containing seismic traces - :type stream: `~obspy.core.stream.Stream` - :param metadata: tuple containing metadata type string and metadata parser object - :type metadata: (str, `~obspy.io.xseed.parser.Parser`) - :param verbosity: if 0 print no information at runtime - :type verbosity: int - :return: stream object with traditionally oriented traces (ZNE) for stations that had misaligned traces (123) before - :rtype: `~obspy.core.stream.Stream` """ - def rotation_required(trace_ids): - """ - Derive if any rotation is required from the orientation code of the input. + self.wfdata = check4rotated(self.wfdata, metadata) - :param trace_ids: string identifier of waveform data trace - :type trace_ids: List(str) - :return: boolean representing if rotation is necessary for any of the traces - :rtype: bool - """ - orientations = [trace_id[-1] for trace_id in trace_ids] - return any([orientation.isnumeric() for orientation in orientations]) - - def rotate_components(wfs_in, metadata=None): - """ - Rotate components if orientation code is numeric (= non traditional orientation). - - Azimut and dip are fetched from metadata. To be rotated, traces of a station have to be cut to the same length. - Returns unrotated traces of no metadata is provided - :param wfs_in: stream containing seismic traces of a station - :type wfs_in: `~obspy.core.stream.Stream` - :param metadata: tuple containing metadata type string and metadata parser object - :type metadata: (str, `~obspy.io.xseed.parser.Parser`) - :return: stream object with traditionally oriented traces (ZNE) - :rtype: `~obspy.core.stream.Stream` - """ - - if len(wfs_in) < 3: - print(f"Stream {wfs_in=}, has not enough components to rotate.") - return wfs_in - - # check if any traces in this station need to be rotated - trace_ids = [trace.id for trace in wfs_in] - if not rotation_required(trace_ids): - logging.debug(f"Stream does not need any rotation: Traces are {trace_ids=}") - return wfs_in - - # check metadata quality - t_start = full_range(wfs_in) - try: - azimuths = [] - dips = [] - for tr_id in trace_ids: - azimuths.append(metadata.get_coordinates(tr_id, t_start)['azimuth']) - dips.append(metadata.get_coordinates(tr_id, t_start)['dip']) - except (KeyError, TypeError) as err: - logging.error( - f"{type(err)=} occurred: {err=} Rotating not possible, not all azimuth and dip information " - f"available in metadata. Stream remains unchanged.") - return wfs_in - except Exception as err: - print(f"Unexpected {err=}, {type(err)=}") - raise - - # to rotate all traces must have same length, so trim them - wfs_out = self.trim_station_components(wfs_in, trim_start=True, trim_end=True) - try: - z, n, e = rotate2zne(wfs_out[0], azimuths[0], dips[0], - wfs_out[1], azimuths[1], dips[1], - wfs_out[2], azimuths[2], dips[2]) - print('check4rotated: rotated trace {} to ZNE'.format(trace_ids)) - # replace old data with rotated data, change the channel code to ZNE - z_index = dips.index(min( - dips)) # get z-trace index, z has minimum dip of -90 (dip is measured from 0 to -90, with -90 - # being vertical) - wfs_out[z_index].data = z - wfs_out[z_index].stats.channel = wfs_out[z_index].stats.channel[0:-1] + 'Z' - del trace_ids[z_index] - for trace_id in trace_ids: - coordinates = metadata.get_coordinates(trace_id, t_start) - dip, az = coordinates['dip'], coordinates['azimuth'] - trace = wfs_out.select(id=trace_id)[0] - if az > 315 or az <= 45 or 135 < az <= 225: - trace.data = n - trace.stats.channel = trace.stats.channel[0:-1] + 'N' - elif 45 < az <= 135 or 225 < az <= 315: - trace.data = e - trace.stats.channel = trace.stats.channel[0:-1] + 'E' - except ValueError as err: - print(f"{err=} Rotation failed. Stream remains unchanged.") - return wfs_in - - return wfs_out - - if metadata is None: - if verbosity: - msg = 'Warning: could not rotate traces since no metadata was given\nset Inventory file!' - print(msg) - return stream - stations = get_stations(stream) - for station in stations: # loop through all stations and rotate data if neccessary - wf_station = stream.select(station=station) - rotate_components(wf_station, metadata) - return stream - - def trim_station_components(stream, trim_start=True, trim_end=True): + def trim_station_traces(self): """ - cut a stream so only the part common to all three traces is kept to avoid dealing with offsets - :param stream: stream of seismic data - :type stream: `~obspy.core.stream.Stream` - :param trim_start: trim start of stream - :type trim_start: bool - :param trim_end: trim end of stream - :type trim_end: bool - :return: data stream - :rtype: `~obspy.core.stream.Stream` + trim data stream to common time window """ - starttime = {False: None} - endtime = {False: None} - stations = get_stations(stream) - - print('trim_station_components: Will trim stream for trim_start: {} and for ' - 'trim_end: {}.'.format(trim_start, trim_end)) - for station in stations: - wf_station = stream.select(station=station) - starttime[True] = max([trace.stats.starttime for trace in wf_station]) - endtime[True] = min([trace.stats.endtime for trace in wf_station]) - wf_station.trim(starttime=starttime[trim_start], endtime=endtime[trim_end]) - - return stream + for station in get_stations(self.wfdata): + station_traces = self.wfdata.select(station=station) + station_traces.trim(starttime=max([trace.stats.starttime for trace in station_traces]), + endtime=min([trace.stats.endtime for trace in station_traces])) diff --git a/pylot/core/util/array_map.py b/pylot/core/util/array_map.py index afc00eaf..ea8d0c00 100644 --- a/pylot/core/util/array_map.py +++ b/pylot/core/util/array_map.py @@ -474,8 +474,8 @@ class Array_map(QtWidgets.QWidget): transform=ccrs.PlateCarree(), label='deleted')) def openPickDlg(self, ind): - wfdata = self._parent.get_data().getWFData() - wfdata_comp = self._parent.get_data().getWFDataComp() + wfdata = self._parent.get_data().get_wf_data() + wfdata_comp = self._parent.get_data().get_wf_dataComp() for index in ind: network, station = self._station_onpick_ids[index].split('.')[:2] pyl_mw = self._parent diff --git a/pylot/core/util/widgets.py b/pylot/core/util/widgets.py index 988b3aa2..7ef01394 100644 --- a/pylot/core/util/widgets.py +++ b/pylot/core/util/widgets.py @@ -140,18 +140,6 @@ class LogWidget(QtWidgets.QWidget): self.stderr.append(60 * '#' + '\n\n') -def getDataType(parent): - type = QInputDialog().getItem(parent, "Select phases type", "Type:", - ["manual", "automatic"]) - - if type[0].startswith('auto'): - type = 'auto' - else: - type = type[0] - - return type - - def plot_pdf(_axes, x, y, annotation, bbox_props, xlabel=None, ylabel=None, title=None): # try method or data @@ -795,7 +783,7 @@ class WaveformWidgetPG(QtWidgets.QWidget): def connect_signals(self): self.qcombo_processed.activated.connect(self.parent().newWF) - self.syn_checkbox.clicked.connect(self.parent().newWF) + self.comp_checkbox.clicked.connect(self.parent().newWF) def init_labels(self): self.label_layout.addWidget(self.status_label) @@ -806,13 +794,13 @@ class WaveformWidgetPG(QtWidgets.QWidget): # use widgets as placeholder, so that child widgets keep position when others are hidden mid_layout = QHBoxLayout() right_layout = QHBoxLayout() - mid_layout.addWidget(self.syn_checkbox) + mid_layout.addWidget(self.comp_checkbox) right_layout.addWidget(self.qcombo_processed) mid_widget.setLayout(mid_layout) right_widget.setLayout(right_layout) self.label_layout.addWidget(mid_widget) self.label_layout.addWidget(right_widget) - self.syn_checkbox.setLayoutDirection(Qt.RightToLeft) + self.comp_checkbox.setLayoutDirection(Qt.RightToLeft) self.label_layout.setStretch(0, 4) self.label_layout.setStretch(1, 0) self.label_layout.setStretch(2, 0) @@ -827,7 +815,7 @@ class WaveformWidgetPG(QtWidgets.QWidget): label = QtWidgets.QLabel() self.perm_labels.append(label) self.qcombo_processed = QtWidgets.QComboBox() - self.syn_checkbox = QtWidgets.QCheckBox('synthetics') + self.comp_checkbox = QtWidgets.QCheckBox('Load comparison data') self.addQCboxItem('processed', 'green') self.addQCboxItem('raw', 'black') # self.perm_qcbox_right.setAlignment(2) @@ -836,9 +824,11 @@ class WaveformWidgetPG(QtWidgets.QWidget): def getPlotDict(self): return self.plotdict - def activateObspyDMToptions(self, activate): - self.syn_checkbox.setVisible(activate) - self.qcombo_processed.setVisible(activate) + def activateObspyDMToptions(self, activate: bool) -> None: + self.qcombo_processed.setEnabled(activate) + + def activateCompareOptions(self, activate: bool) -> None: + self.comp_checkbox.setEnabled(activate) def setPermText(self, number, text=None, color='black'): if not 0 <= number < len(self.perm_labels): @@ -961,7 +951,7 @@ class WaveformWidgetPG(QtWidgets.QWidget): [time for index, time in enumerate(time_ax_syn) if not index % nth_sample] if st_syn else []) trace.data = np.array( [datum * gain + n for index, datum in enumerate(trace.data) if not index % nth_sample]) - trace_syn.data = np.array([datum + n for index, datum in enumerate(trace_syn.data) + trace_syn.data = np.array([datum + n + shift_syn for index, datum in enumerate(trace_syn.data) if not index % nth_sample] if st_syn else []) plots.append((times, trace.data, times_syn, trace_syn.data)) @@ -1007,15 +997,6 @@ class WaveformWidgetPG(QtWidgets.QWidget): time_ax = np.linspace(time_ax[0], time_ax[-1], num=len(data)) return data, time_ax - # def getAxes(self): - # return self.axes - - # def getXLims(self): - # return self.getAxes().get_xlim() - - # def getYLims(self): - # return self.getAxes().get_ylim() - def setXLims(self, lims): vb = self.plotWidget.getPlotItem().getViewBox() vb.setXRange(float(lims[0]), float(lims[1]), padding=0) @@ -1169,8 +1150,6 @@ class PylotCanvas(FigureCanvas): break if not ax_check: return - # self.updateCurrentLimits() #maybe put this down to else: - # calculate delta (relative values in axis) old_x, old_y = self.press_rel xdiff = gui_event.x - old_x @@ -1373,110 +1352,145 @@ class PylotCanvas(FigureCanvas): plot_positions[channel] = plot_pos return plot_positions - def plotWFData(self, wfdata, title=None, zoomx=None, zoomy=None, + def plotWFData(self, wfdata, wfdata_compare=None, title=None, zoomx=None, zoomy=None, noiselevel=None, scaleddata=False, mapping=True, component='*', nth_sample=1, iniPick=None, verbosity=0, plot_additional=False, additional_channel=None, scaleToChannel=None, snr=None): + ax = self.prepare_plot() + self.clearPlotDict() + + wfstart, wfend = self.get_wf_range(wfdata) + compclass = self.get_comp_class() + plot_streams = self.get_plot_streams(wfdata, wfdata_compare, component, compclass) + + st_main = plot_streams['wfdata']['data'] + if mapping: + plot_positions = self.calcPlotPositions(st_main, compclass) + + nslc = self.get_sorted_nslc(st_main) + nmax = self.plot_traces(ax, plot_streams, nslc, wfstart, mapping, plot_positions, + scaleToChannel, noiselevel, scaleddata, nth_sample, verbosity) + + if plot_additional and additional_channel: + self.plot_additional_trace(ax, wfdata, additional_channel, scaleToChannel, + scaleddata, nth_sample, wfstart) + + self.finalize_plot(ax, wfstart, wfend, nmax, zoomx, zoomy, iniPick, title, snr) + + def prepare_plot(self): ax = self.axes[0] ax.cla() + return ax - self.clearPlotDict() - wfstart, wfend = full_range(wfdata) - nmax = 0 + def get_wf_range(self, wfdata): + return full_range(wfdata) + def get_comp_class(self): settings = QSettings() - compclass = SetChannelComponents.from_qsettings(settings) + return SetChannelComponents.from_qsettings(settings) - if not component == '*': - alter_comp = compclass.getCompPosition(component) - # alter_comp = str(alter_comp[0]) - - st_select = wfdata.select(component=component) - st_select += wfdata.select(component=alter_comp) - else: - st_select = wfdata - - if mapping: - plot_positions = self.calcPlotPositions(st_select, compclass) - - # list containing tuples of network, station, channel and plot position (for sorting) - nslc = [] - for plot_pos, trace in enumerate(st_select): - if not trace.stats.channel[-1] in ['Z', 'N', 'E', '1', '2', '3']: - print('Warning: Unrecognized channel {}'.format(trace.stats.channel)) - continue - nslc.append(trace.get_id()) - nslc.sort() - nslc.reverse() + def get_plot_streams(self, wfdata, wfdata_compare, component, compclass): + def get_wf_dict(data=Stream(), linecolor='k', offset=0., **plot_kwargs): + return dict(data=data, linecolor=linecolor, offset=offset, plot_kwargs=plot_kwargs) linecolor = (0., 0., 0., 1.) if not self.style else self.style['linecolor']['rgba_mpl'] + plot_streams = { + 'wfdata': get_wf_dict(linecolor=linecolor, linewidth=0.7), + 'wfdata_comp': get_wf_dict(offset=0.1, linecolor='b', alpha=0.7, linewidth=0.5) + } + if component != '*': + alter_comp = compclass.getCompPosition(component) + plot_streams['wfdata']['data'] = wfdata.select(component=component) + wfdata.select(component=alter_comp) + if wfdata_compare: + plot_streams['wfdata_comp']['data'] = wfdata_compare.select( + component=component) + wfdata_compare.select(component=alter_comp) + else: + plot_streams['wfdata']['data'] = wfdata + if wfdata_compare: + plot_streams['wfdata_comp']['data'] = wfdata_compare + + return plot_streams + + def get_sorted_nslc(self, st_main): + nslc = [trace.get_id() for trace in st_main if trace.stats.channel[-1] in ['Z', 'N', 'E', '1', '2', '3']] + nslc.sort(reverse=True) + return nslc + + def plot_traces(self, ax, plot_streams, nslc, wfstart, mapping, plot_positions, scaleToChannel, noiselevel, + scaleddata, nth_sample, verbosity): + nmax = 0 for n, seed_id in enumerate(nslc): network, station, location, channel = seed_id.split('.') - st = st_select.select(id=seed_id) - trace = st[0].copy() - if mapping: - n = plot_positions[trace.stats.channel] - if n > nmax: - nmax = n - if verbosity: - msg = 'plotting %s channel of station %s' % (channel, station) - print(msg) - stime = trace.stats.starttime - wfstart - time_ax = prep_time_axis(stime, trace) - if time_ax is not None: - if scaleToChannel: - st_scale = wfdata.select(channel=scaleToChannel) - if st_scale: - tr = st_scale[0] - trace.detrend('constant') - trace.normalize(np.max(np.abs(tr.data)) * 2) - scaleddata = True - if not scaleddata: - trace.detrend('constant') - trace.normalize(np.max(np.abs(trace.data)) * 2) - - times = [time for index, time in enumerate(time_ax) if not index % nth_sample] - data = [datum + n for index, datum in enumerate(trace.data) if not index % nth_sample] - ax.axhline(n, color="0.5", lw=0.5) - ax.plot(times, data, color=linecolor, linewidth=0.7) - if noiselevel is not None: - for level in [-noiselevel[channel], noiselevel[channel]]: - ax.plot([time_ax[0], time_ax[-1]], - [n + level, n + level], - color=linecolor, - linestyle='dashed') + for wf_name, wf_dict in plot_streams.items(): + st_select = wf_dict.get('data') + if not st_select: + continue + trace = st_select.select(id=seed_id)[0].copy() + if mapping: + n = plot_positions[trace.stats.channel] + if n > nmax: + nmax = n + if verbosity: + print(f'plotting {channel} channel of station {station}') + time_ax = prep_time_axis(trace.stats.starttime - wfstart, trace) + self.plot_trace(ax, trace, time_ax, wf_dict, n, scaleToChannel, noiselevel, scaleddata, nth_sample) self.setPlotDict(n, seed_id) - if plot_additional and additional_channel: - compare_stream = wfdata.select(channel=additional_channel) - if compare_stream: - trace = compare_stream[0] - if scaleToChannel: - st_scale = wfdata.select(channel=scaleToChannel) - if st_scale: - tr = st_scale[0] - trace.detrend('constant') - trace.normalize(np.max(np.abs(tr.data)) * 2) - scaleddata = True - if not scaleddata: - trace.detrend('constant') - trace.normalize(np.max(np.abs(trace.data)) * 2) - time_ax = prep_time_axis(stime, trace) - times = [time for index, time in enumerate(time_ax) if not index % nth_sample] - p_data = compare_stream[0].data - # #normalize - # p_max = max(abs(p_data)) - # p_data /= p_max - for index in range(3): - ax.plot(times, p_data, color='red', alpha=0.5, linewidth=0.7) - p_data += 1 + return nmax + def plot_trace(self, ax, trace, time_ax, wf_dict, n, scaleToChannel, noiselevel, scaleddata, nth_sample): + if time_ax is not None: + if scaleToChannel: + self.scale_trace(trace, scaleToChannel) + scaleddata = True + if not scaleddata: + trace.detrend('constant') + trace.normalize(np.max(np.abs(trace.data)) * 2) + offset = wf_dict.get('offset') + + times = [time for index, time in enumerate(time_ax) if not index % nth_sample] + data = [datum + n + offset for index, datum in enumerate(trace.data) if not index % nth_sample] + ax.axhline(n, color="0.5", lw=0.5) + ax.plot(times, data, color=wf_dict.get('linecolor'), **wf_dict.get('plot_kwargs')) + if noiselevel is not None: + self.plot_noise_level(ax, time_ax, noiselevel, channel, n, wf_dict.get('linecolor')) + + def scale_trace(self, trace, scaleToChannel): + st_scale = wfdata.select(channel=scaleToChannel) + if st_scale: + tr = st_scale[0] + trace.detrend('constant') + trace.normalize(np.max(np.abs(tr.data)) * 2) + + def plot_noise_level(self, ax, time_ax, noiselevel, channel, n, linecolor): + for level in [-noiselevel[channel], noiselevel[channel]]: + ax.plot([time_ax[0], time_ax[-1]], [n + level, n + level], color=linecolor, linestyle='dashed') + + def plot_additional_trace(self, ax, wfdata, additional_channel, scaleToChannel, scaleddata, nth_sample, wfstart): + compare_stream = wfdata.select(channel=additional_channel) + if compare_stream: + trace = compare_stream[0] + if scaleToChannel: + self.scale_trace(trace, scaleToChannel) + scaleddata = True + if not scaleddata: + trace.detrend('constant') + trace.normalize(np.max(np.abs(trace.data)) * 2) + time_ax = prep_time_axis(trace.stats.starttime - wfstart, trace) + self.plot_additional_data(ax, trace, time_ax, nth_sample) + + def plot_additional_data(self, ax, trace, time_ax, nth_sample): + times = [time for index, time in enumerate(time_ax) if not index % nth_sample] + p_data = trace.data + for index in range(3): + ax.plot(times, p_data, color='red', alpha=0.5, linewidth=0.7) + p_data += 1 + + def finalize_plot(self, ax, wfstart, wfend, nmax, zoomx, zoomy, iniPick, title, snr): if iniPick: - ax.vlines(iniPick, ax.get_ylim()[0], ax.get_ylim()[1], - colors='m', linestyles='dashed', - linewidth=2) - xlabel = 'seconds since {0}'.format(wfstart) + ax.vlines(iniPick, ax.get_ylim()[0], ax.get_ylim()[1], colors='m', linestyles='dashed', linewidth=2) + xlabel = f'seconds since {wfstart}' ylabel = '' self.updateWidget(xlabel, ylabel, title) self.setXLims(ax, [0, wfend - wfstart]) @@ -1486,15 +1500,14 @@ class PylotCanvas(FigureCanvas): if zoomy is not None: self.setYLims(ax, zoomy) if snr is not None: - if snr < 2: - warning = 'LOW SNR' - if snr < 1.5: - warning = 'VERY LOW SNR' - ax.text(0.1, 0.9, 'WARNING - {}'.format(warning), ha='center', va='center', transform=ax.transAxes, - color='red') - + self.plot_snr_warning(ax, snr) self.draw() + def plot_snr_warning(self, ax, snr): + if snr < 2: + warning = 'LOW SNR' if snr >= 1.5 else 'VERY LOW SNR' + ax.text(0.1, 0.9, f'WARNING - {warning}', ha='center', va='center', transform=ax.transAxes, color='red') + @staticmethod def getXLims(ax): return ax.get_xlim() @@ -1846,8 +1859,8 @@ class PhaseDefaults(QtWidgets.QDialog): class PickDlg(QDialog): update_picks = QtCore.Signal(dict) - def __init__(self, parent=None, data=None, station=None, network=None, location=None, picks=None, - autopicks=None, rotate=False, parameter=None, embedded=False, metadata=None, + def __init__(self, parent=None, data=None, data_compare=None, station=None, network=None, location=None, picks=None, + autopicks=None, rotate=False, parameter=None, embedded=False, metadata=None, show_comp_data=False, event=None, filteroptions=None, model=None, wftype=None): super(PickDlg, self).__init__(parent, Qt.Window) self.orig_parent = parent @@ -1856,6 +1869,7 @@ class PickDlg(QDialog): # initialize attributes self.parameter = parameter self._embedded = embedded + self.showCompData = show_comp_data self.station = station self.network = network self.location = location @@ -1894,11 +1908,32 @@ class PickDlg(QDialog): else: self.filteroptions = FILTERDEFAULTS self.pick_block = False + + # set attribute holding data + if data is None or not data: + try: + data = parent.get_data().get_wf_data().copy() + self.data = data.select(station=station) + except AttributeError as e: + errmsg = 'You either have to put in a data or an appropriate ' \ + 'parent (PyLoT MainWindow) object: {0}'.format(e) + raise Exception(errmsg) + else: + self.data = data + self.data_compare = data_compare + self.nextStation = QtWidgets.QCheckBox('Continue with next station ') # comparison channel - self.compareChannel = QtWidgets.QComboBox() - self.compareChannel.activated.connect(self.resetPlot) + self.referenceChannel = QtWidgets.QComboBox() + self.referenceChannel.activated.connect(self.resetPlot) + + # comparison channel + self.compareCB = QtWidgets.QCheckBox() + self.compareCB.setChecked(self.showCompData) + self.compareCB.clicked.connect(self.switchCompData) + self.compareCB.clicked.connect(self.resetPlot) + self.compareCB.setVisible(bool(self.data_compare)) # scale channel self.scaleChannel = QtWidgets.QComboBox() @@ -1911,19 +1946,7 @@ class PickDlg(QDialog): self.cur_xlim = None self.cur_ylim = None - # set attribute holding data - if data is None or not data: - try: - data = parent.get_data().getWFData().copy() - self.data = data.select(station=station) - except AttributeError as e: - errmsg = 'You either have to put in a data or an appropriate ' \ - 'parent (PyLoT MainWindow) object: {0}'.format(e) - raise Exception(errmsg) - else: - self.data = data - - self.stime, self.etime = full_range(self.getWFData()) + self.stime, self.etime = full_range(self.get_wf_data()) # initialize plotting widget self.multicompfig = PylotCanvas(parent=self, multicursor=True) @@ -1934,12 +1957,12 @@ class PickDlg(QDialog): self.setupUi() # fill compare and scale channels - self.compareChannel.addItem('-', None) + self.referenceChannel.addItem('-', None) self.scaleChannel.addItem('individual', None) - for trace in self.getWFData(): + for trace in self.get_wf_data(): channel = trace.stats.channel - self.compareChannel.addItem(channel, trace) + self.referenceChannel.addItem(channel, trace) if not channel[-1] in ['Z', 'N', 'E', '1', '2', '3']: print('Skipping unknown channel for scaling: {}'.format(channel)) continue @@ -1956,7 +1979,7 @@ class PickDlg(QDialog): if self.wftype is not None: title += ' | ({})'.format(self.wftype) - self.multicompfig.plotWFData(wfdata=self.getWFData(), + self.multicompfig.plotWFData(wfdata=self.get_wf_data(), wfdata_compare=self.get_wf_dataComp(), title=title) self.multicompfig.setZoomBorders2content() @@ -2132,8 +2155,11 @@ class PickDlg(QDialog): _dialtoolbar.addWidget(est_label) _dialtoolbar.addWidget(self.plot_arrivals_button) _dialtoolbar.addSeparator() - _dialtoolbar.addWidget(QtWidgets.QLabel('Compare to channel: ')) - _dialtoolbar.addWidget(self.compareChannel) + _dialtoolbar.addWidget(QtWidgets.QLabel('Plot reference channel: ')) + _dialtoolbar.addWidget(self.referenceChannel) + _dialtoolbar.addSeparator() + _dialtoolbar.addWidget(QtWidgets.QLabel('Compare: ')) + _dialtoolbar.addWidget(self.compareCB) _dialtoolbar.addSeparator() _dialtoolbar.addWidget(QtWidgets.QLabel('Scaling: ')) _dialtoolbar.addWidget(self.scaleChannel) @@ -2470,7 +2496,7 @@ class PickDlg(QDialog): def activatePicking(self): self.leave_rename_phase() self.renamePhaseAction.setEnabled(False) - self.compareChannel.setEnabled(False) + self.referenceChannel.setEnabled(False) self.scaleChannel.setEnabled(False) phase = self.currentPhase phaseID = self.getPhaseID(phase) @@ -2488,7 +2514,7 @@ class PickDlg(QDialog): if self.autoFilterAction.isChecked(): for filteraction in [self.filterActionP, self.filterActionS]: filteraction.setChecked(False) - self.filterWFData() + self.filter_wf_data() self.draw() else: self.draw() @@ -2502,7 +2528,7 @@ class PickDlg(QDialog): self.disconnectPressEvent() self.multicompfig.connectEvents() self.renamePhaseAction.setEnabled(True) - self.compareChannel.setEnabled(True) + self.referenceChannel.setEnabled(True) self.scaleChannel.setEnabled(True) self.connect_pick_delete() self.draw() @@ -2572,9 +2598,15 @@ class PickDlg(QDialog): def getGlobalLimits(self, ax, axis): return self.multicompfig.getGlobalLimits(ax, axis) - def getWFData(self): + def get_wf_data(self): return self.data + def get_wf_dataComp(self): + if self.showCompData: + return self.data_compare + else: + return Stream() + def selectWFData(self, channel): component = channel[-1].upper() wfdata = Stream() @@ -2584,17 +2616,17 @@ class PickDlg(QDialog): return tr if component == 'E' or component == 'N': - for trace in self.getWFData(): + for trace in self.get_wf_data(): trace = selectTrace(trace, 'NE') if trace: wfdata.append(trace) elif component == '1' or component == '2': - for trace in self.getWFData(): + for trace in self.get_wf_data(): trace = selectTrace(trace, '12') if trace: wfdata.append(trace) else: - wfdata = self.getWFData().select(component=component) + wfdata = self.get_wf_data().select(component=component) return wfdata def getPicks(self, picktype='manual'): @@ -2696,11 +2728,16 @@ class PickDlg(QDialog): stime = self.getStartTime() - # copy data for plotting - data = self.getWFData().copy() - data = self.getPickPhases(data, phase) - data.normalize() - if not data: + # copy wfdata for plotting + wfdata = self.get_wf_data().copy() + wfdata_comp = self.get_wf_dataComp().copy() + wfdata = self.getPickPhases(wfdata, phase) + wfdata_comp = self.getPickPhases(wfdata_comp, phase) + for wfd in [wfdata, wfdata_comp]: + if wfd: + wfd.normalize() + + if not wfdata: QtWidgets.QMessageBox.warning(self, 'No channel to plot', 'No channel to plot for phase: {}. ' 'Make sure to select the correct channels for P and S ' @@ -2708,14 +2745,16 @@ class PickDlg(QDialog): self.leave_picking_mode() return - # filter data and trace on which is picked prior to determination of SNR + # filter wfdata and trace on which is picked prior to determination of SNR filterphase = self.currentFilterPhase() if filterphase: filteroptions = self.getFilterOptions(filterphase).parseFilterOptions() try: - data.detrend('linear') - data.filter(**filteroptions) - # wfdata.filter(**filteroptions)# MP MP removed filtering of original data + for wfd in [wfdata, wfdata_comp]: + if wfd: + wfd.detrend('linear') + wfd.filter(**filteroptions) + # wfdata.filter(**filteroptions)# MP MP removed filtering of original wfdata except ValueError as e: self.qmb = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Icon.Information, 'Denied', @@ -2725,8 +2764,8 @@ class PickDlg(QDialog): snr = [] noiselevels = {} # determine SNR and noiselevel - for trace in data.traces: - st = data.select(channel=trace.stats.channel) + for trace in wfdata.traces: + st = wfdata.select(channel=trace.stats.channel) stime_diff = trace.stats.starttime - stime result = getSNR(st, (noise_win, gap_win, signal_win), ini_pick - stime_diff) snr.append(result[0]) @@ -2737,23 +2776,25 @@ class PickDlg(QDialog): noiselevel = nfac noiselevels[trace.stats.channel] = noiselevel - # prepare plotting of data - for trace in data: - t = prep_time_axis(trace.stats.starttime - stime, trace) - inoise = getnoisewin(t, ini_pick, noise_win, gap_win) - trace = demeanTrace(trace, inoise) - # upscale trace data in a way that each trace is vertically zoomed to noiselevel*factor - channel = trace.stats.channel - noiselevel = noiselevels[channel] - noiseScaleFactor = self.calcNoiseScaleFactor(noiselevel, zoomfactor=5.) - trace.data *= noiseScaleFactor - noiselevels[channel] *= noiseScaleFactor + # prepare plotting of wfdata + for wfd in [wfdata, wfdata_comp]: + if wfd: + for trace in wfd: + t = prep_time_axis(trace.stats.starttime - stime, trace) + inoise = getnoisewin(t, ini_pick, noise_win, gap_win) + trace = demeanTrace(trace, inoise) + # upscale trace wfdata in a way that each trace is vertically zoomed to noiselevel*factor + channel = trace.stats.channel + noiselevel = noiselevels[channel] + noiseScaleFactor = self.calcNoiseScaleFactor(noiselevel, zoomfactor=5.) + trace.data *= noiseScaleFactor + noiselevels[channel] *= noiseScaleFactor mean_snr = np.mean(snr) x_res = getResolutionWindow(mean_snr, parameter.get('extent')) xlims = [ini_pick - x_res, ini_pick + x_res] - ylims = list(np.array([-.5, .5]) + [0, len(data) - 1]) + ylims = list(np.array([-.5, .5]) + [0, len(wfdata) - 1]) title = self.getStation() + ' picking mode' title += ' | SNR: {}'.format(mean_snr) @@ -2761,9 +2802,10 @@ class PickDlg(QDialog): filtops_str = transformFilteroptions2String(filteroptions) title += ' | Filteroptions: {}'.format(filtops_str) - plot_additional = bool(self.compareChannel.currentText()) - additional_channel = self.compareChannel.currentText() - self.multicompfig.plotWFData(wfdata=data, + plot_additional = bool(self.referenceChannel.currentText()) + additional_channel = self.referenceChannel.currentText() + self.multicompfig.plotWFData(wfdata=wfdata, + wfdata_compare=wfdata_comp, title=title, zoomx=xlims, zoomy=ylims, @@ -2797,7 +2839,7 @@ class PickDlg(QDialog): filteroptions = None # copy and filter data for earliest and latest possible picks - wfdata = self.getWFData().copy().select(channel=channel) + wfdata = self.get_wf_data().copy().select(channel=channel) if filteroptions: try: wfdata.detrend('linear') @@ -2844,7 +2886,7 @@ class PickDlg(QDialog): minFMSNR = parameter.get('minFMSNR') quality = get_quality_class(spe, parameter.get('timeerrorsP')) if quality <= minFMweight and snr >= minFMSNR: - FM = fmpicker(self.getWFData().select(channel=channel).copy(), wfdata.copy(), parameter.get('fmpickwin'), + FM = fmpicker(self.get_wf_data().select(channel=channel).copy(), wfdata.copy(), parameter.get('fmpickwin'), pick - stime_diff) # save pick times for actual phase @@ -3136,7 +3178,7 @@ class PickDlg(QDialog): def togglePickBlocker(self): return not self.pick_block - def filterWFData(self, phase=None): + def filter_wf_data(self, phase=None): if not phase: phase = self.currentPhase if self.getPhaseID(phase) == 'P': @@ -3154,7 +3196,8 @@ class PickDlg(QDialog): self.cur_xlim = self.multicompfig.axes[0].get_xlim() self.cur_ylim = self.multicompfig.axes[0].get_ylim() # self.multicompfig.updateCurrentLimits() - data = self.getWFData().copy() + wfdata = self.get_wf_data().copy() + wfdata_comp = self.get_wf_dataComp().copy() title = self.getStation() if filter: filtoptions = None @@ -3162,19 +3205,22 @@ class PickDlg(QDialog): filtoptions = self.getFilterOptions(self.getPhaseID(phase), gui_filter=True).parseFilterOptions() if filtoptions is not None: - data.detrend('linear') - data.taper(0.02, type='cosine') - data.filter(**filtoptions) + for wfd in [wfdata, wfdata_comp]: + if wfd: + wfd.detrend('linear') + wfd.taper(0.02, type='cosine') + wfd.filter(**filtoptions) filtops_str = transformFilteroptions2String(filtoptions) title += ' | Filteroptions: {}'.format(filtops_str) if self.wftype is not None: title += ' | ({})'.format(self.wftype) - plot_additional = bool(self.compareChannel.currentText()) - additional_channel = self.compareChannel.currentText() + plot_additional = bool(self.referenceChannel.currentText()) + additional_channel = self.referenceChannel.currentText() scale_channel = self.scaleChannel.currentText() - self.multicompfig.plotWFData(wfdata=data, title=title, + self.multicompfig.plotWFData(wfdata=wfdata, wfdata_compare=wfdata_comp, + title=title, zoomx=self.getXLims(), zoomy=self.getYLims(), plot_additional=plot_additional, @@ -3188,14 +3234,14 @@ class PickDlg(QDialog): def filterP(self): self.filterActionS.setChecked(False) if self.filterActionP.isChecked(): - self.filterWFData('P') + self.filter_wf_data('P') else: self.refreshPlot() def filterS(self): self.filterActionP.setChecked(False) if self.filterActionS.isChecked(): - self.filterWFData('S') + self.filter_wf_data('S') else: self.refreshPlot() @@ -3247,11 +3293,14 @@ class PickDlg(QDialog): self.resetZoom() self.refreshPlot() + def switchCompData(self): + self.showCompData = self.compareCB.isChecked() + def refreshPlot(self): if self.autoFilterAction.isChecked(): self.filterActionP.setChecked(False) self.filterActionS.setChecked(False) - # data = self.getWFData().copy() + # data = self.get_wf_data().copy() # title = self.getStation() filter = False phase = None @@ -3751,8 +3800,8 @@ class TuneAutopicker(QWidget): fnames = self.station_ids[self.get_current_station_id()] if not fnames == self.fnames: self.fnames = fnames - self.data.setWFData(fnames) - wfdat = self.data.getWFData() # all available streams + self.data.set_wf_data(fnames) + wfdat = self.data.get_wf_data() # all available streams # remove possible underscores in station names # wfdat = remove_underscores(wfdat) # rotate misaligned stations to ZNE @@ -3857,12 +3906,14 @@ class TuneAutopicker(QWidget): network = None location = None - wfdata = self.data.getWFData() + wfdata = self.data.get_wf_data() + wfdata_comp = self.data.get_wf_dataComp() metadata = self.parent().metadata event = self.get_current_event() filteroptions = self.parent().filteroptions wftype = self.wftype if self.obspy_dmt else '' self.pickDlg = PickDlg(self.parent(), data=wfdata.select(station=station).copy(), + data_comp=wfdata_comp.select(station=station).copy(), station=station, network=network, location=location, parameter=self.parameter, picks=self.get_current_event_picks(station), @@ -3911,7 +3962,7 @@ class TuneAutopicker(QWidget): for plotitem in self._manual_pick_plots: self.clear_plotitem(plotitem) self._manual_pick_plots = [] - st = self.data.getWFData() + st = self.data.get_wf_data() tr = st.select(station=self.get_current_station())[0] starttime = tr.stats.starttime # create two lists with figure names and subindices (for subplots) to get the correct axes