Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • software_public/passoft/sohstationviewer
1 result
Show changes
Commits on Source (4)
Showing
with 161 additions and 88 deletions
......@@ -16,7 +16,8 @@ from sohstationviewer.view.plotting.gps_plot.gps_point import GPSPoint
from sohstationviewer.view.util.enums import LogType
from sohstationviewer.database.process_db import execute_db
from sohstationviewer.model.handling_data import (
combine_traces_except_overlap, sort_data, retrieve_gaps_from_stream_header)
combine_traces_except_gaps_overlaps, sort_data,
retrieve_gaps_from_stream_header)
class ProcessingDataError(Exception):
......@@ -89,9 +90,15 @@ class DataTypeModel():
self.notification_signal = notification_signal
self.pause_signal = pause_signal
"""
gaps_by_key_chan: gap list for each key/chan_id to separate data at
gaps, overlaps
"""
self.gaps_by_key_chan: Dict[Union[str, Tuple[str, str]],
Dict[str, List[List[int]]]] = {}
"""
stream_header_by_key_chan: stream header by key, chan_id to get key
list, gaps by sta_id, nets by sta_id, channels by sta_id
list, gaps by sta_id, nets by sta_id, channels by sta_id
"""
self.stream_header_by_key_chan: Dict[str, Dict[str, Stream]] = {}
"""
......@@ -292,7 +299,7 @@ class DataTypeModel():
"""
self.track_info("Retrieve gaps.", LogType.INFO)
retrieve_gaps_from_stream_header(
self.stream_header_by_key_chan,
self.stream_header_by_key_chan, self.gaps_by_key_chan,
self.gaps, self.read_start, self.read_end)
self.track_info("Sort data.", LogType.INFO)
......@@ -457,29 +464,35 @@ class DataTypeModel():
self.processing_log.append((msg, LogType.WARNING))
def combine_times_data_of_traces_w_spr_less_or_equal_1(
self, sta_data: Dict[str, Dict], data_name: str):
self, data: Dict[str, Dict], selected_key: Union[(str, str), str],
data_name: str):
"""
Create plotting times and data for channels with samplerate
less than or equal to 1, in which plotting times is
all traces' time combined together but split where there is
overlap to prevent plotting line go back and fort,
and same for plotting data.
:param sta_data: chan_data of a station by chan_id
:param data_name: name of data (Waveform, SOH, Mass Position)
:param data: one of waveform_data, soh_data, mass_pos_data
:param selected_key: key of the selected data
:param data_name: name of data (Waveform, SOH, Mass Position) to show
info
:return: the result plotting times and data of each channel will be
used to create times and data items of the channel.
"""
selected_data = data[selected_key]
selected_gaps = self.gaps_by_key_chan[selected_key]
if self.creator_thread.isInterruptionRequested():
raise ThreadStopped()
self.track_info(
f'{data_name}: Combine traces with samplerate < 1', LogType.INFO)
for chan_id in sta_data:
chan_data = sta_data[chan_id]
for chan_id in selected_data:
chan_data = selected_data[chan_id]
if chan_data['samplerate'] > 1:
continue
new_traces = combine_traces_except_overlap(chan_data['tracesInfo'])
new_traces = combine_traces_except_gaps_overlaps(
chan_data['tracesInfo'], selected_gaps[chan_id])
chan_data['tracesInfo'] = new_traces
def sort_all_data(self):
......@@ -507,14 +520,14 @@ class DataTypeModel():
"""
if self.selected_key in self.waveform_data.keys():
self.combine_times_data_of_traces_w_spr_less_or_equal_1(
self.waveform_data[self.selected_key], 'Waveform')
self.waveform_data, self.selected_key, 'Waveform')
if self.selected_key in self.mass_pos_data.keys():
self.combine_times_data_of_traces_w_spr_less_or_equal_1(
self.mass_pos_data[self.selected_key], 'Mass Possition')
self.mass_pos_data, self.selected_key, 'Mass Possition')
try:
if self.selected_key in self.soh_data.keys():
self.combine_times_data_of_traces_w_spr_less_or_equal_1(
self.soh_data[self.selected_key], 'SOH')
self.soh_data, self.selected_key, 'SOH')
except KeyError:
# Reftek's SOH trace doesn't have startTmEpoch and
# actually soh_data consists of only one trace
......
......@@ -415,12 +415,14 @@ def convert_reftek_masspos_data(data: np.ndarray) -> Dict:
return np.round_(data / 32767.0 * 10.0, 1)
def combine_traces_except_overlap(traces: List[Dict]) -> List[Dict]:
def combine_traces_except_gaps_overlaps(
traces: List[Dict], gaps: List[List[float]]) -> List[Dict]:
"""
Return new list of traces in which traces are combined together but split
into different trace at overlap
Return new list of traces for a channel in which traces are combined
together but split into different traces at gaps/overlaps.
:param traces: list of traces
:param traces: list of traces of the channel
:param gaps: list of gaps of the channel
:return new_traces: list of new traces
"""
end_epoch = 0
......@@ -429,16 +431,12 @@ def combine_traces_except_overlap(traces: List[Dict]) -> List[Dict]:
start_epoch = traces[0]['startTmEpoch']
size = 0
new_traces = []
curr_gap_idx = 0
for idx, tr in enumerate(traces):
if tr['startTmEpoch'] > end_epoch:
# not overlap: end of a trace not greater than beginning
# of next trace => combine trace
current_times.append(tr['times'])
current_data.append(tr['data'])
else:
# overlap: fulfill trace to start next trace
if len(current_times) > 0:
try:
start_in_gap = (min(gaps[curr_gap_idx]) <= tr['startTmEpoch']
<= max(gaps[curr_gap_idx]))
if start_in_gap and size != 0:
new_tr = {'samplerate': tr['samplerate'],
'startTmEpoch': start_epoch,
'endTmEpoch': end_epoch,
......@@ -446,14 +444,18 @@ def combine_traces_except_overlap(traces: List[Dict]) -> List[Dict]:
'times': np.hstack(current_times),
'data': np.hstack(current_data)}
new_traces.append(new_tr)
# startTmEpoch of the first tr combined
start_epoch = tr['startTmEpoch']
size = 0
current_times = [tr['times']]
current_data = [tr['data']]
# endTmEpoch of the last tr combined
end_epoch = tr['endTmEpoch']
curr_gap_idx += 1
start_epoch = tr['startTmEpoch']
current_times = []
current_data = []
size = 0
except IndexError:
pass
current_times.append(tr['times'])
current_data.append(tr['data'])
size += tr['size']
end_epoch = tr['endTmEpoch']
new_tr = {
'samplerate': tr['samplerate'],
'startTmEpoch': start_epoch,
......@@ -992,21 +994,25 @@ def find_tps_tm_idx(
def retrieve_gaps_from_stream_header(
streams: Dict[str, Dict[str, Stream]],
gaps_by_key_chan: Dict[Union[str, Tuple[str, str]],
Dict[str, List[List[int]]]],
gaps: Dict[str, List[List[float]]],
read_start: Optional[float], read_end: Optional[float]) -> \
Dict[str, List[List[float]]]:
"""
Retrieve gaps by sta_id from stream_header_by_key_chan
:param: dict of stream header by sta, chan
:return all_gaps: list of gaps by sta_id
:param streams: dict of stream header by sta, chan
:param gaps_by_key_chan: gaps list by key and channel id
:param gaps: gaps list by key
"""
for sta_id in streams:
sta_gaps = []
gaps_by_key_chan[sta_id] = {}
for chan_id in streams[sta_id]:
stream = streams[sta_id][chan_id]
gaps_in_stream = stream.get_gaps()
stream_gaps = [
gaps_by_key_chan[sta_id][chan_id] = stream_gaps = [
[g[4].timestamp, g[5].timestamp] for g in gaps_in_stream
if read_start <= min(g[4].timestamp, g[5].timestamp) <= read_end or # noqa
read_start <= max(g[4].timestamp, g[5].timestamp) <= read_end] # noqa
......
......@@ -4,6 +4,8 @@ from typing import Tuple, Union, Dict, Callable, List, Optional
from PySide2 import QtCore
from sohstationviewer.model.data_type_model import DataTypeModel
from sohstationviewer.view.plotting.plotting_widget.plotting_processor import (
PlottingChannelProcessor)
from sohstationviewer.view.plotting.plotting_widget.plotting_widget import (
......@@ -23,9 +25,8 @@ class MultiThreadedPlottingWidget(PlottingWidget):
stopped = QtCore.Signal()
notification = QtCore.Signal(str)
def __init__(self, parent, tracking_box, name):
PlottingWidget.__init__(
self, parent, tracking_box, name)
def __init__(self, *args, **kwargs):
PlottingWidget.__init__(self, *args, **kwargs)
self.data_processors: List[PlottingChannelProcessor] = []
# Only one data processor can run at a time, so it is not a big problem
......@@ -54,6 +55,7 @@ class MultiThreadedPlottingWidget(PlottingWidget):
self.is_working = False
def init_plot(self,
d_obj: DataTypeModel,
data_time: List[float],
key: Union[str, Tuple[str, str]],
start_tm: float, end_tm: float,
......@@ -61,6 +63,7 @@ class MultiThreadedPlottingWidget(PlottingWidget):
"""
Initiate plotting with gaps, top time bar
:param d_obj: object of data
:param data_time: start and end time of data
:param key: data set's key
:param start_tm: requested start time to plot
......@@ -72,7 +75,7 @@ class MultiThreadedPlottingWidget(PlottingWidget):
self.key = key
self.processing_log = [] # [(message, type)]
self.gap_bar = None
self.date_mode = self.parent.date_format.upper()
self.date_mode = self.main_window.date_format.upper()
self.time_ticks_total = time_ticks_total
self.min_x = max(data_time[0], start_tm)
self.max_x = min(data_time[1], end_tm)
......@@ -98,6 +101,7 @@ class MultiThreadedPlottingWidget(PlottingWidget):
self.draw()
return False
else:
self.plotting_axes.add_gap_bar(d_obj.gaps[key])
return True
def create_plotting_channel_processors(
......
......@@ -10,16 +10,17 @@ class Plotting:
"""
Class that includes different methods to plot channels on a figure.
"""
def __init__(self, parent, plotting_axes, parent_params):
def __init__(self, parent, plotting_axes, main_window):
"""
:param parent: PlottingWidget - widget to plot channels
:param plotting_axes: PlottingAxes - widget that includes a figure
and methods related to create axes
:param parent_params: Object - object that includes needed parameters
:param main_window: QApplication - Main Window to access user's
setting parameters
"""
super().__init__()
self.parent = parent
self.params = parent_params
self.main_window = main_window
self.plotting_axes = plotting_axes
def plot_none(self):
......@@ -334,7 +335,7 @@ class Plotting:
:return ax: matplotlib.axes.Axes - axes of the channel
"""
value_colors = get_masspos_value_colors(
self.params.mass_pos_volt_range_opt, chan_id,
self.main_window.mass_pos_volt_range_opt, chan_id,
self.parent.c_mode, self.parent.processing_log,
ret_type='tupleList')
......
......@@ -17,12 +17,13 @@ class PlottingAxes:
Class that includes a figure to add axes for plotting and all methods
related to create axes, ruler, title.
"""
def __init__(self, parent, parent_params):
def __init__(self, parent, main_window):
"""
:param parent: PlottingWidget - widget to plot channels
:param parent_params: Object - object that includes needed parameters
:param main_window: QApplication - Main Window to access user's
setting parameters
"""
self.params = parent_params
self.main_window = main_window
self.parent = parent
# gaps: list of gaps which is a list of min and max of gaps
self.gaps: List[List[float]] = []
......@@ -275,7 +276,7 @@ class PlottingAxes:
ax.spines['top'].set_visible(True)
ax.spines['bottom'].set_visible(True)
ax.unit_bw = get_unit_bitweight(
chan_db_info, self.params.bit_weight_opt
chan_db_info, self.main_window.bit_weight_opt
)
self.set_axes_ylim(ax, min_y, max_y)
......@@ -310,15 +311,15 @@ class PlottingAxes:
:param gaps: [[float, float], ] - list of [min, max] of gaps
"""
if self.params.min_gap is None:
if self.main_window.min_gap is None:
return
self.gaps = gaps = get_gaps(gaps, self.params.min_gap)
self.gaps = gaps = get_gaps(gaps, self.main_window.min_gap)
self.parent.plotting_bot -= 0.003
self.parent.gap_bar = self.create_axes(self.parent.plotting_bot,
0.001,
has_min_max_lines=False)
gap_label = f"GAP({self.params.min_gap}min)"
gap_label = f"GAP({self.main_window.min_gap}min)"
h = 0.001 # height of rectangle represent gap
self.set_axes_info(self.parent.gap_bar, [len(gaps)],
label=gap_label)
......
"""
Class of which object is used to plot data
"""
from typing import List, Optional
from typing import List, Optional, Union
import matplotlib.text
from PySide2.QtCore import QTimer, Qt
from matplotlib import pyplot as pl
from PySide2 import QtCore, QtWidgets
from PySide2.QtWidgets import QWidget, QApplication, QTextBrowser
from sohstationviewer.conf import constants
from sohstationviewer.view.util.color import set_color_mode
......@@ -28,15 +29,20 @@ class PlottingWidget(QtWidgets.QScrollArea):
events to serve user's purpose.
"""
def __init__(self, parent, tracking_box, name):
def __init__(self, parent: Union[QWidget, QApplication],
tracking_box: QTextBrowser,
name: str,
main_window: QApplication) -> None:
"""
:param parent: QWidget/QMainWindow - widget that contains this plotting
:param parent: widget that contains this plotting
widget
:param tracking_box: QTextBrowser - widget to display tracking info
:param name: str - name of the plotting widget to keep track of what
:param tracking_box: widget to display tracking info
:param name: name of the plotting widget to keep track of what
widget the program is working on
:param main_window: Main window that keep all parameters set by user
"""
self.parent = parent
self.main_window = main_window
self.name = name
self.tracking_box = tracking_box
# =============== declare attributes =======================
......@@ -194,7 +200,7 @@ class PlottingWidget(QtWidgets.QScrollArea):
"""
plotting_axes: object that helps creating axes for plotting
"""
self.plotting_axes = PlottingAxes(self, parent_params=parent)
self.plotting_axes = PlottingAxes(self, main_window)
self.plotting_axes.canvas.setParent(self.main_widget)
self.setWidget(self.main_widget)
......@@ -202,8 +208,7 @@ class PlottingWidget(QtWidgets.QScrollArea):
"""
plotting: object that helps with different types of plotting channels
"""
self.plotting = Plotting(self, self.plotting_axes,
parent_params=parent)
self.plotting = Plotting(self, self.plotting_axes, main_window)
"""
new_min_x: store the new minimum time for zooming; used to fix a
problem where after the first zoom marker is chosen, any ruler that is
......@@ -575,6 +580,8 @@ class PlottingWidget(QtWidgets.QScrollArea):
tr_min_ys = []
tr_max_ys = []
for x, y in zip(ax.x_list, ax.y_list):
if len(x) == 0:
continue
if self.min_x > x[-1] or self.max_x < x[0]:
continue
ret = get_total_miny_maxy(x, y, self.min_x, self.max_x)
......
......@@ -20,8 +20,8 @@ class SOHWidget(MultiThreadedPlottingWidget):
Widget to display soh and mass position data.
"""
def __init__(self, parent, tracking_box, name):
MultiThreadedPlottingWidget.__init__(self, parent, tracking_box, name)
def __init__(self, *args, **kwargs):
MultiThreadedPlottingWidget.__init__(self, *args, **kwargs)
def init_plot(self, d_obj: DataTypeModel, key: Union[str, Tuple[str, str]],
start_tm: float, end_tm: float, time_ticks_total: int):
......@@ -39,12 +39,11 @@ class SOHWidget(MultiThreadedPlottingWidget):
self.plotting_data2 = d_obj.mass_pos_data[key]
channel_list = d_obj.soh_data[key].keys()
data_time = d_obj.data_time[key]
ret = super().init_plot(data_time, key, start_tm, end_tm,
ret = super().init_plot(d_obj, data_time, key, start_tm, end_tm,
time_ticks_total, is_waveform=False)
if not ret:
return False
self.plotting_axes.add_gap_bar(d_obj.gaps[key])
not_found_chan = [c for c in channel_list
if c not in self.plotting_data1.keys()]
if len(not_found_chan) > 0:
......
......@@ -102,7 +102,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
self.min_x = max(d_obj.data_time[key][0], start_tm)
self.max_x = min(d_obj.data_time[key][1], end_tm)
self.date_mode = self.parent.date_format.upper()
self.date_mode = self.main_window.date_format.upper()
if self.plotting_data1 == {}:
title = "NO WAVEFORM DATA TO DISPLAY TPS."
self.processing_log.append(
......@@ -488,10 +488,6 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
super().__init__()
self.parent = parent
"""
date_format: str - format to display date/time
"""
self.date_format = self.parent.date_format
"""
data_type: str - type of data being plotted
"""
self.data_type = None
......@@ -512,7 +508,7 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
for each 5-minute of data
"""
self.plotting_widget = TimePowerSquaredWidget(
self, self.info_text_browser, "TPS")
self, self.info_text_browser, "TPS", self.parent)
main_layout.addWidget(self.plotting_widget, 2)
bottom_layout = QtWidgets.QHBoxLayout()
......
......@@ -18,8 +18,8 @@ class WaveformWidget(MultiThreadedPlottingWidget):
"""
Widget to display waveform and mass position data.
"""
def __init__(self, parent, tracking_box, name):
MultiThreadedPlottingWidget.__init__(self, parent, tracking_box, name)
def __init__(self, *args, **kwargs):
MultiThreadedPlottingWidget.__init__(self, *args, **kwargs)
def init_plot(self, d_obj: DataTypeModel, key: Union[str, Tuple[str, str]],
start_tm: float, end_tm: float, time_ticks_total: int):
......@@ -36,7 +36,7 @@ class WaveformWidget(MultiThreadedPlottingWidget):
self.plotting_data1 = d_obj.waveform_data[key]
self.plotting_data2 = d_obj.mass_pos_data[key]
data_time = d_obj.data_time[key]
return super().init_plot(data_time, key, start_tm, end_tm,
return super().init_plot(d_obj, data_time, key, start_tm, end_tm,
time_ticks_total, is_waveform=True)
def get_plot_info(self, *args, **kwargs):
......@@ -90,20 +90,6 @@ class WaveformDialog(QtWidgets.QWidget):
"""
self.parent = parent
"""
date_format: str - format to display date/time
"""
self.date_format = self.parent.date_format
"""
bit_weight_opt: str - option for bit weight ('', 'low', 'high')
(Menu Options - Q330 Gain)
"""
self.bit_weight_opt = self.parent.bit_weight_opt
"""
mass_pos_volt_range_opt: str - ('regular'/'trilium'): define how to map
values and colors when plotting
"""
self.mass_pos_volt_range_opt = self.parent.mass_pos_volt_range_opt
"""
data_type: str - type of data being plotted
"""
self.data_type = None
......@@ -124,7 +110,7 @@ class WaveformDialog(QtWidgets.QWidget):
mass position channel
"""
self.plotting_widget = WaveformWidget(
self, self.info_text_browser, 'WAVEFORM')
self, self.info_text_browser, 'WAVEFORM', self.parent)
self.plotting_widget.finished.connect(self.plot_finished)
main_layout.addWidget(self.plotting_widget, 2)
......
......@@ -356,7 +356,8 @@ class UIMainWindow(object):
self.plotting_widget = SOHWidget(self.main_window,
self.tracking_info_text_browser,
'SOH')
'SOH',
self.main_window)
h_layout.addWidget(self.plotting_widget, 2)
......
import numpy as np
from unittest import TestCase
from sohstationviewer.model.handling_data import (
sort_data, check_related_gaps, squash_gaps
sort_data, check_related_gaps, squash_gaps,
combine_traces_except_gaps_overlaps
)
......@@ -84,3 +86,60 @@ class TestSquashGaps(TestCase):
def test_mixed_gaps(self):
gaps = squash_gaps((self.mixed_gaps))
self.assertEqual(gaps, [[3, 8], [18, 13]])
class TestCombineTracesExceptGapsOverlaps(TestCase):
def test_combine(self):
traces = [
{'samplerate': 1, 'startTmEpoch': 1, 'endTmEpoch': 2, 'size': 2,
'times': np.array([1, 2]), 'data': np.array([0, 0])},
# overlap at the beginning
{'samplerate': 1, 'startTmEpoch': 1, 'endTmEpoch': 3, 'size': 3,
'times': np.array([1, 2, 3]), 'data': np.array([1, 1, 1])},
{'samplerate': 1, 'startTmEpoch': 4, 'endTmEpoch': 6, 'size': 3,
'times': np.array([4, 5, 6]), 'data': np.array([2, 2, 2])},
# gap
{'samplerate': 1, 'startTmEpoch': 8, 'endTmEpoch': 10, 'size': 3,
'times': np.array([8, 9, 10]), 'data': np.array([3, 3, 3])},
{'samplerate': 1, 'startTmEpoch': 11, 'endTmEpoch': 13, 'size': 3,
'times': np.array([11, 12, 13]), 'data': np.array([4, 4, 4])},
# overlap
{'samplerate': 1, 'startTmEpoch': 12, 'endTmEpoch': 14, 'size': 3,
'times': np.array([12, 13, 14]), 'data': np.array([5, 5, 5])}
]
gaps = [[2, 1], [6, 8], [13, 12]]
new_traces = combine_traces_except_gaps_overlaps(traces, gaps)
self.assertEqual(len(new_traces), 4)
# Trace 0
self.assertEqual(new_traces[0]['samplerate'], 1)
self.assertEqual(new_traces[0]['startTmEpoch'], 1)
self.assertEqual(new_traces[0]['endTmEpoch'], 2)
self.assertEqual(new_traces[0]['size'], 2)
self.assertEqual(new_traces[0]['times'].tolist(), [1, 2])
self.assertEqual(new_traces[0]['data'].tolist(), [0, 0])
# Combine traces 1 & 2
self.assertEqual(new_traces[1]['samplerate'], 1)
self.assertEqual(new_traces[1]['startTmEpoch'], 1)
self.assertEqual(new_traces[1]['endTmEpoch'], 6)
self.assertEqual(new_traces[1]['size'], 6)
self.assertEqual(new_traces[1]['times'].tolist(), [1, 2, 3, 4, 5, 6])
self.assertEqual(new_traces[1]['data'].tolist(), [1, 1, 1, 2, 2, 2])
# Combine traces 3 & 4
self.assertEqual(new_traces[2]['samplerate'], 1)
self.assertEqual(new_traces[2]['startTmEpoch'], 8)
self.assertEqual(new_traces[2]['endTmEpoch'], 13)
self.assertEqual(new_traces[2]['size'], 6)
self.assertEqual(new_traces[2]['times'].tolist(),
[8, 9, 10, 11, 12, 13])
self.assertEqual(new_traces[2]['data'].tolist(), [3, 3, 3, 4, 4, 4])
# Trace 5
self.assertEqual(new_traces[3]['samplerate'], 1)
self.assertEqual(new_traces[3]['startTmEpoch'], 12)
self.assertEqual(new_traces[3]['endTmEpoch'], 14)
self.assertEqual(new_traces[3]['size'], 3)
self.assertEqual(new_traces[3]['times'].tolist(), [12, 13, 14])
self.assertEqual(new_traces[3]['data'].tolist(), [5, 5, 5])