From f4871e846d22abd3e8df1be0c123eb5555362823 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 18 Aug 2021 14:35:19 -0400 Subject: [PATCH 01/65] Better ini parsing --- neuroport_dbs/SweepGUI.py | 182 +++++++++++--------- neuroport_dbs/dbsgui/my_widgets/custom.py | 44 ++++- neuroport_dbs/dbsgui/utilities/pyqtgraph.py | 54 ++++++ neuroport_dbs/resources/config/SweepGUI.ini | 29 ++-- neuroport_dbs/settings/__init__.py | 11 ++ 5 files changed, 220 insertions(+), 100 deletions(-) create mode 100644 neuroport_dbs/dbsgui/utilities/pyqtgraph.py diff --git a/neuroport_dbs/SweepGUI.py b/neuroport_dbs/SweepGUI.py index ad7d0fc..6ccd3c7 100644 --- a/neuroport_dbs/SweepGUI.py +++ b/neuroport_dbs/SweepGUI.py @@ -9,13 +9,12 @@ from cerebuswrapper import CbSdkConnection sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) # Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.settings.defaults import THEMES +from neuroport_dbs.settings import parse_ini_try_numeric +from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, str2qcolor, get_colormap from neuroport_dbs.dbsgui.my_widgets.custom import CustomWidget, get_now_time, CustomGUI - # Import settings # TODO: Make some of these settings configurable via UI elements -from neuroport_dbs.settings.defaults import WINDOWDIMS_SWEEP, WINDOWDIMS_LFP, NPLOTSEGMENTS, XRANGE_SWEEP, uVRANGE, \ - FILTERCONFIG, DSFAC +from neuroport_dbs.settings.defaults import uVRANGE class SweepGUI(CustomGUI): @@ -24,6 +23,10 @@ def __init__(self): super(SweepGUI, self).__init__() self.setWindowTitle('SweepGUI') + @CustomGUI.widget_cls.getter + def widget_cls(self): + return SweepWidget + def restore_from_settings(self): super().restore_from_settings() @@ -32,73 +35,67 @@ def restore_from_settings(self): settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) plot_config = {} - settings.beginGroup("filter") - plot_config['filter'] = { - 'order': settings.value('order', 4), - 'cutoff': settings.value('cutoff', 250), - 'btype': settings.value('btype', 'highpass'), - 'output': settings.value('output', 'sos') - } - settings.endGroup() - # TODO: Setup data filter. - - settings.beginGroup("plot") - plot_config['plot'] = { - 'x_range': settings.value('xrange', 1.05), - 'y_range': settings.value('yrange', 250), - 'labelcolor': settings.value('labelcolor', 'gray'), - 'axiscolor': settings.value('axiscolor', 'gray'), - 'axiswidth': settings.value('axiswidth', 1) - } - colormap = settings.value('colormap') - colors = [] - if colormap == "custom": - settings.beginGroup("colors") + for group_name in ['filter', 'plot']: + plot_config[group_name] = {} + settings.beginGroup(group_name) + for k in settings.allKeys(): + plot_config[group_name][k] = parse_ini_try_numeric(settings, k) + settings.endGroup() + + # theme + settings.beginGroup("theme") + plot_config['theme'] = {} + for k in settings.allKeys(): + if k == 'colormap' or k.lower().startswith('pencolors'): + continue + plot_config['theme'][k] = parse_ini_try_numeric(settings, k) + # theme > pencolors + plot_config['theme']['colormap'] = settings.value('colormap', 'custom') + if plot_config['theme']['colormap'] == "custom": + pencolors = [] + settings.beginGroup("pencolors") for c_id in settings.childGroups(): settings.beginGroup(c_id) cname = settings.value("name", None) if cname is not None: cvalue = QtGui.QColor(cname) else: - cvalue = settings.value("value", QtGui.QColor(255, 255, 255)) - colors.append(cvalue) + cvalue = settings.value("value", "#ffffff") + pencolors.append(cvalue) settings.endGroup() - settings.endGroup() - settings.endGroup() - plot_config['colors'] = {'colormap': colormap, 'colors': colors} - self.update_plot_config(plot_config) - - def on_source_connected(self, data_source): - super().on_source_connected(data_source) - src_dict = self._data_source.data_stats - self.plot_widget = SweepWidget(src_dict) - self.plot_widget.was_closed.connect(self.on_plot_closed) - self.setCentralWidget(self.plot_widget) + settings.endGroup() # pencolors + plot_config['theme']['pencolors'] = pencolors + settings.endGroup() # end theme + self.plot_config = plot_config # Triggers setter --> self.try_reset_widget() def on_plot_closed(self): - if self.plot_widget.awaiting_close: - del self.plot_widget - self.plot_widget = None - if not self.plot_widget: + if self._plot_widget is not None and self._plot_widget.awaiting_close: + del self._plot_widget + self._plot_widget = None + if not self._plot_widget: self._data_source.disconnect_requested() def do_plot_update(self): cont_data = self._data_source.get_continuous_data() if cont_data is not None: cont_chan_ids = [x[0] for x in cont_data] - chart_chan_ids = [x['src'] for x in self.plot_widget.chan_states] + chart_chan_ids = [x['src'] for x in self._plot_widget.chan_states] match_chans = list(set(cont_chan_ids) & set(chart_chan_ids)) for chan_id in match_chans: ch_ix = cont_chan_ids.index(chan_id) data = cont_data[ch_ix][1] - label = self.plot_widget.chan_states[ch_ix]['name'] - self.plot_widget.update(label, data, ch_state=self.plot_widget.chan_states[ch_ix]) + label = self._plot_widget.chan_states[ch_ix]['name'] + self._plot_widget.update(label, data, ch_state=self._plot_widget.chan_states[ch_ix]) class SweepWidget(CustomWidget): UNIT_SCALING = 0.25 # Data are 16-bit integers from -8192 uV to +8192 uV. We want plot scales in uV. def __init__(self, *args, **kwargs): + """ + *args typically contains a data_source dict with entries for 'srate', 'channel_names', 'chan_states' + **kwargs typically contains dicts for configuring the plotter, including 'filter', 'plot', and 'theme'. + """ self._monitor_group = None # QtWidgets.QButtonGroup(parent=self) self.plot_config = {} self.segmented_series = {} # Will contain one array of curves for each line/channel label. @@ -122,13 +119,13 @@ def __init__(self, *args, **kwargs): def keyPressEvent(self, e): valid_keys = [QtCore.Qt.Key_0, QtCore.Qt.Key_1, QtCore.Qt.Key_2, QtCore.Qt.Key_3, QtCore.Qt.Key_4, QtCore.Qt.Key_5, QtCore.Qt.Key_6, QtCore.Qt.Key_7, QtCore.Qt.Key_8, - QtCore.Qt.Key_9][:len(self.group_info) + 1] + QtCore.Qt.Key_9][:len(self.chan_states) + 1] current_button_id = self._monitor_group.checkedId() new_button_id = None if e.key() == QtCore.Qt.Key_Left: - new_button_id = (current_button_id - 1) % (len(self.group_info) + 1) + new_button_id = (current_button_id - 1) % (len(self.chan_states) + 1) elif e.key() == QtCore.Qt.Key_Right: - new_button_id = (current_button_id + 1) % (len(self.group_info) + 1) + new_button_id = (current_button_id + 1) % (len(self.chan_states) + 1) elif e.key() == QtCore.Qt.Key_Space: new_button_id = 0 elif e.key() in valid_keys: @@ -213,18 +210,24 @@ def on_monitor_group_clicked(self, button_id): self.audio['chan_label'] = 'silence' monitor_chan_id = 0 else: - this_label = self.group_info[button_id - 1]['label'] + this_label = self.labels[button_id - 1] self.audio['chan_label'] = this_label - monitor_chan_id = self.group_info[button_id - 1]['chan'] + monitor_chan_id = self.chan_states[button_id - 1]['src'] # Reset plot titles - for gi in self.group_info: - plot_item = self.segmented_series[gi['label']]['plot'] - label_kwargs = {'color': 'y', 'size': '15pt'}\ - if gi['label'] == this_label else {'color': None, 'size': '11pt'} + active_kwargs = {'color': parse_color_str(self.plot_config['theme'].get('labelcolor_active', 'yellow')), + 'size': str(self.plot_config['theme'].get('labelsize_active', 15)) + 'pt'} + inactive_kwargs = {'color': self.plot_config['theme'].get('labelcolor_inactive', None), + 'size': str(self.plot_config['theme'].get('labelsize_inactive', 11)) + 'pt'} + if inactive_kwargs['color'] is not None: + inactive_kwargs['color'] = parse_color_str(inactive_kwargs['color']) + for label in self.labels: + plot_item = self.segmented_series[label]['plot'] + label_kwargs = active_kwargs if label == this_label else inactive_kwargs plot_item.setTitle(title=plot_item.titleLabel.text, **label_kwargs) # Let other processes know we've changed the monitor channel + # TODO: Replace this with a method in the data abstraction layer, arg=self.chan_states[button_id - 1] _cbsdk_conn = CbSdkConnection() if _cbsdk_conn.is_connected: _cbsdk_conn.monitor_chan(monitor_chan_id, spike_only=self.plot_config['spk_aud']) @@ -253,6 +256,8 @@ def on_thresh_line_moved(self, inf_line): ss_info = self.segmented_series[line_label] if ss_info['thresh_line'] == inf_line: new_thresh = int(inf_line.getYPos() / self.UNIT_SCALING) + # TODO: Update through data interface + print("TODO: Update threshold change via data interface") # Let other processes know we've changed the threshold line. cbsdkconn = CbSdkConnection() if cbsdkconn.is_connected: @@ -263,47 +268,56 @@ def on_thresh_line_moved(self, inf_line): def update_config(self, config): print(config) - def create_plots(self, theme='dark', downsample=False, alt_loc=False): + def create_plots(self, plot={}, filter={}, theme={}, alt_loc=False): + # TODO: plot keys: labelcolor # Collect PlotWidget configuration - self.plot_config['downsample'] = downsample - self.plot_config['x_range'] = XRANGE_SWEEP - self.plot_config['y_range'] = uVRANGE self.plot_config['theme'] = theme - self.plot_config['color_iterator'] = -1 - self.plot_config['n_segments'] = NPLOTSEGMENTS + self.plot_config['downsample'] = plot.get('downsample', 100) + self.plot_config['x_range'] = plot.get('x_range', 1.0) + self.plot_config['y_range'] = plot.get('y_range', 250) + self.plot_config['n_segments'] = plot.get('n_segments', 20) self.plot_config['alt_loc'] = alt_loc - if 'do_hp' not in self.plot_config: - self.plot_config['do_hp'] = False + self.plot_config['color_iterator'] = -1 + self.plot_config['do_hp'] = filter.get('order', 4) > 0 + if self.plot_config['do_hp']: + self.plot_config['hp_sos'] = signal.butter(filter.get('order', 4), + 2 * filter.get('cutoff', 250) / self.samplingRate, + btype=filter.get('btype', 'highpass'), + output=filter.get('output', 'sos')) if 'spk_aud' not in self.plot_config: self.plot_config['spk_aud'] = False - self.plot_config['hp_sos'] = signal.butter(FILTERCONFIG['order'], - 2 * FILTERCONFIG['cutoff'] / self.samplingRate, - btype=FILTERCONFIG['type'], - output=FILTERCONFIG['output']) if 'do_ln' not in self.plot_config: self.plot_config['do_ln'] = False self.plot_config['ln_filt'] = None # TODO: comb filter coeffs # Create and add GraphicsLayoutWidget - glw = pg.GraphicsLayoutWidget(parent=self) + glw = pg.GraphicsLayoutWidget(parent=self) # show=False, size=None, title=None, border=None # glw.useOpenGL(True) # Actually seems slower. + if 'bgcolor' in self.plot_config['theme']: + glw.setBackground(parse_color_str(self.plot_config['theme']['bgcolor'])) self.layout().addWidget(glw) + + cmap = self.plot_config['theme']['colormap'] + if cmap != 'custom': + self.plot_config['theme']['pencolors'] = get_colormap(self.plot_config['theme']['colormap'], + len(self.chan_states)) + # Add add a plot with a series of many curve segments for each line. for chan_state in self.chan_states: self.add_series(chan_state) def add_series(self, chan_state): + my_theme = self.plot_config['theme'] + # Plot for this channel glw = self.findChild(pg.GraphicsLayoutWidget) - new_plot = glw.addPlot(row=len(self.segmented_series), col=0, title=chan_state['name'], enableMenu=False) + new_plot = glw.addPlot(row=len(self.segmented_series), col=0, + title=chan_state['name'], enableMenu=False) new_plot.setMouseEnabled(x=False, y=False) - # Appearance settings - my_theme = THEMES[self.plot_config['theme']] + # Prepare plot self.plot_config['color_iterator'] = (self.plot_config['color_iterator'] + 1) % len(my_theme['pencolors']) - pen_color = QtGui.QColor(my_theme['pencolors'][self.plot_config['color_iterator']]) - - # Prepare plot data + pen_color = str2qcolor(my_theme['pencolors'][self.plot_config['color_iterator']]) samples_per_segment = int( np.ceil(self.plot_config['x_range'] * self.samplingRate / self.plot_config['n_segments'])) for ix in range(self.plot_config['n_segments']): @@ -313,13 +327,15 @@ def add_series(self, chan_state): # Last segment might not be full length. seg_x = np.arange(ix * samples_per_segment, int(self.plot_config['x_range'] * self.samplingRate), dtype=np.int16) - if self.plot_config['downsample']: - seg_x = seg_x[::DSFAC] - c = new_plot.plot(parent=new_plot, pen=pen_color) # PlotDataItem + seg_x = seg_x[::self.plot_config['downsample']] + pen = pg.mkPen(pen_color, width=my_theme.get('linewidth', 1)) + c = new_plot.plot(parent=new_plot, pen=pen) # PlotDataItem c.setData(x=seg_x, y=np.zeros_like(seg_x)) # Pre-fill. # Add threshold line - thresh_line = pg.InfiniteLine(angle=0, movable=True, label="{value:.0f}", labelOpts={'position': 0.05}) + thresh_color = str2qcolor(my_theme.get('threshcolor', 'y')) + pen = pg.mkPen(color=thresh_color, width=my_theme.get('threshwidth', 1)) + thresh_line = pg.InfiniteLine(angle=0, pen=pen, movable=True, label="{value:.0f}", labelOpts={'position': 0.05}) thresh_line.sigPositionChangeFinished.connect(self.on_thresh_line_moved) new_plot.addItem(thresh_line) @@ -426,14 +442,14 @@ def update(self, line_label, data, ch_state=None): for pci in ss_info['plot'].dataItems: old_x, old_y = pci.getData() x_lims = [old_x[0], old_x[-1]] - if self.plot_config['downsample']: - x_lims[1] += (DSFAC - 1) + x_lims[1] += (self.plot_config['downsample'] - 1) data_bool = np.logical_and(sample_indices >= x_lims[0], sample_indices <= x_lims[-1]) if np.where(data_bool)[0].size > 0: new_x, new_y = sample_indices[data_bool], data[data_bool] - if self.plot_config['downsample']: - new_x = new_x[::DSFAC] - (new_x[0] % DSFAC) + (old_x[0] % DSFAC) - new_y = new_y[::DSFAC] + if self.plot_config['downsample'] > 1: + _dsfac = self.plot_config['downsample'] + new_x = new_x[::_dsfac] - (new_x[0] % _dsfac) + (old_x[0] % _dsfac) + new_y = new_y[::_dsfac] old_bool = np.in1d(old_x, new_x, assume_unique=True) new_bool = np.in1d(new_x, old_x, assume_unique=True) old_y[old_bool] = new_y[new_bool] diff --git a/neuroport_dbs/dbsgui/my_widgets/custom.py b/neuroport_dbs/dbsgui/my_widgets/custom.py index fd77604..61b987c 100644 --- a/neuroport_dbs/dbsgui/my_widgets/custom.py +++ b/neuroport_dbs/dbsgui/my_widgets/custom.py @@ -42,7 +42,9 @@ def __init__(self, ini_file=None): # Use default ini that ships with module. self._settings_path = Path(__file__).parents[2] / 'resources' / 'config' / ini_path.name - self.plot_widget = None + self._plot_widget = None + self._plot_config = None + self._data_source = None self.restore_from_settings() self.show() @@ -51,6 +53,7 @@ def __del__(self): pass def restore_from_settings(self): + # Should be overridden in child class, but likely calling this super at top of override. settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) # Restore size and position. @@ -74,22 +77,51 @@ def restore_from_settings(self): _data_source = src_cls(scoped_settings=settings, on_connect_cb=self.on_source_connected) settings.endGroup() - # Should continue in child class... - @QtCore.Slot(QtCore.QObject) def on_source_connected(self, data_source): - self._data_source = data_source - # ... continue in child class ... + self.data_source = data_source # Triggers setter --> self.try_reset_widget() + + @property + def widget_cls(self): + return NotImplemented # Child class must override this attribute def update(self): super(CustomGUI, self).update() - if self._data_source.is_connected and self.plot_widget: + if self.data_source.is_connected and self._plot_widget: self.do_plot_update() def do_plot_update(self): # abc.abstractmethod not possible because ABC does not work with Qt-derived classes, so raise error instead. raise NotImplementedError("This method must be overridden by sub-class.") + def try_reset_widget(self): + if self._plot_widget is not None: + # TODO: Close existing self._plot_widget + print("TODO: Close existing self._plot_widget") + if self.plot_config is not None and self.data_source is not None: + src_dict = self.data_source.data_stats + self._plot_widget = self.widget_cls(src_dict, **self.plot_config) + self._plot_widget.was_closed.connect(self.on_plot_closed) + self.setCentralWidget(self._plot_widget) + + @property + def data_source(self): + return self._data_source + + @data_source.setter + def data_source(self, value): + self._data_source = value + self.try_reset_widget() + + @property + def plot_config(self): + return self._plot_config + + @plot_config.setter + def plot_config(self, value): + self._plot_config = value + self.try_reset_widget() + class CustomWidget(QtWidgets.QWidget): """ diff --git a/neuroport_dbs/dbsgui/utilities/pyqtgraph.py b/neuroport_dbs/dbsgui/utilities/pyqtgraph.py new file mode 100644 index 0000000..eb3ad08 --- /dev/null +++ b/neuroport_dbs/dbsgui/utilities/pyqtgraph.py @@ -0,0 +1,54 @@ +import pyqtgraph as pg +import pyqtgraph.colormap +from pyqtgraph.graphicsItems.GradientEditorItem import Gradients +import numpy as np + + +color_sets = list(Gradients.keys()) + pyqtgraph.colormap.listMaps('matplotlib') + + +def parse_color_str(color_str: str): + _col = color_str.replace("'", "") + if len(color_str) > 1: + if color_str == 'black': + _col = 'k' + elif color_str[0] in ['r', 'g', 'b', 'c', 'm', 'y', 'k', 'w']: + _col = color_str[0] + return _col + + +def str2rgb(color_str: str): + from pyqtgraph.functions import mkColor + _col = parse_color_str(color_str) + _col = mkColor(_col).getRgb() + return np.array(_col)[None, :] + + +def str2qcolor(color_str: str): + from qtpy import QtGui + pen_color = color_str + if isinstance(pen_color, str): + pen_color = parse_color_str(pen_color) + if isinstance(pen_color, np.ndarray): + pen_color = pen_color.tolist() + if isinstance(pen_color, list): + pen_color = QtGui.QColor(*pen_color) + else: + pen_color = QtGui.QColor(pen_color) + return pen_color + + +def get_colormap(color_set, n_items): + if color_set == 'random': + colors = np.random.uniform(size=(n_items, 3), low=.5, high=.9) + colors = pg.ColorMap(np.arange(n_items) / max(n_items - 1, 1), (255 * colors).astype(int)) + elif color_set in Gradients: + colors = pg.ColorMap(*zip(*Gradients[color_set]["ticks"])) + elif color_set in pg.colormap.listMaps('matplotlib'): + colors = pg.colormap.get(color_set, source='matplotlib', skipCache=True) + else: + # Solid color + _rgb = str2rgb(color_set) + colors = pg.ColorMap(np.arange(n_items) / max(n_items - 1, 1), np.repeat(_rgb, n_items, axis=0)) + color_map = colors.getLookupTable(nPts=n_items) + return color_map diff --git a/neuroport_dbs/resources/config/SweepGUI.ini b/neuroport_dbs/resources/config/SweepGUI.ini index 61f8817..5045ba9 100644 --- a/neuroport_dbs/resources/config/SweepGUI.ini +++ b/neuroport_dbs/resources/config/SweepGUI.ini @@ -17,19 +17,26 @@ btype=highpass output=sos [plot] -xrange=1.05 -yrange=250 +x_range=1.05 +y_range=250 +downsample=1 +n_segments = 20 + +[theme] bgcolor=black -labelcolor=gray -axiscolor=gray -axiswidth=1 +labelcolor_active=yellow +labelsize_active=15 +labelsize_inactive=11 +threshcolor=yellow +threshwidth=1 +linewidth=1 colormap=custom -colors\0\name=cyan -colors\1\value=@QColor(0, 255, 0) -colors\2\name=magenta -colors\3\name=red -colors\4\name=yellow -colors\5\name=white +pencolors\0\name=cyan +pencolors\1\value=#00ff00 +pencolors\2\name=magenta +pencolors\3\name=red +pencolors\4\name=yellow +pencolors\5\name=white [data-source-LSL-example] class=LSLDataSource diff --git a/neuroport_dbs/settings/__init__.py b/neuroport_dbs/settings/__init__.py index e69de29..4dd9c72 100644 --- a/neuroport_dbs/settings/__init__.py +++ b/neuroport_dbs/settings/__init__.py @@ -0,0 +1,11 @@ +def parse_ini_try_numeric(settings, key): + try: + res = settings.value(key, type=int) + except TypeError: + res = None + if res is None: + try: + res = settings.value(key, type=float) + except TypeError: + res = None + return res if res is not None else settings.value(key) From 3362cf7b3115204ebf6957b12038d8fda05f40b0 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 18 Aug 2021 16:40:48 -0400 Subject: [PATCH 02/65] Fixup auditory state management, esp spike_only --- neuroport_dbs/SweepGUI.py | 53 +++++++++++-------- neuroport_dbs/dbsgui/data_source/cerebus.py | 5 ++ neuroport_dbs/dbsgui/data_source/interface.py | 3 ++ neuroport_dbs/resources/config/SweepGUI.ini | 1 + 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/neuroport_dbs/SweepGUI.py b/neuroport_dbs/SweepGUI.py index 6ccd3c7..2ba774f 100644 --- a/neuroport_dbs/SweepGUI.py +++ b/neuroport_dbs/SweepGUI.py @@ -6,7 +6,6 @@ import qtpy from qtpy import QtCore, QtWidgets, QtGui import pyqtgraph as pg -from cerebuswrapper import CbSdkConnection sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) # Note: If import dbsgui fails, then set the working directory to be this script's directory. from neuroport_dbs.settings import parse_ini_try_numeric @@ -171,25 +170,34 @@ def create_control_panel(self): self._monitor_group.buttonClicked[int].connect(self.on_monitor_group_clicked) # Checkbox for whether the audio out should be spike only spk_aud_checkbox = QtWidgets.QCheckBox("Spike Aud") - spk_aud_checkbox.stateChanged.connect(self.on_spk_aud_changed) + spk_aud_checkbox.setObjectName("spkaud_CheckBox") + spk_aud_checkbox.setTristate(False) spk_aud_checkbox.setChecked(True) + spk_aud_checkbox.stateChanged.connect(self.on_spk_aud_changed) cntrl_layout.addWidget(spk_aud_checkbox) # Checkbox for HP filter filter_checkbox = QtWidgets.QCheckBox("HP") - filter_checkbox.stateChanged.connect(self.on_hp_filter_changed) + filter_checkbox.setObjectName("hpfilt_CheckBox") + filter_checkbox.setTristate(False) filter_checkbox.setChecked(True) + filter_checkbox.stateChanged.connect(self.on_hp_filter_changed) cntrl_layout.addWidget(filter_checkbox) # Checkbox for Comb filter filter_checkbox = QtWidgets.QCheckBox("LN") + filter_checkbox.setObjectName("lnfilt_CheckBox") + filter_checkbox.setTristate(False) + filter_checkbox.setChecked(False) filter_checkbox.setEnabled(False) filter_checkbox.stateChanged.connect(self.on_ln_filter_changed) - filter_checkbox.setChecked(False) cntrl_layout.addWidget(filter_checkbox) # Finish self.layout().addLayout(cntrl_layout) def on_spk_aud_changed(self, state): - self.plot_config['spk_aud'] = state == QtCore.Qt.Checked + current_button_id = self._monitor_group.checkedId() + if current_button_id > 0: + chan_state = self.chan_states[current_button_id - 1] + self.update_monitor_channel(chan_state, state == QtCore.Qt.Checked) def on_hp_filter_changed(self, state): self.plot_config['do_hp'] = state == QtCore.Qt.Checked @@ -208,11 +216,11 @@ def on_monitor_group_clicked(self, button_id): this_label = '' if button_id == 0: self.audio['chan_label'] = 'silence' - monitor_chan_id = 0 + chan_state = {'src': 0} else: this_label = self.labels[button_id - 1] self.audio['chan_label'] = this_label - monitor_chan_id = self.chan_states[button_id - 1]['src'] + chan_state = self.chan_states[button_id - 1] # Reset plot titles active_kwargs = {'color': parse_color_str(self.plot_config['theme'].get('labelcolor_active', 'yellow')), @@ -226,11 +234,12 @@ def on_monitor_group_clicked(self, button_id): label_kwargs = active_kwargs if label == this_label else inactive_kwargs plot_item.setTitle(title=plot_item.titleLabel.text, **label_kwargs) + spk_aud_cb = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") + self.update_monitor_channel(chan_state, spk_aud_cb.checkState() == QtCore.Qt.Checked) + + def update_monitor_channel(self, chan_state, spike_only): # Let other processes know we've changed the monitor channel - # TODO: Replace this with a method in the data abstraction layer, arg=self.chan_states[button_id - 1] - _cbsdk_conn = CbSdkConnection() - if _cbsdk_conn.is_connected: - _cbsdk_conn.monitor_chan(monitor_chan_id, spike_only=self.plot_config['spk_aud']) + self.parent()._data_source.update_monitor(chan_state, spike_only=spike_only) self.update_shared_memory() def update_shared_memory(self): @@ -245,9 +254,10 @@ def update_shared_memory(self): curr_channel = float(chan_labels.index(self.audio['chan_label']) + 1) # 0 == None curr_range = self.plot_config['y_range'] + curr_hp = float(self.plot_config['do_hp']) - to_write = np.array([curr_channel, curr_range, curr_hp], dtype=np.float).tobytes() + to_write = np.array([curr_channel, curr_range, curr_hp], dtype=float).tobytes() self.monitored_shared_mem.data()[-len(to_write):] = memoryview(to_write) self.monitored_shared_mem.unlock() @@ -256,12 +266,8 @@ def on_thresh_line_moved(self, inf_line): ss_info = self.segmented_series[line_label] if ss_info['thresh_line'] == inf_line: new_thresh = int(inf_line.getYPos() / self.UNIT_SCALING) - # TODO: Update through data interface - print("TODO: Update threshold change via data interface") # Let other processes know we've changed the threshold line. - cbsdkconn = CbSdkConnection() - if cbsdkconn.is_connected: - cbsdkconn.set_channel_info(ss_info['chan_id'], {'spkthrlevel': new_thresh}) + self.parent()._data_source.update_threshold(ss_info, new_thresh) # TODO: shared mem? # TODO: If (new required) option is set, also set the other lines. @@ -284,12 +290,17 @@ def create_plots(self, plot={}, filter={}, theme={}, alt_loc=False): 2 * filter.get('cutoff', 250) / self.samplingRate, btype=filter.get('btype', 'highpass'), output=filter.get('output', 'sos')) - if 'spk_aud' not in self.plot_config: - self.plot_config['spk_aud'] = False - if 'do_ln' not in self.plot_config: - self.plot_config['do_ln'] = False + hp_filt_cb = self.findChild(QtWidgets.QCheckBox, name="hpfilt_CheckBox") + hp_filt_cb.setCheckState(2 if self.plot_config['do_hp'] else 0) + + ln_filt_cb = self.findChild(QtWidgets.QCheckBox, name="lnfilt_CheckBox") + self.plot_config['do_ln'] = bool(plot.get('do_ln', True)) + ln_filt_cb.setCheckState(2 if self.plot_config['do_ln'] else 0) self.plot_config['ln_filt'] = None # TODO: comb filter coeffs + spk_aud_cb = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") + spk_aud_cb.setCheckState(2 if bool(plot.get('spk_aud', True)) else 0) + # Create and add GraphicsLayoutWidget glw = pg.GraphicsLayoutWidget(parent=self) # show=False, size=None, title=None, border=None # glw.useOpenGL(True) # Actually seems slower. diff --git a/neuroport_dbs/dbsgui/data_source/cerebus.py b/neuroport_dbs/dbsgui/data_source/cerebus.py index de0f7ca..0d4fcd8 100644 --- a/neuroport_dbs/dbsgui/data_source/cerebus.py +++ b/neuroport_dbs/dbsgui/data_source/cerebus.py @@ -61,3 +61,8 @@ def get_continuous_data(self): def disconnect_requested(self): self._cbsdk_conn.cbsdk_config = {'reset': True, 'get_continuous': False} + + def update_monitor(self, chan_info, spike_only=False): + _cbsdk_conn = CbSdkConnection() + if _cbsdk_conn.is_connected: + _cbsdk_conn.monitor_chan(chan_info['src'], spike_only=spike_only) diff --git a/neuroport_dbs/dbsgui/data_source/interface.py b/neuroport_dbs/dbsgui/data_source/interface.py index eee2eee..eff4db7 100644 --- a/neuroport_dbs/dbsgui/data_source/interface.py +++ b/neuroport_dbs/dbsgui/data_source/interface.py @@ -20,3 +20,6 @@ def disconnect_requested(self): def get_continuous_data(self): raise NotImplementedError("Sub-classes must implement a `get_continuous_data` method.") + + def update_monitor(self, chan_info, spike_only=False): + raise NotImplementedError("Sub-classes must implement a `update_monitor` method.") diff --git a/neuroport_dbs/resources/config/SweepGUI.ini b/neuroport_dbs/resources/config/SweepGUI.ini index 5045ba9..999588d 100644 --- a/neuroport_dbs/resources/config/SweepGUI.ini +++ b/neuroport_dbs/resources/config/SweepGUI.ini @@ -21,6 +21,7 @@ x_range=1.05 y_range=250 downsample=1 n_segments = 20 +spk_aud=true [theme] bgcolor=black From 41ffced1a0a6530f2c447a3da4bfd61bb4616250 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 18 Aug 2021 17:08:13 -0400 Subject: [PATCH 03/65] move threshold change to data_source interface, enable threshold locking --- neuroport_dbs/SweepGUI.py | 38 ++++++++++++------- neuroport_dbs/dbsgui/data_source/cerebus.py | 5 +++ neuroport_dbs/dbsgui/data_source/interface.py | 3 ++ neuroport_dbs/dbsgui/utilities/pyqtgraph.py | 4 +- neuroport_dbs/resources/config/SweepGUI.ini | 1 + 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/neuroport_dbs/SweepGUI.py b/neuroport_dbs/SweepGUI.py index 2ba774f..5e95689 100644 --- a/neuroport_dbs/SweepGUI.py +++ b/neuroport_dbs/SweepGUI.py @@ -9,7 +9,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) # Note: If import dbsgui fails, then set the working directory to be this script's directory. from neuroport_dbs.settings import parse_ini_try_numeric -from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, str2qcolor, get_colormap +from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, make_qcolor, get_colormap from neuroport_dbs.dbsgui.my_widgets.custom import CustomWidget, get_now_time, CustomGUI # Import settings # TODO: Make some of these settings configurable via UI elements @@ -190,9 +190,20 @@ def create_control_panel(self): filter_checkbox.setEnabled(False) filter_checkbox.stateChanged.connect(self.on_ln_filter_changed) cntrl_layout.addWidget(filter_checkbox) + # Checkbox for lock thresholds + threshlock_checkbox = QtWidgets.QCheckBox("Lk Thr") + threshlock_checkbox.setObjectName("lkthr_CheckBox") + threshlock_checkbox.setTristate(False) + threshlock_checkbox.setChecked(False) + cntrl_layout.addWidget(threshlock_checkbox) # Finish self.layout().addLayout(cntrl_layout) + def update_monitor_channel(self, chan_state, spike_only): + # Let other processes know we've changed the monitor channel + self.parent()._data_source.update_monitor(chan_state, spike_only=spike_only) + self.update_shared_memory() + def on_spk_aud_changed(self, state): current_button_id = self._monitor_group.checkedId() if current_button_id > 0: @@ -237,11 +248,6 @@ def on_monitor_group_clicked(self, button_id): spk_aud_cb = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") self.update_monitor_channel(chan_state, spk_aud_cb.checkState() == QtCore.Qt.Checked) - def update_monitor_channel(self, chan_state, spike_only): - # Let other processes know we've changed the monitor channel - self.parent()._data_source.update_monitor(chan_state, spike_only=spike_only) - self.update_shared_memory() - def update_shared_memory(self): # updates only the memory section needed if self.monitored_shared_mem.isAttached(): @@ -262,6 +268,7 @@ def update_shared_memory(self): self.monitored_shared_mem.unlock() def on_thresh_line_moved(self, inf_line): + new_thresh = None for line_label in self.segmented_series: ss_info = self.segmented_series[line_label] if ss_info['thresh_line'] == inf_line: @@ -269,13 +276,15 @@ def on_thresh_line_moved(self, inf_line): # Let other processes know we've changed the threshold line. self.parent()._data_source.update_threshold(ss_info, new_thresh) # TODO: shared mem? - # TODO: If (new required) option is set, also set the other lines. - - def update_config(self, config): - print(config) + lkthr_cb = self.findChild(QtWidgets.QCheckBox, name="lkthr_CheckBox") + if new_thresh is not None and lkthr_cb.checkState() == QtCore.Qt.Checked: + for line_label in self.segmented_series: + ss_info = self.segmented_series[line_label] + if ss_info['thresh_line'] != inf_line: + ss_info['thresh_line'].setPos(new_thresh * self.UNIT_SCALING) + self.parent()._data_source.update_threshold(ss_info, new_thresh) def create_plots(self, plot={}, filter={}, theme={}, alt_loc=False): - # TODO: plot keys: labelcolor # Collect PlotWidget configuration self.plot_config['theme'] = theme self.plot_config['downsample'] = plot.get('downsample', 100) @@ -301,6 +310,9 @@ def create_plots(self, plot={}, filter={}, theme={}, alt_loc=False): spk_aud_cb = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") spk_aud_cb.setCheckState(2 if bool(plot.get('spk_aud', True)) else 0) + thrlk_cb = self.findChild(QtWidgets.QCheckBox, name="lkthr_CheckBox") + thrlk_cb.setCheckState(2 if bool(plot.get('lock_threshold', False)) else 0) + # Create and add GraphicsLayoutWidget glw = pg.GraphicsLayoutWidget(parent=self) # show=False, size=None, title=None, border=None # glw.useOpenGL(True) # Actually seems slower. @@ -328,7 +340,7 @@ def add_series(self, chan_state): # Prepare plot self.plot_config['color_iterator'] = (self.plot_config['color_iterator'] + 1) % len(my_theme['pencolors']) - pen_color = str2qcolor(my_theme['pencolors'][self.plot_config['color_iterator']]) + pen_color = make_qcolor(my_theme['pencolors'][self.plot_config['color_iterator']]) samples_per_segment = int( np.ceil(self.plot_config['x_range'] * self.samplingRate / self.plot_config['n_segments'])) for ix in range(self.plot_config['n_segments']): @@ -344,7 +356,7 @@ def add_series(self, chan_state): c.setData(x=seg_x, y=np.zeros_like(seg_x)) # Pre-fill. # Add threshold line - thresh_color = str2qcolor(my_theme.get('threshcolor', 'y')) + thresh_color = make_qcolor(my_theme.get('threshcolor', 'yellow')) pen = pg.mkPen(color=thresh_color, width=my_theme.get('threshwidth', 1)) thresh_line = pg.InfiniteLine(angle=0, pen=pen, movable=True, label="{value:.0f}", labelOpts={'position': 0.05}) thresh_line.sigPositionChangeFinished.connect(self.on_thresh_line_moved) diff --git a/neuroport_dbs/dbsgui/data_source/cerebus.py b/neuroport_dbs/dbsgui/data_source/cerebus.py index 0d4fcd8..7c47b7a 100644 --- a/neuroport_dbs/dbsgui/data_source/cerebus.py +++ b/neuroport_dbs/dbsgui/data_source/cerebus.py @@ -66,3 +66,8 @@ def update_monitor(self, chan_info, spike_only=False): _cbsdk_conn = CbSdkConnection() if _cbsdk_conn.is_connected: _cbsdk_conn.monitor_chan(chan_info['src'], spike_only=spike_only) + + def update_threshold(self, chan_info, new_value): + cbsdkconn = CbSdkConnection() + if cbsdkconn.is_connected: + cbsdkconn.set_channel_info(chan_info['chan_id'], {'spkthrlevel': new_value}) \ No newline at end of file diff --git a/neuroport_dbs/dbsgui/data_source/interface.py b/neuroport_dbs/dbsgui/data_source/interface.py index eff4db7..d56b7f5 100644 --- a/neuroport_dbs/dbsgui/data_source/interface.py +++ b/neuroport_dbs/dbsgui/data_source/interface.py @@ -23,3 +23,6 @@ def get_continuous_data(self): def update_monitor(self, chan_info, spike_only=False): raise NotImplementedError("Sub-classes must implement a `update_monitor` method.") + + def update_threshold(self, chan_info, new_value): + raise NotImplementedError("Sub-classes must implement a `update_threshold` method.") diff --git a/neuroport_dbs/dbsgui/utilities/pyqtgraph.py b/neuroport_dbs/dbsgui/utilities/pyqtgraph.py index eb3ad08..89955f6 100644 --- a/neuroport_dbs/dbsgui/utilities/pyqtgraph.py +++ b/neuroport_dbs/dbsgui/utilities/pyqtgraph.py @@ -24,11 +24,9 @@ def str2rgb(color_str: str): return np.array(_col)[None, :] -def str2qcolor(color_str: str): +def make_qcolor(color_str: str): from qtpy import QtGui pen_color = color_str - if isinstance(pen_color, str): - pen_color = parse_color_str(pen_color) if isinstance(pen_color, np.ndarray): pen_color = pen_color.tolist() if isinstance(pen_color, list): diff --git a/neuroport_dbs/resources/config/SweepGUI.ini b/neuroport_dbs/resources/config/SweepGUI.ini index 999588d..ca8ca2f 100644 --- a/neuroport_dbs/resources/config/SweepGUI.ini +++ b/neuroport_dbs/resources/config/SweepGUI.ini @@ -22,6 +22,7 @@ y_range=250 downsample=1 n_segments = 20 spk_aud=true +lock_threshold=true [theme] bgcolor=black From f1c9ac8234936e331c01fdf9112a96f9ad234a9f Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 19 Aug 2021 09:27:42 -0400 Subject: [PATCH 04/65] Move more ini parsing into super --- neuroport_dbs/SweepGUI.py | 41 ----------------------- neuroport_dbs/dbsgui/my_widgets/custom.py | 40 ++++++++++++++++++++-- 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/neuroport_dbs/SweepGUI.py b/neuroport_dbs/SweepGUI.py index 5e95689..6908b89 100644 --- a/neuroport_dbs/SweepGUI.py +++ b/neuroport_dbs/SweepGUI.py @@ -26,47 +26,6 @@ def __init__(self): def widget_cls(self): return SweepWidget - def restore_from_settings(self): - super().restore_from_settings() - - # Continue parsing the ini file. Note that the connection might have already been made, but might not. - # The below settings have to be flexible to creating the visualization immediately or later. - settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) - - plot_config = {} - for group_name in ['filter', 'plot']: - plot_config[group_name] = {} - settings.beginGroup(group_name) - for k in settings.allKeys(): - plot_config[group_name][k] = parse_ini_try_numeric(settings, k) - settings.endGroup() - - # theme - settings.beginGroup("theme") - plot_config['theme'] = {} - for k in settings.allKeys(): - if k == 'colormap' or k.lower().startswith('pencolors'): - continue - plot_config['theme'][k] = parse_ini_try_numeric(settings, k) - # theme > pencolors - plot_config['theme']['colormap'] = settings.value('colormap', 'custom') - if plot_config['theme']['colormap'] == "custom": - pencolors = [] - settings.beginGroup("pencolors") - for c_id in settings.childGroups(): - settings.beginGroup(c_id) - cname = settings.value("name", None) - if cname is not None: - cvalue = QtGui.QColor(cname) - else: - cvalue = settings.value("value", "#ffffff") - pencolors.append(cvalue) - settings.endGroup() - settings.endGroup() # pencolors - plot_config['theme']['pencolors'] = pencolors - settings.endGroup() # end theme - self.plot_config = plot_config # Triggers setter --> self.try_reset_widget() - def on_plot_closed(self): if self._plot_widget is not None and self._plot_widget.awaiting_close: del self._plot_widget diff --git a/neuroport_dbs/dbsgui/my_widgets/custom.py b/neuroport_dbs/dbsgui/my_widgets/custom.py index 61b987c..ac33047 100644 --- a/neuroport_dbs/dbsgui/my_widgets/custom.py +++ b/neuroport_dbs/dbsgui/my_widgets/custom.py @@ -1,12 +1,12 @@ import time from pathlib import Path -from qtpy import QtWidgets, QtCore +from qtpy import QtWidgets, QtCore, QtGui from cerebuswrapper import CbSdkConnection # Import settings import neuroport_dbs import neuroport_dbs.dbsgui.data_source -from neuroport_dbs.settings import defaults +from neuroport_dbs.settings import defaults, parse_ini_try_numeric def get_now_time(): @@ -77,6 +77,42 @@ def restore_from_settings(self): _data_source = src_cls(scoped_settings=settings, on_connect_cb=self.on_source_connected) settings.endGroup() + plot_config = {} + for group_name in settings.childGroups(): + if group_name.lower() in ['mainwindow', 'theme'] or group_name.lower().startswith('data-source'): + continue + plot_config[group_name] = {} + settings.beginGroup(group_name) + for k in settings.allKeys(): + plot_config[group_name][k] = parse_ini_try_numeric(settings, k) + settings.endGroup() + + # theme + settings.beginGroup("theme") + plot_config['theme'] = {} + for k in settings.allKeys(): + if k == 'colormap' or k.lower().startswith('pencolors'): + continue + plot_config['theme'][k] = parse_ini_try_numeric(settings, k) + # theme > pencolors + plot_config['theme']['colormap'] = settings.value('colormap', 'custom') + if plot_config['theme']['colormap'] == "custom": + pencolors = [] + settings.beginGroup("pencolors") + for c_id in settings.childGroups(): + settings.beginGroup(c_id) + cname = settings.value("name", None) + if cname is not None: + cvalue = QtGui.QColor(cname) + else: + cvalue = settings.value("value", "#ffffff") + pencolors.append(cvalue) + settings.endGroup() + settings.endGroup() # pencolors + plot_config['theme']['pencolors'] = pencolors + settings.endGroup() # end theme + self.plot_config = plot_config # Triggers setter --> self.try_reset_widget() + @QtCore.Slot(QtCore.QObject) def on_source_connected(self, data_source): self.data_source = data_source # Triggers setter --> self.try_reset_widget() From 3f11d173b50327fc2b20bb297d57b66949713d17 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 19 Aug 2021 09:58:05 -0400 Subject: [PATCH 05/65] CerebusDataSource - Get more parameters from ini --- neuroport_dbs/dbsgui/data_source/cerebus.py | 23 +++++++++++++++++---- neuroport_dbs/resources/config/SweepGUI.ini | 4 ++++ neuroport_dbs/settings/__init__.py | 9 +++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/neuroport_dbs/dbsgui/data_source/cerebus.py b/neuroport_dbs/dbsgui/data_source/cerebus.py index 7c47b7a..30622aa 100644 --- a/neuroport_dbs/dbsgui/data_source/cerebus.py +++ b/neuroport_dbs/dbsgui/data_source/cerebus.py @@ -2,8 +2,11 @@ from qtpy import QtCore import numpy as np from .interface import IDataSource -from neuroport_dbs.settings.defaults import SAMPLINGGROUPS from cerebuswrapper import CbSdkConnection +from neuroport_dbs.settings import parse_ini_try_numeric + + +SAMPLINGGROUPS = ["0", "500", "1000", "2000", "10000", "30000"] # , "RAW"] RAW broken in cbsdk class CerebusDataSource(IDataSource): @@ -13,13 +16,25 @@ def __init__(self, scoped_settings: QtCore.QSettings, **kwargs): self._cbsdk_conn = CbSdkConnection() conn_params = self._cbsdk_conn.con_params.copy() - sampling_group = scoped_settings.value("sampling_group") for key, orig_value in conn_params.items(): conn_params[key] = scoped_settings.value(key, orig_value) self._cbsdk_conn.con_params = conn_params self._cbsdk_conn.connect() - self._cbsdk_conn.cbsdk_config = {'reset': True, 'get_continuous': True} - self._group_ix = SAMPLINGGROUPS.index(sampling_group) + conn_config = {} + for key in scoped_settings.allKeys(): + if key in ['class', 'sampling_group']: + continue + split_key = key.split('/') + if len(split_key) > 1: + if split_key[0] not in conn_config: + conn_config[split_key[0]] = {} + conn_config[split_key[0]][split_key[1]] = parse_ini_try_numeric(scoped_settings, key) + else: + conn_config[key] = parse_ini_try_numeric(scoped_settings, key) + + # get_events, get_comments, get_continuous, buffer_parameter: comment_length + self._cbsdk_conn.cbsdk_config = conn_config + self._group_ix = SAMPLINGGROUPS.index(scoped_settings.value("sampling_group")) self._group_info = self._decode_group_info(self._cbsdk_conn.get_group_config(self._group_ix)) self._on_connect_cb(self) diff --git a/neuroport_dbs/resources/config/SweepGUI.ini b/neuroport_dbs/resources/config/SweepGUI.ini index ca8ca2f..d7a2a73 100644 --- a/neuroport_dbs/resources/config/SweepGUI.ini +++ b/neuroport_dbs/resources/config/SweepGUI.ini @@ -9,6 +9,9 @@ bg_color=#404040 [data-source] class=CerebusDataSource sampling_group=30000 +get_continuous=true +get_events=false +get_comments=false [filter] order=4 @@ -23,6 +26,7 @@ downsample=1 n_segments = 20 spk_aud=true lock_threshold=true +unit_scaling=0.25 [theme] bgcolor=black diff --git a/neuroport_dbs/settings/__init__.py b/neuroport_dbs/settings/__init__.py index 4dd9c72..e0aa51f 100644 --- a/neuroport_dbs/settings/__init__.py +++ b/neuroport_dbs/settings/__init__.py @@ -8,4 +8,11 @@ def parse_ini_try_numeric(settings, key): res = settings.value(key, type=float) except TypeError: res = None - return res if res is not None else settings.value(key) + if res is not None: + return res + res = settings.value(key) + if res == 'false': + return False + elif res == 'true': + return True + return res From f6e1da73cccd20dfaef953d4952f9477db97956e Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 19 Aug 2021 10:54:22 -0400 Subject: [PATCH 06/65] unit_scaling from ini, other cleanup --- neuroport_dbs/SweepGUI.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/neuroport_dbs/SweepGUI.py b/neuroport_dbs/SweepGUI.py index 6908b89..a66f7f6 100644 --- a/neuroport_dbs/SweepGUI.py +++ b/neuroport_dbs/SweepGUI.py @@ -47,7 +47,6 @@ def do_plot_update(self): class SweepWidget(CustomWidget): - UNIT_SCALING = 0.25 # Data are 16-bit integers from -8192 uV to +8192 uV. We want plot scales in uV. def __init__(self, *args, **kwargs): """ @@ -61,7 +60,7 @@ def __init__(self, *args, **kwargs): # - Used by features/depth self.monitored_shared_mem = QtCore.QSharedMemory() - super(SweepWidget, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.refresh_axes() # Even though super __init__ calls this, extra refresh is intentional self.pya_manager = pyaudio.PyAudio() self.pya_stream = None @@ -100,7 +99,7 @@ def closeEvent(self, evnt): self.pya_stream.stop_stream() self.pya_stream.close() self.pya_manager.terminate() - super(SweepWidget, self).closeEvent(evnt) + super().closeEvent(evnt) def create_control_panel(self): # Create control panel @@ -231,7 +230,7 @@ def on_thresh_line_moved(self, inf_line): for line_label in self.segmented_series: ss_info = self.segmented_series[line_label] if ss_info['thresh_line'] == inf_line: - new_thresh = int(inf_line.getYPos() / self.UNIT_SCALING) + new_thresh = int(inf_line.getYPos() / self.plot_config['unit_scaling']) # Let other processes know we've changed the threshold line. self.parent()._data_source.update_threshold(ss_info, new_thresh) # TODO: shared mem? @@ -240,7 +239,7 @@ def on_thresh_line_moved(self, inf_line): for line_label in self.segmented_series: ss_info = self.segmented_series[line_label] if ss_info['thresh_line'] != inf_line: - ss_info['thresh_line'].setPos(new_thresh * self.UNIT_SCALING) + ss_info['thresh_line'].setPos(new_thresh * self.plot_config['unit_scaling']) self.parent()._data_source.update_threshold(ss_info, new_thresh) def create_plots(self, plot={}, filter={}, theme={}, alt_loc=False): @@ -250,6 +249,8 @@ def create_plots(self, plot={}, filter={}, theme={}, alt_loc=False): self.plot_config['x_range'] = plot.get('x_range', 1.0) self.plot_config['y_range'] = plot.get('y_range', 250) self.plot_config['n_segments'] = plot.get('n_segments', 20) + self.plot_config['unit_scaling'] = plot.get('unit_scaling', 0.25) + # Default value of 0.25 -- Data are 16-bit integers from -8192 uV to +8192 uV. We want plot scales in uV. self.plot_config['alt_loc'] = alt_loc self.plot_config['color_iterator'] = -1 self.plot_config['do_hp'] = filter.get('order', 4) > 0 @@ -300,6 +301,7 @@ def add_series(self, chan_state): # Prepare plot self.plot_config['color_iterator'] = (self.plot_config['color_iterator'] + 1) % len(my_theme['pencolors']) pen_color = make_qcolor(my_theme['pencolors'][self.plot_config['color_iterator']]) + pen = pg.mkPen(pen_color, width=my_theme.get('linewidth', 1)) samples_per_segment = int( np.ceil(self.plot_config['x_range'] * self.samplingRate / self.plot_config['n_segments'])) for ix in range(self.plot_config['n_segments']): @@ -310,7 +312,6 @@ def add_series(self, chan_state): seg_x = np.arange(ix * samples_per_segment, int(self.plot_config['x_range'] * self.samplingRate), dtype=np.int16) seg_x = seg_x[::self.plot_config['downsample']] - pen = pg.mkPen(pen_color, width=my_theme.get('linewidth', 1)) c = new_plot.plot(parent=new_plot, pen=pen) # PlotDataItem c.setData(x=seg_x, y=np.zeros_like(seg_x)) # Pre-fill. @@ -352,7 +353,7 @@ def refresh_axes(self): pci.setData(x=old_x, y=np.zeros_like(old_x)) ss_info['last_sample_ix'] = last_sample_ix - gain = chan_state['gain'] if 'gain' in chan_state else self.UNIT_SCALING + gain = chan_state['gain'] if 'gain' in chan_state else self.plot_config['unit_scaling'] if 'spkthrlevel' in chan_state: ss_info['thresh_line'].setValue(chan_state['spkthrlevel'] * gain) @@ -395,7 +396,7 @@ def update(self, line_label, data, ch_state=None): """ ss_info = self.segmented_series[line_label] n_in = data.shape[0] - gain = ch_state['gain'] if ch_state is not None and 'gain' in ch_state else self.UNIT_SCALING + gain = ch_state['gain'] if ch_state is not None and 'gain' in ch_state else self.plot_config['unit_scaling'] data = data * gain if self.plot_config['do_hp']: data, ss_info['hp_zi'] = signal.sosfilt(self.plot_config['hp_sos'], data, zi=ss_info['hp_zi']) From 6e79c6e33a35e9eecef5ea28403dbcb13f658cc6 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 19 Aug 2021 11:13:56 -0400 Subject: [PATCH 07/65] range text from ini --- neuroport_dbs/SweepGUI.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/neuroport_dbs/SweepGUI.py b/neuroport_dbs/SweepGUI.py index a66f7f6..0121740 100644 --- a/neuroport_dbs/SweepGUI.py +++ b/neuroport_dbs/SweepGUI.py @@ -8,12 +8,8 @@ import pyqtgraph as pg sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) # Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.settings import parse_ini_try_numeric from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, make_qcolor, get_colormap from neuroport_dbs.dbsgui.my_widgets.custom import CustomWidget, get_now_time, CustomGUI -# Import settings -# TODO: Make some of these settings configurable via UI elements -from neuroport_dbs.settings.defaults import uVRANGE class SweepGUI(CustomGUI): @@ -106,7 +102,7 @@ def create_control_panel(self): # +/- range cntrl_layout = QtWidgets.QHBoxLayout() cntrl_layout.addWidget(QtWidgets.QLabel("+/- ")) - self.range_edit = QtWidgets.QLineEdit("{:.2f}".format(uVRANGE)) + self.range_edit = QtWidgets.QLineEdit("{:.2f}".format(250)) # Value overridden in create_plots self.range_edit.editingFinished.connect(self.on_range_edit_editingFinished) self.range_edit.setMinimumHeight(23) self.range_edit.setMaximumWidth(80) @@ -147,6 +143,7 @@ def create_control_panel(self): filter_checkbox.setChecked(False) filter_checkbox.setEnabled(False) filter_checkbox.stateChanged.connect(self.on_ln_filter_changed) + filter_checkbox.setVisible(False) cntrl_layout.addWidget(filter_checkbox) # Checkbox for lock thresholds threshlock_checkbox = QtWidgets.QCheckBox("Lk Thr") @@ -245,12 +242,16 @@ def on_thresh_line_moved(self, inf_line): def create_plots(self, plot={}, filter={}, theme={}, alt_loc=False): # Collect PlotWidget configuration self.plot_config['theme'] = theme - self.plot_config['downsample'] = plot.get('downsample', 100) + self.plot_config['downsample'] = plot.get('downsample', 1) self.plot_config['x_range'] = plot.get('x_range', 1.0) self.plot_config['y_range'] = plot.get('y_range', 250) + # Update the range_edit text, but block its signal when doing so + prev_state = self.range_edit.blockSignals(True) + self.range_edit.setText(str(self.plot_config['y_range'])) + self.range_edit.blockSignals(prev_state) self.plot_config['n_segments'] = plot.get('n_segments', 20) + # Default scaling of 0.25 -- Data are 16-bit integers from -8192 uV to +8192 uV. We want plot scales in uV. self.plot_config['unit_scaling'] = plot.get('unit_scaling', 0.25) - # Default value of 0.25 -- Data are 16-bit integers from -8192 uV to +8192 uV. We want plot scales in uV. self.plot_config['alt_loc'] = alt_loc self.plot_config['color_iterator'] = -1 self.plot_config['do_hp'] = filter.get('order', 4) > 0 From a387e24bb18fc48a7002f3bbda8dacdc0f12f88a Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 19 Aug 2021 11:14:34 -0400 Subject: [PATCH 08/65] RasterGUI data abstraction and ini loading --- neuroport_dbs/RasterGUI.py | 111 ++++++++---------- neuroport_dbs/dbsgui/data_source/cerebus.py | 9 +- neuroport_dbs/dbsgui/data_source/interface.py | 6 + neuroport_dbs/resources/config/RasterGUI.ini | 31 +++++ 4 files changed, 95 insertions(+), 62 deletions(-) create mode 100644 neuroport_dbs/resources/config/RasterGUI.ini diff --git a/neuroport_dbs/RasterGUI.py b/neuroport_dbs/RasterGUI.py index 01eb413..8dc739a 100644 --- a/neuroport_dbs/RasterGUI.py +++ b/neuroport_dbs/RasterGUI.py @@ -4,19 +4,12 @@ import sys import os import numpy as np -import qtpy -from qtpy.QtGui import QColor, QFont -from qtpy.QtWidgets import QApplication -from qtpy.QtCore import Qt, QTimer, Signal +from qtpy import QtCore, QtGui, QtWidgets import pyqtgraph as pg sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) # Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget, ConnectDialog, get_now_time - -# Import settings -# TODO: Make some of these settings configurable via UI elements -from neuroport_dbs.settings.defaults import WINDOWDIMS_RASTER, XRANGE_RASTER, YRANGE_RASTER, SIMOK, \ - LABEL_FONT_POINT_SIZE, SAMPLINGRATE, SAMPLINGGROUPS, THEMES +from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, get_colormap +from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget, get_now_time class RasterGUI(CustomGUI): @@ -25,96 +18,92 @@ def __init__(self): super(RasterGUI, self).__init__() self.setWindowTitle('RasterGUI') - def on_connection_established(self): - self.cbsdk_conn.cbsdk_config = { - 'reset': True, 'get_events': True, 'get_comments': True, - 'buffer_parameter': { - 'comment_length': 10 - } - } - # TODO: Or RAW, never both - group_info = self.cbsdk_conn.get_group_config(SAMPLINGGROUPS.index(str(SAMPLINGRATE))) - for gi_item in group_info: - gi_item['label'] = gi_item['label'].decode('utf-8') - gi_item['unit'] = gi_item['unit'].decode('utf-8') - self.plot_widget = RasterWidget(group_info) - self.plot_widget.was_closed.connect(self.on_plot_closed) + @CustomGUI.widget_cls.getter + def widget_cls(self): + return RasterWidget def on_plot_closed(self): - self.plot_widget = None - self.cbsdk_conn.cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} + self._plot_widget = None + self._data_source.disconnect_requested() def do_plot_update(self): - ev_timestamps = self.cbsdk_conn.get_event_data() + ev_timestamps = self._data_source.get_event_data() ev_chan_ids = [x[0] for x in ev_timestamps] - for chan_label in self.plot_widget.rasters: - ri = self.plot_widget.rasters[chan_label] + for chan_label in self._plot_widget.rasters: + ri = self._plot_widget.rasters[chan_label] if ri['chan_id'] in ev_chan_ids: data = ev_timestamps[ev_chan_ids.index(ri['chan_id'])][1]['timestamps'] else: data = [[], ] - self.plot_widget.update(chan_label, data) + self._plot_widget.update(chan_label, data) - # Fetching comments is slow! - comments = self.cbsdk_conn.get_comments() + comments = self._data_source.get_comments() if comments: - self.plot_widget.parse_comments(comments) + self._plot_widget.parse_comments(comments) class RasterWidget(CustomWidget): - frate_changed = Signal(str, float) + frate_changed = QtCore.Signal(str, float) def __init__(self, *args, **kwargs): - super(RasterWidget, self).__init__(*args, **kwargs) - self.move(WINDOWDIMS_RASTER[0], WINDOWDIMS_RASTER[1]) - self.resize(WINDOWDIMS_RASTER[2], WINDOWDIMS_RASTER[3]) self.DTT = None + self.plot_config = {} + super().__init__(*args, **kwargs) + + def create_plots(self, plot={}, theme={}): + self.plot_config['theme'] = theme + self.plot_config['x_range'] = plot.get('x_range', 0.5) + self.plot_config['y_range'] = plot.get('y_range', 8) + self.plot_config['color_iterator'] = -1 - def create_plots(self, theme='dark', **kwargs): - # Collect PlotWidget configuration - self.plot_config = { - 'x_range': XRANGE_RASTER, - 'y_range': YRANGE_RASTER, - 'theme': theme, - 'color_iterator': -1 - } # Create and add GraphicsLayoutWidget glw = pg.GraphicsLayoutWidget(parent=self) # glw.useOpenGL(True) + if 'bgcolor' in self.plot_config['theme']: + glw.setBackground(parse_color_str(self.plot_config['theme']['bgcolor'])) self.layout().addWidget(glw) + + cmap = self.plot_config['theme']['colormap'] + if cmap != 'custom': + self.plot_config['theme']['pencolors'] = get_colormap(self.plot_config['theme']['colormap'], + len(self.chan_states)) + self.rasters = {} # Will contain one dictionary for each line/channel label. - for chan_ix in range(len(self.group_info)): - self.add_series(self.group_info[chan_ix]) + for chan_state in self.chan_states: + self.add_series(chan_state) - def add_series(self, chan_info): + def add_series(self, chan_state): + my_theme = self.plot_config['theme'] glw = self.findChild(pg.GraphicsLayoutWidget) - new_plot = glw.addPlot(row=len(self.rasters), col=0) + new_plot = glw.addPlot(row=len(self.rasters), col=0, enableMenu=False) + new_plot.setMouseEnabled(x=False, y=False) + # Appearance settings - my_theme = THEMES[self.plot_config['theme']] self.plot_config['color_iterator'] = (self.plot_config['color_iterator'] + 1) % len(my_theme['pencolors']) - pen_color = QColor(my_theme['pencolors'][self.plot_config['color_iterator']]) + pen_color = QtGui.QColor(my_theme['pencolors'][self.plot_config['color_iterator']]) + pen = pg.mkPen(pen_color, width=my_theme.get('linewidth', 1)) # Create PlotCurveItem for latest spikes (bottom row) and slower-updating old spikes (upper rows) pcis = [] for pci_ix in range(2): pci = pg.PlotCurveItem(parent=new_plot, connect='pairs') - pci.setPen(pen_color) + pci.setPen(pen) new_plot.addItem(pci) pcis.append(pci) # Create text for displaying firing rate. Placeholder text is channel label. - frate_annotation = pg.TextItem(text=chan_info['label'], + frate_annotation = pg.TextItem(text=chan_state['name'], color=(255, 255, 255)) frate_annotation.setPos(0, self.plot_config['y_range']) - my_font = QFont() - my_font.setPointSize(24) + my_font = QtGui.QFont() + my_font.setPointSize(my_theme.get('frate_size', 24)) frate_annotation.setFont(my_font) new_plot.addItem(frate_annotation) # Store information - self.rasters[chan_info['label']] = { + self.rasters[chan_state['name']] = { 'plot': new_plot, 'old': pcis[0], 'latest': pcis[1], 'line_ix': len(self.rasters), - 'chan_id': chan_info['chan'], + 'chan_id': chan_state['src'], 'frate_item': frate_annotation } self.clear() @@ -233,14 +222,14 @@ def update(self, line_label, data): def main(): - _ = QApplication(sys.argv) + _ = QtWidgets.QApplication(sys.argv) aw = RasterGUI() - timer = QTimer() + timer = QtCore.QTimer() timer.timeout.connect(aw.update) timer.start(1) - if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): - QApplication.instance().exec_() + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() if __name__ == '__main__': diff --git a/neuroport_dbs/dbsgui/data_source/cerebus.py b/neuroport_dbs/dbsgui/data_source/cerebus.py index 30622aa..decd631 100644 --- a/neuroport_dbs/dbsgui/data_source/cerebus.py +++ b/neuroport_dbs/dbsgui/data_source/cerebus.py @@ -74,8 +74,15 @@ def is_connected(self): def get_continuous_data(self): return self._cbsdk_conn.get_continuous_data() + def get_event_data(self): + return self._cbsdk_conn.get_event_data() + + def get_comments(self): + return self._cbsdk_conn.get_comments() + def disconnect_requested(self): - self._cbsdk_conn.cbsdk_config = {'reset': True, 'get_continuous': False} + self._cbsdk_conn.cbsdk_config = {'reset': True, 'get_continuous': False, + 'get_events': False, 'get_comments': False} def update_monitor(self, chan_info, spike_only=False): _cbsdk_conn = CbSdkConnection() diff --git a/neuroport_dbs/dbsgui/data_source/interface.py b/neuroport_dbs/dbsgui/data_source/interface.py index d56b7f5..94cb4db 100644 --- a/neuroport_dbs/dbsgui/data_source/interface.py +++ b/neuroport_dbs/dbsgui/data_source/interface.py @@ -21,6 +21,12 @@ def disconnect_requested(self): def get_continuous_data(self): raise NotImplementedError("Sub-classes must implement a `get_continuous_data` method.") + def get_event_data(self): + raise NotImplementedError("Sub-classes must implement a `get_event_data` method.") + + def get_comments(self): + raise NotImplementedError("Sub-classes must implement a `get_comments` method.") + def update_monitor(self, chan_info, spike_only=False): raise NotImplementedError("Sub-classes must implement a `update_monitor` method.") diff --git a/neuroport_dbs/resources/config/RasterGUI.ini b/neuroport_dbs/resources/config/RasterGUI.ini new file mode 100644 index 0000000..6079e00 --- /dev/null +++ b/neuroport_dbs/resources/config/RasterGUI.ini @@ -0,0 +1,31 @@ +[MainWindow] +fullScreen=false +maximized=false +frameless=true +size=@Size(300 1080) +pos=@Point(620 0) +bg_color=#404040 + +[data-source] +class=CerebusDataSource +sampling_group=30000 +get_continuous=false +get_events=true +get_comments=true +buffer_parameter\comment_length=10 + +[plot] +x_range=0.5 +y_range=8 + +[theme] +bgcolor=black +frate_size=24 +linewidth=1 +colormap=custom +pencolors\0\name=cyan +pencolors\1\value=#00ff00 +pencolors\2\name=magenta +pencolors\3\name=red +pencolors\4\name=yellow +pencolors\5\name=white From f99f642282ea67a4a8bae998e77e70eb8563fee8 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 19 Aug 2021 12:59:30 -0400 Subject: [PATCH 09/65] WaveformGUI - Data abstraction and ini settings loading. --- neuroport_dbs/WaveformGUI.py | 137 ++++++++---------- neuroport_dbs/dbsgui/data_source/cerebus.py | 6 +- neuroport_dbs/dbsgui/utilities/pyqtgraph.py | 4 +- .../resources/config/WaveformGUI.ini | 29 ++++ 4 files changed, 99 insertions(+), 77 deletions(-) create mode 100644 neuroport_dbs/resources/config/WaveformGUI.ini diff --git a/neuroport_dbs/WaveformGUI.py b/neuroport_dbs/WaveformGUI.py index 98771f9..f27797f 100644 --- a/neuroport_dbs/WaveformGUI.py +++ b/neuroport_dbs/WaveformGUI.py @@ -4,73 +4,53 @@ import sys import os import numpy as np -import qtpy.QtCore -from qtpy.QtGui import QColor -from qtpy.QtWidgets import QPushButton, QLineEdit, QHBoxLayout, QLabel +from qtpy import QtCore, QtWidgets import pyqtgraph as pg -# Import settings -# TODO: Make some of these settings configurable via UI elements -from neuroport_dbs.settings.defaults import WINDOWDIMS_WAVEFORMS, XRANGE_WAVEFORMS, uVRANGE, NWAVEFORMS, SIMOK, \ - WF_COLORS, SAMPLINGGROUPS, SAMPLINGRATE, THEMES - sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) # Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget, ConnectDialog +from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, make_qcolor, get_colormap +from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget class WaveformGUI(CustomGUI): def __init__(self): - super(WaveformGUI, self).__init__() + super().__init__() self.setWindowTitle('WaveformGUI') - def on_connection_established(self): - self.cbsdk_conn.cbsdk_config = { - 'reset': True, 'get_events': False, 'get_comments': True, - 'buffer_parameter': { - 'comment_length': 10 - } - } - group_info = self.cbsdk_conn.get_group_config(SAMPLINGGROUPS.index(str(SAMPLINGRATE))) - for gi_item in group_info: - gi_item['label'] = gi_item['label'].decode('utf-8') - gi_item['unit'] = gi_item['unit'].decode('utf-8') - self.plot_widget = WaveformWidget(group_info) - self.plot_widget.was_closed.connect(self.on_plot_closed) - self.wf_config = self.cbsdk_conn.get_sys_config() # {'spklength': 48, 'spkpretrig': 10, 'sysfreq': 30000} + @CustomGUI.widget_cls.getter + def widget_cls(self): + return WaveformWidget def on_plot_closed(self): - self.plot_widget = None - self.cbsdk_conn.cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} + self._plot_widget = None + self._data_source.disconnect_requested() def do_plot_update(self): - for label in self.plot_widget.wf_info: - this_info = self.plot_widget.wf_info[label] - temp_wfs, unit_ids = self.cbsdk_conn.get_waveforms(this_info['chan_id']) - self.plot_widget.update(label, [temp_wfs, unit_ids]) + for label in self._plot_widget.wf_info: + this_info = self._plot_widget.wf_info[label] + temp_wfs, unit_ids = self._data_source.get_waveforms(this_info) + self._plot_widget.update(label, [temp_wfs, unit_ids]) - # Fetching comments is SLOW! - comments = self.cbsdk_conn.get_comments() + comments = self._data_source.get_comments() if comments: - self.plot_widget.parse_comments(comments) + self._plot_widget.parse_comments(comments) class WaveformWidget(CustomWidget): def __init__(self, *args, **kwargs): - super(WaveformWidget, self).__init__(*args, **kwargs) - # super calls self.create_control_panel(), self.create_plots(**kwargs), self.refresh_axes() - self.move(WINDOWDIMS_WAVEFORMS[0], WINDOWDIMS_WAVEFORMS[1]) - self.resize(WINDOWDIMS_WAVEFORMS[2], WINDOWDIMS_WAVEFORMS[3]) self.DTT = None + self.plot_config = {} + super().__init__(*args, **kwargs) def create_control_panel(self): # Create control panel - cntrl_layout = QHBoxLayout() + cntrl_layout = QtWidgets.QHBoxLayout() # +/- amplitude range - cntrl_layout.addWidget(QLabel("+/- ")) - self.range_edit = QLineEdit("{:.2f}".format(uVRANGE)) + cntrl_layout.addWidget(QtWidgets.QLabel("+/- ")) + self.range_edit = QtWidgets.QLineEdit("{:.2f}".format(250)) # Value overridden in create_plots self.range_edit.setMaximumWidth(80) self.range_edit.setMinimumHeight(23) self.range_edit.editingFinished.connect(self.on_range_edit_editingFinished) @@ -78,8 +58,8 @@ def create_control_panel(self): cntrl_layout.addStretch() # N Spikes - cntrl_layout.addWidget(QLabel("N Spikes ")) - self.n_spikes_edit = QLineEdit("{}".format(NWAVEFORMS)) + cntrl_layout.addWidget(QtWidgets.QLabel("N Spikes ")) + self.n_spikes_edit = QtWidgets.QLineEdit("{}".format(100)) # Value overridden in create_plots self.n_spikes_edit.setMaximumWidth(80) self.n_spikes_edit.setMinimumHeight(23) self.n_spikes_edit.editingFinished.connect(self.on_n_spikes_edit_editingFinished) @@ -87,43 +67,55 @@ def create_control_panel(self): cntrl_layout.addStretch() # Clear button - clear_button = QPushButton("Clear") + clear_button = QtWidgets.QPushButton("Clear") clear_button.clicked.connect(self.clear) clear_button.setMaximumWidth(80) cntrl_layout.addWidget(clear_button) # Finish self.layout().addLayout(cntrl_layout) - def create_plots(self, theme='dark', **kwargs): + def create_plots(self, plot={}, theme={}): # Collect PlotWidget configuration - self.plot_config = { - 'x_range': XRANGE_WAVEFORMS, - 'y_range': uVRANGE, - 'theme': theme, - 'color_iterator': -1, - 'n_wfs': NWAVEFORMS - } + self.plot_config['theme'] = theme + self.plot_config['x_range'] = (plot.get('x_start', -300), plot.get('x_stop', 1140)) + self.plot_config['y_range'] = plot.get('y_range', 250) + self.plot_config['n_waveforms'] = plot.get('n_waveforms', 200) + self.plot_config['unit_scaling'] = plot.get('unit_scaling', 0.25) + + # Update widget values without triggering signals + prev_state = self.range_edit.blockSignals(True) + self.range_edit.setText(str(self.plot_config['y_range'])) + self.range_edit.blockSignals(prev_state) + prev_state = self.n_spikes_edit.blockSignals(True) + self.n_spikes_edit.setText(str(self.plot_config['n_waveforms'])) + self.n_spikes_edit.blockSignals(prev_state) + # Create and add GraphicsLayoutWidget glw = pg.GraphicsLayoutWidget(parent=self) # self.glw.useOpenGL(True) + if 'bgcolor' in self.plot_config['theme']: + glw.setBackground(parse_color_str(self.plot_config['theme']['bgcolor'])) + + cmap = self.plot_config['theme']['colormap'] + if cmap != 'custom': + cmap_colors = get_colormap(self.plot_config['theme']['colormap'], + plot.get('n_colors', 6)) + self.plot_config['theme']['pencolors'] = np.vstack((255 * np.ones_like(cmap_colors[0]), cmap_colors)) + self.layout().addWidget(glw) self.wf_info = {} # Will contain one dictionary for each line/channel label. - for chan_ix in range(len(self.group_info)): - self.add_series(self.group_info[chan_ix]) + for chan_state in self.chan_states: + self.add_series(chan_state) - def add_series(self, chan_info): + def add_series(self, chan_state): glw = self.findChild(pg.GraphicsLayoutWidget) - new_plot = glw.addPlot(row=len(self.wf_info), col=0) + new_plot = glw.addPlot(row=len(self.wf_info), col=0, enableMenu=False) + new_plot.setMouseEnabled(x=False, y=False) - # Appearance settings - # my_theme = THEMES[self.plot_config['theme']] - # self.plot_config['color_iterator'] = (self.plot_config['color_iterator'] + 1) % len(my_theme['pencolors']) - # pen_color = QColor(my_theme['pencolors'][self.plot_config['color_iterator']]) - - self.wf_info[chan_info['label']] = { + self.wf_info[chan_state['name']] = { 'plot': new_plot, 'line_ix': len(self.wf_info), - 'chan_id': chan_info['chan'] + 'chan_id': chan_state['src'] } def refresh_axes(self): @@ -143,7 +135,7 @@ def on_range_edit_editingFinished(self): self.refresh_axes() def on_n_spikes_edit_editingFinished(self): - self.plot_config['n_wfs'] = int(self.n_spikes_edit.text()) + self.plot_config['n_waveforms'] = int(self.n_spikes_edit.text()) self.refresh_axes() def parse_comments(self, comments): @@ -161,7 +153,6 @@ def parse_comments(self, comments): def update(self, line_label, data): """ - :param line_label: Label of the plot series :param data: Replace data in the plot with these data :return: @@ -171,27 +162,25 @@ def update(self, line_label, data): for ix in range(wfs.shape[0]): if np.sum(np.nonzero(wfs[ix])) > 0: c = pg.PlotCurveItem() - pen_color = QColor(WF_COLORS[unit_ids[ix]]) + pen_color = self.plot_config['theme']['pencolors'][unit_ids[ix]] c.setPen(pen_color) self.wf_info[line_label]['plot'].addItem(c) - c.setData(x=x, y=0.25*wfs[ix]) + c.setData(x=x, y=self.plot_config['unit_scaling']*wfs[ix]) data_items = self.wf_info[line_label]['plot'].listDataItems() - if len(data_items) > self.plot_config['n_wfs']: - for di in data_items[:-self.plot_config['n_wfs']]: + if len(data_items) > self.plot_config['n_waveforms']: + for di in data_items[:-self.plot_config['n_waveforms']]: self.wf_info[line_label]['plot'].removeItem(di) def main(): - from qtpy.QtWidgets import QApplication - from qtpy.QtCore import QTimer - _ = QApplication(sys.argv) + _ = QtWidgets.QApplication(sys.argv) aw = WaveformGUI() - timer = QTimer() + timer = QtCore.QTimer() timer.timeout.connect(aw.update) timer.start(1) - if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): - QApplication.instance().exec_() + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() if __name__ == '__main__': diff --git a/neuroport_dbs/dbsgui/data_source/cerebus.py b/neuroport_dbs/dbsgui/data_source/cerebus.py index decd631..fd28974 100644 --- a/neuroport_dbs/dbsgui/data_source/cerebus.py +++ b/neuroport_dbs/dbsgui/data_source/cerebus.py @@ -36,6 +36,7 @@ def __init__(self, scoped_settings: QtCore.QSettings, **kwargs): self._cbsdk_conn.cbsdk_config = conn_config self._group_ix = SAMPLINGGROUPS.index(scoped_settings.value("sampling_group")) self._group_info = self._decode_group_info(self._cbsdk_conn.get_group_config(self._group_ix)) + # self.wf_config = self.cbsdk_conn.get_sys_config() # {'spklength': 48, 'spkpretrig': 10, 'sysfreq': 30000} self._on_connect_cb(self) @staticmethod @@ -80,6 +81,9 @@ def get_event_data(self): def get_comments(self): return self._cbsdk_conn.get_comments() + def get_waveforms(self, chan_info): + return self._cbsdk_conn.get_waveforms(chan_info['chan_id']) + def disconnect_requested(self): self._cbsdk_conn.cbsdk_config = {'reset': True, 'get_continuous': False, 'get_events': False, 'get_comments': False} @@ -92,4 +96,4 @@ def update_monitor(self, chan_info, spike_only=False): def update_threshold(self, chan_info, new_value): cbsdkconn = CbSdkConnection() if cbsdkconn.is_connected: - cbsdkconn.set_channel_info(chan_info['chan_id'], {'spkthrlevel': new_value}) \ No newline at end of file + cbsdkconn.set_channel_info(chan_info['chan_id'], {'spkthrlevel': new_value}) diff --git a/neuroport_dbs/dbsgui/utilities/pyqtgraph.py b/neuroport_dbs/dbsgui/utilities/pyqtgraph.py index 89955f6..c21ba91 100644 --- a/neuroport_dbs/dbsgui/utilities/pyqtgraph.py +++ b/neuroport_dbs/dbsgui/utilities/pyqtgraph.py @@ -24,9 +24,9 @@ def str2rgb(color_str: str): return np.array(_col)[None, :] -def make_qcolor(color_str: str): +def make_qcolor(input_color): from qtpy import QtGui - pen_color = color_str + pen_color = input_color if isinstance(pen_color, np.ndarray): pen_color = pen_color.tolist() if isinstance(pen_color, list): diff --git a/neuroport_dbs/resources/config/WaveformGUI.ini b/neuroport_dbs/resources/config/WaveformGUI.ini new file mode 100644 index 0000000..3b3023b --- /dev/null +++ b/neuroport_dbs/resources/config/WaveformGUI.ini @@ -0,0 +1,29 @@ +[MainWindow] +fullScreen=false +maximized=false +frameless=true +size=@Size(300 1080) +pos=@Point(620 0) +bg_color=#404040 + +[data-source] +class=CerebusDataSource +sampling_group=30000 +get_continuous=false +get_events=false +get_comments=true +buffer_parameter\comment_length=10 + +[plot] +x_start=-300 +x_stop=1140 +y_range=250 +n_waveforms=50 +unit_scaling=0.25 + +[theme] +bgcolor=black +frate_size=24 +linewidth=1 +colormap=hsv +n_colors=4 From 1554c548fd7473c070c3ddef8af43bc4957b0ed5 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 23 Aug 2021 10:52:20 -0400 Subject: [PATCH 10/65] MappingGUI - data abstraction --- neuroport_dbs/MappingGUI.py | 117 +++++++----------- neuroport_dbs/resources/config/MappingGUI.ini | 18 +++ 2 files changed, 60 insertions(+), 75 deletions(-) create mode 100644 neuroport_dbs/resources/config/MappingGUI.ini diff --git a/neuroport_dbs/MappingGUI.py b/neuroport_dbs/MappingGUI.py index 3c5b15b..1ff8ac9 100644 --- a/neuroport_dbs/MappingGUI.py +++ b/neuroport_dbs/MappingGUI.py @@ -2,20 +2,16 @@ import os import qtpy import json -from qtpy.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget, QCheckBox, QLabel -from qtpy.QtWidgets import QLineEdit, QGridLayout, QTextEdit -from qtpy.QtGui import QPixmap, QFont - -from qtpy.QtCore import Qt +from qtpy import QtWidgets, QtGui, QtCore import pylsl sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) # Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget, SAMPLINGGROUPS, THEMES +from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget # Import settings # TODO: Make some of these settings configurable via UI elements -from neuroport_dbs.settings.defaults import WINDOWDIMS_MAPPING, SAMPLINGRATE, MAPPINGSTIMULI +from neuroport_dbs.settings.defaults import MAPPINGSTIMULI class MappingGUI(CustomGUI): @@ -24,24 +20,13 @@ def __init__(self): super(MappingGUI, self).__init__() self.setWindowTitle('MappingGUI') - def on_action_add_plot_triggered(self): - self.cbsdk_conn.cbsdk_config = { - 'reset': True, 'get_events': True, 'get_comments': True, - 'buffer_parameter': { - 'comment_length': 10 - } - } - # TODO: Or RAW, never both - group_info = self.cbsdk_conn.get_group_config(SAMPLINGGROUPS.index(str(SAMPLINGRATE))) - for gi_item in group_info: - gi_item['label'] = gi_item['label'].decode('utf-8') - gi_item['unit'] = gi_item['unit'].decode('utf-8') - self.plot_widget = MappingWidget(group_info) - self.plot_widget.was_closed.connect(self.on_plot_closed) + @CustomGUI.widget_cls.getter + def widget_cls(self): + return MappingWidget def on_plot_closed(self): - self.plot_widget = None - self.cbsdk_conn.cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} + self._plot_widget = None + self._data_source.disconnect_requested() def do_plot_update(self): pass @@ -49,10 +34,8 @@ def do_plot_update(self): class MappingWidget(CustomWidget): def __init__(self, *args, **kwargs): - super(MappingWidget, self).__init__(*args, **kwargs) - self.move(WINDOWDIMS_MAPPING[0], WINDOWDIMS_MAPPING[1]) - self.resize(WINDOWDIMS_MAPPING[2], WINDOWDIMS_MAPPING[3]) - # configure lsl stream + super().__init__(*args, **kwargs) + # configure lsl stream for outputting markers outlet_info = pylsl.StreamInfo(name='sensorimotor_mapping', type='map', channel_count=1, nominal_srate=pylsl.IRREGULAR_RATE, channel_format=pylsl.cf_string, source_id='mapping1214') @@ -62,120 +45,104 @@ def create_control_panel(self): pass def create_plots(self, theme='dark', **kwargs): - bold_font = QFont() + bold_font = QtGui.QFont() bold_font.setPixelSize(12) bold_font.setBold(True) self.layout().addStretch() - self.layout().addWidget(QLabel("Channels: ", font=bold_font)) + self.layout().addWidget(QtWidgets.QLabel("Channels: ", font=bold_font)) self.layout().addSpacing(10) # Add check boxes for channels self.channels = {} - for chan_ix in range(len(self.group_info)): - chan_lbl = self.group_info[chan_ix]['label'] - chk_box = QCheckBox(chan_lbl) + for chan_state in self.chan_states: + chan_lbl = chan_state['name'] + chk_box = QtWidgets.QCheckBox(chan_lbl) chk_box.stateChanged.connect(self.check_channel_and_stim) self.channels[chan_lbl] = chk_box self.layout().addWidget(chk_box) self.layout().addSpacing(10) - bt_clear = QPushButton("Uncheck all") + bt_clear = QtWidgets.QPushButton("Uncheck all") bt_clear.clicked.connect(lambda: self.uncheck_all(self.channels)) self.layout().addWidget(bt_clear) self.layout().addSpacing(20) # Stimuli types - self.layout().addWidget(QLabel("Stimuli: ", font=bold_font)) + self.layout().addWidget(QtWidgets.QLabel("Stimuli: ", font=bold_font)) self.layout().addSpacing(10) self.mapping_stimuli = {} for stim in MAPPINGSTIMULI: - self.mapping_stimuli[stim] = QCheckBox(stim) + self.mapping_stimuli[stim] = QtWidgets.QCheckBox(stim) self.mapping_stimuli[stim].stateChanged.connect(self.check_channel_and_stim) self.layout().addWidget(self.mapping_stimuli[stim]) - self.mapping_stimuli['Custom'] = QCheckBox() - l = QHBoxLayout() - self.custom_stimulus = QLineEdit("Custom") + self.mapping_stimuli['Custom'] = QtWidgets.QCheckBox() + l = QtWidgets.QHBoxLayout() + self.custom_stimulus = QtWidgets.QLineEdit("Custom") l.addWidget(self.mapping_stimuli['Custom']) l.addSpacing(3) l.addWidget(self.custom_stimulus) self.layout().addLayout(l) self.layout().addSpacing(10) - bt_clear = QPushButton("Uncheck all") + bt_clear = QtWidgets.QPushButton("Uncheck all") bt_clear.clicked.connect(lambda: self.uncheck_all(self.mapping_stimuli)) self.layout().addWidget(bt_clear) self.layout().addSpacing(20) # side - self.layout().addWidget(QLabel("Body Side: ", font=bold_font)) + self.layout().addWidget(QtWidgets.QLabel("Body Side: ", font=bold_font)) self.layout().addSpacing(10) self.sides = { - 'Left': QCheckBox("Left"), - 'Right': QCheckBox("Right") + 'Left': QtWidgets.QCheckBox("Left"), + 'Right': QtWidgets.QCheckBox("Right") } - l = QHBoxLayout() + l = QtWidgets.QHBoxLayout() l.addWidget(self.sides['Left']) l.addWidget(self.sides['Right']) self.layout().addLayout(l) self.layout().addSpacing(20) # Body part - self.layout().addWidget(QLabel("Limb: ", font=bold_font)) - body_widget = QWidget(self) - body_widget.setLayout(QGridLayout()) + self.layout().addWidget(QtWidgets.QLabel("Limb: ", font=bold_font)) + body_widget = QtWidgets.QWidget(self) + body_widget.setLayout(QtWidgets.QGridLayout()) body_widget.layout().setContentsMargins(0, 0, 0, 0) - lbl = QLabel() - lbl.setPixmap(QPixmap(os.path.join(os.path.dirname(__file__), 'resources', 'icons', 'HalfBody.png'))) + lbl = QtWidgets.QLabel() + lbl.setPixmap(QtGui.QPixmap(os.path.join(os.path.dirname(__file__), 'resources', 'icons', 'HalfBody.png'))) body_widget.layout().addWidget(lbl, 0, 0, 20, 10) self.body_parts = {} - cb = QCheckBox('') - self.body_parts['Head'] = cb - body_widget.layout().addWidget(cb, 1, 0, 1, 1) - - cb = QCheckBox('') - self.body_parts['Arm'] = cb - body_widget.layout().addWidget(cb, 8, 4, 1, 1) - - cb = QCheckBox('') - self.body_parts['Hand'] = cb - body_widget.layout().addWidget(cb, 10, 5, 1, 1) - - cb = QCheckBox('') - self.body_parts['Leg'] = cb - body_widget.layout().addWidget(cb, 14, 1, 1, 1) - - cb = QCheckBox('') - self.body_parts['Foot'] = cb - body_widget.layout().addWidget(cb, 18, 1, 1, 1) + for bp_name, rc in {'Head': (1, 0), 'Arm': (8, 4), 'Hand': (10, 5), 'Leg': (14, 1), 'Foot': (18, 1)}.items(): + self.body_parts[bp_name] = QtWidgets.QCheckBox('') + body_widget.layout().addWidget(self.body_parts[bp_name], rc[0], rc[1], 1, 1) - bt_clear = QPushButton("Uncheck all") + bt_clear = QtWidgets.QPushButton("Uncheck all") bt_clear.clicked.connect(lambda: self.uncheck_all(self.body_parts)) body_widget.layout().addWidget(bt_clear, 20, 0, 1, 10) self.layout().addWidget(body_widget) self.layout().addSpacing(20) - self.bt_map = QPushButton("Submit Response") + self.bt_map = QtWidgets.QPushButton("Submit Response") self.bt_map.setEnabled(False) self.bt_map.setMinimumHeight(40) self.bt_map.clicked.connect(self.submit_map) self.layout().addWidget(self.bt_map) self.layout().addSpacing(10) - bt_clear = QPushButton("Clear Channel") + bt_clear = QtWidgets.QPushButton("Clear Channel") bt_clear.setMinimumHeight(20) bt_clear.clicked.connect(self.clear_data) self.layout().addWidget(bt_clear) self.layout().addStretch() # manual notes - self.layout().addWidget(QLabel("Note: ", font=bold_font)) - self.note_field = QTextEdit() + self.layout().addWidget(QtWidgets.QLabel("Note: ", font=bold_font)) + self.note_field = QtWidgets.QTextEdit() self.note_field.setMaximumHeight(80) self.note_field.textChanged.connect(self.check_note) self.layout().addWidget(self.note_field) self.layout().addSpacing(10) - self.bt_note = QPushButton("Submit Note") + self.bt_note = QtWidgets.QPushButton("Submit Note") self.bt_note.setEnabled(False) self.bt_note.setMinimumHeight(20) self.bt_note.clicked.connect(self.submit_note) @@ -262,14 +229,14 @@ def clear(self): def main(): - _ = QApplication(sys.argv) + _ = QtWidgets.QApplication(sys.argv) aw = MappingGUI() # timer = QTimer() # timer.timeout.connect(aw.update) # timer.start(1000) if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): - QApplication.instance().exec_() + QtWidgets.QApplication.instance().exec_() if __name__ == '__main__': diff --git a/neuroport_dbs/resources/config/MappingGUI.ini b/neuroport_dbs/resources/config/MappingGUI.ini new file mode 100644 index 0000000..ff05091 --- /dev/null +++ b/neuroport_dbs/resources/config/MappingGUI.ini @@ -0,0 +1,18 @@ +[MainWindow] +fullScreen=false +maximized=false +frameless=true +size=@Size(100 1080) +pos=@Point(1220 0) +bg_color=#404040 + +[data-source] +class=CerebusDataSource +sampling_group=30000 +get_continuous=false +get_events=true +get_comments=true +buffer_parameter\comment_length=10 + +[theme] +linewidth=1 From 9581f1a5c95fca0b1404b5169a0fc65ead3a95b0 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 23 Aug 2021 16:41:09 -0400 Subject: [PATCH 11/65] Update build instructions to use qt 5.15 Refactor DDUGUI -> DepthGUI; depth_source abstraction and settings from ini --- neuroport_dbs/DDUGUI.py | 334 ------------------ neuroport_dbs/DepthGUI.py | 222 ++++++++++++ neuroport_dbs/dbsgui/depth_source/__init__.py | 93 +++++ neuroport_dbs/resources/config/DepthGUI.ini | 24 ++ neuroport_dbs/settings/defaults.py | 2 +- setup.py | 2 +- 6 files changed, 341 insertions(+), 336 deletions(-) delete mode 100644 neuroport_dbs/DDUGUI.py create mode 100644 neuroport_dbs/DepthGUI.py create mode 100644 neuroport_dbs/dbsgui/depth_source/__init__.py create mode 100644 neuroport_dbs/resources/config/DepthGUI.ini diff --git a/neuroport_dbs/DDUGUI.py b/neuroport_dbs/DDUGUI.py deleted file mode 100644 index dc5c927..0000000 --- a/neuroport_dbs/DDUGUI.py +++ /dev/null @@ -1,334 +0,0 @@ -import re -import sys - -import serial -import serial.tools.list_ports -# use the same GUI format as the other ones -import qtpy.QtCore -from qtpy.QtCore import Qt -from qtpy.QtWidgets import QComboBox, QLabel, QLCDNumber, QPushButton, QDoubleSpinBox, \ - QCheckBox, QHBoxLayout, QWidget, QVBoxLayout, QMainWindow, \ - QFrame, QGridLayout -from qtpy.QtWidgets import QApplication -from qtpy.QtCore import QTimer -from cerebuswrapper import CbSdkConnection -import pylsl - -# settings -from neuroport_dbs.settings.defaults import WINDOWDIMS_DEPTH, DDUSCALEFACTOR - - -class DepthGUI(QMainWindow): - - def __init__(self): - super(DepthGUI, self).__init__() - - self.display_string = None - - # Serial port config - self.ser = serial.Serial(timeout=1) - self.ser.baudrate = 19200 - - self._is_v2 = False - self._gain = 1.000 - - self.depth_stream = None - self._prev_port = None - - self.setup_ui() - - @property - def is_v2(self): - return self._is_v2 - - @is_v2.setter - def is_v2(self, value): - self._is_v2 = value - self._gain = 0.001 if value else 1.0 - self.doubleSpinBox_offset.setValue(60.00 if value else 0.00) - - def setup_ui(self): - self.setWindowTitle('Neuroport DBS - Electrodes Depth') - self.setWindowFlags(Qt.FramelessWindowHint) - self.move(WINDOWDIMS_DEPTH[0], WINDOWDIMS_DEPTH[1]) - self.setFixedSize(WINDOWDIMS_DEPTH[2], WINDOWDIMS_DEPTH[3]) - - self.show() - - self.plot_widget = QWidget() - self.setCentralWidget(self.plot_widget) - - # define Qt GUI elements - v_layout = QVBoxLayout() - v_layout.setSpacing(0) - v_layout.setContentsMargins(10, 0, 10, 10) - - h_layout = QHBoxLayout() - - self.comboBox_com_port = QComboBox() - h_layout.addWidget(self.comboBox_com_port) - - self.pushButton_open = QPushButton("Open") - h_layout.addWidget(self.pushButton_open) - h_layout.addStretch() - - self.label_DTT = QLabel("DTT: ") - h_layout.addWidget(self.label_DTT) - - self.doubleSpinBox_DTT = QDoubleSpinBox() - self.doubleSpinBox_DTT = QDoubleSpinBox() - self.doubleSpinBox_DTT.setMinimum(-100.00) - self.doubleSpinBox_DTT.setMaximum(100.00) - self.doubleSpinBox_DTT.setSingleStep(1.00) - self.doubleSpinBox_DTT.setDecimals(2) - self.doubleSpinBox_DTT.setValue(0.00) - self.doubleSpinBox_DTT.setFixedWidth(60) - h_layout.addWidget(self.doubleSpinBox_DTT) - - self.label_offset = QLabel("Offset: ") - h_layout.addWidget(self.label_offset) - - self.doubleSpinBox_offset = QDoubleSpinBox() - self.doubleSpinBox_offset.setMinimum(-100.00) - self.doubleSpinBox_offset.setMaximum(100.00) - self.doubleSpinBox_offset.setSingleStep(1.00) - self.doubleSpinBox_offset.setDecimals(2) - self.doubleSpinBox_offset.setValue(0.00) - self.doubleSpinBox_offset.setFixedWidth(60) - h_layout.addWidget(self.doubleSpinBox_offset) - - h_layout.addStretch() - - h_layout.addWidget(QLabel("Stream to :")) - - self.chk_NSP = QCheckBox("NSP") - self.chk_NSP.setChecked(True) - h_layout.addWidget(self.chk_NSP) - - h_layout.addSpacing(5) - - self.chk_LSL = QCheckBox("LSL") - self.chk_LSL.clicked.connect(self.on_chk_LSL_clicked) - self.chk_LSL.click() # default is enabled, click call to trigger LSL stream creation. - h_layout.addWidget(self.chk_LSL) - - h_layout.addSpacing(5) - - send_btn = QPushButton("Send") - send_btn.clicked.connect(self.send) - - h_layout.addWidget(send_btn) - h_layout.addSpacing(5) - - quit_btn = QPushButton('X') - quit_btn.setMaximumWidth(20) - quit_btn.clicked.connect(QApplication.instance().quit) - - quit_btn.setStyleSheet("QPushButton { color: white; " - "background-color : red; " - "border-color : red; " - "border-width: 2px}") - - h_layout.addWidget(quit_btn) - - v_layout.addLayout(h_layout) - - # define Qt GUI elements - # add a frame for the LCD numbers - self.lcd_frame = QFrame() - self.lcd_frame.setFrameShape(qtpy.QtWidgets.QFrame.Shape.Box) - lcd_layout = QGridLayout() - self.lcd_frame.setLayout(lcd_layout) - - # RAW reading from DDU - self.raw_ddu = QLCDNumber() - self.raw_ddu.setDigitCount(7) - self.raw_ddu.setFrameShape(qtpy.QtWidgets.QFrame.Shape.NoFrame) - self.raw_ddu.setSmallDecimalPoint(True) - self.raw_ddu.setFixedHeight(50) - self.raw_ddu.display("{0:.3f}".format(0)) - lcd_layout.addWidget(self.raw_ddu, 0, 3, 2, 3) - - self.offset_ddu = QLCDNumber() - self.offset_ddu.setDigitCount(7) - self.offset_ddu.setFixedHeight(150) - self.offset_ddu.display("{0:.3f}".format(-10)) - self.offset_ddu.setFrameShape(qtpy.QtWidgets.QFrame.Shape.NoFrame) - # TODO: Use custom class and reimplement self.offset_ddu.mouseDoubleClickEvent(), then git rid of "Send!" - lcd_layout.addWidget(self.offset_ddu, 2, 0, 5, 6) - v_layout.addWidget(self.lcd_frame) - - self.plot_widget.setLayout(v_layout) - - # Populate control panel items - for port in serial.tools.list_ports.comports(): - self.comboBox_com_port.addItem(port.device) - self.comboBox_com_port.addItem("cbsdk playback") - - # Connect signals & slots - self.pushButton_open.clicked.connect(self.on_open_clicked) - self.comboBox_com_port.currentIndexChanged.connect(self.on_comboBox_com_port_changed) - # TODO: Add signal for comboBox_com_port --> when cbsdk playback, uncheck NSP then re-open connection. - - def on_chk_LSL_clicked(self, state): - print(f"LSL clicked state: {state}") - if self.chk_LSL.isChecked(): - outlet_info = pylsl.StreamInfo(name='electrode_depth', type='depth', channel_count=1, - nominal_srate=pylsl.IRREGULAR_RATE, channel_format=pylsl.cf_float32, - source_id='depth1214') - self.depth_stream = pylsl.StreamOutlet(outlet_info) - else: - self.depth_stream = None - - def _do_close(self, from_port): - if from_port == "cbsdk playback": - CbSdkConnection().disconnect() - else: - self.ser.close() - self.pushButton_open.setText("Open") - - def on_comboBox_com_port_changed(self, new_ix): - # If a connection was already open, close it before proceeding. - if self.pushButton_open.text() == "Close": - self._do_close(self._prev_port) - - # at this point the Open/Close pushbutton should show: Open - # we will only enable/disable the Send to NSP button to leave the current checked status. - # The default should be checked. - if self.comboBox_com_port.currentText() == "cbsdk playback": - # If switching _to_ cbsdk playback, disable sending out comments. - self.chk_NSP.setEnabled(False) - - elif self._prev_port == "cbsdk playback": - # If switching _from_ cbsdk playback, re-enable sending out comments. - self.chk_NSP.setEnabled(True) - - self._prev_port = self.comboBox_com_port.currentText() - - def on_open_clicked(self): - com_port = self.comboBox_com_port.currentText() - cmd_text = self.pushButton_open.text() - if cmd_text == 'Open': - - if com_port == "cbsdk playback": - CbSdkConnection().connect() - CbSdkConnection().cbsdk_config = { - 'reset': True, 'get_events': False, 'get_comments': True, - 'buffer_parameter': { - 'comment_length': 10 - } - } - self.pushButton_open.setText("Close") - else: - if self.chk_NSP.isEnabled() and self.chk_NSP.isChecked(): - CbSdkConnection().connect() - CbSdkConnection().cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} - - if not self.ser.is_open: - self.ser.port = com_port - try: - self.ser.open() # TODO: Add error handling. - # Quiety printing for a minute - self.ser.write("AXON-\r".encode()) - self.ser.write("V\r".encode()) - # readlines will capture until timeout - pattern = "([0-9]+\.[0-9]+)" - for line in self.ser.readlines(): - match = re.search(pattern, line.decode("utf-8").strip()) - if match is not None: - v = match.group() # e.g., "2.20" - v_maj = int(v.split(".")[0]) - self.is_v2 = v_maj >= 2 - break - # Resume printing - self.ser.write("AXON+\r".encode()) - _ = self.ser.readline() # Clear out the first response to AXON+ - except serial.serialutil.SerialException: - print("Could not open serial port") - finally: - self.pushButton_open.setText("Close" if self.ser.is_open else "Open") - else: - self._do_close(com_port) - - def update(self): - # Added new_value handling for playback if we ever want to post-process depth - # on previously recorded sessions. - new_value = False - out_value = None - - if self.comboBox_com_port.currentText() == "cbsdk playback": - cbsdk_conn = CbSdkConnection() - if cbsdk_conn.is_connected: - comments = cbsdk_conn.get_comments() - if comments: - comment_strings = [x[1].decode("utf8") for x in comments] - else: - comment_strings = "" - dtts = [] - for comm_str in comment_strings: - if 'DTT:' in comm_str: - dtts.append(float(comm_str[4:])) - if len(dtts) > 0: - out_value = dtts[-1] - new_value = True - self.offset_ddu.display("{0:.3f}".format(out_value)) - offset = self.doubleSpinBox_offset.value() - self.raw_ddu.display("{0:.3f}".format(out_value - offset)) - - elif self.ser.is_open: - in_str = self.ser.readline().decode("utf-8").strip() - if in_str: - try: - in_value = float(in_str) - in_value *= self._gain # Uncomment this for FHC DDU V2. - - self.raw_ddu.display("{0:.3f}".format(in_value)) - - out_value = in_value + self.doubleSpinBox_DTT.value() + self.doubleSpinBox_offset.value() - display_string = "{0:.3f}".format(out_value) - self.offset_ddu.display(display_string) - - # Check if new value - if display_string != self.display_string: - new_value = True - self.display_string = display_string - - # Push to NSP - cbsdk_conn = CbSdkConnection() - if cbsdk_conn.is_connected: - if self.chk_NSP.isChecked() and self.chk_NSP.isEnabled() and new_value: - cbsdk_conn.set_comments("DTT:" + display_string) - else: - # try connecting if not connected but button is active - if self.chk_NSP.isChecked() and self.chk_NSP.isEnabled(): - cbsdk_conn.connect() - cbsdk_conn.cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} - # set button to connection status - self.chk_NSP.setChecked(cbsdk_conn.is_connected) - - except ValueError: - print("DDU result: {}".format(in_str)) - - # Push to LSL - if self.depth_stream is not None and new_value: - self.depth_stream.push_sample([out_value]) - - def send(self): - self.display_string = None # make sure the update function runs - self.update() - - -def main(): - _ = QApplication(sys.argv) - window = DepthGUI() - window.show() - timer = QTimer() - timer.timeout.connect(window.update) - timer.start(100) - - if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): - QApplication.instance().exec_() - - -if __name__ == '__main__': - main() diff --git a/neuroport_dbs/DepthGUI.py b/neuroport_dbs/DepthGUI.py new file mode 100644 index 0000000..88e231b --- /dev/null +++ b/neuroport_dbs/DepthGUI.py @@ -0,0 +1,222 @@ +import sys +from pathlib import Path +from qtpy import QtCore, QtWidgets +from cerebuswrapper import CbSdkConnection +import pylsl +import neuroport_dbs +from neuroport_dbs.settings import defaults +from neuroport_dbs.dbsgui.depth_source import CBSDKPlayback + + +class DepthGUI(QtWidgets.QMainWindow): + + def __init__(self, ini_file=None): + super().__init__() + + # Infer path to ini + ini_name = ini_file if ini_file is not None else (type(self).__name__ + '.ini') + ini_path = Path(ini_name) + if ini_path.exists(): + self._settings_path = ini_path + else: + # Try home / .dbs_suite first + home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) + ini_path = home_dir / '.dbs_suite' / ini_path.name + if ini_path.exists(): + self._settings_path = ini_path + else: + # Use default ini that ships with module. + self._settings_path = Path(__file__).parents[0] / 'resources' / 'config' / ini_path.name + + self.display_string = None + self._depth_stream = None + self._mirror = {'lsl': None, 'nsp': None} + self.restore_from_settings() + self.setup_ui() + + def restore_from_settings(self): + settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) + + # Restore size and position. + default_dims = defaults.WINDOWDIMS_DICT[type(self).__name__] + settings.beginGroup("MainWindow") + self.resize(settings.value("size", QtCore.QSize(default_dims[2], default_dims[3]))) + self.move(settings.value("pos", QtCore.QPoint(default_dims[0], default_dims[1]))) + if settings.value("fullScreen", 'false') == 'true': + self.showFullScreen() + elif settings.value("maximized", 'false') == 'true': + self.showMaximized() + if settings.value("frameless", 'false') == 'true': + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + settings.endGroup() + + # Infer depth source from ini file, setup data source + settings.beginGroup("depth-source") + src_cls = getattr(neuroport_dbs.dbsgui.depth_source, settings.value("class")) + self._depth_source = src_cls(scoped_settings=settings) + settings.endGroup() + + settings.beginGroup("depth-mirror") + self._mirror['lsl'] = bool(settings.value("lsl_mirror", False)) + self._mirror['nsp'] = bool(settings.value("lsl_mirror", False)) + settings.endGroup() + + def setup_ui(self): + self.setWindowTitle('Neuroport DBS - Electrodes Depth') + self.show() + self.plot_widget = QtWidgets.QWidget() + self.setCentralWidget(self.plot_widget) + + # define Qt GUI elements + v_layout = QtWidgets.QVBoxLayout() + v_layout.setSpacing(0) + v_layout.setContentsMargins(10, 0, 10, 10) + h_layout = QtWidgets.QHBoxLayout() + + # Manual offset added to the depth before display and mirroring + h_layout.addWidget(QtWidgets.QLabel("Offset: ")) + self._doubleSpinBox_offset = QtWidgets.QDoubleSpinBox() + self._doubleSpinBox_offset.setMinimum(-100.00) + self._doubleSpinBox_offset.setMaximum(100.00) + self._doubleSpinBox_offset.setSingleStep(1.00) + self._doubleSpinBox_offset.setDecimals(2) + self._doubleSpinBox_offset.setValue(-10.00) + self._doubleSpinBox_offset.setFixedWidth(60) + h_layout.addWidget(self._doubleSpinBox_offset) + + h_layout.addStretch() + + # Widgets to manage mirroring the resulting value (including scale and offset) to other outputs + h_layout.addWidget(QtWidgets.QLabel("Stream to :")) + + cb = QtWidgets.QCheckBox("NSP") + cb.setObjectName("NSP_CheckBox") + if isinstance(self._depth_source, CBSDKPlayback): + cb.setChecked(False) + cb.setEnabled(False) + else: + cb.setChecked(self._mirror['nsp']) + cb.setEnabled(True) + cb.clicked.connect(self.on_mirror_NSP_clicked) + cb.click() + h_layout.addWidget(cb) + h_layout.addSpacing(5) + + cb = QtWidgets.QCheckBox("LSL") + cb.setChecked(self._mirror['lsl']) + cb.clicked.connect(self.on_mirror_LSL_clicked) + cb.click() + h_layout.addWidget(cb) + h_layout.addSpacing(5) + + # Manual close button because window has no frame. + quit_btn = QtWidgets.QPushButton('X') + quit_btn.setMaximumWidth(20) + quit_btn.clicked.connect(QtWidgets.QApplication.instance().quit) + + quit_btn.setStyleSheet("QPushButton { color: white; " + "background-color : red; " + "border-color : red; " + "border-width: 2px}") + + h_layout.addWidget(quit_btn) + + v_layout.addLayout(h_layout) + + # add a frame for the LCD numbers + self.lcd_frame = QtWidgets.QFrame() + self.lcd_frame.setFrameShape(1) + lcd_layout = QtWidgets.QGridLayout() + self.lcd_frame.setLayout(lcd_layout) + + # RAW reading from DDU + self.raw_ddu = QtWidgets.QLCDNumber() + self.raw_ddu.setDigitCount(7) + self.raw_ddu.setFrameShape(0) + self.raw_ddu.setSmallDecimalPoint(True) + self.raw_ddu.setFixedHeight(50) + self.raw_ddu.display("{0:.3f}".format(0)) + lcd_layout.addWidget(self.raw_ddu, 0, 3, 2, 3) + + self.offset_ddu = QtWidgets.QLCDNumber() + self.offset_ddu.setDigitCount(7) + self.offset_ddu.setFixedHeight(150) + self.offset_ddu.display("{0:.3f}".format(-10)) + self.offset_ddu.setFrameShape(0) + lcd_layout.addWidget(self.offset_ddu, 2, 0, 5, 6) + v_layout.addWidget(self.lcd_frame) + + self.plot_widget.setLayout(v_layout) + + def on_mirror_LSL_clicked(self, state): + if state > 0: + outlet_info = pylsl.StreamInfo(name='electrode_depth', type='depth', channel_count=1, + nominal_srate=pylsl.IRREGULAR_RATE, channel_format=pylsl.cf_float32, + source_id='depth1214') + self._depth_stream = pylsl.StreamOutlet(outlet_info) + else: + self._depth_stream = None + + def on_mirror_NSP_clicked(self, state): + if not isinstance(self._depth_source, CBSDKPlayback): + if state > 0: + CbSdkConnection().connect() + CbSdkConnection().cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} + # cbsdk_conn.set_comments("DTT:" + display_string) + else: + CbSdkConnection().disconnect() + + def _do_close(self, from_port): + self._depth_source.do_close() + + def update(self): + # Added new_value handling for playback if we ever want to post-process depth + # on previously recorded sessions. + new_value = False + + value = self._depth_source.update() + if value is not None: + self.raw_ddu.display("{0:.3f}".format(value)) + value = value + self._doubleSpinBox_offset.value() + display_string = "{0:.3f}".format(value) + if display_string != self.display_string: + new_value = True + self.display_string = display_string + self.offset_ddu.display(display_string) + + # Push to NSP (only if this is not NSP playback) + nsp_cb = self.findChild(QtWidgets.QCheckBox, "NSP_CheckBox") + if nsp_cb and nsp_cb.isChecked() and not isinstance(self._depth_source, CBSDKPlayback): + cbsdk_conn = CbSdkConnection() + if cbsdk_conn.is_connected: + if self.chk_NSP.isChecked() and self.chk_NSP.isEnabled() and new_value: + cbsdk_conn.set_comments("DTT:" + display_string) + else: + # try connecting if not connected but button is active + if self.chk_NSP.isChecked() and self.chk_NSP.isEnabled(): + cbsdk_conn.connect() + cbsdk_conn.cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} + + # Push to LSL + if self._depth_stream is not None and new_value: + self._depth_stream.push_sample([value]) + + def send(self): + self.display_string = None # make sure the update function runs + self.update() + + +def main(): + _ = QtWidgets.QApplication(sys.argv) + window = DepthGUI() + window.show() + timer = QtCore.QTimer() + timer.timeout.connect(window.update) + timer.start(100) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + + +if __name__ == '__main__': + main() diff --git a/neuroport_dbs/dbsgui/depth_source/__init__.py b/neuroport_dbs/dbsgui/depth_source/__init__.py new file mode 100644 index 0000000..0fd1563 --- /dev/null +++ b/neuroport_dbs/dbsgui/depth_source/__init__.py @@ -0,0 +1,93 @@ +from qtpy import QtCore +from cerebuswrapper import CbSdkConnection +import serial +import serial.tools.list_ports +from neuroport_dbs.settings import parse_ini_try_numeric + + +class MerDepthSource: + def __init__(self, scoped_settings: QtCore.QSettings): + # scale_factor should be 0.001 for FHC DDU V2, 1.0 otherwise. + self._scale_factor = parse_ini_try_numeric(scoped_settings, 'scale_factor') or 1.0 + self.do_open() + + def do_open(self): + raise NotImplementedError() + + def do_close(self): + raise NotImplementedError() + + def update(self): + raise NotImplementedError + + +class CBSDKPlayback(MerDepthSource): + + def __init__(self, scoped_settings: QtCore.QSettings): + super().__init__(scoped_settings) + + def do_open(self): + CbSdkConnection().connect() + CbSdkConnection().cbsdk_config = { + 'reset': True, 'get_events': False, 'get_comments': True, + 'buffer_parameter': { + 'comment_length': 10 + } + } + + def do_close(self): + CbSdkConnection().disconnect() + + def update(self): + cbsdk_conn = CbSdkConnection() + if cbsdk_conn.is_connected: + comments = cbsdk_conn.get_comments() + if comments: + comment_strings = [x[1].decode('utf8') for x in comments] + else: + comment_strings = "" + dtts = [] + for comm_str in comment_strings: + if 'DTT:' in comm_str: + dtts.append(float(comm_str[4:])) + if len(dtts) > 0: + raw_value = dtts[-1] + return raw_value + return None + + +class FHCSerial(MerDepthSource): + + def __init__(self, scoped_settings: QtCore.QSettings): + super().__init__(scoped_settings) + self._baudrate = parse_ini_try_numeric(scoped_settings, 'baudrate') or 19200 + self._com_port = scoped_settings.value("com_port") + self.ser = serial.Serial() + + def do_open(self): + super().do_open() + self.ser.baudrate = self._baudrate + if self._com_port not in serial.tools.list_ports.comports(): + print(f"Port {self._com_port} not found in list of comports.") + if not self.ser.is_open: + self.ser.port = self._com_port + try: + self.ser.open() # TODO: Add timeout; Add error. + self.ser.write('AXON+\r'.encode()) + except serial.serialutil.SerialException: + print("Could not open serial port") + + def do_close(self): + self.ser.close() + + def update(self): + if self.ser.is_open: + in_str = self.ser.readline().decode('utf-8').strip() + if in_str: + try: + raw_value = float(in_str) * self._scale_factor + return raw_value + + except ValueError: + print("DDU result: {}".format(in_str)) + return None diff --git a/neuroport_dbs/resources/config/DepthGUI.ini b/neuroport_dbs/resources/config/DepthGUI.ini new file mode 100644 index 0000000..f2d4a32 --- /dev/null +++ b/neuroport_dbs/resources/config/DepthGUI.ini @@ -0,0 +1,24 @@ +[MainWindow] +fullScreen=false +maximized=false +frameless=true +size=@Size(600 250) +pos=@Point(1320 0) +bg_color=#404040 + +[depth-source] +class=CBSDKPlayback +offset=0 + +[depth-source-serial] +class=FHCSerial +baudrate=19200 +com_port=COM6 +scale_factor=0.001 + +[depth-mirror] +lsl_mirror=true +nsp_mirror=true + +[theme] +linewidth=1 diff --git a/neuroport_dbs/settings/defaults.py b/neuroport_dbs/settings/defaults.py index 669fc99..85e5b80 100644 --- a/neuroport_dbs/settings/defaults.py +++ b/neuroport_dbs/settings/defaults.py @@ -30,7 +30,7 @@ WINDOWDIMS_FEATURES = [1320, 250, 600, 830] WINDOWDIMS_DICT = { 'SweepGUI': WINDOWDIMS_SWEEP, 'RasterGUI': WINDOWDIMS_RASTER, - 'WaveformGUI': WINDOWDIMS_WAVEFORMS, 'DDUGUI': WINDOWDIMS_DEPTH, + 'WaveformGUI': WINDOWDIMS_WAVEFORMS, 'DepthGUI': WINDOWDIMS_DEPTH, 'FeaturesGUI': WINDOWDIMS_FEATURES, 'MappingGUI': WINDOWDIMS_MAPPING } diff --git a/setup.py b/setup.py index 860f89a..7c07977 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ 'dbs-features=neuroport_dbs.FeaturesGUI:main', 'dbs-mapping=neuroport_dbs.MappingGUI:main', 'dbs-comments=neuroport_dbs.CommentsGUI:main', - 'dbs-ddu=neuroport_dbs.DDUGUI:main', + 'dbs-ddu=neuroport_dbs.DepthGUI:main', ], } ) From d342c6c7f8f3ed81cd9a075f2ca1ce6eb283b809 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Tue, 21 Sep 2021 06:43:11 -0400 Subject: [PATCH 12/65] not functional; switching computers --- neuroport_dbs/FeaturesGUI.py | 814 ++++++------------ neuroport_dbs/FeaturesGUI_old.py | 262 ++++++ neuroport_dbs/ProcessGUI.py | 256 ++++++ neuroport_dbs/dbsgui/data_source/cerebus.py | 18 +- neuroport_dbs/dbsgui/data_source/interface.py | 3 + neuroport_dbs/dbsgui/my_widgets/custom.py | 14 +- .../resources/config/FeaturesGUI.ini | 53 ++ neuroport_dbs/resources/config/ProcessGUI.ini | 14 + neuroport_dbs/settings/__init__.py | 19 + neuroport_dbs/settings/defaults.py | 4 +- 10 files changed, 885 insertions(+), 572 deletions(-) create mode 100644 neuroport_dbs/FeaturesGUI_old.py create mode 100644 neuroport_dbs/ProcessGUI.py create mode 100644 neuroport_dbs/resources/config/FeaturesGUI.ini create mode 100644 neuroport_dbs/resources/config/ProcessGUI.ini diff --git a/neuroport_dbs/FeaturesGUI.py b/neuroport_dbs/FeaturesGUI.py index 9ee3db9..ef59446 100644 --- a/neuroport_dbs/FeaturesGUI.py +++ b/neuroport_dbs/FeaturesGUI.py @@ -1,593 +1,285 @@ -import os import sys -import numpy as np -import qtpy.QtCore -from qtpy.QtWidgets import QApplication -from qtpy.QtWidgets import QComboBox, QLineEdit, QLabel, QDialog, QPushButton, \ - QCheckBox, QHBoxLayout, QVBoxLayout, QStackedWidget, QAction -from qtpy.QtCore import QSharedMemory, Signal, QTimer, Qt -from qtpy.QtGui import QPixmap - -from cerebuswrapper import CbSdkConnection - -# Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget, SAMPLINGGROUPS -from neuroport_dbs.feature_plots import * +import argparse +from pathlib import Path +from qtpy import QtCore, QtGui, QtWidgets +from neuroport_dbs.settings import defaults, parse_ini_try_numeric, locate_ini from neuroport_dbs.SettingsDialog import SettingsDialog - from serf.tools.db_wrap import DBWrapper, ProcessWrapper +from neuroport_dbs.feature_plots import * -# Settings -from neuroport_dbs.settings.defaults import WINDOWDIMS_FEATURES, XRANGE_FEATURES, uVRANGE, BASEPATH, SAMPLINGRATE, \ - BUFFERLENGTH, SAMPLELENGTH, DELAYBUFFER, OVERWRITEDEPTH, DEPTHSETTINGS - - -class FeaturesGUI(CustomGUI): - - def __init__(self): - super(FeaturesGUI, self).__init__() - self.setWindowTitle('FeaturesGUI') - self.plot_widget = None - - # settings dictionaries - self.subject_settings = {} - self.procedure_settings = {} - self.buffer_settings = {} - self.features_settings = {} - # DB wrapper - self.db_wrapper = DBWrapper() +class FeaturesGUI(QtWidgets.QMainWindow): + status_icons = { + k: Path(__file__).parents[0] / 'resources' / 'icons' / (v + '.png') for k, v in + ((-2, 'depth_status_delay'), (-1, 'depth_status_in_use'), (1, 'depth_status_done'), (0, 'depth_status_off')) + } + + def __init__(self, ini_file: str = None, **kwargs): + """ + FeaturesGUI is the most complicated of all applications in this package. + - Has a GUI to configure many different settings. + - Sets up a sub-process (Depth_Process in serf package) to capture data from Central and store in the database + - Sets up a sub-process (Features_Process in serf package) to compute features on new entries in the database + - Visualizes raw segments and features from the database + Args: + ini_file: + **kwargs: + """ + super().__init__(**kwargs) + + self._plot_settings = {} + self._subject_settings = {} + self._procedure_settings = {} + self._buffer_settings = {} + self._features_settings = {} + + self._restore_from_settings(ini_file) + self._setup_ui() + self._setup_shmem() + + def closeEvent(self, *args, **kwargs): + super().closeEvent(*args, **kwargs) + + def _restore_from_settings(self, ini_file=None): + ini_name = ini_file if ini_file is not None else (type(self).__name__ + '.ini') + settings_path = locate_ini(ini_name) + settings = QtCore.QSettings(str(settings_path), QtCore.QSettings.IniFormat) + default_dims = defaults.WINDOWDIMS_DICT[type(self).__name__] + settings.beginGroup("MainWindow") + self.move(settings.value("pos", QtCore.QPoint(default_dims[0], default_dims[1]))) + size_xy = settings.value("size", QtCore.QSize(default_dims[2], default_dims[3])) + self.resize(size_xy) + self.setMaximumWidth(size_xy[0]) + if settings.value("fullScreen", 'false') == 'true': + self.showFullScreen() + elif settings.value("maximized", 'false') == 'true': + self.showMaximized() + if settings.value("frameless", 'false') == 'true': + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + settings.endGroup() + + # TODO: Other settings + # theme + # x_start + # x_stop + # y_range + # do_hp + # buffer_length <-- Depth buffer duration (s) + # sample_length <-- Depth sample size (s) + # delay_buffer <-- Delay depth recording (s) + # overwrite_depth <- Overwrite depth values + # electrode_settings + # chk_threshold + + # self._plot_settings['theme'] = theme + # self._plot_settings['color_iterator'] = -1 + # self._plot_settings['x_start'] = plot.get('x_start', -4000) + # self._plot_settings['x_stop'] = plot.get('x_stop', 120000) + # self._plot_settings['y_range'] = plot.get('y_range', 250) + # self._plot_settings['do_hp'] = True + + def _setup_ui(self): + main_widget = QtWidgets.QWidget() + main_widget.setLayout(QtWidgets.QVBoxLayout()) + self.setCentralWidget(main_widget) + + self._setup_control_panel() + self._reset_widget_stack() + + def _setup_control_panel(self): + # Top row + lo_L1 = QtWidgets.QHBoxLayout() + # Channel select + lo_L1.addWidget(QtWidgets.QLabel("Electrode: ", alignment=QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight)) + chan_select_cb = QtWidgets.QComboBox() + chan_select_cb.setObjectName("ChanSelect_ComboBox") + chan_select_cb.setMinimumWidth(70) + chan_select_cb.setEnabled(False) + self._reset_chan_select_items() + lo_L1.addWidget(chan_select_cb) + # Feature select + lo_L1.addSpacing(20) + lo_L1.addWidget(QtWidgets.QLabel("Feature set: ", alignment=QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight)) + feat_select_cb = QtWidgets.QComboBox() + feat_select_cb.setObjectName("FeatureSelect_ComboBox") + feat_select_cb.setMinimumWidth(60) + self._reset_feat_select_items() + lo_L1.addWidget(feat_select_cb) + + # Second row + lo_L2 = QtWidgets.QHBoxLayout() + lo_L2.addSpacing(10) + # Range Edit + lo_L2.addWidget(QtWidgets.QLabel("+/- ", alignment=QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight)) + range_edit = QtWidgets.QLineEdit("{:.2f}".format(self._plot_settings['y_range'])) + range_edit.setObjectName("Range_LineEdit") + range_edit.setMaximumWidth(50) + range_edit.editingFinished.connect(self.on_range_edited) + lo_L2.addWidget(range_edit) + # HP + lo_L2.addSpacing(30) + hp_chk = QtWidgets.QCheckBox("HP") + hp_chk.setObjectName("HP_CheckBox") + hp_chk.setChecked(self._plot_settings['hp']) + lo_L2.addWidget(hp_chk) + # Match Sweep + lo_L2.addSpacing(30) + sweep_chk = QtWidgets.QCheckBox("Match SweepGUI") + sweep_chk.setObjectName("Sweep_CheckBox") + sweep_chk.setChecked(True) + sweep_chk.setEnabled(True) + sweep_chk.clicked.connect(self.on_sweep_clicked) + lo_L2.addWidget(sweep_chk) + + lo_R = QtWidgets.QHBoxLayout() + # Refresh button + refresh_pb = QtWidgets.QPushButton("Refresh") + refresh_pb.setObjectName("Refresh_PushButton") + refresh_pb.setMaximumWidth(50) + refresh_pb.clicked.connect(self.on_refresh_clicked) + lo_R.addWidget(refresh_pb) + lo_R.addSpacing(10) + + lo_L = QtWidgets.QVBoxLayout() + lo_L.addLayout(lo_L1) + lo_L.addSpacing(5) + lo_L.addLayout(lo_L2) + + lo = QtWidgets.QHBoxLayout() + lo.addLayout(lo_L) + lo.addStretch() + lo.addLayout(lo_R) + self.centralWidget().layout().addLayout(lo) + + def _reset_feat_select_items(self): + feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") + feat_combo.disconnect() + feat_combo.clear() + feat_combo.addItems(["Raw", "Mapping"]) # TODO: Put these in features_settings + # TODO: feat_combo.addItems(self._features_settings['features'].keys()) + feat_combo.currentIndexChanged.connect(lambda idx: self.reset_stack()) + feat_combo.setCurrentIndex(0) + + def _reset_chan_select_items(self): + chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + chan_combo.disconnect() + chan_combo.clear() + chan_combo.addItem("None") + # TODO: chan_combo.addItems(self._depth_settings['electrode_settings'].keys()) + chan_combo.currentIndexChanged.connect(lambda idx: self.reset_stack()) + chan_combo.setCurrentIndex(0) + + def _reset_widget_stack(self): + plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") + if plot_stack is None: + plot_stack = QtWidgets.QStackedWidget() + plot_stack.setObjectName("Plot_Stack") + self.centralWidget().layout().addWidget(plot_stack) + + to_delete = [plot_stack.widget(_) for _ in range(plot_stack.count())] + for wid in to_delete: + plot_stack.removeWidget(wid) + wid.deleteLater() - # Override create actions to call the clean_exit script - def create_actions(self): - # Actions - self.actions = { - 'Connect': QAction("Connect", self), - 'Quit': QAction("Quit", self), - 'AddPlot': QAction("Add Plot", self) - } - self.actions['Connect'].triggered.connect(self.on_action_connect_triggered) - self.actions['Quit'].triggered.connect(self.clean_exit) - self.actions['AddPlot'].triggered.connect(self.on_action_add_plot_triggered) - - def clean_exit(self): - if self.plot_widget: - self.plot_widget.kill_processes() - - if self.plot_widget.awaiting_close: - del self.plot_widget - - QApplication.instance().quit() - - # defined in the CustomGUI class, is triggered when the "Add Plot" button - # is pressed in the default GUI (Connect, Add Plot, Quit) - def on_action_add_plot_triggered(self): - # Get all the available information for settings - # NSP info, None if not connected - sampling_group_id = SAMPLINGGROUPS.index(str(SAMPLINGRATE)) - self.group_info = self.cbsdk_conn.get_group_config(sampling_group_id) - - # we only need to set the default values for the depth buffer here since it requires electrode - # information. The rest accepts empty dicts - self.buffer_settings['sampling_rate'] = SAMPLINGRATE - self.buffer_settings['sampling_group_id'] = sampling_group_id - self.buffer_settings['buffer_length'] = '{:.3f}'.format(BUFFERLENGTH) - self.buffer_settings['sample_length'] = '{:.3f}'.format(SAMPLELENGTH) - self.buffer_settings['delay_buffer'] = '{:.3f}'.format(DELAYBUFFER) - self.buffer_settings['overwrite_depth'] = OVERWRITEDEPTH - self.buffer_settings['electrode_settings'] = {} - - if self.group_info: - for electrode in self.group_info: - self.buffer_settings['electrode_settings'][electrode['label'].decode('utf-8')] = DEPTHSETTINGS - - # Open settings dialog and update DBWrapper subject and settings dicts - if not self.manage_settings(): - return - - # Configure CB SDK connection - self.cbsdk_conn.cbsdk_config = { - 'reset': True, - 'get_continuous': True, - 'get_events': False, - 'get_comments': True, - 'buffer_parameter': { - 'comment_length': 10 - } + self._widget_stack = {} + plot_class_map = { + 'Raw': RawPlots, 'Mapping': MappingPlots, 'STN': STNPlots, 'LFP': LFPPlots, 'Spikes': SpikePlots, + None: NullPlotWidget } + n_feats = len(self._features_settings['features']) + for chan_ix, chan_label in enumerate(["None"] + list(self._depth_settings['electrode_settings'].keys())): + self._widget_stack[chan_label] = {} + for feat_ix, feat_label in enumerate(self._features_settings['features'].keys()): + self._widget_stack[chan_label][feat_label] = [n_feats*chan_ix + feat_ix, 0] + w_cls = plot_class_map[feat_label] if feat_label in plot_class_map else NullPlotWidget + plot_stack.addWidget(w_cls)(dict(self.plot_config)) + plot_stack.setCurrentIndex(0) + + def _setup_shmem(self): + # shared memory to display the currently monitored electrode + self.monitored_channel_mem = QtCore.QSharedMemory() + self.monitored_channel_mem.setKey("MonitoredChannelMemory") + self.monitored_channel_mem.attach(QtCore.QSharedMemory.ReadOnly) - # NSP Buffer widget - # this widget handles the process creation that scans depth values, buffers the data and sends it to the DB. - self.plot_widget = FeaturesPlotWidget(self.group_info) - self.plot_widget.was_closed.connect(self.clean_exit) - self.plot_widget.call_manage_settings.connect(self.get_settings) - - # send values to widgets - self.send_settings() - - # The custom GUI class has an update function, which calls the - # do_plot_update function. This function then calls the update - # function of all display widgets. - def do_plot_update(self): - # since all widgets have different use, they will each handle their own data collection. - self.plot_widget.update() - - def manage_settings(self): - # Open prompt to input subject details - win = SettingsDialog(self.subject_settings, - self.procedure_settings, - self.buffer_settings, - self.features_settings) - result = win.exec_() - if result == QDialog.Accepted: - win.update_settings() + def manage_depth_process(self, on_off): + # start process + if on_off and not self._depth_process_running: + self._depth_wrapper.start_worker() + self._depth_process_running = True else: - return False + self._depth_wrapper.kill_worker() + self._depth_process_running = False - # Create or load subject - # Returns subject_id/-1 whether subject is properly created or not - sub_id = self.db_wrapper.load_or_create_subject(self.subject_settings) - - if sub_id == -1: - print("Subject not created.") - return False + def manage_feature_process(self, on_off): + if on_off and not self._features_process_running: + self._features_wrapper.start_worker() + self._features_process_running = True else: - self.subject_settings['subject_id'] = sub_id - self.procedure_settings['subject_id'] = sub_id - proc_id = self.db_wrapper.load_or_create_procedure(self.procedure_settings) - - self.buffer_settings['procedure_id'] = proc_id - self.features_settings['procedure_id'] = proc_id - - return True - - def get_settings(self): - # open prompt and update values - if self.manage_settings(): - # update values to widgets - self.send_settings() - - def send_settings(self): - self.plot_widget.process_settings(self.subject_settings, - self.procedure_settings, - self.buffer_settings, - self.features_settings) - + self._features_wrapper.kill_worker() + self._features_process_running = False -class FeaturesPlotWidget(CustomWidget): - call_manage_settings = Signal() + def reset_stack(self): + plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") + chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") + if plot_stack is not None and chan_combo is not None and feat_combo is not None: + plot_stack.setCurrentIndex(self._widget_stack[chan_combo.currentText()][feat_combo.currentText()][0]) - def __init__(self, *args, **kwargs): - - # define status images - self.status_icons = { - -2: QPixmap(os.path.join(os.path.dirname(__file__), 'resources', 'icons', 'depth_status_delay.png')), - -1: QPixmap(os.path.join(os.path.dirname(__file__), 'resources', 'icons', 'depth_status_in_use.png')), - 1: QPixmap(os.path.join(os.path.dirname(__file__), 'resources', 'icons', 'depth_status_done.png')), - 0: QPixmap(os.path.join(os.path.dirname(__file__), 'resources', 'icons', 'depth_status_off.png')), - } - - # Settings - self.subject_settings = None - self.procedure_settings = None - self.depth_settings = None - self.features_settings = None - - # Plot options - self.plot_config = {} - self.y_range = uVRANGE - self.plot_stack = QStackedWidget() - # generate a dict {chan_label: {Feature:[stack idx, latest_datum]}} - self.stack_dict = {} + def manage_refresh(self): + plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") + chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") + stack_item = self._widget_stack[chan_combo.currentText()][feat_combo.currentText()] + plot_stack.widget(stack_item[0]).clear_plot() + stack_item[1] = 0 + + def on_range_edited(self): + range_edit = self.findChild(QtWidgets.QLineEdit, "Range_LineEdit") + self._plot_settings["y_range"] = float(range_edit.text()) + + def on_sweep_clicked(self): + sweep_control = self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox") + chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + hp_chk = self.findChild(QtWidgets.QCheckBox, "HP_CheckBox") + range_edit = self.findChild(QtWidgets.QLineEdit, "Range_LineEdit") + for _ in [chan_combo, hp_chk, range_edit]: + _.setEnabled(sweep_control.isChecked() and self.monitored_channel_mem.isAttached()) + if sweep_control.isChecked() and self.monitored_channel_mem.isAttached(): + self.read_from_shared_memory() - # shared memory to display the currently monitored electrode - self.monitored_channel_mem = QSharedMemory() - self.monitored_channel_mem.setKey("MonitoredChannelMemory") - self.monitored_channel_mem.attach(QSharedMemory.ReadOnly) - - # wrap up init - super(FeaturesPlotWidget, self).__init__(*args, **kwargs) - self.move(WINDOWDIMS_FEATURES[0], WINDOWDIMS_FEATURES[1]) - self.resize(WINDOWDIMS_FEATURES[2], WINDOWDIMS_FEATURES[3]) - self.setMaximumWidth(WINDOWDIMS_FEATURES[2]) - - # initialize plots - self.layout().addWidget(self.plot_stack) - self.refresh_axes() # Extra time on purpose. - - # Define and start processes - # will only start processes when settings are received - self.depth_wrapper = ProcessWrapper('Depth_Process') - self.depth_process_running = False - - self.features_wrapper = ProcessWrapper('Features_Process') - self.features_process_running = False - - def create_control_panel(self): - # define Qt GUI elements - layout = QHBoxLayout() - - layout_L = QVBoxLayout() - layout_L1 = QHBoxLayout() - - # layout_L1.addSpacing(10) - layout_L1.addWidget(QLabel("Electrode: ", alignment=Qt.AlignVCenter | Qt.AlignRight)) - # Channel selection - self.chan_select = QComboBox() - self.chan_select.addItem("None") - self.chan_select.setMinimumWidth(70) - self.chan_select.setEnabled(False) - layout_L1.addWidget(self.chan_select) - - layout_L1.addSpacing(20) - - # features selection - layout_L1.addWidget(QLabel("Feature set: ", alignment=Qt.AlignVCenter | Qt.AlignRight)) - self.feature_select = QComboBox() - self.feature_select.setMinimumWidth(60) - self.feature_select.addItems(['Raw', 'Mapping']) - self.feature_select.setCurrentIndex(0) - layout_L1.addWidget(self.feature_select) - - layout_L.addLayout(layout_L1) - layout_L.addSpacing(5) - - layout_L2 = QHBoxLayout() - layout_L2.addSpacing(10) - layout_L2.addWidget(QLabel("+/- ", alignment=Qt.AlignVCenter | Qt.AlignRight)) - self.range_edit = QLineEdit("{:.2f}".format(uVRANGE)) - self.range_edit.setMaximumWidth(50) - layout_L2.addWidget(self.range_edit) - - layout_L2.addSpacing(30) - - self.do_hp = QCheckBox('HP') - self.do_hp.setChecked(True) - layout_L2.addWidget(self.do_hp) - - layout_L2.addSpacing(30) - - self.sweep_control = QCheckBox("Match SweepGUI.") - self.sweep_control.setChecked(True) - self.sweep_control.setEnabled(True) - layout_L2.addWidget(self.sweep_control) - - layout_L.addLayout(layout_L2) - - layout_R = QHBoxLayout() - - self.bt_refresh = QPushButton("Refresh") - self.bt_refresh.setMaximumWidth(50) - layout_R.addWidget(self.bt_refresh) - - layout_R.addSpacing(20) - - self.btn_settings = QPushButton("Settings") - self.btn_settings.setMaximumWidth(50) - layout_R.addWidget(self.btn_settings) - - layout_R.addSpacing(20) - - self.features_process_btn = QPushButton('Features') - self.features_process_btn.setMaximumWidth(50) - self.features_process_btn.setStyleSheet("QPushButton { color: white; " - "background-color : red; " - "border-color : red; " - "border-width: 2px}") - self.features_process_btn.clicked.connect(self.features_process_btn_callback) - layout_R.addWidget(self.features_process_btn) - - layout_R.addSpacing(5) - - self.depth_process_btn = QPushButton('Record') - self.depth_process_btn.setMaximumWidth(50) - self.depth_process_btn.setStyleSheet("QPushButton { color: white; " - "background-color : red; " - "border-color : red; " - "border-width: 2px}") - self.depth_process_btn.clicked.connect(self.depth_process_btn_callback) - layout_R.addWidget(self.depth_process_btn) - - layout_R.addSpacing(20) - - self.status_label = QLabel() - self.status_label.setPixmap(self.status_icons[0]) - layout_R.addWidget(self.status_label) - layout_R.addSpacing(10) - - layout.addLayout(layout_L) - layout.addStretch() - layout.addLayout(layout_R) - - # layout.addSpacing(10) - self.layout().addLayout(layout) - - # callbacks - self.btn_settings.clicked.connect(self.call_manage_settings.emit) - self.chan_select.currentIndexChanged.connect(self.manage_feat_chan_select) - self.feature_select.currentIndexChanged.connect(self.manage_feat_chan_select) - self.sweep_control.clicked.connect(self.manage_sweep_control) - self.range_edit.editingFinished.connect(self.manage_range_edit) - self.bt_refresh.clicked.connect(self.manage_refresh) - - def depth_process_btn_callback(self): + def on_record_clicked(self): # kill - if self.depth_process_running: + if self._depth_process_running: self.manage_depth_process(False) self.manage_nsp(False) else: # if we terminate and re-start the processes, we need to re-enable the shared memory - self.depth_wrapper.manage_shared_memory() + self._depth_wrapper.manage_shared_memory() # re-send the settings - self.depth_wrapper.send_settings(self.depth_settings) + self._depth_wrapper.send_settings(self.depth_settings) # start nsp recording if self.manage_nsp(True) == 0: # re-start the worker self.manage_depth_process(True) - def features_process_btn_callback(self): + def on_features_process_clicked(self): # kill - if self.features_process_running: + if self._features_process_running: self.manage_feature_process(False) else: # if we terminate and re-start the processes, we need to re-enable the shared memory - self.features_wrapper.manage_shared_memory() + self._features_wrapper.manage_shared_memory() # re-send the settings - self.features_wrapper.send_settings(self.features_settings) + self._features_wrapper.send_settings(self.features_settings) # re-start the worker self.manage_feature_process(True) - # GUI Callbacks - def manage_feat_chan_select(self): - self.plot_stack.setCurrentIndex( - self.stack_dict[self.chan_select.currentText()][self.feature_select.currentText()][0]) - - def manage_sweep_control(self): - if self.sweep_control.isChecked() and self.monitored_channel_mem.isAttached(): - self.chan_select.setEnabled(False) - self.do_hp.setEnabled(False) - self.range_edit.setEnabled(False) - self.read_from_shared_memory() - else: - self.chan_select.setEnabled(True) - self.do_hp.setEnabled(True) - self.range_edit.setEnabled(True) - - def manage_range_edit(self): - # need to do it like this because if we simply read the QLineEdit.text() on update calls, it breaks during - # typing the new range values. - self.y_range = float(self.range_edit.text()) - - @staticmethod - def parse_patient_name(full_name): - # parse the subject information - names = full_name.split(' ') - m_name = '' - m_idx = -1 - l_idx = -1 - for idx, n in enumerate(names): - if all([x.isupper() for x in n]): - m_idx = idx - l_idx = idx + 1 - m_name = n - break - - f_name = str.join(' ', names[:m_idx]) - l_name = str.join(' ', names[l_idx:]) - return f_name, m_name, l_name - - def manage_nsp(self, on_off): - f_name, m_name, l_name = self.parse_patient_name(self.subject_settings['name']) - file_info = {'filename': os.path.normpath(os.path.join(BASEPATH, - self.subject_settings['id'], - self.procedure_settings['date'].strftime('%m%d%y') + '_' + - self.subject_settings['id'] + '_' + - self.procedure_settings['target_name'] + '_' + - self.procedure_settings['recording_config'])), - 'comment': self.subject_settings['NSP_comment'], - 'patient_info': {'ID': self.subject_settings['id'], - # if only single name, returned in l_name - 'firstname': f_name if f_name else l_name, - 'middlename': m_name, # TODO: implement MiddleName - 'lastname': l_name, - 'DOBMonth': self.subject_settings['birthday'].month, - 'DOBDay': self.subject_settings['birthday'].day, - 'DOBYear': self.subject_settings['birthday'].year - }} - - if not CbSdkConnection().is_connected: - CbSdkConnection().connect() - - return CbSdkConnection().set_recording_state(on_off, file_info) - - def manage_depth_process(self, on_off): - # start process - if on_off and not self.depth_process_running: - self.depth_wrapper.start_worker() - self.depth_process_running = True - else: - self.depth_wrapper.kill_worker() - self.depth_process_running = False - - def manage_feature_process(self, on_off): - if on_off and not self.features_process_running: - self.features_wrapper.start_worker() - self.features_process_running = True - else: - self.features_wrapper.kill_worker() - self.features_process_running = False - - def manage_refresh(self): - self.plot_stack.widget( - self.stack_dict[self.chan_select.currentText()][self.feature_select.currentText()][0]).clear_plot() - self.stack_dict[self.chan_select.currentText()][self.feature_select.currentText()][1] = 0 - - def process_settings(self, sub_sett, proc_sett, depth_sett, feat_sett): - self.subject_settings = dict(sub_sett) - self.procedure_settings = dict(proc_sett) - self.depth_settings = dict(depth_sett) - self.features_settings = dict(feat_sett) - - # validate that we have some data in the electrode_settings. If the NSP is not connected we will have - # to load the channel names from the DB. Also we want to keep the FeaturesGUI unaware of the DB channels. - if len(self.depth_settings['electrode_settings']) == 0: - self.depth_settings['electrode_settings'] = {} - for lbl in DBWrapper().list_channel_labels(): - self.depth_settings['electrode_settings'][lbl] = DEPTHSETTINGS - CbSdkConnection().is_simulating = True - - # set new features - self.feature_select.setCurrentIndex(0) # Raw - while self.feature_select.count() > 2: # Raw and Mapping - self.feature_select.removeItem(2) - self.feature_select.addItems(self.features_settings['features'].keys()) - - # set new channels - self.chan_select.setCurrentIndex(0) # None - while self.chan_select.count() > 1: - self.chan_select.removeItem(1) - self.chan_select.addItems(self.depth_settings['electrode_settings'].keys()) - - # clear and update stacked widget - to_delete = [self.plot_stack.widget(x) for x in range(self.plot_stack.count())] - for wid in to_delete: - self.plot_stack.removeWidget(wid) - wid.deleteLater() - wid = None - self.stack_dict = {} - self.create_plots() - - self.depth_wrapper.send_settings(self.depth_settings) - self.features_wrapper.send_settings(self.features_settings) - - if not self.features_process_running: - self.manage_feature_process(True) - - # self.clear() - self.read_from_shared_memory() - - def create_plots(self, theme='dark', **kwargs): - # Collect PlotWidget configuration - self.plot_config['theme'] = theme - self.plot_config['color_iterator'] = -1 - self.plot_config['x_range'] = XRANGE_FEATURES - self.plot_config['y_range'] = uVRANGE - self.plot_config['do_hp'] = True - - labels = [] - for ii in range(0, self.chan_select.count()): - labels.append(self.chan_select.itemText(ii)) - # labels.extend(self.channel_labels) - - features = [] - for ii in range(0, self.feature_select.count()): - features.append(self.feature_select.itemText(ii)) - - stack_idx = 0 - for lbl_idx, lbl in enumerate(labels): - self.stack_dict[lbl] = {} - self.plot_config['color_iterator'] = lbl_idx - 1 - self.plot_config['title'] = lbl - for feat in features: - self.stack_dict[lbl][feat] = [stack_idx, 0] - # TODO: not hard-coding?? - if feat == 'Raw': - self.plot_stack.addWidget(RawPlots(dict(self.plot_config))) - elif feat == 'Mapping': - self.plot_stack.addWidget(MappingPlots(dict(self.plot_config))) - elif feat == 'STN': - self.plot_stack.addWidget(STNPlots(dict(self.plot_config))) - elif feat == 'LFP': - self.plot_stack.addWidget(LFPPlots(dict(self.plot_config))) - elif feat == 'Spikes': - self.plot_stack.addWidget(SpikePlots(dict(self.plot_config))) - else: - self.plot_stack.addWidget(NullPlotWidget(dict(self.plot_config))) - stack_idx += 1 - - self.plot_stack.setCurrentIndex(0) - - def refresh_axes(self): - pass - - def clear(self): - # set the current datum of all stacks to 0 - for lbl in self.stack_dict: - for feat in self.stack_dict[lbl]: - self.stack_dict[lbl][feat][1] = 0 - self.plot_stack.widget(self.stack_dict[lbl][feat][0]).clear_plot() - - def update(self): - # Depth process - output = self.depth_wrapper.worker_status() - self.status_label.setPixmap(self.status_icons[output]) - - if self.depth_wrapper.is_running(): - self.depth_process_btn.setStyleSheet("QPushButton { color: white; " - "background-color : green; " - "border-color : green; " - "border-width: 2px}") - else: - self.depth_process_running = False - self.depth_process_btn.setStyleSheet("QPushButton { color: white; " - "background-color : red; " - "border-color : red; " - "border-width: 2px}") - - if self.features_wrapper.is_running(): - self.features_process_btn.setStyleSheet("QPushButton { color: white; " - "background-color : green; " - "border-color : green; " - "border-width: 2px}") - else: - self.features_process_running = False - self.features_process_btn.setStyleSheet("QPushButton { color: white; " - "background-color : red; " - "border-color : red; " - "border-width: 2px}") - - if self.sweep_control.isChecked(): - self.read_from_shared_memory() - - # features plot - curr_chan_lbl = self.chan_select.currentText() - if curr_chan_lbl != 'None': - curr_feat = self.feature_select.currentText() - do_hp = self.do_hp.isChecked() - - if do_hp != self.plot_stack.currentWidget().plot_config['do_hp'] or \ - self.y_range != self.plot_stack.currentWidget().plot_config['y_range']: - self.plot_stack.currentWidget().clear_plot() - self.stack_dict[curr_chan_lbl][curr_feat][1] = 0 - self.plot_stack.currentWidget().plot_config['do_hp'] = do_hp - self.plot_stack.currentWidget().plot_config['y_range'] = self.y_range - - curr_datum = self.stack_dict[curr_chan_lbl][curr_feat][1] - if curr_feat == 'Raw': - all_data = DBWrapper().load_depth_data(chan_lbl=curr_chan_lbl, - gt=curr_datum, - do_hp=do_hp, - return_uV=True) - elif curr_feat == 'Mapping': - all_data = DBWrapper().load_mapping_response(chan_lbl=curr_chan_lbl, - gt=curr_datum) - else: - all_data = DBWrapper().load_features_data(category=curr_feat, - chan_lbl=curr_chan_lbl, - gt=curr_datum) - if all_data: - self.plot_stack.currentWidget().update_plot(dict(all_data)) - self.stack_dict[curr_chan_lbl][curr_feat][1] = max(all_data.keys()) - - def kill_processes(self): - self.manage_depth_process(False) - self.manage_feature_process(False) - def read_from_shared_memory(self): + import numpy as np if self.monitored_channel_mem.isAttached(): self.monitored_channel_mem.lock() settings = np.frombuffer(self.monitored_channel_mem.data(), dtype=np.float)[-3:] @@ -602,17 +294,21 @@ def read_from_shared_memory(self): self.manage_sweep_control() -def main(): - _ = QApplication(sys.argv) - window = FeaturesGUI() +def main(**kwargs): + app = QtWidgets.QApplication(sys.argv) + window = FeaturesGUI(**kwargs) window.show() - timer = QTimer() + timer = QtCore.QTimer() timer.timeout.connect(window.update) timer.start(100) - if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): - QApplication.instance().exec_() + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + sys.exit(app.exec_()) if __name__ == '__main__': - main() + parser = argparse.ArgumentParser(prog="FeaturesGUI", + description="Visualize MER trajectory segments and features.") + parser.add_argument('-i', '--ini_file', nargs='?', help="Path to ini settings file.") + args = parser.parse_args() + main(**args.__dict__) diff --git a/neuroport_dbs/FeaturesGUI_old.py b/neuroport_dbs/FeaturesGUI_old.py new file mode 100644 index 0000000..78ac753 --- /dev/null +++ b/neuroport_dbs/FeaturesGUI_old.py @@ -0,0 +1,262 @@ +import os +import sys +import numpy as np +from qtpy import QtCore, QtWidgets, QtGui +from cerebuswrapper import CbSdkConnection + +# Note: If import dbsgui fails, then set the working directory to be this script's directory. +from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget +from neuroport_dbs.feature_plots import * +from neuroport_dbs.SettingsDialog import SettingsDialog + +from serf.tools.db_wrap import DBWrapper, ProcessWrapper + +# Settings +from neuroport_dbs.settings.defaults import uVRANGE, BASEPATH, SAMPLINGRATE, \ + BUFFERLENGTH, SAMPLELENGTH, DELAYBUFFER, OVERWRITEDEPTH, DEPTHSETTINGS + + +# TODO: Rewrite FeaturesGUI so it doesn't inherit from CustomGUI +# Non-patient settings from INI (including data-source, but only used for recording_mirror) +# Build UI +# Launch modal settings window on startup +# Manage sub-processes +# All data interaction (other than starting stopping recording_mirror) should happen through DB + + +class FeaturesGUI(CustomGUI): + + def __init__(self): + # settings dictionaries + self.subject_settings = {} + self.procedure_settings = {} + self.features_settings = {} + + super().__init__() + # Calls restore_from_settings + # --> Eventually try_reset_widget will be called and allowed to proceed because both + # self.data_source (set via on_source_connected cb) and + # self.plot_config (setter at end of restore_from_settings are both set. + # --> Creates self._plot_widget = widget_cls(self.data_source.data_stats, **self.plot_config) + self.setWindowTitle('FeaturesGUI') + + # DB wrapper + self.db_wrapper = DBWrapper() + + @CustomGUI.widget_cls.getter + def widget_cls(self): + return FeaturesPlotWidget + + def on_plot_closed(self): + if self.plot_widget: + self.plot_widget.kill_processes() + + if self.plot_widget.awaiting_close: + del self.plot_widget + + QtWidgets.QApplication.instance().quit() + + # defined in the CustomGUI class, is triggered when the "Add Plot" button + # is pressed in the default GUI (Connect, Add Plot, Quit) + def on_action_add_plot_triggered(self): + # Get all the available information for settings + # NSP info, None if not connected + sampling_group_id = SAMPLINGGROUPS.index(str(SAMPLINGRATE)) + self.group_info = self.cbsdk_conn.get_group_config(sampling_group_id) + + # we only need to set the default values for the depth buffer here since it requires electrode + # information. The rest accepts empty dicts + self._plot_config['buffer']['sampling_rate'] = SAMPLINGRATE + self._plot_config['buffer']['sampling_group_id'] = sampling_group_id + self._plot_config['buffer']['buffer_length'] = '{:.3f}'.format(BUFFERLENGTH) + self._plot_config['buffer']['sample_length'] = '{:.3f}'.format(SAMPLELENGTH) + self._plot_config['buffer']['delay_buffer'] = '{:.3f}'.format(DELAYBUFFER) + self._plot_config['buffer']['overwrite_depth'] = OVERWRITEDEPTH + self._plot_config['buffer']['electrode_settings'] = {} + + if self.group_info: + for electrode in self.group_info: + self._plot_config['buffer']['electrode_settings'][electrode['label'].decode('utf-8')] = DEPTHSETTINGS + + # Open settings dialog and update DBWrapper subject and settings dicts + if not self.manage_settings(): + return + + self._plot_widget.call_manage_settings.connect(self.get_settings) + + # send values to widgets + self.send_settings() + + def do_plot_update(self): + # since all widgets have different use, they will each handle their own data collection. + self._plot_widget.update() + + def manage_settings(self): + # Open prompt to input subject details + win = SettingsDialog(self.subject_settings, + self.procedure_settings, + self._plot_config['buffer'], + self.features_settings) + result = win.exec_() + if result == QtWidgets.QDialog.Accepted: + win.update_settings() + else: + return False + + # Create or load subject + # Returns subject_id/-1 whether subject is properly created or not + sub_id = self.db_wrapper.load_or_create_subject(self.subject_settings) + + if sub_id == -1: + print("Subject not created.") + return False + else: + self.subject_settings['subject_id'] = sub_id + self.procedure_settings['subject_id'] = sub_id + proc_id = self.db_wrapper.load_or_create_procedure(self.procedure_settings) + + self._plot_config['buffer']['procedure_id'] = proc_id + self.features_settings['procedure_id'] = proc_id + + return True + + def get_settings(self): + # open prompt and update values + if self.manage_settings(): + # update values to widgets + self.send_settings() + + def send_settings(self): + self.plot_widget.process_settings(self.subject_settings, + self.procedure_settings, + self._plot_config['buffer'], + self.features_settings) + + +class FeaturesPlotWidget(CustomWidget): + + def __init__(self, *args, **kwargs): + + # Settings + self.subject_settings = None + self.procedure_settings = None + self.depth_settings = None + self.features_settings = None + + # Plot options + self.plot_config = {} + self.plot_stack = QtWidgets.QStackedWidget() + # generate a dict {chan_label: {Feature:[stack idx, latest_datum]}} + self.stack_dict = {} + + # wrap up init + super().__init__(*args, **kwargs) + + # initialize plots + self.layout().addWidget(self.plot_stack) + self.refresh_axes() # Extra time on purpose. + + # Define and start processes + # will only start processes when settings are received + self.depth_wrapper = ProcessWrapper('Depth_Process') + self.depth_process_running = False + + self.features_wrapper = ProcessWrapper('Features_Process') + self.features_process_running = False + + def process_settings(self, sub_sett, proc_sett, depth_sett, feat_sett): + self.subject_settings = dict(sub_sett) + self.procedure_settings = dict(proc_sett) + self.depth_settings = dict(depth_sett) + self.features_settings = dict(feat_sett) + + # validate that we have some data in the electrode_settings. If the NSP is not connected we will have + # to load the channel names from the DB. Also we want to keep the FeaturesGUI unaware of the DB channels. + if len(self.depth_settings['electrode_settings']) == 0: + self.depth_settings['electrode_settings'] = {} + for lbl in DBWrapper().list_channel_labels(): + self.depth_settings['electrode_settings'][lbl] = DEPTHSETTINGS + CbSdkConnection().is_simulating = True + + # set new features + self.feature_select.setCurrentIndex(0) # Raw + while self.feature_select.count() > 2: # Raw and Mapping + self.feature_select.removeItem(2) + self.feature_select.addItems(self.features_settings['features'].keys()) + + # set new channels + self.chan_select.setCurrentIndex(0) # None + while self.chan_select.count() > 1: + self.chan_select.removeItem(1) + self.chan_select.addItems(self.depth_settings['electrode_settings'].keys()) + + # clear and update stacked widget + to_delete = [self.plot_stack.widget(x) for x in range(self.plot_stack.count())] + for wid in to_delete: + self.plot_stack.removeWidget(wid) + wid.deleteLater() + wid = None + self.stack_dict = {} + self.create_plots() + + self.depth_wrapper.send_settings(self.depth_settings) + self.features_wrapper.send_settings(self.features_settings) + + if not self.features_process_running: + self.manage_feature_process(True) + + # self.clear() + self.read_from_shared_memory() + + def clear(self): + # set the current datum of all stacks to 0 + for lbl in self.stack_dict: + for feat in self.stack_dict[lbl]: + self.stack_dict[lbl][feat][1] = 0 + self.plot_stack.widget(self.stack_dict[lbl][feat][0]).clear_plot() + + def update(self): + # features plot + curr_chan_lbl = self.chan_select.currentText() + if curr_chan_lbl != 'None': + curr_feat = self.feature_select.currentText() + do_hp = self.do_hp.isChecked() + + if do_hp != self.plot_stack.currentWidget().plot_config['do_hp'] or \ + self.y_range != self.plot_stack.currentWidget().plot_config['y_range']: + self.plot_stack.currentWidget().clear_plot() + self.stack_dict[curr_chan_lbl][curr_feat][1] = 0 + self.plot_stack.currentWidget().plot_config['do_hp'] = do_hp + self.plot_stack.currentWidget().plot_config['y_range'] = self.y_range + + curr_datum = self.stack_dict[curr_chan_lbl][curr_feat][1] + if curr_feat == 'Raw': + all_data = DBWrapper().load_depth_data(chan_lbl=curr_chan_lbl, + gt=curr_datum, + do_hp=do_hp, + return_uV=True) + elif curr_feat == 'Mapping': + all_data = DBWrapper().load_mapping_response(chan_lbl=curr_chan_lbl, + gt=curr_datum) + else: + all_data = DBWrapper().load_features_data(category=curr_feat, + chan_lbl=curr_chan_lbl, + gt=curr_datum) + if all_data: + self.plot_stack.currentWidget().update_plot(dict(all_data)) + self.stack_dict[curr_chan_lbl][curr_feat][1] = max(all_data.keys()) + +def main(): + _ = QtWidgets.QApplication(sys.argv) + window = FeaturesGUI() + window.show() + timer = QtCore.QTimer() + timer.timeout.connect(window.update) + timer.start(100) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + + +if __name__ == '__main__': + main() diff --git a/neuroport_dbs/ProcessGUI.py b/neuroport_dbs/ProcessGUI.py new file mode 100644 index 0000000..2d24932 --- /dev/null +++ b/neuroport_dbs/ProcessGUI.py @@ -0,0 +1,256 @@ +import sys +import argparse +from pathlib import Path +from qtpy import QtCore, QtWidgets, QtGui +from neuroport_dbs.settings import defaults, parse_ini_try_numeric, locate_ini +from neuroport_dbs.SettingsDialog import SettingsDialog +from serf.tools.db_wrap import DBWrapper, ProcessWrapper + + +class ProcessGUI(QtWidgets.QMainWindow): + + def __init__(self, ini_file: str = None, **kwargs): + super().__init__(**kwargs) + self.status_icons = { + k: QtGui.QPixmap(str(Path(__file__).parents[0] / 'resources' / 'icons' / (v + '.png'))) for k, v in + ((-2, 'depth_status_delay'), (-1, 'depth_status_in_use'), (1, 'depth_status_done'), (0, 'depth_status_off')) + } + self._restore_from_settings(ini_file) + self._setup_ui() + self._setup_subproc() + + self._subject_settings = {} + self._procedure_settings = {} + self._buffer_settings = {} + self._features_settings = {} + + self._do_modal_settings() + + def closeEvent(self, *args, **kwargs): + self.run_record_process(False) + self.run_feature_process(False) + self._save_settings() + + def _restore_from_settings(self, ini_file=None): + # Infer path to ini + ini_name = ini_file if ini_file is not None else (type(self).__name__ + '.ini') + self._settings_path = locate_ini(ini_name) + settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) + default_dims = defaults.WINDOWDIMS_DICT[type(self).__name__] + settings.beginGroup("MainWindow") + self.move(settings.value("pos", QtCore.QPoint(default_dims[0], default_dims[1]))) + size_xy = settings.value("size", QtCore.QSize(default_dims[2], default_dims[3])) + self.resize(size_xy) + self.setMaximumWidth(size_xy.width()) + if settings.value("fullScreen", 'false') == 'true': + self.showFullScreen() + elif settings.value("maximized", 'false') == 'true': + self.showMaximized() + if settings.value("frameless", 'false') == 'true': + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + settings.endGroup() + + # TODO: Other settings + + def _save_settings(self): + if self._settings_path.parents[0] == 'config' and self._settings_path.parents[1] == 'resources': + # If this was loaded with the shipped settings, then write a new one in ~/.dbs_suite + home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) + self._settings_path = home_dir / '.dbs_suite' / self._settings_path.name + + settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) + + # Save MainWindow geometry. + settings.beginGroup("MainWindow") + settings.setValue("fullScreen", self.isFullScreen()) + settings.setValue("maximized", self.isMaximized()) + if not self.isFullScreen() and not self.isMaximized(): + settings.setValue("size", self.size()) + settings.setValue("pos", self.pos()) + settings.endGroup() + + # TODO: Save more settings. + + settings.sync() + + def _setup_ui(self): + main_widget = QtWidgets.QWidget() + main_widget.setLayout(QtWidgets.QVBoxLayout()) + self.setCentralWidget(main_widget) + self._setup_control_panel() + + def _setup_control_panel(self): + lo = QtWidgets.QHBoxLayout() + # Settings button + settings_pb = QtWidgets.QPushButton("Settings") + settings_pb.setObjectName("Settings_PushButton") + settings_pb.setMaximumWidth(50) + settings_pb.clicked.connect(lambda state: self._do_modal_settings()) + lo.addWidget(settings_pb) + # Features button + lo.addSpacing(20) + features_pb = QtWidgets.QPushButton("Features") + features_pb.setObjectName("Features_PushButton") + features_pb.setMaximumWidth(50) + features_pb.clicked.connect(self._on_features_process_clicked) + lo.addWidget(features_pb) + # Record button + lo.addSpacing(5) + record_pb = QtWidgets.QPushButton("Record") + record_pb.setObjectName("Record_PushButton") + record_pb.setMaximumWidth(50) + record_pb.clicked.connect(self._on_record_clicked) + lo.addWidget(record_pb) + # Status Label + lo.addSpacing(20) + status_label = QtWidgets.QLabel() + status_label.setObjectName("Status_Label") + status_label.setPixmap(self.status_icons[0]) + lo.addWidget(status_label) + + self.centralWidget().layout().addLayout(lo) + + def _setup_subproc(self): + # Define and start processes + # will only start processes when settings are received + self._record_wrapper = ProcessWrapper('Depth_Process') + self._record_process_running = False + + self._features_wrapper = ProcessWrapper('Features_Process') + self._features_process_running = False + + def _do_modal_settings(self): + win = SettingsDialog(self._subject_settings, + self._procedure_settings, + self._buffer_settings, + self._features_settings) + result = win.exec_() + if result == QtWidgets.QDialog.Accepted: + win.update_settings() + else: + return False + + def _on_features_process_clicked(self): + # kill + if self._features_process_running: + self.run_feature_process(False) + else: + # if we terminate and re-start the processes, we need to re-enable the shared memory + self._features_wrapper.manage_shared_memory() + + # re-send the settings + self._features_wrapper.send_settings(self.features_settings) + + # re-start the worker + self.run_feature_process(True) + + def _on_record_clicked(self): + # kill + if self._record_wrapper.is_running(): + self.run_record_process(False) + self._run_recording(False) + else: + # if we terminate and re-start the processes, we need to re-enable the shared memory + self._record_wrapper.manage_shared_memory() + + # re-send the settings + self._record_wrapper.send_settings(self.depth_settings) + + # start nsp recording + if self._run_recording(True) == 0: + # re-start the worker + self.run_record_process(True) + + def run_record_process(self, on_off: bool): + # start process + if on_off and not self._record_wrapper.is_running(): + self._record_wrapper.start_worker() + else: + self._record_wrapper.kill_worker() + + def run_feature_process(self, on_off: bool): + if on_off and not self._features_process_running: + self._features_wrapper.start_worker() + self._features_process_running = True + else: + self._features_wrapper.kill_worker() + self._features_process_running = False + + def _run_recording(self, on_off: bool): + # TODO: Use settings + import os + f_name, m_name, l_name = self.parse_patient_name(self.subject_settings['name']) + file_info = {'filename': os.path.normpath(os.path.join(BASEPATH, + self.subject_settings['id'], + self.procedure_settings['date'].strftime('%m%d%y') + '_' + + self.subject_settings['id'] + '_' + + self.procedure_settings['target_name'] + '_' + + self.procedure_settings['recording_config'])), + 'comment': self.subject_settings['NSP_comment'], + 'patient_info': {'ID': self.subject_settings['id'], + # if only single name, returned in l_name + 'firstname': f_name if f_name else l_name, + 'middlename': m_name, # TODO: implement MiddleName + 'lastname': l_name, + 'DOBMonth': self.subject_settings['birthday'].month, + 'DOBDay': self.subject_settings['birthday'].day, + 'DOBYear': self.subject_settings['birthday'].year + }} + return self._data_source.set_recording_state(on_off, file_info) + + @staticmethod + def parse_patient_name(full_name): + # parse the subject information + names = full_name.split(' ') + m_name = '' + m_idx = -1 + l_idx = -1 + for idx, n in enumerate(names): + if all([x.isupper() for x in n]): + m_idx = idx + l_idx = idx + 1 + m_name = n + break + + f_name = str.join(' ', names[:m_idx]) + l_name = str.join(' ', names[l_idx:]) + return f_name, m_name, l_name + + def update(self): + status_label = self.findChild(QtWidgets.QLabel, "Status_Label") + record_pb = self.findChild(QtWidgets.QPushButton, "Record_PushButton") + features_pb = self.findChild(QtWidgets.QPushButton, "Features_PushButton") + + status_label.setPixmap(self.status_icons[self._record_wrapper.worker_status()]) + + rec_facecolor = "green" if self._record_wrapper.is_running() else "red" + record_pb.setStyleSheet("QPushButton { color: white; " + f"background-color : {rec_facecolor}; " + f"border-color : {rec_facecolor}; " + "border-width: 2px}") + + feat_facecolor = "green" if self._features_wrapper.is_running() else "red" + features_pb.setStyleSheet("QPushButton { color: white; " + f"background-color : {feat_facecolor}; " + f"border-color : {feat_facecolor}; " + "border-width: 2px}") + + +def main(**kwargs): + app = QtWidgets.QApplication(sys.argv) + window = ProcessGUI(**kwargs) + window.show() + timer = QtCore.QTimer() + timer.timeout.connect(window.update) + timer.start(100) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + sys.exit(app.exec_()) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog="ProcessGUI", + description="Manage subprocesses to store segments and compute features.") + parser.add_argument('-i', '--ini_file', nargs='?', help="Path to ini settings file.") + args = parser.parse_args() + main(**args.__dict__) diff --git a/neuroport_dbs/dbsgui/data_source/cerebus.py b/neuroport_dbs/dbsgui/data_source/cerebus.py index fd28974..61c2f5f 100644 --- a/neuroport_dbs/dbsgui/data_source/cerebus.py +++ b/neuroport_dbs/dbsgui/data_source/cerebus.py @@ -87,13 +87,19 @@ def get_waveforms(self, chan_info): def disconnect_requested(self): self._cbsdk_conn.cbsdk_config = {'reset': True, 'get_continuous': False, 'get_events': False, 'get_comments': False} + self._cbsdk_conn.disconnect() def update_monitor(self, chan_info, spike_only=False): - _cbsdk_conn = CbSdkConnection() - if _cbsdk_conn.is_connected: - _cbsdk_conn.monitor_chan(chan_info['src'], spike_only=spike_only) + if not self._cbsdk_conn.is_connected: + self._cbsdk_conn.connect() + self._cbsdk_conn.monitor_chan(chan_info['src'], spike_only=spike_only) def update_threshold(self, chan_info, new_value): - cbsdkconn = CbSdkConnection() - if cbsdkconn.is_connected: - cbsdkconn.set_channel_info(chan_info['chan_id'], {'spkthrlevel': new_value}) + if not self._cbsdk_conn.is_connected: + self._cbsdk_conn.connect() + self._cbsdk_conn.set_channel_info(chan_info['chan_id'], {'spkthrlevel': new_value}) + + def set_recording_state(self, on_off, file_info): + if not self._cbsdk_conn.is_connected: + self._cbsdk_conn.connect() + return self._cbsdk_conn.set_recording_state(on_off, file_info) diff --git a/neuroport_dbs/dbsgui/data_source/interface.py b/neuroport_dbs/dbsgui/data_source/interface.py index 94cb4db..bdd568c 100644 --- a/neuroport_dbs/dbsgui/data_source/interface.py +++ b/neuroport_dbs/dbsgui/data_source/interface.py @@ -32,3 +32,6 @@ def update_monitor(self, chan_info, spike_only=False): def update_threshold(self, chan_info, new_value): raise NotImplementedError("Sub-classes must implement a `update_threshold` method.") + + def set_recording_state(self, on_off, file_info): + raise NotImplementedError("Sub-classes must implement a `set_recording_state` method.") diff --git a/neuroport_dbs/dbsgui/my_widgets/custom.py b/neuroport_dbs/dbsgui/my_widgets/custom.py index ac33047..7cc928e 100644 --- a/neuroport_dbs/dbsgui/my_widgets/custom.py +++ b/neuroport_dbs/dbsgui/my_widgets/custom.py @@ -25,7 +25,7 @@ class CustomGUI(QtWidgets.QMainWindow): """ def __init__(self, ini_file=None): - super(CustomGUI, self).__init__() + super().__init__() # Infer path to ini ini_name = ini_file if ini_file is not None else (type(self).__name__ + '.ini') @@ -59,8 +59,10 @@ def restore_from_settings(self): # Restore size and position. default_dims = defaults.WINDOWDIMS_DICT[type(self).__name__] settings.beginGroup("MainWindow") - self.resize(settings.value("size", QtCore.QSize(default_dims[2], default_dims[3]))) self.move(settings.value("pos", QtCore.QPoint(default_dims[0], default_dims[1]))) + size_xy = settings.value("size", QtCore.QSize(default_dims[2], default_dims[3])) + self.resize(size_xy) + self.setMaximumWidth(size_xy.width()) if settings.value("fullScreen", 'false') == 'true': self.showFullScreen() elif settings.value("maximized", 'false') == 'true': @@ -122,7 +124,7 @@ def widget_cls(self): return NotImplemented # Child class must override this attribute def update(self): - super(CustomGUI, self).update() + super().update() if self.data_source.is_connected and self._plot_widget: self.do_plot_update() @@ -167,7 +169,7 @@ class CustomWidget(QtWidgets.QWidget): was_closed = QtCore.Signal() def __init__(self, source_dict, **kwargs): - super(CustomWidget, self).__init__() + super().__init__() # Init member variables self.awaiting_close = False @@ -192,7 +194,7 @@ def create_control_panel(self): cntrl_layout.addWidget(clear_button) self.layout().addLayout(cntrl_layout) - def create_plots(self, theme='dark', **kwargs): + def create_plots(self, **kwargs): raise TypeError("Must be implemented by sub-class.") def refresh_axes(self): @@ -202,6 +204,6 @@ def clear(self): raise TypeError("Must be implemented by sub-class.") def closeEvent(self, evnt): - super(CustomWidget, self).closeEvent(evnt) + super().closeEvent(evnt) self.awaiting_close = True self.was_closed.emit() diff --git a/neuroport_dbs/resources/config/FeaturesGUI.ini b/neuroport_dbs/resources/config/FeaturesGUI.ini new file mode 100644 index 0000000..c7f618b --- /dev/null +++ b/neuroport_dbs/resources/config/FeaturesGUI.ini @@ -0,0 +1,53 @@ +[MainWindow] +fullScreen=false +maximized=false +frameless=true +size=@Size(600 680) +pos=@Point(1320 400) +bg_color=#404040 + +[data-source] +class=CerebusDataSource +sampling_group=30000 +get_continuous=true +get_events=false +get_comments=true +buffer_parameter\comment_length=10 + +[buffer] +buffer_length=6.0 +sample_length=4.0 +delay_buffer=0.5 +overwrite_depth=true +threshold=true +validity=90.0 + +[plot] +x_start=-4000 +x_stop=120000 +y_range=250 +downsample=1 +n_segments = 20 +spk_aud=true +lock_threshold=true +unit_scaling=0.25 + +[theme] +bgcolor=black +labelcolor_active=yellow +labelsize_active=15 +labelsize_inactive=11 +threshcolor=yellow +threshwidth=1 +linewidth=1 +colormap=custom +pencolors\0\name=cyan +pencolors\1\value=#00ff00 +pencolors\2\name=magenta +pencolors\3\name=red +pencolors\4\name=yellow +pencolors\5\name=white + +[data-source-LSL-example] +class=LSLDataSource +identifier="{\"name\": \"MyAudioStream\", \"type\": \"audio\"}" \ No newline at end of file diff --git a/neuroport_dbs/resources/config/ProcessGUI.ini b/neuroport_dbs/resources/config/ProcessGUI.ini new file mode 100644 index 0000000..0ddf35b --- /dev/null +++ b/neuroport_dbs/resources/config/ProcessGUI.ini @@ -0,0 +1,14 @@ +[MainWindow] +fullScreen=false +maximized=false +frameless=true +size=@Size(600 680) +pos=@Point(1320 400) +bg_color=#404040 + +[data-source] +class=CerebusDataSource +basepath=C:\\Recordings + +[segment-source] +class=SERF diff --git a/neuroport_dbs/settings/__init__.py b/neuroport_dbs/settings/__init__.py index e0aa51f..3c50d58 100644 --- a/neuroport_dbs/settings/__init__.py +++ b/neuroport_dbs/settings/__init__.py @@ -16,3 +16,22 @@ def parse_ini_try_numeric(settings, key): elif res == 'true': return True return res + + +def locate_ini(ini_name): + from pathlib import Path + from qtpy import QtCore + ini_path = Path(ini_name) + if ini_path.exists(): + _settings_path = ini_path + else: + # Try home / .dbs_suite first + home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) + ini_path = home_dir / '.dbs_suite' / ini_path.name + if ini_path.exists(): + _settings_path = ini_path + else: + # Use default ini that ships with module. + _settings_path = Path(__file__).parents[1] / 'resources' / 'config' / ini_path.name + return _settings_path + diff --git a/neuroport_dbs/settings/defaults.py b/neuroport_dbs/settings/defaults.py index 85e5b80..f44bbbf 100644 --- a/neuroport_dbs/settings/defaults.py +++ b/neuroport_dbs/settings/defaults.py @@ -27,10 +27,12 @@ WINDOWDIMS_WAVEFORMS = [920, 0, 300, 1080] WINDOWDIMS_MAPPING = [1220, 0, 100, 1080] WINDOWDIMS_DEPTH = [1320, 0, 600, 250] -WINDOWDIMS_FEATURES = [1320, 250, 600, 830] +WINDOWDIMS_SUBPROC = [1320, 250, 600, 150] +WINDOWDIMS_FEATURES = [1320, 400, 600, 680] WINDOWDIMS_DICT = { 'SweepGUI': WINDOWDIMS_SWEEP, 'RasterGUI': WINDOWDIMS_RASTER, 'WaveformGUI': WINDOWDIMS_WAVEFORMS, 'DepthGUI': WINDOWDIMS_DEPTH, + 'ProcessGUI': WINDOWDIMS_SUBPROC, 'FeaturesGUI': WINDOWDIMS_FEATURES, 'MappingGUI': WINDOWDIMS_MAPPING } From 2fdfec29f15dc3df8f046ba8d2225db16e7755b2 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 2 May 2022 17:00:44 -0400 Subject: [PATCH 13/65] Moved top-level windows out of scripts and into dbsgui sub-package --- .../{CommentsGUI.py => dbsgui/comments.py} | 27 +++----------- .../{DepthGUI.py => dbsgui/depth.py} | 28 ++++---------- .../{FeaturesGUI.py => dbsgui/features.py} | 31 ++-------------- .../{MappingGUI.py => dbsgui/mapping.py} | 27 +------------- .../{ProcessGUI.py => dbsgui/process.py} | 26 +------------ .../{RasterGUI.py => dbsgui/raster.py} | 37 ++++--------------- .../{SweepGUI.py => dbsgui/sweep.py} | 28 ++------------ .../{WaveformGUI.py => dbsgui/waveform.py} | 31 ++-------------- neuroport_dbs/scripts/CommentsGUI.py | 14 +++++++ neuroport_dbs/scripts/DepthGUI.py | 19 ++++++++++ neuroport_dbs/scripts/FeaturesGUI.py | 24 ++++++++++++ .../{ => scripts}/FeaturesGUI_old.py | 4 +- .../{ => scripts}/ImportNS5FeaturesGUI.py | 4 +- neuroport_dbs/scripts/MappingGUI.py | 19 ++++++++++ neuroport_dbs/scripts/ProcessGUI.py | 24 ++++++++++++ neuroport_dbs/scripts/RasterGUI.py | 18 +++++++++ neuroport_dbs/scripts/SweepGUI.py | 19 ++++++++++ neuroport_dbs/scripts/WaveformGUI.py | 18 +++++++++ neuroport_dbs/scripts/__init__.py | 0 19 files changed, 196 insertions(+), 202 deletions(-) rename neuroport_dbs/{CommentsGUI.py => dbsgui/comments.py} (78%) rename neuroport_dbs/{DepthGUI.py => dbsgui/depth.py} (94%) rename neuroport_dbs/{FeaturesGUI.py => dbsgui/features.py} (93%) rename neuroport_dbs/{MappingGUI.py => dbsgui/mapping.py} (91%) rename neuroport_dbs/{ProcessGUI.py => dbsgui/process.py} (92%) rename neuroport_dbs/{RasterGUI.py => dbsgui/raster.py} (90%) rename neuroport_dbs/{SweepGUI.py => dbsgui/sweep.py} (96%) rename neuroport_dbs/{WaveformGUI.py => dbsgui/waveform.py} (88%) create mode 100644 neuroport_dbs/scripts/CommentsGUI.py create mode 100644 neuroport_dbs/scripts/DepthGUI.py create mode 100644 neuroport_dbs/scripts/FeaturesGUI.py rename neuroport_dbs/{ => scripts}/FeaturesGUI_old.py (98%) rename neuroport_dbs/{ => scripts}/ImportNS5FeaturesGUI.py (98%) create mode 100644 neuroport_dbs/scripts/MappingGUI.py create mode 100644 neuroport_dbs/scripts/ProcessGUI.py create mode 100644 neuroport_dbs/scripts/RasterGUI.py create mode 100644 neuroport_dbs/scripts/SweepGUI.py create mode 100644 neuroport_dbs/scripts/WaveformGUI.py create mode 100644 neuroport_dbs/scripts/__init__.py diff --git a/neuroport_dbs/CommentsGUI.py b/neuroport_dbs/dbsgui/comments.py similarity index 78% rename from neuroport_dbs/CommentsGUI.py rename to neuroport_dbs/dbsgui/comments.py index 4581f77..4f1f5ae 100644 --- a/neuroport_dbs/CommentsGUI.py +++ b/neuroport_dbs/dbsgui/comments.py @@ -1,17 +1,14 @@ import os -import sys -from qtpy import QtCore, QtGui, QtWidgets +from qtpy import QtWidgets from qtpy import uic +import neuroport_dbs.dbsgui -sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) -import dbsgui -# Note: If import dbsgui fails, then set the working directory to be this script's directory. -ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui', 'my_widgets', 'ui') -Ui_MainWindow, QtBaseClass = uic.loadUiType(os.path.join(ui_path, 'send_comments.ui')) +Ui_MainWindow, QtBaseClass = uic.loadUiType(os.path.join(os.path.dirname(neuroport_dbs.dbsgui.widgets.ui.__file__), + 'send_comments.ui')) -class MyGUI(QtWidgets.QMainWindow, Ui_MainWindow): +class CommentsGUI(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self): QtWidgets.QMainWindow.__init__(self) @@ -37,7 +34,7 @@ def __init__(self): # CbSdkConnection().disconnect() def on_connect(self): - from dbsgui.my_widgets.cbsdk_connect import ConnectDialog + from ..dbsgui.widgets.cbsdk_connect import ConnectDialog dlg = ConnectDialog() if dlg.exec_() == QtWidgets.QDialog.Accepted: dlg.do_connect() @@ -69,15 +66,3 @@ def on_button(self, modality, region): comment_string = json.dumps(comment_dict, sort_keys=True) conn.set_comments(comment_string) self.statusBar().showMessage("Sent: " + comment_string) - - -def main(): - import sys - qapp = QtWidgets.QApplication(sys.argv) - window = MyGUI() - window.show() - sys.exit(qapp.exec_()) - - -if __name__ == '__main__': - main() diff --git a/neuroport_dbs/DepthGUI.py b/neuroport_dbs/dbsgui/depth.py similarity index 94% rename from neuroport_dbs/DepthGUI.py rename to neuroport_dbs/dbsgui/depth.py index 88e231b..e70d5cf 100644 --- a/neuroport_dbs/DepthGUI.py +++ b/neuroport_dbs/dbsgui/depth.py @@ -1,11 +1,13 @@ -import sys -from pathlib import Path from qtpy import QtCore, QtWidgets -from cerebuswrapper import CbSdkConnection +from pathlib import Path import pylsl import neuroport_dbs -from neuroport_dbs.settings import defaults -from neuroport_dbs.dbsgui.depth_source import CBSDKPlayback +from ..settings import defaults +from ..depth_source import CBSDKPlayback +try: + from cerebuswrapper import CbSdkConnection +except ModuleNotFoundError as e: + print(e, "Try `pip install git+https://github.com/SachsLab/cerebuswrapper.git`.") class DepthGUI(QtWidgets.QMainWindow): @@ -204,19 +206,3 @@ def update(self): def send(self): self.display_string = None # make sure the update function runs self.update() - - -def main(): - _ = QtWidgets.QApplication(sys.argv) - window = DepthGUI() - window.show() - timer = QtCore.QTimer() - timer.timeout.connect(window.update) - timer.start(100) - - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() - - -if __name__ == '__main__': - main() diff --git a/neuroport_dbs/FeaturesGUI.py b/neuroport_dbs/dbsgui/features.py similarity index 93% rename from neuroport_dbs/FeaturesGUI.py rename to neuroport_dbs/dbsgui/features.py index ef59446..d3be9f1 100644 --- a/neuroport_dbs/FeaturesGUI.py +++ b/neuroport_dbs/dbsgui/features.py @@ -1,11 +1,8 @@ -import sys -import argparse from pathlib import Path -from qtpy import QtCore, QtGui, QtWidgets -from neuroport_dbs.settings import defaults, parse_ini_try_numeric, locate_ini -from neuroport_dbs.SettingsDialog import SettingsDialog +from qtpy import QtCore, QtWidgets +from ..settings import defaults, locate_ini from serf.tools.db_wrap import DBWrapper, ProcessWrapper -from neuroport_dbs.feature_plots import * +from ..feature_plots import * class FeaturesGUI(QtWidgets.QMainWindow): @@ -291,24 +288,4 @@ def read_from_shared_memory(self): else: self.monitored_channel_mem.attach() # self.sweep_control.setChecked(False) - self.manage_sweep_control() - - -def main(**kwargs): - app = QtWidgets.QApplication(sys.argv) - window = FeaturesGUI(**kwargs) - window.show() - timer = QtCore.QTimer() - timer.timeout.connect(window.update) - timer.start(100) - - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - sys.exit(app.exec_()) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(prog="FeaturesGUI", - description="Visualize MER trajectory segments and features.") - parser.add_argument('-i', '--ini_file', nargs='?', help="Path to ini settings file.") - args = parser.parse_args() - main(**args.__dict__) + self.manage_sweep_control() \ No newline at end of file diff --git a/neuroport_dbs/MappingGUI.py b/neuroport_dbs/dbsgui/mapping.py similarity index 91% rename from neuroport_dbs/MappingGUI.py rename to neuroport_dbs/dbsgui/mapping.py index 1ff8ac9..b4eacd4 100644 --- a/neuroport_dbs/MappingGUI.py +++ b/neuroport_dbs/dbsgui/mapping.py @@ -1,16 +1,8 @@ -import sys import os -import qtpy import json -from qtpy import QtWidgets, QtGui, QtCore +from qtpy import QtWidgets, QtGui import pylsl - -sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) -# Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget - -# Import settings -# TODO: Make some of these settings configurable via UI elements +from neuroport_dbs.dbsgui.widgets.custom import CustomGUI, CustomWidget from neuroport_dbs.settings.defaults import MAPPINGSTIMULI @@ -226,18 +218,3 @@ def refresh_axes(self): def clear(self): pass - - -def main(): - _ = QtWidgets.QApplication(sys.argv) - aw = MappingGUI() - # timer = QTimer() - # timer.timeout.connect(aw.update) - # timer.start(1000) - - if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() - - -if __name__ == '__main__': - main() diff --git a/neuroport_dbs/ProcessGUI.py b/neuroport_dbs/dbsgui/process.py similarity index 92% rename from neuroport_dbs/ProcessGUI.py rename to neuroport_dbs/dbsgui/process.py index 2d24932..7c3d63b 100644 --- a/neuroport_dbs/ProcessGUI.py +++ b/neuroport_dbs/dbsgui/process.py @@ -1,9 +1,7 @@ -import sys -import argparse from pathlib import Path from qtpy import QtCore, QtWidgets, QtGui -from neuroport_dbs.settings import defaults, parse_ini_try_numeric, locate_ini -from neuroport_dbs.SettingsDialog import SettingsDialog +from neuroport_dbs.settings import defaults, locate_ini +from neuroport_dbs.dbsgui.widgets.SettingsDialog import SettingsDialog from serf.tools.db_wrap import DBWrapper, ProcessWrapper @@ -234,23 +232,3 @@ def update(self): f"background-color : {feat_facecolor}; " f"border-color : {feat_facecolor}; " "border-width: 2px}") - - -def main(**kwargs): - app = QtWidgets.QApplication(sys.argv) - window = ProcessGUI(**kwargs) - window.show() - timer = QtCore.QTimer() - timer.timeout.connect(window.update) - timer.start(100) - - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - sys.exit(app.exec_()) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(prog="ProcessGUI", - description="Manage subprocesses to store segments and compute features.") - parser.add_argument('-i', '--ini_file', nargs='?', help="Path to ini settings file.") - args = parser.parse_args() - main(**args.__dict__) diff --git a/neuroport_dbs/RasterGUI.py b/neuroport_dbs/dbsgui/raster.py similarity index 90% rename from neuroport_dbs/RasterGUI.py rename to neuroport_dbs/dbsgui/raster.py index 8dc739a..c3e61a1 100644 --- a/neuroport_dbs/RasterGUI.py +++ b/neuroport_dbs/dbsgui/raster.py @@ -1,15 +1,9 @@ -""" -chadwick.boulay@gmail.com -""" -import sys -import os import numpy as np -from qtpy import QtCore, QtGui, QtWidgets +from qtpy import QtCore, QtGui import pyqtgraph as pg -sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) -# Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, get_colormap -from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget, get_now_time +from ..dbsgui.utilities.pyqtgraph import parse_color_str, get_colormap +from ..dbsgui.widgets.custom import CustomGUI, CustomWidget +from ..data_source import get_now_time class RasterGUI(CustomGUI): @@ -113,7 +107,7 @@ def refresh_axes(self): for rs_key in self.rasters: plot = self.rasters[rs_key]['plot'] plot.setXRange(0, self.plot_config['x_range'] * self.samplingRate) - plot.setYRange(-0.05, self.plot_config['y_range']+0.05) + plot.setYRange(-0.05, self.plot_config['y_range'] + 0.05) plot.hideAxis('bottom') plot.hideAxis('left') @@ -123,7 +117,7 @@ def clear(self): rs = self.rasters[key] rs['old'].clear() rs['latest'].clear() - rs['old_timestamps'] = np.empty(0, dtype=np.uint32) # Row 1 to top row + rs['old_timestamps'] = np.empty(0, dtype=np.uint32) # Row 1 to top row rs['latest_timestamps'] = np.empty(0, dtype=np.uint32) # Bottom row rs['count'] = 0 rs['start_time'] = start_time @@ -158,7 +152,7 @@ def update(self, line_label, data): :return: """ rs = self.rasters[line_label] # A dictionary of info unique to each channel - + # Calculate timestamp of last sample in bottom row now_time = int(get_now_time()) new_r0_tmin = now_time - (now_time % self.x_lim) @@ -218,19 +212,4 @@ def update(self, line_label, data): samples_elapsed = max(now_time, rs['last_spike_time']) - rs['start_time'] if samples_elapsed > 0: frate = rs['count'] * self.samplingRate / samples_elapsed - self.modify_frate(line_label, frate) - - -def main(): - _ = QtWidgets.QApplication(sys.argv) - aw = RasterGUI() - timer = QtCore.QTimer() - timer.timeout.connect(aw.update) - timer.start(1) - - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() - - -if __name__ == '__main__': - main() + self.modify_frate(line_label, frate) \ No newline at end of file diff --git a/neuroport_dbs/SweepGUI.py b/neuroport_dbs/dbsgui/sweep.py similarity index 96% rename from neuroport_dbs/SweepGUI.py rename to neuroport_dbs/dbsgui/sweep.py index 0121740..03d5950 100644 --- a/neuroport_dbs/SweepGUI.py +++ b/neuroport_dbs/dbsgui/sweep.py @@ -1,15 +1,11 @@ -import sys -import os import numpy as np from scipy import signal import pyaudio -import qtpy -from qtpy import QtCore, QtWidgets, QtGui +from qtpy import QtCore, QtWidgets import pyqtgraph as pg -sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) -# Note: If import dbsgui fails, then set the working directory to be this script's directory. from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, make_qcolor, get_colormap -from neuroport_dbs.dbsgui.my_widgets.custom import CustomWidget, get_now_time, CustomGUI +from neuroport_dbs.dbsgui.widgets.custom import CustomWidget, CustomGUI +from neuroport_dbs.data_source import get_now_time class SweepGUI(CustomGUI): @@ -440,20 +436,4 @@ def update(self, line_label, data, ch_state=None): # old_y[np.where(old_bool)[0][-1]+1:] = 0 # Uncomment to zero out the end of the last seg. pci.setData(x=old_x, y=old_y) # Store last_sample_ix for next iteration. - self.segmented_series[line_label]['last_sample_ix'] = sample_indices[-1] - - -def main(): - from qtpy import QtWidgets, QtCore - _ = QtWidgets.QApplication(sys.argv) - aw = SweepGUI() - timer = QtCore.QTimer() - timer.timeout.connect(aw.update) - timer.start(1) - - if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() - - -if __name__ == '__main__': - main() + self.segmented_series[line_label]['last_sample_ix'] = sample_indices[-1] \ No newline at end of file diff --git a/neuroport_dbs/WaveformGUI.py b/neuroport_dbs/dbsgui/waveform.py similarity index 88% rename from neuroport_dbs/WaveformGUI.py rename to neuroport_dbs/dbsgui/waveform.py index f27797f..9f206a7 100644 --- a/neuroport_dbs/WaveformGUI.py +++ b/neuroport_dbs/dbsgui/waveform.py @@ -1,16 +1,8 @@ -""" -chadwick.boulay@gmail.com -""" -import sys -import os import numpy as np -from qtpy import QtCore, QtWidgets +from qtpy import QtWidgets import pyqtgraph as pg - -sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbsgui')) -# Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, make_qcolor, get_colormap -from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget +from .utilities.pyqtgraph import parse_color_str, get_colormap +from .widgets.custom import CustomGUI, CustomWidget class WaveformGUI(CustomGUI): @@ -169,19 +161,4 @@ def update(self, line_label, data): data_items = self.wf_info[line_label]['plot'].listDataItems() if len(data_items) > self.plot_config['n_waveforms']: for di in data_items[:-self.plot_config['n_waveforms']]: - self.wf_info[line_label]['plot'].removeItem(di) - - -def main(): - _ = QtWidgets.QApplication(sys.argv) - aw = WaveformGUI() - timer = QtCore.QTimer() - timer.timeout.connect(aw.update) - timer.start(1) - - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() - - -if __name__ == '__main__': - main() + self.wf_info[line_label]['plot'].removeItem(di) \ No newline at end of file diff --git a/neuroport_dbs/scripts/CommentsGUI.py b/neuroport_dbs/scripts/CommentsGUI.py new file mode 100644 index 0000000..7e5d192 --- /dev/null +++ b/neuroport_dbs/scripts/CommentsGUI.py @@ -0,0 +1,14 @@ +from qtpy import QtWidgets +from neuroport_dbs.dbsgui.comments import CommentsGUI + + +def main(): + import sys + qapp = QtWidgets.QApplication(sys.argv) + window = CommentsGUI() + window.show() + sys.exit(qapp.exec_()) + + +if __name__ == '__main__': + main() diff --git a/neuroport_dbs/scripts/DepthGUI.py b/neuroport_dbs/scripts/DepthGUI.py new file mode 100644 index 0000000..e8af766 --- /dev/null +++ b/neuroport_dbs/scripts/DepthGUI.py @@ -0,0 +1,19 @@ +import sys +from qtpy import QtCore, QtWidgets +from neuroport_dbs.dbsgui.depth import DepthGUI + + +def main(): + _ = QtWidgets.QApplication(sys.argv) + window = DepthGUI() + window.show() + timer = QtCore.QTimer() + timer.timeout.connect(window.update) + timer.start(100) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + + +if __name__ == '__main__': + main() diff --git a/neuroport_dbs/scripts/FeaturesGUI.py b/neuroport_dbs/scripts/FeaturesGUI.py new file mode 100644 index 0000000..3ca2fa5 --- /dev/null +++ b/neuroport_dbs/scripts/FeaturesGUI.py @@ -0,0 +1,24 @@ +import sys +import argparse +from qtpy import QtCore, QtWidgets +from neuroport_dbs.dbsgui.features import FeaturesGUI + + +def main(**kwargs): + app = QtWidgets.QApplication(sys.argv) + window = FeaturesGUI(**kwargs) + window.show() + timer = QtCore.QTimer() + timer.timeout.connect(window.update) + timer.start(100) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + sys.exit(app.exec_()) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog="FeaturesGUI", + description="Visualize MER trajectory segments and features.") + parser.add_argument('-i', '--ini_file', nargs='?', help="Path to ini settings file.") + args = parser.parse_args() + main(**args.__dict__) diff --git a/neuroport_dbs/FeaturesGUI_old.py b/neuroport_dbs/scripts/FeaturesGUI_old.py similarity index 98% rename from neuroport_dbs/FeaturesGUI_old.py rename to neuroport_dbs/scripts/FeaturesGUI_old.py index 78ac753..1a4c07b 100644 --- a/neuroport_dbs/FeaturesGUI_old.py +++ b/neuroport_dbs/scripts/FeaturesGUI_old.py @@ -5,9 +5,9 @@ from cerebuswrapper import CbSdkConnection # Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.dbsgui.my_widgets.custom import CustomGUI, CustomWidget +from neuroport_dbs.dbsgui.widgets.custom import CustomGUI, CustomWidget from neuroport_dbs.feature_plots import * -from neuroport_dbs.SettingsDialog import SettingsDialog +from neuroport_dbs.dbsgui.widgets.SettingsDialog import SettingsDialog from serf.tools.db_wrap import DBWrapper, ProcessWrapper diff --git a/neuroport_dbs/ImportNS5FeaturesGUI.py b/neuroport_dbs/scripts/ImportNS5FeaturesGUI.py similarity index 98% rename from neuroport_dbs/ImportNS5FeaturesGUI.py rename to neuroport_dbs/scripts/ImportNS5FeaturesGUI.py index 975875e..6ac5020 100644 --- a/neuroport_dbs/ImportNS5FeaturesGUI.py +++ b/neuroport_dbs/scripts/ImportNS5FeaturesGUI.py @@ -3,9 +3,9 @@ import os import sys import datetime -import regex as re +import re import numpy as np -from neuroport_dbs.SettingsDialog import SettingsDialog +from neuroport_dbs.dbsgui.widgets.SettingsDialog import SettingsDialog from qtpy.QtWidgets import QProgressDialog from qtpy.QtCore import Qt from serf.tools.db_wrap import DBWrapper diff --git a/neuroport_dbs/scripts/MappingGUI.py b/neuroport_dbs/scripts/MappingGUI.py new file mode 100644 index 0000000..752283e --- /dev/null +++ b/neuroport_dbs/scripts/MappingGUI.py @@ -0,0 +1,19 @@ +import sys +import qtpy +from qtpy import QtWidgets, QtCore +from neuroport_dbs.dbsgui.mapping import MappingGUI + + +def main(): + _ = QtWidgets.QApplication(sys.argv) + aw = MappingGUI() + # timer = QTimer() + # timer.timeout.connect(aw.update) + # timer.start(1000) + + if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + + +if __name__ == '__main__': + main() diff --git a/neuroport_dbs/scripts/ProcessGUI.py b/neuroport_dbs/scripts/ProcessGUI.py new file mode 100644 index 0000000..19decd0 --- /dev/null +++ b/neuroport_dbs/scripts/ProcessGUI.py @@ -0,0 +1,24 @@ +import sys +import argparse +from qtpy import QtCore, QtWidgets +from neuroport_dbs.dbsgui.process import ProcessGUI + + +def main(**kwargs): + app = QtWidgets.QApplication(sys.argv) + window = ProcessGUI(**kwargs) + window.show() + timer = QtCore.QTimer() + timer.timeout.connect(window.update) + timer.start(100) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + sys.exit(app.exec_()) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog="ProcessGUI", + description="Manage subprocesses to store segments and compute features.") + parser.add_argument('-i', '--ini_file', nargs='?', help="Path to ini settings file.") + args = parser.parse_args() + main(**args.__dict__) diff --git a/neuroport_dbs/scripts/RasterGUI.py b/neuroport_dbs/scripts/RasterGUI.py new file mode 100644 index 0000000..55e7dd1 --- /dev/null +++ b/neuroport_dbs/scripts/RasterGUI.py @@ -0,0 +1,18 @@ +import sys +from qtpy import QtCore, QtWidgets +from neuroport_dbs.dbsgui.raster import RasterGUI + + +def main(): + _ = QtWidgets.QApplication(sys.argv) + aw = RasterGUI() + timer = QtCore.QTimer() + timer.timeout.connect(aw.update) + timer.start(1) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + + +if __name__ == '__main__': + main() diff --git a/neuroport_dbs/scripts/SweepGUI.py b/neuroport_dbs/scripts/SweepGUI.py new file mode 100644 index 0000000..1507edb --- /dev/null +++ b/neuroport_dbs/scripts/SweepGUI.py @@ -0,0 +1,19 @@ +import sys +import qtpy +from neuroport_dbs.dbsgui.sweep import SweepGUI + + +def main(): + from qtpy import QtWidgets, QtCore + _ = QtWidgets.QApplication(sys.argv) + aw = SweepGUI() + timer = QtCore.QTimer() + timer.timeout.connect(aw.update) + timer.start(1) + + if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + + +if __name__ == '__main__': + main() diff --git a/neuroport_dbs/scripts/WaveformGUI.py b/neuroport_dbs/scripts/WaveformGUI.py new file mode 100644 index 0000000..c0bbed3 --- /dev/null +++ b/neuroport_dbs/scripts/WaveformGUI.py @@ -0,0 +1,18 @@ +import sys +from qtpy import QtCore, QtWidgets +from neuroport_dbs.dbsgui.waveform import WaveformGUI + + +def main(): + _ = QtWidgets.QApplication(sys.argv) + aw = WaveformGUI() + timer = QtCore.QTimer() + timer.timeout.connect(aw.update) + timer.start(1) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + + +if __name__ == '__main__': + main() diff --git a/neuroport_dbs/scripts/__init__.py b/neuroport_dbs/scripts/__init__.py new file mode 100644 index 0000000..e69de29 From dfe6d3f1c5fc9a5eab96ebe847e7efd34a33933e Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 2 May 2022 17:03:34 -0400 Subject: [PATCH 14/65] Reorganization; relative imports. --- neuroport_dbs/__init__.py | 1 + neuroport_dbs/data_source/__init__.py | 20 ++++ .../{dbsgui => }/data_source/cerebus.py | 9 +- .../{dbsgui => }/data_source/interface.py | 0 neuroport_dbs/{dbsgui => }/data_source/lsl.py | 0 neuroport_dbs/dbsgui/data_source/__init__.py | 3 - neuroport_dbs/dbsgui/depth_source/__init__.py | 93 ------------------- .../dbsgui/{my_models => models}/__init__.py | 0 .../dbsgui/{my_models => models}/_shared.py | 0 .../{ => dbsgui/widgets}/SettingsDialog.py | 0 .../{my_widgets => widgets}/__init__.py | 0 .../{my_widgets => widgets}/cbsdk_connect.py | 0 .../dbsgui/{my_widgets => widgets}/custom.py | 18 +--- .../ui/cbsdk_connect.ui | 0 .../{my_widgets => widgets}/ui/ddu_display.ui | 0 .../ui/send_comments.ui | 0 neuroport_dbs/depth_source/__init__.py | 2 + neuroport_dbs/depth_source/base.py | 18 ++++ neuroport_dbs/depth_source/cerebus.py | 41 ++++++++ neuroport_dbs/depth_source/fhc.py | 42 +++++++++ .../feature_plots/FeaturePlotWidgets.py | 7 +- neuroport_dbs/version.py | 1 - {neuroport_dbs => scripts}/scratch.py | 0 .../test_pg_memory_leak.py | 10 +- {neuroport_dbs => scripts}/tests.py | 0 25 files changed, 140 insertions(+), 125 deletions(-) create mode 100644 neuroport_dbs/data_source/__init__.py rename neuroport_dbs/{dbsgui => }/data_source/cerebus.py (94%) rename neuroport_dbs/{dbsgui => }/data_source/interface.py (100%) rename neuroport_dbs/{dbsgui => }/data_source/lsl.py (100%) delete mode 100644 neuroport_dbs/dbsgui/data_source/__init__.py delete mode 100644 neuroport_dbs/dbsgui/depth_source/__init__.py rename neuroport_dbs/dbsgui/{my_models => models}/__init__.py (100%) rename neuroport_dbs/dbsgui/{my_models => models}/_shared.py (100%) rename neuroport_dbs/{ => dbsgui/widgets}/SettingsDialog.py (100%) rename neuroport_dbs/dbsgui/{my_widgets => widgets}/__init__.py (100%) rename neuroport_dbs/dbsgui/{my_widgets => widgets}/cbsdk_connect.py (100%) rename neuroport_dbs/dbsgui/{my_widgets => widgets}/custom.py (93%) rename neuroport_dbs/dbsgui/{my_widgets => widgets}/ui/cbsdk_connect.ui (100%) rename neuroport_dbs/dbsgui/{my_widgets => widgets}/ui/ddu_display.ui (100%) rename neuroport_dbs/dbsgui/{my_widgets => widgets}/ui/send_comments.ui (100%) create mode 100644 neuroport_dbs/depth_source/__init__.py create mode 100644 neuroport_dbs/depth_source/base.py create mode 100644 neuroport_dbs/depth_source/cerebus.py create mode 100644 neuroport_dbs/depth_source/fhc.py delete mode 100644 neuroport_dbs/version.py rename {neuroport_dbs => scripts}/scratch.py (100%) rename {neuroport_dbs => scripts}/test_pg_memory_leak.py (89%) rename {neuroport_dbs => scripts}/tests.py (100%) diff --git a/neuroport_dbs/__init__.py b/neuroport_dbs/__init__.py index e69de29..1f356cc 100644 --- a/neuroport_dbs/__init__.py +++ b/neuroport_dbs/__init__.py @@ -0,0 +1 @@ +__version__ = '1.0.0' diff --git a/neuroport_dbs/data_source/__init__.py b/neuroport_dbs/data_source/__init__.py new file mode 100644 index 0000000..61369d1 --- /dev/null +++ b/neuroport_dbs/data_source/__init__.py @@ -0,0 +1,20 @@ +import time +from .lsl import LSLDataSource +from .cerebus import CerebusDataSource + + +def get_now_time(data_class="CerebusDataSource"): + """ + Returns: Device time in whatever units the device likes. + """ + if data_class == "CerebusDataSource": + from cerebuswrapper import CbSdkConnection + # Attempt to synchronize different series using machine time. + cbsdk_conn = CbSdkConnection() + if cbsdk_conn.is_connected: + now = cbsdk_conn.time() + else: + now = None + else: + now = time.time() + return now diff --git a/neuroport_dbs/dbsgui/data_source/cerebus.py b/neuroport_dbs/data_source/cerebus.py similarity index 94% rename from neuroport_dbs/dbsgui/data_source/cerebus.py rename to neuroport_dbs/data_source/cerebus.py index 61c2f5f..7191ef1 100644 --- a/neuroport_dbs/dbsgui/data_source/cerebus.py +++ b/neuroport_dbs/data_source/cerebus.py @@ -1,9 +1,10 @@ -from typing import Union, Tuple from qtpy import QtCore -import numpy as np from .interface import IDataSource -from cerebuswrapper import CbSdkConnection -from neuroport_dbs.settings import parse_ini_try_numeric +from ..settings import parse_ini_try_numeric +try: + from cerebuswrapper import CbSdkConnection +except ModuleNotFoundError as e: + print(e, "Try `pip install git+https://github.com/SachsLab/cerebuswrapper.git`.") SAMPLINGGROUPS = ["0", "500", "1000", "2000", "10000", "30000"] # , "RAW"] RAW broken in cbsdk diff --git a/neuroport_dbs/dbsgui/data_source/interface.py b/neuroport_dbs/data_source/interface.py similarity index 100% rename from neuroport_dbs/dbsgui/data_source/interface.py rename to neuroport_dbs/data_source/interface.py diff --git a/neuroport_dbs/dbsgui/data_source/lsl.py b/neuroport_dbs/data_source/lsl.py similarity index 100% rename from neuroport_dbs/dbsgui/data_source/lsl.py rename to neuroport_dbs/data_source/lsl.py diff --git a/neuroport_dbs/dbsgui/data_source/__init__.py b/neuroport_dbs/dbsgui/data_source/__init__.py deleted file mode 100644 index af99b9d..0000000 --- a/neuroport_dbs/dbsgui/data_source/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from neuroport_dbs.dbsgui.data_source.interface import IDataSource -from neuroport_dbs.dbsgui.data_source.lsl import LSLDataSource -from neuroport_dbs.dbsgui.data_source.cerebus import CerebusDataSource diff --git a/neuroport_dbs/dbsgui/depth_source/__init__.py b/neuroport_dbs/dbsgui/depth_source/__init__.py deleted file mode 100644 index 0fd1563..0000000 --- a/neuroport_dbs/dbsgui/depth_source/__init__.py +++ /dev/null @@ -1,93 +0,0 @@ -from qtpy import QtCore -from cerebuswrapper import CbSdkConnection -import serial -import serial.tools.list_ports -from neuroport_dbs.settings import parse_ini_try_numeric - - -class MerDepthSource: - def __init__(self, scoped_settings: QtCore.QSettings): - # scale_factor should be 0.001 for FHC DDU V2, 1.0 otherwise. - self._scale_factor = parse_ini_try_numeric(scoped_settings, 'scale_factor') or 1.0 - self.do_open() - - def do_open(self): - raise NotImplementedError() - - def do_close(self): - raise NotImplementedError() - - def update(self): - raise NotImplementedError - - -class CBSDKPlayback(MerDepthSource): - - def __init__(self, scoped_settings: QtCore.QSettings): - super().__init__(scoped_settings) - - def do_open(self): - CbSdkConnection().connect() - CbSdkConnection().cbsdk_config = { - 'reset': True, 'get_events': False, 'get_comments': True, - 'buffer_parameter': { - 'comment_length': 10 - } - } - - def do_close(self): - CbSdkConnection().disconnect() - - def update(self): - cbsdk_conn = CbSdkConnection() - if cbsdk_conn.is_connected: - comments = cbsdk_conn.get_comments() - if comments: - comment_strings = [x[1].decode('utf8') for x in comments] - else: - comment_strings = "" - dtts = [] - for comm_str in comment_strings: - if 'DTT:' in comm_str: - dtts.append(float(comm_str[4:])) - if len(dtts) > 0: - raw_value = dtts[-1] - return raw_value - return None - - -class FHCSerial(MerDepthSource): - - def __init__(self, scoped_settings: QtCore.QSettings): - super().__init__(scoped_settings) - self._baudrate = parse_ini_try_numeric(scoped_settings, 'baudrate') or 19200 - self._com_port = scoped_settings.value("com_port") - self.ser = serial.Serial() - - def do_open(self): - super().do_open() - self.ser.baudrate = self._baudrate - if self._com_port not in serial.tools.list_ports.comports(): - print(f"Port {self._com_port} not found in list of comports.") - if not self.ser.is_open: - self.ser.port = self._com_port - try: - self.ser.open() # TODO: Add timeout; Add error. - self.ser.write('AXON+\r'.encode()) - except serial.serialutil.SerialException: - print("Could not open serial port") - - def do_close(self): - self.ser.close() - - def update(self): - if self.ser.is_open: - in_str = self.ser.readline().decode('utf-8').strip() - if in_str: - try: - raw_value = float(in_str) * self._scale_factor - return raw_value - - except ValueError: - print("DDU result: {}".format(in_str)) - return None diff --git a/neuroport_dbs/dbsgui/my_models/__init__.py b/neuroport_dbs/dbsgui/models/__init__.py similarity index 100% rename from neuroport_dbs/dbsgui/my_models/__init__.py rename to neuroport_dbs/dbsgui/models/__init__.py diff --git a/neuroport_dbs/dbsgui/my_models/_shared.py b/neuroport_dbs/dbsgui/models/_shared.py similarity index 100% rename from neuroport_dbs/dbsgui/my_models/_shared.py rename to neuroport_dbs/dbsgui/models/_shared.py diff --git a/neuroport_dbs/SettingsDialog.py b/neuroport_dbs/dbsgui/widgets/SettingsDialog.py similarity index 100% rename from neuroport_dbs/SettingsDialog.py rename to neuroport_dbs/dbsgui/widgets/SettingsDialog.py diff --git a/neuroport_dbs/dbsgui/my_widgets/__init__.py b/neuroport_dbs/dbsgui/widgets/__init__.py similarity index 100% rename from neuroport_dbs/dbsgui/my_widgets/__init__.py rename to neuroport_dbs/dbsgui/widgets/__init__.py diff --git a/neuroport_dbs/dbsgui/my_widgets/cbsdk_connect.py b/neuroport_dbs/dbsgui/widgets/cbsdk_connect.py similarity index 100% rename from neuroport_dbs/dbsgui/my_widgets/cbsdk_connect.py rename to neuroport_dbs/dbsgui/widgets/cbsdk_connect.py diff --git a/neuroport_dbs/dbsgui/my_widgets/custom.py b/neuroport_dbs/dbsgui/widgets/custom.py similarity index 93% rename from neuroport_dbs/dbsgui/my_widgets/custom.py rename to neuroport_dbs/dbsgui/widgets/custom.py index 7cc928e..1e80a86 100644 --- a/neuroport_dbs/dbsgui/my_widgets/custom.py +++ b/neuroport_dbs/dbsgui/widgets/custom.py @@ -1,22 +1,10 @@ import time from pathlib import Path from qtpy import QtWidgets, QtCore, QtGui -from cerebuswrapper import CbSdkConnection # Import settings -import neuroport_dbs -import neuroport_dbs.dbsgui.data_source -from neuroport_dbs.settings import defaults, parse_ini_try_numeric - - -def get_now_time(): - # Attempt to synchronize different series using machine time. - cbsdk_conn = CbSdkConnection() - if cbsdk_conn.is_connected: - now = cbsdk_conn.time() - else: - now = time.time() - return now +from ...settings import defaults, parse_ini_try_numeric +import neuroport_dbs.data_source class CustomGUI(QtWidgets.QMainWindow): @@ -73,7 +61,7 @@ def restore_from_settings(self): # Infer data source from ini file, setup data source settings.beginGroup("data-source") - src_cls = getattr(neuroport_dbs.dbsgui.data_source, settings.value("class")) + src_cls = getattr(neuroport_dbs.data_source, settings.value("class")) # Get the _data_source. Note this might trigger on_source_connected before child # finishes parsing settings. _data_source = src_cls(scoped_settings=settings, on_connect_cb=self.on_source_connected) diff --git a/neuroport_dbs/dbsgui/my_widgets/ui/cbsdk_connect.ui b/neuroport_dbs/dbsgui/widgets/ui/cbsdk_connect.ui similarity index 100% rename from neuroport_dbs/dbsgui/my_widgets/ui/cbsdk_connect.ui rename to neuroport_dbs/dbsgui/widgets/ui/cbsdk_connect.ui diff --git a/neuroport_dbs/dbsgui/my_widgets/ui/ddu_display.ui b/neuroport_dbs/dbsgui/widgets/ui/ddu_display.ui similarity index 100% rename from neuroport_dbs/dbsgui/my_widgets/ui/ddu_display.ui rename to neuroport_dbs/dbsgui/widgets/ui/ddu_display.ui diff --git a/neuroport_dbs/dbsgui/my_widgets/ui/send_comments.ui b/neuroport_dbs/dbsgui/widgets/ui/send_comments.ui similarity index 100% rename from neuroport_dbs/dbsgui/my_widgets/ui/send_comments.ui rename to neuroport_dbs/dbsgui/widgets/ui/send_comments.ui diff --git a/neuroport_dbs/depth_source/__init__.py b/neuroport_dbs/depth_source/__init__.py new file mode 100644 index 0000000..ea21080 --- /dev/null +++ b/neuroport_dbs/depth_source/__init__.py @@ -0,0 +1,2 @@ +from .fhc import FHCSerial +from .cerebus import CBSDKPlayback diff --git a/neuroport_dbs/depth_source/base.py b/neuroport_dbs/depth_source/base.py new file mode 100644 index 0000000..101a537 --- /dev/null +++ b/neuroport_dbs/depth_source/base.py @@ -0,0 +1,18 @@ +from qtpy import QtCore +from ..settings import parse_ini_try_numeric + + +class MerDepthSource: + def __init__(self, scoped_settings: QtCore.QSettings): + # scale_factor should be 0.001 for FHC DDU V2, 1.0 otherwise. + self._scale_factor = parse_ini_try_numeric(scoped_settings, 'scale_factor') or 1.0 + self.do_open() + + def do_open(self): + raise NotImplementedError() + + def do_close(self): + raise NotImplementedError() + + def update(self): + raise NotImplementedError diff --git a/neuroport_dbs/depth_source/cerebus.py b/neuroport_dbs/depth_source/cerebus.py new file mode 100644 index 0000000..f2aa270 --- /dev/null +++ b/neuroport_dbs/depth_source/cerebus.py @@ -0,0 +1,41 @@ +from qtpy import QtCore +from .base import MerDepthSource +try: + from cerebuswrapper import CbSdkConnection +except ModuleNotFoundError as e: + print(e, "Try `pip install git+https://github.com/SachsLab/cerebuswrapper.git`.") + + +class CBSDKPlayback(MerDepthSource): + + def __init__(self, scoped_settings: QtCore.QSettings): + super().__init__(scoped_settings) + + def do_open(self): + CbSdkConnection().connect() + CbSdkConnection().cbsdk_config = { + 'reset': True, 'get_events': False, 'get_comments': True, + 'buffer_parameter': { + 'comment_length': 10 + } + } + + def do_close(self): + CbSdkConnection().disconnect() + + def update(self): + cbsdk_conn = CbSdkConnection() + if cbsdk_conn.is_connected: + comments = cbsdk_conn.get_comments() + if comments: + comment_strings = [x[1].decode('utf8') for x in comments] + else: + comment_strings = "" + dtts = [] + for comm_str in comment_strings: + if 'DTT:' in comm_str: + dtts.append(float(comm_str[4:])) + if len(dtts) > 0: + raw_value = dtts[-1] + return raw_value + return None \ No newline at end of file diff --git a/neuroport_dbs/depth_source/fhc.py b/neuroport_dbs/depth_source/fhc.py new file mode 100644 index 0000000..21c454f --- /dev/null +++ b/neuroport_dbs/depth_source/fhc.py @@ -0,0 +1,42 @@ +from qtpy import QtCore +import serial +import serial.tools.list_ports +from .base import MerDepthSource +from neuroport_dbs.settings import parse_ini_try_numeric + + +class FHCSerial(MerDepthSource): + + def __init__(self, scoped_settings: QtCore.QSettings): + super().__init__(scoped_settings) + self._baudrate = parse_ini_try_numeric(scoped_settings, 'baudrate') or 19200 + self._com_port = scoped_settings.value("com_port") + self.ser = serial.Serial() + + def do_open(self): + super().do_open() + self.ser.baudrate = self._baudrate + if self._com_port not in serial.tools.list_ports.comports(): + print(f"Port {self._com_port} not found in list of comports.") + if not self.ser.is_open: + self.ser.port = self._com_port + try: + self.ser.open() # TODO: Add timeout; Add error. + self.ser.write('AXON+\r'.encode()) + except serial.serialutil.SerialException: + print("Could not open serial port") + + def do_close(self): + self.ser.close() + + def update(self): + if self.ser.is_open: + in_str = self.ser.readline().decode('utf-8').strip() + if in_str: + try: + raw_value = float(in_str) * self._scale_factor + return raw_value + + except ValueError: + print("DDU result: {}".format(in_str)) + return None diff --git a/neuroport_dbs/feature_plots/FeaturePlotWidgets.py b/neuroport_dbs/feature_plots/FeaturePlotWidgets.py index 654b705..dbf8f30 100644 --- a/neuroport_dbs/feature_plots/FeaturePlotWidgets.py +++ b/neuroport_dbs/feature_plots/FeaturePlotWidgets.py @@ -1,16 +1,15 @@ from matplotlib import cm import numpy as np - -# use the same GUI format as the other ones from qtpy.QtWidgets import QGridLayout, QWidget, QVBoxLayout, QApplication from qtpy.QtCore import QEvent, Qt from qtpy.QtGui import QColor, QFont - import pyqtgraph as pg +from ..settings.defaults import THEMES, DEPTHRANGE, DEPTHTARGET, NPLOTSRAW + -from neuroport_dbs.settings.defaults import THEMES, DEPTHRANGE, DEPTHTARGET, NPLOTSRAW pen_colors = THEMES['dark']['pencolors'] + # Plot settings dictionaries DEFAULTPLOT = { 'title': None, diff --git a/neuroport_dbs/version.py b/neuroport_dbs/version.py deleted file mode 100644 index 1f356cc..0000000 --- a/neuroport_dbs/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '1.0.0' diff --git a/neuroport_dbs/scratch.py b/scripts/scratch.py similarity index 100% rename from neuroport_dbs/scratch.py rename to scripts/scratch.py diff --git a/neuroport_dbs/test_pg_memory_leak.py b/scripts/test_pg_memory_leak.py similarity index 89% rename from neuroport_dbs/test_pg_memory_leak.py rename to scripts/test_pg_memory_leak.py index ab627b0..fb263de 100644 --- a/neuroport_dbs/test_pg_memory_leak.py +++ b/scripts/test_pg_memory_leak.py @@ -1,8 +1,8 @@ import sys -import PyQt5 -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * +import qtpy +from qtpy.QtCore import * +from qtpy.QtWidgets import * +from qtpy.QtGui import * import numpy as np import pyqtgraph as pg from pympler import tracker @@ -56,5 +56,5 @@ def update(self): timer.timeout.connect(aw.update) timer.start(1) - if (sys.flags.interactive != 1) or not hasattr(PyQt5.QtCore, 'PYQT_VERSION'): + if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): QApplication.instance().exec_() \ No newline at end of file diff --git a/neuroport_dbs/tests.py b/scripts/tests.py similarity index 100% rename from neuroport_dbs/tests.py rename to scripts/tests.py From 74f7eb562777b290bc40dc97bb2eaa28c2c2f8ca Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 2 May 2022 17:04:25 -0400 Subject: [PATCH 15/65] modernize setuptools --- pyproject.toml | 3 +++ setup.cfg | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 49 +++++-------------------------------------- 3 files changed, 65 insertions(+), 44 deletions(-) create mode 100644 pyproject.toml create mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..323eb5b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,57 @@ +[metadata] +name = neuroport_dbs +version = attr: neuroport_dbs.__version__ +author = Chadwick Boulay +author_email = chboulay@ohri.ca +description = Tools for clinical research using Blackrock Neuroport in DBS MER +long_description = file: docs/README.md +long_description_content_type = text/markdown +url = https://github.com/SachsLab/NeuroportDBS +project_urls = + Bug Tracker = https://github.com/SachsLab/NeuroportDBS/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + +[options] +packages = find: +include_package_data = True +python_requires = >=3.9 +install_requires = + numpy + pyserial + pyside6 + qtpy + scipy + pyqtgraph + pyfftw + mysqlclient + django + quantities + neo @ git+https://github.com/NeuralEnsemble/python-neo.git + pylsl + pytf @ git+https://github.com/SachsLab/pytf.git + mspacman @ git+https://github.com/SachsLab/mspacman.git + serf @ git+https://github.com/cboulay/SERF.git#subdirectory=python + +[options.packages.find] +exclude = + tests + scripts + site + docs + +[options.package_data] +"neuroport_dbs/resources/icons" = "*.png" +"neuroport_dbs/resources/config" = "*.ini" + +[options.entry_points] # https://setuptools.pypa.io/en/latest/userguide/entry_point.html +gui_scripts = + dbs-sweep = neuroport_dbs.scripts.SweepGUI:main + dbs-waveform = neuroport_dbs.scripts.WaveformGUI:main + dbs-raster = neuroport_dbs.scripts.RasterGUI:main + dbs-features = neuroport_dbs.scripts.FeaturesGUI:main + dbs-mapping = neuroport_dbs.scripts.MappingGUI:main + dbs-comments = neuroport_dbs.scripts.CommentsGUI:main + dbs-ddu = neuroport_dbs.scripts.DepthGUI:main diff --git a/setup.py b/setup.py index 7c07977..cdba86d 100644 --- a/setup.py +++ b/setup.py @@ -1,45 +1,6 @@ -from setuptools import setup, find_packages -from os import path +""" +This file exists solely to appease any scripts that might call `python setup.py ...` +""" +from setuptools import setup - -package_name = "neuroport_dbs" -here = path.abspath(path.dirname(__file__)) - -# Get the long description from the relevant file -with open(path.join(here, 'docs', 'README.md'), encoding='utf-8') as f: - long_description = f.read() - -# Get the version number from the version file -# Versions should comply with PEP440. For a discussion on single-sourcing -# the version across setup.py and the project code, see -# https://packaging.python.org/en/latest/single_source_version.html -version = {} -with open(path.join(package_name, "version.py")) as fp: - exec(fp.read(), version) - -setup( - name=package_name, - version=version['__version__'], - packages=find_packages() + ['neuroport_dbs/resources/icons'] + ['neuroport_dbs/resources/config'], - package_data={"neuroport_dbs/resources/icons": ["*.png"], "neuroport_dbs/resources/config": ["*.ini"]}, - description='Tools for clinical research using Blackrock Neuroport in DBS MER', - long_description=long_description, - long_description_content_type="text/markdown", - author='Chadwick Boulay', - author_email='chadwick.boulay@gmail.com', - maintainer='Guillaume Doucet', - maintainer_email='gudoucet@ohri.ca', - url='https://github.com/SachsLab/NeuroportDBS', - license='GPL v3', - - entry_points={ - 'gui_scripts': ['dbs-sweep=neuroport_dbs.SweepGUI:main', - 'dbs-waveform=neuroport_dbs.WaveformGUI:main', - 'dbs-raster=neuroport_dbs.RasterGUI:main', - 'dbs-features=neuroport_dbs.FeaturesGUI:main', - 'dbs-mapping=neuroport_dbs.MappingGUI:main', - 'dbs-comments=neuroport_dbs.CommentsGUI:main', - 'dbs-ddu=neuroport_dbs.DepthGUI:main', - ], - } -) +setup() From 44fc4eb7c913177fcabfa3ad3b341496ee0873ed Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Tue, 3 May 2022 01:17:17 -0400 Subject: [PATCH 16/65] Fixup gui position in settings --- neuroport_dbs/resources/config/WaveformGUI.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neuroport_dbs/resources/config/WaveformGUI.ini b/neuroport_dbs/resources/config/WaveformGUI.ini index 3b3023b..f6bbf8c 100644 --- a/neuroport_dbs/resources/config/WaveformGUI.ini +++ b/neuroport_dbs/resources/config/WaveformGUI.ini @@ -3,7 +3,7 @@ fullScreen=false maximized=false frameless=true size=@Size(300 1080) -pos=@Point(620 0) +pos=@Point(920 0) bg_color=#404040 [data-source] From 931f3ff19c2b64e8255841c36838fe272dc1f598 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Tue, 3 May 2022 01:17:50 -0400 Subject: [PATCH 17/65] PySide6 compatibility --- neuroport_dbs/data_source/cerebus.py | 4 ++-- neuroport_dbs/dbsgui/sweep.py | 20 +++++++++++--------- neuroport_dbs/settings/__init__.py | 16 ++++++++-------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/neuroport_dbs/data_source/cerebus.py b/neuroport_dbs/data_source/cerebus.py index 7191ef1..6ae72ef 100644 --- a/neuroport_dbs/data_source/cerebus.py +++ b/neuroport_dbs/data_source/cerebus.py @@ -43,8 +43,8 @@ def __init__(self, scoped_settings: QtCore.QSettings, **kwargs): @staticmethod def _decode_group_info(group_info: dict): for gi_item in group_info: - gi_item['label'] = gi_item['label'].decode('utf-8') - gi_item['unit'] = gi_item['unit'].decode('utf-8') + gi_item['label'] = gi_item['label'] + gi_item['unit'] = gi_item['unit'] return group_info @property diff --git a/neuroport_dbs/dbsgui/sweep.py b/neuroport_dbs/dbsgui/sweep.py index 03d5950..a7919d3 100644 --- a/neuroport_dbs/dbsgui/sweep.py +++ b/neuroport_dbs/dbsgui/sweep.py @@ -117,7 +117,7 @@ def create_control_panel(self): self._monitor_group.addButton(new_button) self._monitor_group.setId(new_button, chan_ix + 1) cntrl_layout.addWidget(new_button) - self._monitor_group.buttonClicked[int].connect(self.on_monitor_group_clicked) + self._monitor_group.buttonClicked.connect(self.on_monitor_group_clicked) # Checkbox for whether the audio out should be spike only spk_aud_checkbox = QtWidgets.QCheckBox("Spike Aud") spk_aud_checkbox.setObjectName("spkaud_CheckBox") @@ -173,16 +173,17 @@ def on_range_edit_editingFinished(self): self.refresh_axes() self.update_shared_memory() - def on_monitor_group_clicked(self, button_id): + def on_monitor_group_clicked(self, button): self.reset_audio() this_label = '' - if button_id == 0: + if button.text() == 'None': self.audio['chan_label'] = 'silence' chan_state = {'src': 0} else: - this_label = self.labels[button_id - 1] + this_label = button.text() + button_idx = self.labels.index(this_label) self.audio['chan_label'] = this_label - chan_state = self.chan_states[button_id - 1] + chan_state = self.chan_states[button_idx] # Reset plot titles active_kwargs = {'color': parse_color_str(self.plot_config['theme'].get('labelcolor_active', 'yellow')), @@ -257,18 +258,19 @@ def create_plots(self, plot={}, filter={}, theme={}, alt_loc=False): btype=filter.get('btype', 'highpass'), output=filter.get('output', 'sos')) hp_filt_cb = self.findChild(QtWidgets.QCheckBox, name="hpfilt_CheckBox") - hp_filt_cb.setCheckState(2 if self.plot_config['do_hp'] else 0) + + hp_filt_cb.setCheckState(QtCore.Qt.Checked if self.plot_config['do_hp'] else QtCore.Qt.Unchecked) ln_filt_cb = self.findChild(QtWidgets.QCheckBox, name="lnfilt_CheckBox") self.plot_config['do_ln'] = bool(plot.get('do_ln', True)) - ln_filt_cb.setCheckState(2 if self.plot_config['do_ln'] else 0) + ln_filt_cb.setCheckState(QtCore.Qt.Checked if self.plot_config['do_ln'] else QtCore.Qt.Unchecked) self.plot_config['ln_filt'] = None # TODO: comb filter coeffs spk_aud_cb = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") - spk_aud_cb.setCheckState(2 if bool(plot.get('spk_aud', True)) else 0) + spk_aud_cb.setCheckState(QtCore.Qt.Checked if bool(plot.get('spk_aud', True)) else QtCore.Qt.Unchecked) thrlk_cb = self.findChild(QtWidgets.QCheckBox, name="lkthr_CheckBox") - thrlk_cb.setCheckState(2 if bool(plot.get('lock_threshold', False)) else 0) + thrlk_cb.setCheckState(QtCore.Qt.Checked if bool(plot.get('lock_threshold', False)) else QtCore.Qt.Unchecked) # Create and add GraphicsLayoutWidget glw = pg.GraphicsLayoutWidget(parent=self) # show=False, size=None, title=None, border=None diff --git a/neuroport_dbs/settings/__init__.py b/neuroport_dbs/settings/__init__.py index 3c50d58..25cbc74 100644 --- a/neuroport_dbs/settings/__init__.py +++ b/neuroport_dbs/settings/__init__.py @@ -1,20 +1,20 @@ def parse_ini_try_numeric(settings, key): + putative = settings.value(key) + if putative == 'false': + return False + elif putative == 'true': + return True try: res = settings.value(key, type=int) except TypeError: res = None - if res is None: + if res is None or (res == 0 and putative): try: res = settings.value(key, type=float) except TypeError: res = None - if res is not None: - return res - res = settings.value(key) - if res == 'false': - return False - elif res == 'true': - return True + if res is None or (res == 0.0 and putative): + res = settings.value(key) return res From e985eb340db2d58bbb397f6cd6dcc61b9490ab13 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 29 May 2023 22:18:08 -0400 Subject: [PATCH 18/65] Re-add DDU detection lost in merge. --- neuroport_dbs/depth_source/base.py | 2 +- neuroport_dbs/depth_source/fhc.py | 35 ++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/neuroport_dbs/depth_source/base.py b/neuroport_dbs/depth_source/base.py index 101a537..b65f957 100644 --- a/neuroport_dbs/depth_source/base.py +++ b/neuroport_dbs/depth_source/base.py @@ -5,7 +5,7 @@ class MerDepthSource: def __init__(self, scoped_settings: QtCore.QSettings): # scale_factor should be 0.001 for FHC DDU V2, 1.0 otherwise. - self._scale_factor = parse_ini_try_numeric(scoped_settings, 'scale_factor') or 1.0 + self._scale_factor = parse_ini_try_numeric(scoped_settings, 'scale_factor') or None self.do_open() def do_open(self): diff --git a/neuroport_dbs/depth_source/fhc.py b/neuroport_dbs/depth_source/fhc.py index 21c454f..76ce248 100644 --- a/neuroport_dbs/depth_source/fhc.py +++ b/neuroport_dbs/depth_source/fhc.py @@ -1,3 +1,5 @@ +import re + from qtpy import QtCore import serial import serial.tools.list_ports @@ -11,7 +13,18 @@ def __init__(self, scoped_settings: QtCore.QSettings): super().__init__(scoped_settings) self._baudrate = parse_ini_try_numeric(scoped_settings, 'baudrate') or 19200 self._com_port = scoped_settings.value("com_port") - self.ser = serial.Serial() + self.ser = serial.Serial(timeout=1) + self._is_v2 = False + + @property + def is_v2(self): + return self._is_v2 + + @is_v2.setter + def is_v2(self, value): + self._is_v2 = value + self._scale_factor = 0.001 if value else 1.0 + self.doubleSpinBox_offset.setValue(60.00 if value else 0.00) def do_open(self): super().do_open() @@ -21,8 +34,22 @@ def do_open(self): if not self.ser.is_open: self.ser.port = self._com_port try: - self.ser.open() # TODO: Add timeout; Add error. - self.ser.write('AXON+\r'.encode()) + self.ser.open() # TODO: Add error. + # Quiet transmission for a minute + self.ser.write("AXON-\r".encode()) + self.ser.write("V\r".encode()) + # readlines will capture until timeout + pattern = "([0-9]+\.[0-9]+)" + for line in self.ser.readlines(): + match = re.search(pattern, line.decode("utf-8").strip()) + if match is not None: + v = match.group() # e.g., "2.20" + v_maj = int(v.split(".")[0]) + self.is_v2 = v_maj >= 2 + break + # Resume transmission. + self.ser.write("AXON+\r".encode()) + _ = self.ser.readline() # Clear out the first response to AXON+ except serial.serialutil.SerialException: print("Could not open serial port") @@ -34,7 +61,7 @@ def update(self): in_str = self.ser.readline().decode('utf-8').strip() if in_str: try: - raw_value = float(in_str) * self._scale_factor + raw_value = float(in_str) * (self._scale_factor or 1.0) return raw_value except ValueError: From 1e9c49f440bec77f0fb6fffa056b779e4d1bd9cb Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 29 May 2023 22:19:18 -0400 Subject: [PATCH 19/65] Qt6 compat fixup. --- neuroport_dbs/dbsgui/depth.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/neuroport_dbs/dbsgui/depth.py b/neuroport_dbs/dbsgui/depth.py index e70d5cf..066094b 100644 --- a/neuroport_dbs/dbsgui/depth.py +++ b/neuroport_dbs/dbsgui/depth.py @@ -127,24 +127,25 @@ def setup_ui(self): # add a frame for the LCD numbers self.lcd_frame = QtWidgets.QFrame() - self.lcd_frame.setFrameShape(1) + self.lcd_frame.setFrameShape(QtWidgets.QFrame.Shape.Box) lcd_layout = QtWidgets.QGridLayout() self.lcd_frame.setLayout(lcd_layout) # RAW reading from DDU self.raw_ddu = QtWidgets.QLCDNumber() self.raw_ddu.setDigitCount(7) - self.raw_ddu.setFrameShape(0) + self.raw_ddu.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) self.raw_ddu.setSmallDecimalPoint(True) self.raw_ddu.setFixedHeight(50) self.raw_ddu.display("{0:.3f}".format(0)) lcd_layout.addWidget(self.raw_ddu, 0, 3, 2, 3) + # TODO: Use custom class and reimplement self.offset_ddu.mouseDoubleClickEvent(), then git rid of "Send!" self.offset_ddu = QtWidgets.QLCDNumber() self.offset_ddu.setDigitCount(7) self.offset_ddu.setFixedHeight(150) self.offset_ddu.display("{0:.3f}".format(-10)) - self.offset_ddu.setFrameShape(0) + self.offset_ddu.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) lcd_layout.addWidget(self.offset_ddu, 2, 0, 5, 6) v_layout.addWidget(self.lcd_frame) From b5589fa005ef665ae6783334e1eda6f70d75a092 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 29 May 2023 22:20:04 -0400 Subject: [PATCH 20/65] Add DTT widget. --- neuroport_dbs/dbsgui/depth.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/neuroport_dbs/dbsgui/depth.py b/neuroport_dbs/dbsgui/depth.py index 066094b..688a514 100644 --- a/neuroport_dbs/dbsgui/depth.py +++ b/neuroport_dbs/dbsgui/depth.py @@ -75,6 +75,17 @@ def setup_ui(self): v_layout.setContentsMargins(10, 0, 10, 10) h_layout = QtWidgets.QHBoxLayout() + h_layout.addWidget(QLabel("DTT: ")) + self._doubleSpinBox_DTT = QDoubleSpinBox() + self._doubleSpinBox_DTT = QDoubleSpinBox() + self._doubleSpinBox_DTT.setMinimum(-100.00) + self._doubleSpinBox_DTT.setMaximum(100.00) + self._doubleSpinBox_DTT.setSingleStep(1.00) + self._doubleSpinBox_DTT.setDecimals(2) + self._doubleSpinBox_DTT.setValue(0.00) + self._doubleSpinBox_DTT.setFixedWidth(60) + h_layout.addWidget(self._doubleSpinBox_DTT) + # Manual offset added to the depth before display and mirroring h_layout.addWidget(QtWidgets.QLabel("Offset: ")) self._doubleSpinBox_offset = QtWidgets.QDoubleSpinBox() @@ -82,7 +93,7 @@ def setup_ui(self): self._doubleSpinBox_offset.setMaximum(100.00) self._doubleSpinBox_offset.setSingleStep(1.00) self._doubleSpinBox_offset.setDecimals(2) - self._doubleSpinBox_offset.setValue(-10.00) + self._doubleSpinBox_offset.setValue(0.00) self._doubleSpinBox_offset.setFixedWidth(60) h_layout.addWidget(self._doubleSpinBox_offset) @@ -180,7 +191,7 @@ def update(self): value = self._depth_source.update() if value is not None: self.raw_ddu.display("{0:.3f}".format(value)) - value = value + self._doubleSpinBox_offset.value() + value += self._doubleSpinBox_DTT.value() + self._doubleSpinBox_offset.value() display_string = "{0:.3f}".format(value) if display_string != self.display_string: new_value = True From eda1bcb7ad52bbb5791d74d82d59024f4bb29a75 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 13 Aug 2023 18:15:23 -0400 Subject: [PATCH 21/65] Finished renaming from neuroport_dbs to open_mer. Unfortunately, git didn't recognize it as a move so it's a huge delete and addition. --- neuroport_dbs/README.md | 34 ------------------ {neuroport_dbs => open_mer}/__init__.py | 0 .../data_source/__init__.py | 0 .../data_source/cerebus.py | 0 .../data_source/interface.py | 0 .../data_source/lsl.py | 0 .../dbsgui/__init__.py | 0 .../dbsgui/comments.py | 5 +-- {neuroport_dbs => open_mer}/dbsgui/depth.py | 5 +-- .../dbsgui/features.py | 2 +- {neuroport_dbs => open_mer}/dbsgui/mapping.py | 4 +-- .../dbsgui/models/__init__.py | 0 .../dbsgui/models/_shared.py | 0 {neuroport_dbs => open_mer}/dbsgui/process.py | 4 +-- {neuroport_dbs => open_mer}/dbsgui/raster.py | 0 {neuroport_dbs => open_mer}/dbsgui/sweep.py | 7 ++-- .../dbsgui/utilities/__init__.py | 0 .../dbsgui/utilities/pyqtgraph.py | 0 .../dbsgui/waveform.py | 0 .../dbsgui/widgets/SettingsDialog.py | 2 +- .../dbsgui/widgets/__init__.py | 0 .../dbsgui/widgets/cbsdk_connect.py | 0 .../dbsgui/widgets/custom.py | 4 +-- .../dbsgui/widgets/ui/cbsdk_connect.ui | 0 .../dbsgui/widgets/ui/ddu_display.ui | 0 .../dbsgui/widgets/ui/send_comments.ui | 0 .../depth_source/__init__.py | 0 .../depth_source/base.py | 0 .../depth_source/cerebus.py | 0 .../depth_source/fhc.py | 2 +- .../feature_plots/FeaturePlotWidgets.py | 0 .../feature_plots/__init__.py | 0 .../resources/config/DepthGUI.ini | 0 .../resources/config/FeaturesGUI.ini | 0 .../resources/config/MappingGUI.ini | 0 .../resources/config/ProcessGUI.ini | 0 .../resources/config/RasterGUI.ini | 0 .../resources/config/SweepGUI.ini | 0 .../resources/config/WaveformGUI.ini | 0 .../resources/icons/HalfBody.png | Bin .../resources/icons/depth_status_delay.png | Bin .../resources/icons/depth_status_done.png | Bin .../resources/icons/depth_status_in_use.png | Bin .../resources/icons/depth_status_off.png | Bin .../scripts/CommentsGUI.py | 3 +- .../scripts/DepthGUI.py | 2 +- .../scripts/FeaturesGUI.py | 2 +- .../scripts/FeaturesGUI_old.py | 10 +++--- .../scripts/ImportNS5FeaturesGUI.py | 2 +- .../scripts/MappingGUI.py | 2 +- .../scripts/ProcessGUI.py | 2 +- .../scripts/RasterGUI.py | 2 +- .../scripts/SweepGUI.py | 2 +- .../scripts/WaveformGUI.py | 2 +- .../scripts/__init__.py | 0 .../settings/__init__.py | 0 .../settings/defaults.py | 0 scripts/scratch.py | 4 +-- setup.cfg | 28 +++++++-------- 59 files changed, 50 insertions(+), 80 deletions(-) delete mode 100644 neuroport_dbs/README.md rename {neuroport_dbs => open_mer}/__init__.py (100%) rename {neuroport_dbs => open_mer}/data_source/__init__.py (100%) rename {neuroport_dbs => open_mer}/data_source/cerebus.py (100%) rename {neuroport_dbs => open_mer}/data_source/interface.py (100%) rename {neuroport_dbs => open_mer}/data_source/lsl.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/__init__.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/comments.py (97%) rename {neuroport_dbs => open_mer}/dbsgui/depth.py (98%) rename {neuroport_dbs => open_mer}/dbsgui/features.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/mapping.py (98%) rename {neuroport_dbs => open_mer}/dbsgui/models/__init__.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/models/_shared.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/process.py (98%) rename {neuroport_dbs => open_mer}/dbsgui/raster.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/sweep.py (99%) rename {neuroport_dbs => open_mer}/dbsgui/utilities/__init__.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/utilities/pyqtgraph.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/waveform.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/widgets/SettingsDialog.py (99%) rename {neuroport_dbs => open_mer}/dbsgui/widgets/__init__.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/widgets/cbsdk_connect.py (100%) rename {neuroport_dbs => open_mer}/dbsgui/widgets/custom.py (98%) rename {neuroport_dbs => open_mer}/dbsgui/widgets/ui/cbsdk_connect.ui (100%) rename {neuroport_dbs => open_mer}/dbsgui/widgets/ui/ddu_display.ui (100%) rename {neuroport_dbs => open_mer}/dbsgui/widgets/ui/send_comments.ui (100%) rename {neuroport_dbs => open_mer}/depth_source/__init__.py (100%) rename {neuroport_dbs => open_mer}/depth_source/base.py (100%) rename {neuroport_dbs => open_mer}/depth_source/cerebus.py (100%) rename {neuroport_dbs => open_mer}/depth_source/fhc.py (97%) rename {neuroport_dbs => open_mer}/feature_plots/FeaturePlotWidgets.py (100%) rename {neuroport_dbs => open_mer}/feature_plots/__init__.py (100%) rename {neuroport_dbs => open_mer}/resources/config/DepthGUI.ini (100%) rename {neuroport_dbs => open_mer}/resources/config/FeaturesGUI.ini (100%) rename {neuroport_dbs => open_mer}/resources/config/MappingGUI.ini (100%) rename {neuroport_dbs => open_mer}/resources/config/ProcessGUI.ini (100%) rename {neuroport_dbs => open_mer}/resources/config/RasterGUI.ini (100%) rename {neuroport_dbs => open_mer}/resources/config/SweepGUI.ini (100%) rename {neuroport_dbs => open_mer}/resources/config/WaveformGUI.ini (100%) rename {neuroport_dbs => open_mer}/resources/icons/HalfBody.png (100%) rename {neuroport_dbs => open_mer}/resources/icons/depth_status_delay.png (100%) rename {neuroport_dbs => open_mer}/resources/icons/depth_status_done.png (100%) rename {neuroport_dbs => open_mer}/resources/icons/depth_status_in_use.png (100%) rename {neuroport_dbs => open_mer}/resources/icons/depth_status_off.png (100%) rename {neuroport_dbs => open_mer}/scripts/CommentsGUI.py (79%) rename {neuroport_dbs => open_mer}/scripts/DepthGUI.py (89%) rename {neuroport_dbs => open_mer}/scripts/FeaturesGUI.py (92%) rename {neuroport_dbs => open_mer}/scripts/FeaturesGUI_old.py (96%) rename {neuroport_dbs => open_mer}/scripts/ImportNS5FeaturesGUI.py (99%) rename {neuroport_dbs => open_mer}/scripts/MappingGUI.py (88%) rename {neuroport_dbs => open_mer}/scripts/ProcessGUI.py (93%) rename {neuroport_dbs => open_mer}/scripts/RasterGUI.py (88%) rename {neuroport_dbs => open_mer}/scripts/SweepGUI.py (89%) rename {neuroport_dbs => open_mer}/scripts/WaveformGUI.py (87%) rename {neuroport_dbs => open_mer}/scripts/__init__.py (100%) rename {neuroport_dbs => open_mer}/settings/__init__.py (100%) rename {neuroport_dbs => open_mer}/settings/defaults.py (100%) diff --git a/neuroport_dbs/README.md b/neuroport_dbs/README.md deleted file mode 100644 index 9180257..0000000 --- a/neuroport_dbs/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# NeuroportDBS - -## DBSGUI - -This contains graphical user interfaces for signal visualization and interaction -using a Blackrock Microsystems Neuroport during DBS surgeries. - -### Requirements - -* Python 3 - * pyserial - * pyqt5 - * qtpy - * numpy - * pyaudio - * scipy - * pyqtgraph - * cerebus.cbpy (see below) - * [cerebuswrapper](https://github.com/SachsLab/cerebuswrapper) - * `pip install git+https://github.com/SachsLab/cerebuswrapper.git` - -cerebus is a Python package to interface with the Neuroport over the network protocol. -Download the cerebus python wheel for your platform and Python version from [here](https://github.com/dashesy/CereLink/releases/tag/7.0) -If a matching wheel is not found, see [CereLink](https://github.com/dashesy/CereLink/) for instructions on how to build cbsdk then install the Python package. -Note that we use our [CereLink7.0 branch](https://github.com/dashesy/CereLink/tree/CereLink7.0). - -There can be some tricks to connecting to the neuroport with this library. -Mac users, see [here](http://support.blackrockmicro.com/KB/View/168747-using-cbmex-on-osx). - -## DBSGUI Usage instructions - -TODO: More detailed instructions needed. - -Try running one or more of SweepGUI.py, RasterGUI.py, WaveformGUI.py, DDUGUI.py diff --git a/neuroport_dbs/__init__.py b/open_mer/__init__.py similarity index 100% rename from neuroport_dbs/__init__.py rename to open_mer/__init__.py diff --git a/neuroport_dbs/data_source/__init__.py b/open_mer/data_source/__init__.py similarity index 100% rename from neuroport_dbs/data_source/__init__.py rename to open_mer/data_source/__init__.py diff --git a/neuroport_dbs/data_source/cerebus.py b/open_mer/data_source/cerebus.py similarity index 100% rename from neuroport_dbs/data_source/cerebus.py rename to open_mer/data_source/cerebus.py diff --git a/neuroport_dbs/data_source/interface.py b/open_mer/data_source/interface.py similarity index 100% rename from neuroport_dbs/data_source/interface.py rename to open_mer/data_source/interface.py diff --git a/neuroport_dbs/data_source/lsl.py b/open_mer/data_source/lsl.py similarity index 100% rename from neuroport_dbs/data_source/lsl.py rename to open_mer/data_source/lsl.py diff --git a/neuroport_dbs/dbsgui/__init__.py b/open_mer/dbsgui/__init__.py similarity index 100% rename from neuroport_dbs/dbsgui/__init__.py rename to open_mer/dbsgui/__init__.py diff --git a/neuroport_dbs/dbsgui/comments.py b/open_mer/dbsgui/comments.py similarity index 97% rename from neuroport_dbs/dbsgui/comments.py rename to open_mer/dbsgui/comments.py index 4f1f5ae..b062e93 100644 --- a/neuroport_dbs/dbsgui/comments.py +++ b/open_mer/dbsgui/comments.py @@ -1,10 +1,11 @@ import os from qtpy import QtWidgets from qtpy import uic -import neuroport_dbs.dbsgui +from open_mer import dbsgui -Ui_MainWindow, QtBaseClass = uic.loadUiType(os.path.join(os.path.dirname(neuroport_dbs.dbsgui.widgets.ui.__file__), + +Ui_MainWindow, QtBaseClass = uic.loadUiType(os.path.join(os.path.dirname(dbsgui.widgets.ui.__file__), 'send_comments.ui')) diff --git a/neuroport_dbs/dbsgui/depth.py b/open_mer/dbsgui/depth.py similarity index 98% rename from neuroport_dbs/dbsgui/depth.py rename to open_mer/dbsgui/depth.py index 688a514..0ebc444 100644 --- a/neuroport_dbs/dbsgui/depth.py +++ b/open_mer/dbsgui/depth.py @@ -1,7 +1,6 @@ from qtpy import QtCore, QtWidgets from pathlib import Path import pylsl -import neuroport_dbs from ..settings import defaults from ..depth_source import CBSDKPlayback try: @@ -9,6 +8,8 @@ except ModuleNotFoundError as e: print(e, "Try `pip install git+https://github.com/SachsLab/cerebuswrapper.git`.") +from open_mer import dbsgui + class DepthGUI(QtWidgets.QMainWindow): @@ -54,7 +55,7 @@ def restore_from_settings(self): # Infer depth source from ini file, setup data source settings.beginGroup("depth-source") - src_cls = getattr(neuroport_dbs.dbsgui.depth_source, settings.value("class")) + src_cls = getattr(dbsgui.depth_source, settings.value("class")) self._depth_source = src_cls(scoped_settings=settings) settings.endGroup() diff --git a/neuroport_dbs/dbsgui/features.py b/open_mer/dbsgui/features.py similarity index 100% rename from neuroport_dbs/dbsgui/features.py rename to open_mer/dbsgui/features.py index d3be9f1..0670c0d 100644 --- a/neuroport_dbs/dbsgui/features.py +++ b/open_mer/dbsgui/features.py @@ -1,7 +1,7 @@ from pathlib import Path from qtpy import QtCore, QtWidgets -from ..settings import defaults, locate_ini from serf.tools.db_wrap import DBWrapper, ProcessWrapper +from ..settings import defaults, locate_ini from ..feature_plots import * diff --git a/neuroport_dbs/dbsgui/mapping.py b/open_mer/dbsgui/mapping.py similarity index 98% rename from neuroport_dbs/dbsgui/mapping.py rename to open_mer/dbsgui/mapping.py index b4eacd4..594a4eb 100644 --- a/neuroport_dbs/dbsgui/mapping.py +++ b/open_mer/dbsgui/mapping.py @@ -2,8 +2,8 @@ import json from qtpy import QtWidgets, QtGui import pylsl -from neuroport_dbs.dbsgui.widgets.custom import CustomGUI, CustomWidget -from neuroport_dbs.settings.defaults import MAPPINGSTIMULI +from .widgets.custom import CustomGUI, CustomWidget +from ..settings.defaults import MAPPINGSTIMULI class MappingGUI(CustomGUI): diff --git a/neuroport_dbs/dbsgui/models/__init__.py b/open_mer/dbsgui/models/__init__.py similarity index 100% rename from neuroport_dbs/dbsgui/models/__init__.py rename to open_mer/dbsgui/models/__init__.py diff --git a/neuroport_dbs/dbsgui/models/_shared.py b/open_mer/dbsgui/models/_shared.py similarity index 100% rename from neuroport_dbs/dbsgui/models/_shared.py rename to open_mer/dbsgui/models/_shared.py diff --git a/neuroport_dbs/dbsgui/process.py b/open_mer/dbsgui/process.py similarity index 98% rename from neuroport_dbs/dbsgui/process.py rename to open_mer/dbsgui/process.py index 7c3d63b..86fab07 100644 --- a/neuroport_dbs/dbsgui/process.py +++ b/open_mer/dbsgui/process.py @@ -1,8 +1,8 @@ from pathlib import Path from qtpy import QtCore, QtWidgets, QtGui -from neuroport_dbs.settings import defaults, locate_ini -from neuroport_dbs.dbsgui.widgets.SettingsDialog import SettingsDialog from serf.tools.db_wrap import DBWrapper, ProcessWrapper +from ..settings import defaults, locate_ini +from .widgets.SettingsDialog import SettingsDialog class ProcessGUI(QtWidgets.QMainWindow): diff --git a/neuroport_dbs/dbsgui/raster.py b/open_mer/dbsgui/raster.py similarity index 100% rename from neuroport_dbs/dbsgui/raster.py rename to open_mer/dbsgui/raster.py diff --git a/neuroport_dbs/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py similarity index 99% rename from neuroport_dbs/dbsgui/sweep.py rename to open_mer/dbsgui/sweep.py index a7919d3..dbaf7bf 100644 --- a/neuroport_dbs/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -3,9 +3,10 @@ import pyaudio from qtpy import QtCore, QtWidgets import pyqtgraph as pg -from neuroport_dbs.dbsgui.utilities.pyqtgraph import parse_color_str, make_qcolor, get_colormap -from neuroport_dbs.dbsgui.widgets.custom import CustomWidget, CustomGUI -from neuroport_dbs.data_source import get_now_time + +from .utilities.pyqtgraph import parse_color_str, make_qcolor, get_colormap +from .widgets.custom import CustomWidget, CustomGUI +from ..data_source import get_now_time class SweepGUI(CustomGUI): diff --git a/neuroport_dbs/dbsgui/utilities/__init__.py b/open_mer/dbsgui/utilities/__init__.py similarity index 100% rename from neuroport_dbs/dbsgui/utilities/__init__.py rename to open_mer/dbsgui/utilities/__init__.py diff --git a/neuroport_dbs/dbsgui/utilities/pyqtgraph.py b/open_mer/dbsgui/utilities/pyqtgraph.py similarity index 100% rename from neuroport_dbs/dbsgui/utilities/pyqtgraph.py rename to open_mer/dbsgui/utilities/pyqtgraph.py diff --git a/neuroport_dbs/dbsgui/waveform.py b/open_mer/dbsgui/waveform.py similarity index 100% rename from neuroport_dbs/dbsgui/waveform.py rename to open_mer/dbsgui/waveform.py diff --git a/neuroport_dbs/dbsgui/widgets/SettingsDialog.py b/open_mer/dbsgui/widgets/SettingsDialog.py similarity index 99% rename from neuroport_dbs/dbsgui/widgets/SettingsDialog.py rename to open_mer/dbsgui/widgets/SettingsDialog.py index 66a504c..e491d29 100644 --- a/neuroport_dbs/dbsgui/widgets/SettingsDialog.py +++ b/open_mer/dbsgui/widgets/SettingsDialog.py @@ -8,7 +8,7 @@ from serf.tools.db_wrap import DBWrapper # Settings -from neuroport_dbs.settings.defaults import BUFFERLENGTH, SAMPLELENGTH, DELAYBUFFER, OVERWRITEDEPTH +from ...settings.defaults import BUFFERLENGTH, SAMPLELENGTH, DELAYBUFFER, OVERWRITEDEPTH class SubjectWidget(QWidget): diff --git a/neuroport_dbs/dbsgui/widgets/__init__.py b/open_mer/dbsgui/widgets/__init__.py similarity index 100% rename from neuroport_dbs/dbsgui/widgets/__init__.py rename to open_mer/dbsgui/widgets/__init__.py diff --git a/neuroport_dbs/dbsgui/widgets/cbsdk_connect.py b/open_mer/dbsgui/widgets/cbsdk_connect.py similarity index 100% rename from neuroport_dbs/dbsgui/widgets/cbsdk_connect.py rename to open_mer/dbsgui/widgets/cbsdk_connect.py diff --git a/neuroport_dbs/dbsgui/widgets/custom.py b/open_mer/dbsgui/widgets/custom.py similarity index 98% rename from neuroport_dbs/dbsgui/widgets/custom.py rename to open_mer/dbsgui/widgets/custom.py index 1e80a86..ec3e6be 100644 --- a/neuroport_dbs/dbsgui/widgets/custom.py +++ b/open_mer/dbsgui/widgets/custom.py @@ -4,7 +4,7 @@ # Import settings from ...settings import defaults, parse_ini_try_numeric -import neuroport_dbs.data_source +import open_mer.data_source class CustomGUI(QtWidgets.QMainWindow): @@ -61,7 +61,7 @@ def restore_from_settings(self): # Infer data source from ini file, setup data source settings.beginGroup("data-source") - src_cls = getattr(neuroport_dbs.data_source, settings.value("class")) + src_cls = getattr(open_mer.data_source, settings.value("class")) # Get the _data_source. Note this might trigger on_source_connected before child # finishes parsing settings. _data_source = src_cls(scoped_settings=settings, on_connect_cb=self.on_source_connected) diff --git a/neuroport_dbs/dbsgui/widgets/ui/cbsdk_connect.ui b/open_mer/dbsgui/widgets/ui/cbsdk_connect.ui similarity index 100% rename from neuroport_dbs/dbsgui/widgets/ui/cbsdk_connect.ui rename to open_mer/dbsgui/widgets/ui/cbsdk_connect.ui diff --git a/neuroport_dbs/dbsgui/widgets/ui/ddu_display.ui b/open_mer/dbsgui/widgets/ui/ddu_display.ui similarity index 100% rename from neuroport_dbs/dbsgui/widgets/ui/ddu_display.ui rename to open_mer/dbsgui/widgets/ui/ddu_display.ui diff --git a/neuroport_dbs/dbsgui/widgets/ui/send_comments.ui b/open_mer/dbsgui/widgets/ui/send_comments.ui similarity index 100% rename from neuroport_dbs/dbsgui/widgets/ui/send_comments.ui rename to open_mer/dbsgui/widgets/ui/send_comments.ui diff --git a/neuroport_dbs/depth_source/__init__.py b/open_mer/depth_source/__init__.py similarity index 100% rename from neuroport_dbs/depth_source/__init__.py rename to open_mer/depth_source/__init__.py diff --git a/neuroport_dbs/depth_source/base.py b/open_mer/depth_source/base.py similarity index 100% rename from neuroport_dbs/depth_source/base.py rename to open_mer/depth_source/base.py diff --git a/neuroport_dbs/depth_source/cerebus.py b/open_mer/depth_source/cerebus.py similarity index 100% rename from neuroport_dbs/depth_source/cerebus.py rename to open_mer/depth_source/cerebus.py diff --git a/neuroport_dbs/depth_source/fhc.py b/open_mer/depth_source/fhc.py similarity index 97% rename from neuroport_dbs/depth_source/fhc.py rename to open_mer/depth_source/fhc.py index 76ce248..fe30f7e 100644 --- a/neuroport_dbs/depth_source/fhc.py +++ b/open_mer/depth_source/fhc.py @@ -4,7 +4,7 @@ import serial import serial.tools.list_ports from .base import MerDepthSource -from neuroport_dbs.settings import parse_ini_try_numeric +from ..settings import parse_ini_try_numeric class FHCSerial(MerDepthSource): diff --git a/neuroport_dbs/feature_plots/FeaturePlotWidgets.py b/open_mer/feature_plots/FeaturePlotWidgets.py similarity index 100% rename from neuroport_dbs/feature_plots/FeaturePlotWidgets.py rename to open_mer/feature_plots/FeaturePlotWidgets.py diff --git a/neuroport_dbs/feature_plots/__init__.py b/open_mer/feature_plots/__init__.py similarity index 100% rename from neuroport_dbs/feature_plots/__init__.py rename to open_mer/feature_plots/__init__.py diff --git a/neuroport_dbs/resources/config/DepthGUI.ini b/open_mer/resources/config/DepthGUI.ini similarity index 100% rename from neuroport_dbs/resources/config/DepthGUI.ini rename to open_mer/resources/config/DepthGUI.ini diff --git a/neuroport_dbs/resources/config/FeaturesGUI.ini b/open_mer/resources/config/FeaturesGUI.ini similarity index 100% rename from neuroport_dbs/resources/config/FeaturesGUI.ini rename to open_mer/resources/config/FeaturesGUI.ini diff --git a/neuroport_dbs/resources/config/MappingGUI.ini b/open_mer/resources/config/MappingGUI.ini similarity index 100% rename from neuroport_dbs/resources/config/MappingGUI.ini rename to open_mer/resources/config/MappingGUI.ini diff --git a/neuroport_dbs/resources/config/ProcessGUI.ini b/open_mer/resources/config/ProcessGUI.ini similarity index 100% rename from neuroport_dbs/resources/config/ProcessGUI.ini rename to open_mer/resources/config/ProcessGUI.ini diff --git a/neuroport_dbs/resources/config/RasterGUI.ini b/open_mer/resources/config/RasterGUI.ini similarity index 100% rename from neuroport_dbs/resources/config/RasterGUI.ini rename to open_mer/resources/config/RasterGUI.ini diff --git a/neuroport_dbs/resources/config/SweepGUI.ini b/open_mer/resources/config/SweepGUI.ini similarity index 100% rename from neuroport_dbs/resources/config/SweepGUI.ini rename to open_mer/resources/config/SweepGUI.ini diff --git a/neuroport_dbs/resources/config/WaveformGUI.ini b/open_mer/resources/config/WaveformGUI.ini similarity index 100% rename from neuroport_dbs/resources/config/WaveformGUI.ini rename to open_mer/resources/config/WaveformGUI.ini diff --git a/neuroport_dbs/resources/icons/HalfBody.png b/open_mer/resources/icons/HalfBody.png similarity index 100% rename from neuroport_dbs/resources/icons/HalfBody.png rename to open_mer/resources/icons/HalfBody.png diff --git a/neuroport_dbs/resources/icons/depth_status_delay.png b/open_mer/resources/icons/depth_status_delay.png similarity index 100% rename from neuroport_dbs/resources/icons/depth_status_delay.png rename to open_mer/resources/icons/depth_status_delay.png diff --git a/neuroport_dbs/resources/icons/depth_status_done.png b/open_mer/resources/icons/depth_status_done.png similarity index 100% rename from neuroport_dbs/resources/icons/depth_status_done.png rename to open_mer/resources/icons/depth_status_done.png diff --git a/neuroport_dbs/resources/icons/depth_status_in_use.png b/open_mer/resources/icons/depth_status_in_use.png similarity index 100% rename from neuroport_dbs/resources/icons/depth_status_in_use.png rename to open_mer/resources/icons/depth_status_in_use.png diff --git a/neuroport_dbs/resources/icons/depth_status_off.png b/open_mer/resources/icons/depth_status_off.png similarity index 100% rename from neuroport_dbs/resources/icons/depth_status_off.png rename to open_mer/resources/icons/depth_status_off.png diff --git a/neuroport_dbs/scripts/CommentsGUI.py b/open_mer/scripts/CommentsGUI.py similarity index 79% rename from neuroport_dbs/scripts/CommentsGUI.py rename to open_mer/scripts/CommentsGUI.py index 7e5d192..bcce816 100644 --- a/neuroport_dbs/scripts/CommentsGUI.py +++ b/open_mer/scripts/CommentsGUI.py @@ -1,5 +1,6 @@ from qtpy import QtWidgets -from neuroport_dbs.dbsgui.comments import CommentsGUI + +from open_mer.dbsgui.comments import CommentsGUI def main(): diff --git a/neuroport_dbs/scripts/DepthGUI.py b/open_mer/scripts/DepthGUI.py similarity index 89% rename from neuroport_dbs/scripts/DepthGUI.py rename to open_mer/scripts/DepthGUI.py index e8af766..9f194f0 100644 --- a/neuroport_dbs/scripts/DepthGUI.py +++ b/open_mer/scripts/DepthGUI.py @@ -1,6 +1,6 @@ import sys from qtpy import QtCore, QtWidgets -from neuroport_dbs.dbsgui.depth import DepthGUI +from open_mer.dbsgui.depth import DepthGUI def main(): diff --git a/neuroport_dbs/scripts/FeaturesGUI.py b/open_mer/scripts/FeaturesGUI.py similarity index 92% rename from neuroport_dbs/scripts/FeaturesGUI.py rename to open_mer/scripts/FeaturesGUI.py index 3ca2fa5..fa7e063 100644 --- a/neuroport_dbs/scripts/FeaturesGUI.py +++ b/open_mer/scripts/FeaturesGUI.py @@ -1,7 +1,7 @@ import sys import argparse from qtpy import QtCore, QtWidgets -from neuroport_dbs.dbsgui.features import FeaturesGUI +from open_mer.dbsgui.features import FeaturesGUI def main(**kwargs): diff --git a/neuroport_dbs/scripts/FeaturesGUI_old.py b/open_mer/scripts/FeaturesGUI_old.py similarity index 96% rename from neuroport_dbs/scripts/FeaturesGUI_old.py rename to open_mer/scripts/FeaturesGUI_old.py index 1a4c07b..c2ab6ab 100644 --- a/neuroport_dbs/scripts/FeaturesGUI_old.py +++ b/open_mer/scripts/FeaturesGUI_old.py @@ -5,15 +5,15 @@ from cerebuswrapper import CbSdkConnection # Note: If import dbsgui fails, then set the working directory to be this script's directory. -from neuroport_dbs.dbsgui.widgets.custom import CustomGUI, CustomWidget -from neuroport_dbs.feature_plots import * -from neuroport_dbs.dbsgui.widgets.SettingsDialog import SettingsDialog +from open_mer.dbsgui.widgets.custom import CustomGUI, CustomWidget +from open_mer.feature_plots import * +from open_mer.dbsgui.widgets.SettingsDialog import SettingsDialog from serf.tools.db_wrap import DBWrapper, ProcessWrapper # Settings -from neuroport_dbs.settings.defaults import uVRANGE, BASEPATH, SAMPLINGRATE, \ - BUFFERLENGTH, SAMPLELENGTH, DELAYBUFFER, OVERWRITEDEPTH, DEPTHSETTINGS +from open_mer.settings.defaults import uVRANGE, BASEPATH, SAMPLINGRATE, \ + BUFFERLENGTH, SAMPLELENGTH, DELAYBUFFER, OVERWRITEDEPTH, DEPTHSETTINGS # TODO: Rewrite FeaturesGUI so it doesn't inherit from CustomGUI diff --git a/neuroport_dbs/scripts/ImportNS5FeaturesGUI.py b/open_mer/scripts/ImportNS5FeaturesGUI.py similarity index 99% rename from neuroport_dbs/scripts/ImportNS5FeaturesGUI.py rename to open_mer/scripts/ImportNS5FeaturesGUI.py index 6ac5020..f2f4c26 100644 --- a/neuroport_dbs/scripts/ImportNS5FeaturesGUI.py +++ b/open_mer/scripts/ImportNS5FeaturesGUI.py @@ -5,7 +5,7 @@ import datetime import re import numpy as np -from neuroport_dbs.dbsgui.widgets.SettingsDialog import SettingsDialog +from open_mer.dbsgui.widgets.SettingsDialog import SettingsDialog from qtpy.QtWidgets import QProgressDialog from qtpy.QtCore import Qt from serf.tools.db_wrap import DBWrapper diff --git a/neuroport_dbs/scripts/MappingGUI.py b/open_mer/scripts/MappingGUI.py similarity index 88% rename from neuroport_dbs/scripts/MappingGUI.py rename to open_mer/scripts/MappingGUI.py index 752283e..65f7a84 100644 --- a/neuroport_dbs/scripts/MappingGUI.py +++ b/open_mer/scripts/MappingGUI.py @@ -1,7 +1,7 @@ import sys import qtpy from qtpy import QtWidgets, QtCore -from neuroport_dbs.dbsgui.mapping import MappingGUI +from open_mer.dbsgui.mapping import MappingGUI def main(): diff --git a/neuroport_dbs/scripts/ProcessGUI.py b/open_mer/scripts/ProcessGUI.py similarity index 93% rename from neuroport_dbs/scripts/ProcessGUI.py rename to open_mer/scripts/ProcessGUI.py index 19decd0..ef6b5c8 100644 --- a/neuroport_dbs/scripts/ProcessGUI.py +++ b/open_mer/scripts/ProcessGUI.py @@ -1,7 +1,7 @@ import sys import argparse from qtpy import QtCore, QtWidgets -from neuroport_dbs.dbsgui.process import ProcessGUI +from open_mer.dbsgui.process import ProcessGUI def main(**kwargs): diff --git a/neuroport_dbs/scripts/RasterGUI.py b/open_mer/scripts/RasterGUI.py similarity index 88% rename from neuroport_dbs/scripts/RasterGUI.py rename to open_mer/scripts/RasterGUI.py index 55e7dd1..e3bd2c9 100644 --- a/neuroport_dbs/scripts/RasterGUI.py +++ b/open_mer/scripts/RasterGUI.py @@ -1,6 +1,6 @@ import sys from qtpy import QtCore, QtWidgets -from neuroport_dbs.dbsgui.raster import RasterGUI +from open_mer.dbsgui.raster import RasterGUI def main(): diff --git a/neuroport_dbs/scripts/SweepGUI.py b/open_mer/scripts/SweepGUI.py similarity index 89% rename from neuroport_dbs/scripts/SweepGUI.py rename to open_mer/scripts/SweepGUI.py index 1507edb..e8791de 100644 --- a/neuroport_dbs/scripts/SweepGUI.py +++ b/open_mer/scripts/SweepGUI.py @@ -1,6 +1,6 @@ import sys import qtpy -from neuroport_dbs.dbsgui.sweep import SweepGUI +from open_mer.dbsgui.sweep import SweepGUI def main(): diff --git a/neuroport_dbs/scripts/WaveformGUI.py b/open_mer/scripts/WaveformGUI.py similarity index 87% rename from neuroport_dbs/scripts/WaveformGUI.py rename to open_mer/scripts/WaveformGUI.py index c0bbed3..885006e 100644 --- a/neuroport_dbs/scripts/WaveformGUI.py +++ b/open_mer/scripts/WaveformGUI.py @@ -1,6 +1,6 @@ import sys from qtpy import QtCore, QtWidgets -from neuroport_dbs.dbsgui.waveform import WaveformGUI +from open_mer.dbsgui.waveform import WaveformGUI def main(): diff --git a/neuroport_dbs/scripts/__init__.py b/open_mer/scripts/__init__.py similarity index 100% rename from neuroport_dbs/scripts/__init__.py rename to open_mer/scripts/__init__.py diff --git a/neuroport_dbs/settings/__init__.py b/open_mer/settings/__init__.py similarity index 100% rename from neuroport_dbs/settings/__init__.py rename to open_mer/settings/__init__.py diff --git a/neuroport_dbs/settings/defaults.py b/open_mer/settings/defaults.py similarity index 100% rename from neuroport_dbs/settings/defaults.py rename to open_mer/settings/defaults.py diff --git a/scripts/scratch.py b/scripts/scratch.py index c51e40b..25f4d94 100644 --- a/scripts/scratch.py +++ b/scripts/scratch.py @@ -1,8 +1,8 @@ # Just some chunks of code to help with development. import sys, os import time -dbsgui_path = os.path.abspath(os.path.join('..', 'NeuroportDBS', 'DBSGUI')) -sys.path.append(dbsgui_path) +open_mer_path = os.path.abspath(os.path.join('..', 'OpenMER', 'neuroport_dbs')) +sys.path.append(open_mer_path) from cerebuswrapper import CbSdkConnection SAMPLINGGROUPS = ["0", "500", "1000", "2000", "10000", "30000", "RAW"] diff --git a/setup.cfg b/setup.cfg index 323eb5b..32ceb86 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,14 +1,14 @@ [metadata] -name = neuroport_dbs -version = attr: neuroport_dbs.__version__ +name = open_mer +version = attr: open_mer.__version__ author = Chadwick Boulay author_email = chboulay@ohri.ca -description = Tools for clinical research using Blackrock Neuroport in DBS MER +description = Tools for deep brain stimulation (DBS) surgery intraoperative mapping with microelectrode recording (MER) long_description = file: docs/README.md long_description_content_type = text/markdown -url = https://github.com/SachsLab/NeuroportDBS +url = https://github.com/SachsLab/OpenMER project_urls = - Bug Tracker = https://github.com/SachsLab/NeuroportDBS/issues + Bug Tracker = https://github.com/SachsLab/OpenMER/issues classifiers = Programming Language :: Python :: 3 License :: OSI Approved :: Apache Software License @@ -43,15 +43,15 @@ exclude = docs [options.package_data] -"neuroport_dbs/resources/icons" = "*.png" -"neuroport_dbs/resources/config" = "*.ini" +"open_mer/resources/icons" = "*.png" +"open_mer/resources/config" = "*.ini" [options.entry_points] # https://setuptools.pypa.io/en/latest/userguide/entry_point.html gui_scripts = - dbs-sweep = neuroport_dbs.scripts.SweepGUI:main - dbs-waveform = neuroport_dbs.scripts.WaveformGUI:main - dbs-raster = neuroport_dbs.scripts.RasterGUI:main - dbs-features = neuroport_dbs.scripts.FeaturesGUI:main - dbs-mapping = neuroport_dbs.scripts.MappingGUI:main - dbs-comments = neuroport_dbs.scripts.CommentsGUI:main - dbs-ddu = neuroport_dbs.scripts.DepthGUI:main + dbs-sweep = open_mer.scripts.SweepGUI:main + dbs-waveform = open_mer.scripts.WaveformGUI:main + dbs-raster = open_mer.scripts.RasterGUI:main + dbs-features = open_mer.scripts.FeaturesGUI:main + dbs-mapping = open_mer.scripts.MappingGUI:main + dbs-comments = open_mer.scripts.CommentsGUI:main + dbs-ddu = open_mer.scripts.DepthGUI:main From db022aeb11f8d8d8a3289e3051b66309053cbd94 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 13 Aug 2023 18:38:33 -0400 Subject: [PATCH 22/65] Update a couple more paths to open_mer --- .gitignore | 2 +- open_mer/dbsgui/depth.py | 4 ++-- open_mer/dbsgui/process.py | 4 ++-- open_mer/dbsgui/widgets/custom.py | 4 ++-- open_mer/settings/__init__.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 233906d..b96763d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,7 @@ QtSerialPort/build* build-* .idea CereStimGUI/matlab/cerestim_dbs/ -neuroport_dbs.egg-info +*.egg-info *.whl /site diff --git a/open_mer/dbsgui/depth.py b/open_mer/dbsgui/depth.py index 0ebc444..635dd95 100644 --- a/open_mer/dbsgui/depth.py +++ b/open_mer/dbsgui/depth.py @@ -22,9 +22,9 @@ def __init__(self, ini_file=None): if ini_path.exists(): self._settings_path = ini_path else: - # Try home / .dbs_suite first + # Try home / .open_mer first home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) - ini_path = home_dir / '.dbs_suite' / ini_path.name + ini_path = home_dir / '.open_mer' / ini_path.name if ini_path.exists(): self._settings_path = ini_path else: diff --git a/open_mer/dbsgui/process.py b/open_mer/dbsgui/process.py index 86fab07..fc15631 100644 --- a/open_mer/dbsgui/process.py +++ b/open_mer/dbsgui/process.py @@ -52,9 +52,9 @@ def _restore_from_settings(self, ini_file=None): def _save_settings(self): if self._settings_path.parents[0] == 'config' and self._settings_path.parents[1] == 'resources': - # If this was loaded with the shipped settings, then write a new one in ~/.dbs_suite + # If this was loaded with the shipped settings, then write a new one in ~/.open_mer home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) - self._settings_path = home_dir / '.dbs_suite' / self._settings_path.name + self._settings_path = home_dir / '.open_mer' / self._settings_path.name settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) diff --git a/open_mer/dbsgui/widgets/custom.py b/open_mer/dbsgui/widgets/custom.py index ec3e6be..c56989a 100644 --- a/open_mer/dbsgui/widgets/custom.py +++ b/open_mer/dbsgui/widgets/custom.py @@ -21,9 +21,9 @@ def __init__(self, ini_file=None): if ini_path.exists(): self._settings_path = ini_path else: - # Try home / .dbs_suite first + # Try home / .open_mer first home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) - ini_path = home_dir / '.dbs_suite' / ini_path.name + ini_path = home_dir / '.open_mer' / ini_path.name if ini_path.exists(): self._settings_path = ini_path else: diff --git a/open_mer/settings/__init__.py b/open_mer/settings/__init__.py index 25cbc74..1dc2624 100644 --- a/open_mer/settings/__init__.py +++ b/open_mer/settings/__init__.py @@ -25,9 +25,9 @@ def locate_ini(ini_name): if ini_path.exists(): _settings_path = ini_path else: - # Try home / .dbs_suite first + # Try home / .open_mer first home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) - ini_path = home_dir / '.dbs_suite' / ini_path.name + ini_path = home_dir / '.open_mer' / ini_path.name if ini_path.exists(): _settings_path = ini_path else: From c77b660e1cfbd8861b4c43c53eb6accb766802f3 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 13 Aug 2023 20:37:51 -0400 Subject: [PATCH 23/65] Add CbSdkConnection.ini to settings files. --- open_mer/data_source/cerebus.py | 14 ++++++++++++-- open_mer/resources/config/CbSdkConnection.ini | 6 ++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 open_mer/resources/config/CbSdkConnection.ini diff --git a/open_mer/data_source/cerebus.py b/open_mer/data_source/cerebus.py index 6ae72ef..282f83a 100644 --- a/open_mer/data_source/cerebus.py +++ b/open_mer/data_source/cerebus.py @@ -1,3 +1,4 @@ +from pathlib import Path from qtpy import QtCore from .interface import IDataSource from ..settings import parse_ini_try_numeric @@ -15,12 +16,21 @@ class CerebusDataSource(IDataSource): def __init__(self, scoped_settings: QtCore.QSettings, **kwargs): super().__init__(**kwargs) # Sets on_connect_cb + cbsdk_settings_path = Path(scoped_settings.fileName()).parents[0] / "CbSdkConnection.ini" + conn_settings = QtCore.QSettings(str(cbsdk_settings_path), QtCore.QSettings.IniFormat) + conn_settings.beginGroup("conn-params") + self._cbsdk_conn = CbSdkConnection() conn_params = self._cbsdk_conn.con_params.copy() for key, orig_value in conn_params.items(): - conn_params[key] = scoped_settings.value(key, orig_value) + new_value = conn_settings.value(key, orig_value) + if key in ["inst-port", "client-port", "receive-buffer-size"]: + new_value = int(new_value) + conn_params[key] = new_value self._cbsdk_conn.con_params = conn_params - self._cbsdk_conn.connect() + result = self._cbsdk_conn.connect() + if result != 0: + raise ConnectionError("Could not connect to CerebusDataSource: {}".format(result)) conn_config = {} for key in scoped_settings.allKeys(): if key in ['class', 'sampling_group']: diff --git a/open_mer/resources/config/CbSdkConnection.ini b/open_mer/resources/config/CbSdkConnection.ini new file mode 100644 index 0000000..6b314ba --- /dev/null +++ b/open_mer/resources/config/CbSdkConnection.ini @@ -0,0 +1,6 @@ +[conn-params] +;client-addr=192.168.137.1 +;client-port=51002 +;inst-addr=192.168.137.128 +;inst-port=51001 +;receive-buffer-size=8388608 From 64111504f11bbf45e7c514ad2773d7b837fbb8cb Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 14 Aug 2023 00:52:00 -0400 Subject: [PATCH 24/65] Updates to GUIs. Some regressions... --- open_mer/dbsgui/comments.py | 9 +++-- open_mer/dbsgui/depth.py | 10 ++--- open_mer/dbsgui/features.py | 45 +++++++++++------------ open_mer/resources/config/FeaturesGUI.ini | 1 + 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/open_mer/dbsgui/comments.py b/open_mer/dbsgui/comments.py index b062e93..036f068 100644 --- a/open_mer/dbsgui/comments.py +++ b/open_mer/dbsgui/comments.py @@ -1,12 +1,13 @@ import os from qtpy import QtWidgets -from qtpy import uic +from qtpy.uic import loadUiType -from open_mer import dbsgui +import open_mer.dbsgui.widgets.ui -Ui_MainWindow, QtBaseClass = uic.loadUiType(os.path.join(os.path.dirname(dbsgui.widgets.ui.__file__), - 'send_comments.ui')) +ui_filename = os.path.join(os.path.dirname(open_mer.dbsgui.widgets.__file__), "ui", "send_comments.ui") +# The next line will fail unless the python environment's Scripts folder is on the PATH +Ui_MainWindow, QtBaseClass = loadUiType(ui_filename) class CommentsGUI(QtWidgets.QMainWindow, Ui_MainWindow): diff --git a/open_mer/dbsgui/depth.py b/open_mer/dbsgui/depth.py index 635dd95..eeaac99 100644 --- a/open_mer/dbsgui/depth.py +++ b/open_mer/dbsgui/depth.py @@ -8,7 +8,7 @@ except ModuleNotFoundError as e: print(e, "Try `pip install git+https://github.com/SachsLab/cerebuswrapper.git`.") -from open_mer import dbsgui +import open_mer.depth_source class DepthGUI(QtWidgets.QMainWindow): @@ -55,7 +55,7 @@ def restore_from_settings(self): # Infer depth source from ini file, setup data source settings.beginGroup("depth-source") - src_cls = getattr(dbsgui.depth_source, settings.value("class")) + src_cls = getattr(open_mer.depth_source, settings.value("class")) self._depth_source = src_cls(scoped_settings=settings) settings.endGroup() @@ -76,9 +76,9 @@ def setup_ui(self): v_layout.setContentsMargins(10, 0, 10, 10) h_layout = QtWidgets.QHBoxLayout() - h_layout.addWidget(QLabel("DTT: ")) - self._doubleSpinBox_DTT = QDoubleSpinBox() - self._doubleSpinBox_DTT = QDoubleSpinBox() + h_layout.addWidget(QtWidgets.QLabel("DTT: ")) + self._doubleSpinBox_DTT = QtWidgets.QDoubleSpinBox() + self._doubleSpinBox_DTT = QtWidgets.QDoubleSpinBox() self._doubleSpinBox_DTT.setMinimum(-100.00) self._doubleSpinBox_DTT.setMaximum(100.00) self._doubleSpinBox_DTT.setSingleStep(1.00) diff --git a/open_mer/dbsgui/features.py b/open_mer/dbsgui/features.py index 0670c0d..ad60c30 100644 --- a/open_mer/dbsgui/features.py +++ b/open_mer/dbsgui/features.py @@ -42,11 +42,12 @@ def _restore_from_settings(self, ini_file=None): settings_path = locate_ini(ini_name) settings = QtCore.QSettings(str(settings_path), QtCore.QSettings.IniFormat) default_dims = defaults.WINDOWDIMS_DICT[type(self).__name__] + settings.beginGroup("MainWindow") self.move(settings.value("pos", QtCore.QPoint(default_dims[0], default_dims[1]))) size_xy = settings.value("size", QtCore.QSize(default_dims[2], default_dims[3])) self.resize(size_xy) - self.setMaximumWidth(size_xy[0]) + self.setMaximumWidth(size_xy.width()) if settings.value("fullScreen", 'false') == 'true': self.showFullScreen() elif settings.value("maximized", 'false') == 'true': @@ -55,28 +56,24 @@ def _restore_from_settings(self, ini_file=None): self.setWindowFlags(QtCore.Qt.FramelessWindowHint) settings.endGroup() - # TODO: Other settings - # theme - # x_start - # x_stop - # y_range - # do_hp + settings.beginGroup("plot") + self._plot_settings['x_start'] = int(settings.value("x_start", -4000)) + self._plot_settings['x_stop'] = int(settings.value("x_stop", 120000)) + self._plot_settings['y_range'] = int(settings.value("y_range", 250)) + settings.endGroup() + + settings.beginGroup("buffer") + self._plot_settings['highpass'] = settings.value("highpass", "true") == "true" # buffer_length <-- Depth buffer duration (s) # sample_length <-- Depth sample size (s) # delay_buffer <-- Delay depth recording (s) # overwrite_depth <- Overwrite depth values # electrode_settings # chk_threshold - - # self._plot_settings['theme'] = theme - # self._plot_settings['color_iterator'] = -1 - # self._plot_settings['x_start'] = plot.get('x_start', -4000) - # self._plot_settings['x_stop'] = plot.get('x_stop', 120000) - # self._plot_settings['y_range'] = plot.get('y_range', 250) - # self._plot_settings['do_hp'] = True + settings.endGroup() def _setup_ui(self): - main_widget = QtWidgets.QWidget() + main_widget = QtWidgets.QWidget(self) main_widget.setLayout(QtWidgets.QVBoxLayout()) self.setCentralWidget(main_widget) @@ -88,19 +85,21 @@ def _setup_control_panel(self): lo_L1 = QtWidgets.QHBoxLayout() # Channel select lo_L1.addWidget(QtWidgets.QLabel("Electrode: ", alignment=QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight)) - chan_select_cb = QtWidgets.QComboBox() + chan_select_cb = QtWidgets.QComboBox(self.centralWidget()) chan_select_cb.setObjectName("ChanSelect_ComboBox") chan_select_cb.setMinimumWidth(70) chan_select_cb.setEnabled(False) + chan_select_cb.currentIndexChanged.connect(lambda idx: self.reset_stack()) self._reset_chan_select_items() lo_L1.addWidget(chan_select_cb) # Feature select lo_L1.addSpacing(20) lo_L1.addWidget(QtWidgets.QLabel("Feature set: ", alignment=QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight)) - feat_select_cb = QtWidgets.QComboBox() + feat_select_cb = QtWidgets.QComboBox(self.centralWidget()) feat_select_cb.setObjectName("FeatureSelect_ComboBox") feat_select_cb.setMinimumWidth(60) self._reset_feat_select_items() + feat_select_cb.currentIndexChanged.connect(lambda idx: self.reset_stack()) lo_L1.addWidget(feat_select_cb) # Second row @@ -117,7 +116,7 @@ def _setup_control_panel(self): lo_L2.addSpacing(30) hp_chk = QtWidgets.QCheckBox("HP") hp_chk.setObjectName("HP_CheckBox") - hp_chk.setChecked(self._plot_settings['hp']) + hp_chk.setChecked(self._plot_settings["highpass"]) lo_L2.addWidget(hp_chk) # Match Sweep lo_L2.addSpacing(30) @@ -133,7 +132,7 @@ def _setup_control_panel(self): refresh_pb = QtWidgets.QPushButton("Refresh") refresh_pb.setObjectName("Refresh_PushButton") refresh_pb.setMaximumWidth(50) - refresh_pb.clicked.connect(self.on_refresh_clicked) + # refresh_pb.clicked.connect(self.on_refresh_clicked) lo_R.addWidget(refresh_pb) lo_R.addSpacing(10) @@ -150,20 +149,20 @@ def _setup_control_panel(self): def _reset_feat_select_items(self): feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") - feat_combo.disconnect() + feat_combo.blockSignals(True) feat_combo.clear() feat_combo.addItems(["Raw", "Mapping"]) # TODO: Put these in features_settings # TODO: feat_combo.addItems(self._features_settings['features'].keys()) - feat_combo.currentIndexChanged.connect(lambda idx: self.reset_stack()) + feat_combo.blockSignals(False) feat_combo.setCurrentIndex(0) def _reset_chan_select_items(self): chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") - chan_combo.disconnect() + chan_combo.blockSignals(True) chan_combo.clear() chan_combo.addItem("None") # TODO: chan_combo.addItems(self._depth_settings['electrode_settings'].keys()) - chan_combo.currentIndexChanged.connect(lambda idx: self.reset_stack()) + chan_combo.blockSignals(False) chan_combo.setCurrentIndex(0) def _reset_widget_stack(self): diff --git a/open_mer/resources/config/FeaturesGUI.ini b/open_mer/resources/config/FeaturesGUI.ini index c7f618b..d612f59 100644 --- a/open_mer/resources/config/FeaturesGUI.ini +++ b/open_mer/resources/config/FeaturesGUI.ini @@ -21,6 +21,7 @@ delay_buffer=0.5 overwrite_depth=true threshold=true validity=90.0 +highpass=true [plot] x_start=-4000 From 41512a4629c4293adc21e4bda7384977b5d07b74 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 14 Aug 2023 00:52:18 -0400 Subject: [PATCH 25/65] Big docs update. --- docs/README.md | 2 +- docs/for-developers.md | 79 +----------------------- docs/getting-started.md | 34 ++++++++-- docs/introduction.md | 6 +- docs/preparing-distribution.md | 109 +++++++++++++++++++++++++++++++++ docs/usage-instructions.md | 7 +-- 6 files changed, 145 insertions(+), 92 deletions(-) create mode 100644 docs/preparing-distribution.md diff --git a/docs/README.md b/docs/README.md index 5f457a5..53f1388 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # OpenMER -A collection of software developed in the Sachs Lab at the Ottawa Hospital for for deep brain stimulation (DBS) surgery intraoperative mapping with microelectrode recording (MER). +OpenMER is a collection of software developed in the Sachs Lab at the Ottawa Hospital for deep brain stimulation (DBS) surgery intraoperative mapping with microelectrode recording (MER). ![Image of vis apps](https://github.com/SachsLab/OpenMER/blob/master/vis_apps_screenshot.PNG?raw=true) diff --git a/docs/for-developers.md b/docs/for-developers.md index 72875e5..1dc6979 100644 --- a/docs/for-developers.md +++ b/docs/for-developers.md @@ -33,84 +33,7 @@ If you build the docs locally then you'll also get the /site directory, but this TODO -## Maintaining the Zip Distribution - -* Download the latest [WinPython release](https://github.com/winpython/winpython/releases/latest). - * These instructions were tested with Winpython64-3.8.5.0 -* Run the WinPython self-extracting executable. This will create a folder containing a full Python distribution with many useful packages installed (see full list [here](https://github.com/winpython/winpython/blob/master/changelogs/WinPython-64bit-3.8.5.0.md)). -* [Edit the `WPy64-3850\settings\winpython.ini` file](https://sourceforge.net/p/winpython/wiki/Environment/) and add the following line: `PATH = %WINPYDIR%\Lib\site-packages\PyQt5\Qt\bin;%PATH%` -* Download [MySQL Windows ZIP Archive](https://dev.mysql.com/downloads/mysql/) - * Tested with mysql-8.0.29-win64.zip -* Next to the WinPython folder, extract the mysql zip and rename the extracted folder to `mysql` -* In the WinPython folder, run "WinPython Command Prompt". This will open a Command Prompt with all the paths configured to use this new Python distribution. -* Install all of the Python packages listed in the table below. - * Version numbers may not be important. Please try the latest version and report to us if it does not work. - * The method to install the packages isn't important. If you're on an internet-connected computer then you can use the pip commands. Otherwise you can first download the wheels then bring them to the development computer to pip install the wheels. - * If you wish to be able to modify any of the SachsLab packages that are pure python (mspacman, cerebuswrapper, serf, **neuroport_dbs**) then you may do so by first cloning the repository to get the source and installing the package in-place: Using the WinPython command prompt, run `pip install -e .` from within the cloned directory. - * The `cerebus` package may complain "DLL load failed". This happens when cerebus.cbpy can't find Qt5 or it finds the wrong version. This SHOULD be fixed by editing the PATH in the 3rd step above, but I also found it necessary to copy Qt5Core.dll and Qt5Xml.dll from the above path directly into the site-packages\cerebus folder. We hope to remove the qt dependency from cerebus to avoid this in the future. -* In the command prompt, `cd` into the `bin` subfolder of the unzipped mysql folder. -* Create a mysql\data folder along with the base databases: `mysqld --initialize-insecure --console` - * You can change the default data directory, username, and password. See the section below "Configuring MySQL Database Server" -* Run `mysqld` in the `mysql\bin` folder. (Windows: `start /B mysqld.exe`; allow network access if asked.) -* Back in the command prompt, run `mysqladmin --user=root create serf` -* Install the serf databases with the following commands: - ``` - serf-makemigrations - serf-migrate - ``` -* Make a batch file `WPy64-3850\scripts\NeuroportDBS.bat` with the following contents: - ```shell script - @echo off - call "%~dp0env_for_icons.bat" - start "" "%WINPYDIR%\Scripts\dbs-sweep.exe" /command:%1 /B - start "" "%WINPYDIR%\Scripts\dbs-raster.exe" /command:%1 /B - start "" "%WINPYDIR%\Scripts\dbs-waveform.exe" /command:%1 /B - start "" "%WINPYDIR%\Scripts\dbs-ddu.exe" /command:%1 /B - start "" "%WINPYDIR%\Scripts\dbs-features.exe" /command:%1 /B - ``` -* Jump ahead to [Usage Instructions](#usage-instructions) below. - -### Required Python Packages - -| Package | Version | Wheel | pip command | -| ------- | ------- | ----- | ----------- | -| pyFFTW | 0.12.0 | [Link](https://files.pythonhosted.org/packages/2b/e4/822d4cf12cd907fb8e80844db48aef7adf9e888c89256feb510fa81ae83f/pyFFTW-0.12.0-cp38-cp38-win_amd64.whl) -| mysqlclient | 2.0.1 | [Link](https://files.pythonhosted.org/packages/b2/72/e205fcf877dd0ec05d71b975def8ecef3ae4bb7fee14434615140ebdc168/mysqlclient-2.0.1-cp38-cp38-win_amd64.whl) -| Django | 3.1 | [Link](https://files.pythonhosted.org/packages/2b/5a/4bd5624546912082a1bd2709d0edc0685f5c7827a278d806a20cf6adea28/Django-3.1-py3-none-any.whl) -| quantities | 0.12.4 | | | -| python-neo | 0.9.0 | | `pip install git+https://github.com/NeuralEnsemble/python-neo.git` -| pylsl | 1.13.6 | [Link](https://files.pythonhosted.org/packages/02/c2/7b58adda02dbfa8f76bf835879d36b83dfc1da2eaa50d124d13a515e148c/pylsl-1.13.6-py2.py3-none-win_amd64.whl) -| pytf | 0.1 | [Link](https://github.com/SachsLab/pytf/releases/download/v0.1/pytf-0.1-py2.py3-none-any.whl) |`pip install git+https://github.com/SachsLab/pytf.git`| -| mspacman | 0.1 | [Link](https://github.com/SachsLab/mspacman/releases/download/v0.1/mspacman-0.1-py2.py3-none-any.whl) |`pip install git+https://github.com/SachsLab/mspacman.git`| -| cerebus | 0.0.4 | [Link](https://github.com/dashesy/CereLink/releases/download/v7.0.5/cerebus-0.0.4-cp38-cp38-win_amd64.whl) |N/A - must use wheel| -| cerebuswrapper | 0.1 | [Link](https://github.com/SachsLab/cerebuswrapper/releases/download/v0.1/cerebuswrapper-0.1.0-py3-none-any.whl) |`pip install git+https://github.com/SachsLab/cerebuswrapper.git`| -| serf | 1.1 | [Link](https://github.com/cboulay/SERF/releases/download/v1.1/serf-1.1-py3-none-any.whl) | `pip install git+https://github.com/cboulay/SERF.git#subdirectory=python`| -| neurport_dbs | 1.0 | [Link](https://github.com/SachsLab/NeuroportDBS/releases/download/v1.0/neuroport_dbs-1.0.0-py3-none-any.whl) | `pip install git+https://github.com/SachsLab/NeuroportDBS.git`| - -### Configuring MySQL Database Server - -* If you wish to use a different datadir then you must first create a `my.cnf` file in the root `mysql` folder with the following contents (commented out lines aren't necessary, just keeping them here for reference): - ``` - [mysqld] - datadir=path/to/data - #port = 3306 - #socket = /tmp/mysql.sock - #pid-file = /Volumes/STORE/eerfdata/Chadwicks-MacBook-Pro.local.pid - #default-storage-engine = MyISAM - #default_tmp_storage_engine = MyISAM - #query_cache_type = 1 - #key_buffer_size = 2G - #query_cache_limit = 400M - ``` -* If you wish to secure the database then you'll need to give the root account a password. Do so with `mysql_secure_installation`. -* If you change from the default username (`root`) and password (none) then you will have to tell `serf` what the username and password are. Create a file named `my_serf.cnf` and put it in the path identified by the following command: `python -c "import os; print(os.path.expanduser('~'))"` The file contents should be - ``` - [client] - user = root - password = {password} - #port = 3306 - #socket = /tmp/mysql.sock - ``` + ## For experts who want to use their existing environment diff --git a/docs/getting-started.md b/docs/getting-started.md index 95d7ad8..711e108 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,17 +1,39 @@ -Our primary method of distributing the full OpenMER Suite is as a giant zip file. TODO: Link! +The PC which runs our software is directly connected to the acquisition system but it is never connected to the internet. Thus, we copy everything we need to a thumb drive which we then copy to the clinical PC and install. -If you want to setup the individual pieces on an internet-connected computer (e.g., for development or testing) then please look at the [For Developers](./for-developers.md) documentation. +> For development or testing on an internet-connected computer, please look at the [For Developers](./for-developers.md) documentation. -It is expected that the target computer is a standalone computer that has a dedicated connection to the data acquisition system, such as a manufacturer-provided PC which is usually not connected to the internet. Testing without the hardware is also possible using a signal generator source or a data playback source (see below for example). +## Installation -Extract the zip file to the target computer. Choose a destination with a lot of disk space because the data segments will be saved within. +### Distribution -Updates may come in the form of a smaller zip file to extract within a specific subfolder of the extracted distribution. +Copy the `` folder from the thumb drive to the instrument PC. Be sure to choose a location with lots of disk space because many recording segments will be stored within this folder. -Proceed with the [Usage Instructions](./usage-instructions.md) +> If you do not have the `` folder then follow the [Preparing Distribution](./preparing-distribution.md) instructions to create it. + +### Configure + +The `` folder is ready to use as-is. However, with some additional steps it can be more useful on the target PC. + +#### Shortcuts + +* Make a desktop shortcut to `\mysql\bin\mysqld.exe`. +* Make a desktop shortcut to `\\scripts\OpenMER.bat` + +#### Settings files + +Copy all of the .ini files from `\\\Lib\site-packages\open_mer\resources\config` +to %HOME%\.open_mer\. + +You can then edit these settings files to change some parameters. CbSkConnection.ini can be particularly important if not using Central. + +## Using OpenMER + +See [Usage Instructions](./usage-instructions.md) ## Test Environment - Without Hardware +Testing without the hardware is also possible using a signal generator source or a data playback source (see below for example). + ### Emulate Blackrock NSP * Run "C:\Program Files (x86)\Blackrock Microsystems\NeuroPort Windows Suite\runNPlayAndCentral.bat" diff --git a/docs/introduction.md b/docs/introduction.md index 7dba74b..c8a52a5 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -1,6 +1,8 @@ -Dr. Adam Sachs is a neurosurgeon at the Ottawa Hospital where he and his team implant DBS electrodes to treat motor disorders such as Parkinson's Disease, dystonia, and tremor. Part of the surgical procedure includes microelectrode recording (MER) to map the tissue around the intended DBS target to correct for intrinsic error in the DBS targeting process (i.e., due to imaging, planning, stereotaxy, and brain shift) and to refine the target location. +The documentation is hosted online at [https://sachslab.github.io/OpenMER/](https://sachslab.github.io/OpenMER/). Please navigate there for the latest version. -MER requires specialized equipment and software. While all-in-one medical devices are recommended, Dr. Sachs' research requires a more bespoke solution with multipurpose equipment and custom software. The software and some documentation are made available here with the goal of helping other clinician-scientists to use custom systems for DBS MER. +Dr. Adam Sachs is a neurosurgeon at the Ottawa Hospital where he and his team perform DBS surgery to treat motor disorders such as Parkinson's Disease, dystonia, and tremor. Part of the surgical procedure includes microelectrode recording (MER) to map the tissue around the intended DBS target to correct for intrinsic error in the DBS targeting process (i.e., due to imaging, planning, stereotaxy, and brain shift) and to refine the target location. + +MER requires specialized equipment and software. While all-in-one medical devices are recommended, Dr. Sachs' research requires a more bespoke solution with multipurpose equipment and custom software. The software and some documentation are made available here with the goal of helping other clinician-scientists who wish to use custom systems for DBS MER. ## Equipment diff --git a/docs/preparing-distribution.md b/docs/preparing-distribution.md new file mode 100644 index 0000000..7ad10da --- /dev/null +++ b/docs/preparing-distribution.md @@ -0,0 +1,109 @@ +The target PC is usually a clinical device that is not internet connected. Thus, we prepare a distribution to be copied onto a thumb drive then copied to the target PC. + +After completing these instructions to prepare the distribution, head back to [Getting Started](./getting-started.md) for further instructions. + +## Base + +Create a folder somewhere you have write access. The location should have at least 2 GB disk space. Name the folder whatever you like. Throughout this documentation, we refer to the folder as ``. + +## Python + +* Download the latest [WinPython release](https://winpython.github.io/#releases). + * Get the one ending in `dot` as this excludes some unnecessary bloat. Even so, it's rather huge. + * These instructions were tested with WinPython64-3.11.4.0dot +* Run the WinPython self-extracting executable and choose the `` folder as the extraction location. This will create a `/` folder containing a full Python distribution with many useful packages installed. + * The full list of installed packages can be found [here](https://github.com/winpython/winpython/blob/master/changelogs/). +* [Edit the `\settings\winpython.ini` file](https://sourceforge.net/p/winpython/wiki/Environment/) and add the following line: `PATH = %WINPYDIR%\Lib\site-packages\PyQt5\Qt\bin;%PATH%` + * TODO: Is this still necessary with PySide6? +* In the WinPython folder, run "WinPython Command Prompt". This will open a Command Prompt with all the paths configured to use this new Python distribution. + * Confirm with `echo %PATH%`. There should be many paths in the WinPython tree. +* Uninstall PyQt5 (we will be installing PySide6): `pip uninstall pyqt5` +* Install all the Python packages according to the [Required Python Packages](#required-python-packages) section below. +* Make a batch file `WPy64-3850\scripts\OpenMER.bat` with the following contents: + ```shell script + @echo off + call "%~dp0env_for_icons.bat" + start "" "%WINPYDIR%\Scripts\dbs-sweep.exe" /command:%1 /B + start "" "%WINPYDIR%\Scripts\dbs-raster.exe" /command:%1 /B + start "" "%WINPYDIR%\Scripts\dbs-waveform.exe" /command:%1 /B + start "" "%WINPYDIR%\Scripts\dbs-ddu.exe" /command:%1 /B + start "" "%WINPYDIR%\Scripts\dbs-features.exe" /command:%1 /B + ``` + +### Required Python Packages + +In your WinPython Command Prompt, try the following commands first. If they fail, download the wheels matching the version in the table and install the wheels directly. +> A great place to find wheels is http://www.lfd.uci.edu/~gohlke/pythonlibs/. The second best place to find wheels is in pypi.org and clicking on the "Download files" button for a particular package. + +> Developers only: For each of the SachsLab packages (mspacman, cerebuswrapper, serf, **neuroport_dbs**), you have the option of cloning the repo then installing the package in-place for easier editing. Using the WinPython command prompt, run `pip install -e .` from within the cloned directory. +> +``` +pip install PySide6 pyFFTW mysqlclient Django quantities pylsl +pip install git+https://github.com/NeuralEnsemble/python-neo.git +pip install git+https://github.com/SachsLab/pytf.git +pip install git+https://github.com/SachsLab/mspacman.git +pip install git+https://github.com/CerebusOSS/cerebuswrapper.git +pip install git+https://github.com/cboulay/SERF.git#subdirectory=python +pip install git+https://github.com/SachsLab/OpenMER.git +``` + +| Package | Version | Wheel | pip command | +|----------------|---------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------| +| pySide6 | 6.5.2 | | | +| qtpy | 2.4.0 | | https://github.com/spyder-ide/qtpy.git | +| pyFFTW | 0.13.1 | | | +| mysqlclient | 2.2.0 | | | +| Django | 3.11.4 | (Comes in WinPython) | | +| quantities | 0.14.1 | | | +| python-neo | 0.13.0 | | `pip install git+https://github.com/NeuralEnsemble/python-neo.git` | +| pylsl | 1.16.1 | | `pip install pylsl` | +| pytf | 0.1 | [Link](https://github.com/SachsLab/pytf/releases/download/v0.1/pytf-0.1-py2.py3-none-any.whl) | `pip install git+https://github.com/SachsLab/pytf.git` | +| mspacman | 0.1 | [Link](https://github.com/SachsLab/mspacman/releases/download/v0.1/mspacman-0.1-py2.py3-none-any.whl) | `pip install git+https://github.com/SachsLab/mspacman.git` | +| cerebus | 0.4 | | `pip install https://github.com/CerebusOSS/CereLink/releases/download/v7.6.4/cerebus-0.4-cp311-cp311-win_amd64.whl` | +| cerebuswrapper | 0.1 | [Link](https://github.com/SachsLab/cerebuswrapper/releases/download/v0.1/cerebuswrapper-0.1.0-py3-none-any.whl) | `pip install git+https://github.com/CerebusOSS/cerebuswrapper.git` | +| serf | 1.2 | | `pip install git+https://github.com/cboulay/SERF.git#subdirectory=python` | +| neurport_dbs | 1.0 | [Link](https://github.com/SachsLab/NeuroportDBS/releases/download/v1.0/neuroport_dbs-1.0.0-py3-none-any.whl) | `pip install git+https://github.com/SachsLab/OpenMER.git` | + +## MySQL + +* Download [MySQL Windows ZIP Archive](https://dev.mysql.com/downloads/mysql/) + * Tested with mysql-8.1.0-win64.zip + * Don't choose the .msi + * After selecting the zip, on the following page look for the "No thanks, just start my download" link. +* Extract the mysql zip into `` and rename the extracted folder to `mysql` +* In the command prompt, `cd` into the `bin` subfolder of the unzipped mysql folder. +* Create a mysql\data folder along with the base databases: `mysqld --initialize-insecure --console` + * You can change the default data directory, username, and password. See the section below on [Configuring MySQL Database Server](#configuring-mysql-database-server) +* Run `mysqld` in the `mysql\bin` folder. (Windows: `start /B mysqld.exe`; allow network access if asked.) +* Back in the command prompt, run `mysqladmin --user=root create serf` + +* Install the serf databases with the following commands (available after serf Python package is installed): + ``` + serf-makemigrations + serf-migrate + ``` + +### Configuring MySQL Database Server + +* If you wish to use a different datadir then you must first create a `my.cnf` file in the root `mysql` folder with the following contents (commented out lines aren't necessary, just keeping them here for reference): + ``` + [mysqld] + datadir=path/to/data + #port = 3306 + #socket = /tmp/mysql.sock + #pid-file = /Volumes/STORE/eerfdata/Chadwicks-MacBook-Pro.local.pid + #default-storage-engine = MyISAM + #default_tmp_storage_engine = MyISAM + #query_cache_type = 1 + #key_buffer_size = 2G + #query_cache_limit = 400M + ``` +* If you wish to secure the database then you'll need to give the root account a password. Do so with `mysql_secure_installation`. +* If you change from the default username (`root`) and password (none) then you will have to tell `serf` what the username and password are. Create a file named `my_serf.cnf` and put it in the path identified by the following command: `python -c "import os; print(os.path.expanduser('~'))"` The file contents should be + ``` + [client] + user = root + password = {password} + #port = 3306 + #socket = /tmp/mysql.sock + ``` \ No newline at end of file diff --git a/docs/usage-instructions.md b/docs/usage-instructions.md index 6347484..af07251 100644 --- a/docs/usage-instructions.md +++ b/docs/usage-instructions.md @@ -1,8 +1,5 @@ -For easier running: -* Make a shortcut to `mysql\bin\mysqld.exe`. -* Make a shortcut to `WPy64-3850\scripts\NeuroportDBS.bat` - -First run the `mysqld` binary by double-clicking its shortcut. Then do the same for the `NeuroportDBS` batch file. +First run the `mysqld` binary by double-clicking its shortcut. +Then do the same for the `NeuroportDBS` batch file. Additional details follow. From 2a834eaf5fe5f810d884be6aebea6a752ea006d1 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 14 Aug 2023 01:10:52 -0400 Subject: [PATCH 26/65] Update Qt RegExp names. --- open_mer/dbsgui/widgets/SettingsDialog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/open_mer/dbsgui/widgets/SettingsDialog.py b/open_mer/dbsgui/widgets/SettingsDialog.py index e491d29..41b42e1 100644 --- a/open_mer/dbsgui/widgets/SettingsDialog.py +++ b/open_mer/dbsgui/widgets/SettingsDialog.py @@ -3,8 +3,8 @@ from qtpy.QtWidgets import QComboBox, QLineEdit, QLabel, QDialog, QVBoxLayout, QWidget, \ QGridLayout, QDialogButtonBox, QCalendarWidget, \ QCheckBox, QTabWidget, QTextEdit -from qtpy.QtCore import QDate, QRegExp, Qt, Signal -from qtpy.QtGui import QRegExpValidator +from qtpy.QtCore import QDate, QRegularExpression, Qt, Signal +from qtpy.QtGui import QRegularExpressionValidator from serf.tools.db_wrap import DBWrapper # Settings @@ -226,8 +226,8 @@ def combo_from_enum(self, enum_name): return combo def coord_line_edit(self): - template = QRegExp(r"[-]?\d*\.?\d{0,3}") - validator = QRegExpValidator(template) + template = QRegularExpression(r"[-]?\d*\.?\d{0,3}") + validator = QRegularExpressionValidator(template) line = QLineEdit("0.0") line.setValidator(validator) From 418af22100656275e11fb300b8c15b8a466f2405 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 14 Aug 2023 22:58:30 -0400 Subject: [PATCH 27/65] Still working * np dtypes * some other minor changes --- docs/preparing-distribution.md | 6 ++++-- open_mer/data_source/cerebus.py | 2 +- open_mer/dbsgui/depth.py | 4 ++-- open_mer/dbsgui/features.py | 2 +- open_mer/dbsgui/sweep.py | 10 +++++----- open_mer/dbsgui/widgets/SettingsDialog.py | 12 ++++++------ open_mer/feature_plots/FeaturePlotWidgets.py | 6 +++--- 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/preparing-distribution.md b/docs/preparing-distribution.md index 7ad10da..f7386e3 100644 --- a/docs/preparing-distribution.md +++ b/docs/preparing-distribution.md @@ -9,7 +9,7 @@ Create a folder somewhere you have write access. The location should have at lea ## Python * Download the latest [WinPython release](https://winpython.github.io/#releases). - * Get the one ending in `dot` as this excludes some unnecessary bloat. Even so, it's rather huge. + * Get the one ending in `dot` as this excludes some unnecessary bloat. * These instructions were tested with WinPython64-3.11.4.0dot * Run the WinPython self-extracting executable and choose the `` folder as the extraction location. This will create a `/` folder containing a full Python distribution with many useful packages installed. * The full list of installed packages can be found [here](https://github.com/winpython/winpython/blob/master/changelogs/). @@ -38,10 +38,12 @@ In your WinPython Command Prompt, try the following commands first. If they fail > Developers only: For each of the SachsLab packages (mspacman, cerebuswrapper, serf, **neuroport_dbs**), you have the option of cloning the repo then installing the package in-place for easier editing. Using the WinPython command prompt, run `pip install -e .` from within the cloned directory. > ``` -pip install PySide6 pyFFTW mysqlclient Django quantities pylsl +python.exe -m pip install --upgrade pip +pip install PySide6 pyFFTW mysqlclient Django quantities pylsl numpy scipy Cython pyaudio pip install git+https://github.com/NeuralEnsemble/python-neo.git pip install git+https://github.com/SachsLab/pytf.git pip install git+https://github.com/SachsLab/mspacman.git +pip install https://github.com/CerebusOSS/CereLink/releases/download/v7.6.4/cerebus-0.4-cp311-cp311-win_amd64.whl pip install git+https://github.com/CerebusOSS/cerebuswrapper.git pip install git+https://github.com/cboulay/SERF.git#subdirectory=python pip install git+https://github.com/SachsLab/OpenMER.git diff --git a/open_mer/data_source/cerebus.py b/open_mer/data_source/cerebus.py index 282f83a..79b3aa6 100644 --- a/open_mer/data_source/cerebus.py +++ b/open_mer/data_source/cerebus.py @@ -45,7 +45,7 @@ def __init__(self, scoped_settings: QtCore.QSettings, **kwargs): # get_events, get_comments, get_continuous, buffer_parameter: comment_length self._cbsdk_conn.cbsdk_config = conn_config - self._group_ix = SAMPLINGGROUPS.index(scoped_settings.value("sampling_group")) + self._group_ix = SAMPLINGGROUPS.index(scoped_settings.value("sampling_group"), type=str) self._group_info = self._decode_group_info(self._cbsdk_conn.get_group_config(self._group_ix)) # self.wf_config = self.cbsdk_conn.get_sys_config() # {'spklength': 48, 'spkpretrig': 10, 'sysfreq': 30000} self._on_connect_cb(self) diff --git a/open_mer/dbsgui/depth.py b/open_mer/dbsgui/depth.py index eeaac99..0595953 100644 --- a/open_mer/dbsgui/depth.py +++ b/open_mer/dbsgui/depth.py @@ -29,7 +29,7 @@ def __init__(self, ini_file=None): self._settings_path = ini_path else: # Use default ini that ships with module. - self._settings_path = Path(__file__).parents[0] / 'resources' / 'config' / ini_path.name + self._settings_path = Path(__file__).parents[1] / 'resources' / 'config' / ini_path.name self.display_string = None self._depth_stream = None @@ -55,7 +55,7 @@ def restore_from_settings(self): # Infer depth source from ini file, setup data source settings.beginGroup("depth-source") - src_cls = getattr(open_mer.depth_source, settings.value("class")) + src_cls = getattr(open_mer.depth_source, settings.value("class", "CBSDKPlayback")) self._depth_source = src_cls(scoped_settings=settings) settings.endGroup() diff --git a/open_mer/dbsgui/features.py b/open_mer/dbsgui/features.py index ad60c30..0c0c375 100644 --- a/open_mer/dbsgui/features.py +++ b/open_mer/dbsgui/features.py @@ -278,7 +278,7 @@ def read_from_shared_memory(self): import numpy as np if self.monitored_channel_mem.isAttached(): self.monitored_channel_mem.lock() - settings = np.frombuffer(self.monitored_channel_mem.data(), dtype=np.float)[-3:] + settings = np.frombuffer(self.monitored_channel_mem.data(), dtype=np.float64)[-3:] self.chan_select.setCurrentIndex(int(settings[0])) self.range_edit.setText(str(settings[1])) self.manage_range_edit() diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index dbaf7bf..dbc8c1c 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -170,7 +170,7 @@ def on_ln_filter_changed(self, state): self.plot_config['do_ln'] = state == QtCore.Qt.Checked def on_range_edit_editingFinished(self): - self.plot_config['y_range'] = float(self.range_edit.text()) + self.plot_config['y_range'] = np.float64(self.range_edit.text()) self.refresh_axes() self.update_shared_memory() @@ -208,15 +208,15 @@ def update_shared_memory(self): self.monitored_shared_mem.lock() chan_labels = [_['name'] for _ in self.chan_states] if self.audio['chan_label'] in ['silence', None]: - curr_channel = float(0) + curr_channel = np.float64(0) else: - curr_channel = float(chan_labels.index(self.audio['chan_label']) + 1) # 0 == None + curr_channel = np.float64(chan_labels.index(self.audio['chan_label']) + 1) # 0 == None curr_range = self.plot_config['y_range'] - curr_hp = float(self.plot_config['do_hp']) + curr_hp = np.float64(self.plot_config['do_hp']) - to_write = np.array([curr_channel, curr_range, curr_hp], dtype=float).tobytes() + to_write = np.array([curr_channel, curr_range, curr_hp], dtype=np.float64).tobytes() self.monitored_shared_mem.data()[-len(to_write):] = memoryview(to_write) self.monitored_shared_mem.unlock() diff --git a/open_mer/dbsgui/widgets/SettingsDialog.py b/open_mer/dbsgui/widgets/SettingsDialog.py index 41b42e1..eb2937b 100644 --- a/open_mer/dbsgui/widgets/SettingsDialog.py +++ b/open_mer/dbsgui/widgets/SettingsDialog.py @@ -8,7 +8,7 @@ from serf.tools.db_wrap import DBWrapper # Settings -from ...settings.defaults import BUFFERLENGTH, SAMPLELENGTH, DELAYBUFFER, OVERWRITEDEPTH +from open_mer.settings.defaults import BUFFERLENGTH, SAMPLELENGTH, DELAYBUFFER, OVERWRITEDEPTH class SubjectWidget(QWidget): @@ -98,7 +98,7 @@ def to_dict(self): self.subject_settings['id'] = self.id_combo.currentText() self.subject_settings['name'] = self.name_edit.text() self.subject_settings['sex'] = self.sex_combo.currentText() - self.subject_settings['birthday'] = self.dob_calendar.selectedDate().toPyDate() + self.subject_settings['birthday'] = self.dob_calendar.selectedDate().toPython() self.subject_settings['NSP_comment'] = self.file_comment.toPlainText() @@ -305,21 +305,21 @@ def to_dict(self): self.procedure_settings['type'] = self.type_combo.currentText() self.procedure_settings['a'] = np.array([float(self.a_x.text()), float(self.a_y.text()), - float(self.a_z.text())], dtype=np.float) + float(self.a_z.text())], dtype=float) self.procedure_settings['distance_to_target'] = float(self.dist_to_target.text()) self.procedure_settings['e'] = np.array([float(self.e_x.text()), float(self.e_y.text()), - float(self.e_z.text())], dtype=np.float) + float(self.e_z.text())], dtype=float) self.procedure_settings['electrode_config'] = self.electrode_combo.currentText() self.procedure_settings['entry'] = np.array([float(self.entry_x.text()), float(self.entry_y.text()), - float(self.entry_z.text())], dtype=np.float) + float(self.entry_z.text())], dtype=float) self.procedure_settings['medication_status'] = self.medic_combo.currentText() self.procedure_settings['target_name'] = self.target_name.text() self.procedure_settings['recording_config'] = self.rec_combo.currentText() self.procedure_settings['target'] = np.array([float(self.target_x.text()), float(self.target_y.text()), - float(self.target_z.text())], dtype=np.float) + float(self.target_z.text())], dtype=float) self.procedure_settings['offset_direction'] = self.offset_direction_combo.currentText() self.procedure_settings['offset_size'] = float(self.offset_size.text()) diff --git a/open_mer/feature_plots/FeaturePlotWidgets.py b/open_mer/feature_plots/FeaturePlotWidgets.py index dbf8f30..b675fec 100644 --- a/open_mer/feature_plots/FeaturePlotWidgets.py +++ b/open_mer/feature_plots/FeaturePlotWidgets.py @@ -624,11 +624,11 @@ def __init__(self, plot_config, *args, **kwargs): # 0.001 mm steps self.spectrum_x = np.arange(pwr_sett['x_range'][0] * 1000, - pwr_sett['x_range'][1] * 1000, 1, dtype=np.int) - self.spectrum_depths = np.array(np.round(pwr_sett['x_range'][0]*1000), dtype=np.int) + pwr_sett['x_range'][1] * 1000, 1, dtype=int) + self.spectrum_depths = np.array(np.round(pwr_sett['x_range'][0]*1000), dtype=int) self.spectrum_data = np.zeros((pwr_sett['y_range'][1], self.spectrum_x.shape[0])) - self.episodes_depths = np.array(np.round(pep_sett['x_range'][0] * 1000), dtype=np.int) + self.episodes_depths = np.array(np.round(pep_sett['x_range'][0] * 1000), dtype=int) self.episodes_data = np.zeros((pep_sett['y_range'][1], self.spectrum_x.shape[0])) self.depth_data = {} From d2d472c2bac0ccadbcb7e191d9b05248cc3db726 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Tue, 15 Aug 2023 06:55:50 -0400 Subject: [PATCH 28/65] Fix misplaced argument. --- open_mer/data_source/cerebus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_mer/data_source/cerebus.py b/open_mer/data_source/cerebus.py index 79b3aa6..24e2dcb 100644 --- a/open_mer/data_source/cerebus.py +++ b/open_mer/data_source/cerebus.py @@ -45,7 +45,7 @@ def __init__(self, scoped_settings: QtCore.QSettings, **kwargs): # get_events, get_comments, get_continuous, buffer_parameter: comment_length self._cbsdk_conn.cbsdk_config = conn_config - self._group_ix = SAMPLINGGROUPS.index(scoped_settings.value("sampling_group"), type=str) + self._group_ix = SAMPLINGGROUPS.index(scoped_settings.value("sampling_group", type=str)) self._group_info = self._decode_group_info(self._cbsdk_conn.get_group_config(self._group_ix)) # self.wf_config = self.cbsdk_conn.get_sys_config() # {'spklength': 48, 'spkpretrig': 10, 'sysfreq': 30000} self._on_connect_cb(self) From 3ffde9b33a3de218d9ee3092e068b25550030eef Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 16 Aug 2023 17:12:11 -0400 Subject: [PATCH 29/65] WIP update --- .gitignore | 1 + docs/preparing-distribution.md | 2 +- open_mer/data_source/cerebus.py | 11 +- open_mer/dbsgui/features.py | 152 +++++++-------- open_mer/dbsgui/process.py | 194 ++++++++++--------- open_mer/dbsgui/widgets/SettingsDialog.py | 40 ++-- open_mer/feature_plots/FeaturePlotWidgets.py | 4 +- open_mer/resources/config/DepthGUI.ini | 2 +- open_mer/resources/config/FeaturesGUI.ini | 13 +- open_mer/resources/config/ProcessGUI.ini | 8 +- setup.cfg | 2 + 11 files changed, 216 insertions(+), 213 deletions(-) diff --git a/.gitignore b/.gitignore index b96763d..177a0d7 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ CereStimGUI/matlab/cerestim_dbs/ *.whl /site +build/ diff --git a/docs/preparing-distribution.md b/docs/preparing-distribution.md index f7386e3..29aac7b 100644 --- a/docs/preparing-distribution.md +++ b/docs/preparing-distribution.md @@ -39,7 +39,7 @@ In your WinPython Command Prompt, try the following commands first. If they fail > ``` python.exe -m pip install --upgrade pip -pip install PySide6 pyFFTW mysqlclient Django quantities pylsl numpy scipy Cython pyaudio +pip install PySide6 pyFFTW mysqlclient Django quantities pylsl numpy scipy Cython pyaudio pyzmq pip install git+https://github.com/NeuralEnsemble/python-neo.git pip install git+https://github.com/SachsLab/pytf.git pip install git+https://github.com/SachsLab/mspacman.git diff --git a/open_mer/data_source/cerebus.py b/open_mer/data_source/cerebus.py index 24e2dcb..7ad6ea0 100644 --- a/open_mer/data_source/cerebus.py +++ b/open_mer/data_source/cerebus.py @@ -16,24 +16,25 @@ class CerebusDataSource(IDataSource): def __init__(self, scoped_settings: QtCore.QSettings, **kwargs): super().__init__(**kwargs) # Sets on_connect_cb + self._cbsdk_conn = CbSdkConnection() cbsdk_settings_path = Path(scoped_settings.fileName()).parents[0] / "CbSdkConnection.ini" conn_settings = QtCore.QSettings(str(cbsdk_settings_path), QtCore.QSettings.IniFormat) conn_settings.beginGroup("conn-params") - - self._cbsdk_conn = CbSdkConnection() conn_params = self._cbsdk_conn.con_params.copy() for key, orig_value in conn_params.items(): new_value = conn_settings.value(key, orig_value) if key in ["inst-port", "client-port", "receive-buffer-size"]: new_value = int(new_value) conn_params[key] = new_value + conn_settings.endGroup() + self._cbsdk_conn.con_params = conn_params result = self._cbsdk_conn.connect() if result != 0: raise ConnectionError("Could not connect to CerebusDataSource: {}".format(result)) conn_config = {} for key in scoped_settings.allKeys(): - if key in ['class', 'sampling_group']: + if key in ['basepath', 'class', 'sampling_group']: continue split_key = key.split('/') if len(split_key) > 1: @@ -48,7 +49,9 @@ def __init__(self, scoped_settings: QtCore.QSettings, **kwargs): self._group_ix = SAMPLINGGROUPS.index(scoped_settings.value("sampling_group", type=str)) self._group_info = self._decode_group_info(self._cbsdk_conn.get_group_config(self._group_ix)) # self.wf_config = self.cbsdk_conn.get_sys_config() # {'spklength': 48, 'spkpretrig': 10, 'sysfreq': 30000} - self._on_connect_cb(self) + + if self._on_connect_cb is not None: + self._on_connect_cb(self) @staticmethod def _decode_group_info(group_info: dict): diff --git a/open_mer/dbsgui/features.py b/open_mer/dbsgui/features.py index 0c0c375..0f3d37c 100644 --- a/open_mer/dbsgui/features.py +++ b/open_mer/dbsgui/features.py @@ -29,10 +29,12 @@ def __init__(self, ini_file: str = None, **kwargs): self._procedure_settings = {} self._buffer_settings = {} self._features_settings = {} + self._chan_labels = [] + + self._db = DBWrapper() self._restore_from_settings(ini_file) self._setup_ui() - self._setup_shmem() def closeEvent(self, *args, **kwargs): super().closeEvent(*args, **kwargs) @@ -61,14 +63,20 @@ def _restore_from_settings(self, ini_file=None): self._plot_settings['x_stop'] = int(settings.value("x_stop", 120000)) self._plot_settings['y_range'] = int(settings.value("y_range", 250)) settings.endGroup() + self._plot_settings["color_iterator"] = -1 + self._plot_settings["image_plot"] = False + + settings.beginGroup("features") + add_features = [] + for feat_grp in settings.childKeys(): + b_add = settings.value(feat_grp, True, type=bool) + if b_add: + add_features.append(feat_grp) + self._features_settings["features"] = add_features + settings.endGroup() settings.beginGroup("buffer") - self._plot_settings['highpass'] = settings.value("highpass", "true") == "true" - # buffer_length <-- Depth buffer duration (s) - # sample_length <-- Depth sample size (s) - # delay_buffer <-- Delay depth recording (s) - # overwrite_depth <- Overwrite depth values - # electrode_settings + self._plot_settings["highpass"] = settings.value("highpass", "true") == "true" # chk_threshold settings.endGroup() @@ -151,8 +159,7 @@ def _reset_feat_select_items(self): feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") feat_combo.blockSignals(True) feat_combo.clear() - feat_combo.addItems(["Raw", "Mapping"]) # TODO: Put these in features_settings - # TODO: feat_combo.addItems(self._features_settings['features'].keys()) + feat_combo.addItems(self._features_settings['features']) feat_combo.blockSignals(False) feat_combo.setCurrentIndex(0) @@ -161,7 +168,7 @@ def _reset_chan_select_items(self): chan_combo.blockSignals(True) chan_combo.clear() chan_combo.addItem("None") - # TODO: chan_combo.addItems(self._depth_settings['electrode_settings'].keys()) + chan_combo.addItems(self._chan_labels) chan_combo.blockSignals(False) chan_combo.setCurrentIndex(0) @@ -182,38 +189,16 @@ def _reset_widget_stack(self): 'Raw': RawPlots, 'Mapping': MappingPlots, 'STN': STNPlots, 'LFP': LFPPlots, 'Spikes': SpikePlots, None: NullPlotWidget } + n_feats = len(self._features_settings['features']) - for chan_ix, chan_label in enumerate(["None"] + list(self._depth_settings['electrode_settings'].keys())): + for chan_ix, chan_label in enumerate(["None"] + self._chan_labels): self._widget_stack[chan_label] = {} - for feat_ix, feat_label in enumerate(self._features_settings['features'].keys()): + for feat_ix, feat_label in enumerate(self._features_settings['features']): self._widget_stack[chan_label][feat_label] = [n_feats*chan_ix + feat_ix, 0] w_cls = plot_class_map[feat_label] if feat_label in plot_class_map else NullPlotWidget - plot_stack.addWidget(w_cls)(dict(self.plot_config)) + plot_stack.addWidget(w_cls(dict(self._plot_settings))) plot_stack.setCurrentIndex(0) - def _setup_shmem(self): - # shared memory to display the currently monitored electrode - self.monitored_channel_mem = QtCore.QSharedMemory() - self.monitored_channel_mem.setKey("MonitoredChannelMemory") - self.monitored_channel_mem.attach(QtCore.QSharedMemory.ReadOnly) - - def manage_depth_process(self, on_off): - # start process - if on_off and not self._depth_process_running: - self._depth_wrapper.start_worker() - self._depth_process_running = True - else: - self._depth_wrapper.kill_worker() - self._depth_process_running = False - - def manage_feature_process(self, on_off): - if on_off and not self._features_process_running: - self._features_wrapper.start_worker() - self._features_process_running = True - else: - self._features_wrapper.kill_worker() - self._features_process_running = False - def reset_stack(self): plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") @@ -221,6 +206,11 @@ def reset_stack(self): if plot_stack is not None and chan_combo is not None and feat_combo is not None: plot_stack.setCurrentIndex(self._widget_stack[chan_combo.currentText()][feat_combo.currentText()][0]) + def handle_procedure_id(self, procedure_id): + self._db.select_procedure(procedure_id) + self._chan_labels = self._db.list_channel_labels() + self._reset_chan_select_items() + def manage_refresh(self): plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") @@ -239,52 +229,44 @@ def on_sweep_clicked(self): hp_chk = self.findChild(QtWidgets.QCheckBox, "HP_CheckBox") range_edit = self.findChild(QtWidgets.QLineEdit, "Range_LineEdit") for _ in [chan_combo, hp_chk, range_edit]: - _.setEnabled(sweep_control.isChecked() and self.monitored_channel_mem.isAttached()) - if sweep_control.isChecked() and self.monitored_channel_mem.isAttached(): - self.read_from_shared_memory() - - def on_record_clicked(self): - # kill - if self._depth_process_running: - self.manage_depth_process(False) - self.manage_nsp(False) - else: - # if we terminate and re-start the processes, we need to re-enable the shared memory - self._depth_wrapper.manage_shared_memory() - - # re-send the settings - self._depth_wrapper.send_settings(self.depth_settings) - - # start nsp recording - if self.manage_nsp(True) == 0: - # re-start the worker - self.manage_depth_process(True) - - def on_features_process_clicked(self): - # kill - if self._features_process_running: - self.manage_feature_process(False) - else: - # if we terminate and re-start the processes, we need to re-enable the shared memory - self._features_wrapper.manage_shared_memory() - - # re-send the settings - self._features_wrapper.send_settings(self.features_settings) - - # re-start the worker - self.manage_feature_process(True) - - def read_from_shared_memory(self): - import numpy as np - if self.monitored_channel_mem.isAttached(): - self.monitored_channel_mem.lock() - settings = np.frombuffer(self.monitored_channel_mem.data(), dtype=np.float64)[-3:] - self.chan_select.setCurrentIndex(int(settings[0])) - self.range_edit.setText(str(settings[1])) - self.manage_range_edit() - self.do_hp.setChecked(bool(settings[2])) - self.monitored_channel_mem.unlock() - else: - self.monitored_channel_mem.attach() - # self.sweep_control.setChecked(False) - self.manage_sweep_control() \ No newline at end of file + _.setEnabled(sweep_control.isChecked()) + + def update(self): + if self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox").isChecked(): + pass # TODO: Read from zeromq sweep sub + + # features plot + plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") + chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") + + curr_chan_lbl = chan_combo.currentText() + curr_feat = feat_combo.currentText() + stack_item = self._widget_stack[curr_chan_lbl][feat_combo.currentText()] + + if curr_chan_lbl != 'None': + do_hp = self._plot_settings["highpass"] + + if self.y_range != plot_stack.currentWidget().plot_config['y_range']\ + or do_hp != plot_stack.currentWidget().plot_config['do_hp']: + plot_stack.currentWidget().clear_plot() + stack_item[1] = 0 + plot_stack.currentWidget().plot_config['do_hp'] = do_hp + plot_stack.currentWidget().plot_config['y_range'] = self.y_range + + curr_datum = stack_item[1] + if curr_feat == 'Raw': + all_data = DBWrapper().load_depth_data(chan_lbl=curr_chan_lbl, + gt=curr_datum, + do_hp=do_hp, + return_uV=True) + elif curr_feat == 'Mapping': + all_data = DBWrapper().load_mapping_response(chan_lbl=curr_chan_lbl, + gt=curr_datum) + else: + all_data = DBWrapper().load_features_data(category=curr_feat, + chan_lbl=curr_chan_lbl, + gt=curr_datum) + if all_data: + plot_stack.currentWidget().update_plot(dict(all_data)) + stack_item[1] = max(all_data.keys()) diff --git a/open_mer/dbsgui/process.py b/open_mer/dbsgui/process.py index fc15631..7f92a59 100644 --- a/open_mer/dbsgui/process.py +++ b/open_mer/dbsgui/process.py @@ -1,8 +1,13 @@ from pathlib import Path +from typing import Optional +import time +import json +import zmq from qtpy import QtCore, QtWidgets, QtGui from serf.tools.db_wrap import DBWrapper, ProcessWrapper from ..settings import defaults, locate_ini from .widgets.SettingsDialog import SettingsDialog +import open_mer.data_source class ProcessGUI(QtWidgets.QMainWindow): @@ -11,11 +16,13 @@ def __init__(self, ini_file: str = None, **kwargs): super().__init__(**kwargs) self.status_icons = { k: QtGui.QPixmap(str(Path(__file__).parents[0] / 'resources' / 'icons' / (v + '.png'))) for k, v in - ((-2, 'depth_status_delay'), (-1, 'depth_status_in_use'), (1, 'depth_status_done'), (0, 'depth_status_off')) + {"accumulating": "depth_status_delay", "recording": "depth_status_in_use", "done": "depth_status_done", "notrecording": "depth_status_off"}.items() } + self._b_recording = False + self._data_source = None self._restore_from_settings(ini_file) self._setup_ui() - self._setup_subproc() + self._setup_pubsub() self._subject_settings = {} self._procedure_settings = {} @@ -25,8 +32,7 @@ def __init__(self, ini_file: str = None, **kwargs): self._do_modal_settings() def closeEvent(self, *args, **kwargs): - self.run_record_process(False) - self.run_feature_process(False) + self.toggle_recording(False) self._save_settings() def _restore_from_settings(self, ini_file=None): @@ -48,7 +54,14 @@ def _restore_from_settings(self, ini_file=None): self.setWindowFlags(QtCore.Qt.FramelessWindowHint) settings.endGroup() - # TODO: Other settings + # Infer data source from ini file, setup data source + settings.beginGroup("data-source") + src_cls = getattr(open_mer.data_source, settings.value("class")) + # Get the _data_source. Note this might trigger on_source_connected before child + # finishes parsing settings. + self._data_source = src_cls(scoped_settings=settings) + self._recording_path = settings.value("basepath", defaults.BASEPATH, type=str) + settings.endGroup() def _save_settings(self): if self._settings_path.parents[0] == 'config' and self._settings_path.parents[1] == 'resources': @@ -85,37 +98,50 @@ def _setup_control_panel(self): settings_pb.setMaximumWidth(50) settings_pb.clicked.connect(lambda state: self._do_modal_settings()) lo.addWidget(settings_pb) - # Features button - lo.addSpacing(20) - features_pb = QtWidgets.QPushButton("Features") - features_pb.setObjectName("Features_PushButton") - features_pb.setMaximumWidth(50) - features_pb.clicked.connect(self._on_features_process_clicked) - lo.addWidget(features_pb) # Record button lo.addSpacing(5) record_pb = QtWidgets.QPushButton("Record") record_pb.setObjectName("Record_PushButton") record_pb.setMaximumWidth(50) - record_pb.clicked.connect(self._on_record_clicked) + record_pb.clicked.connect(lambda: self.toggle_recording(None)) lo.addWidget(record_pb) # Status Label lo.addSpacing(20) status_label = QtWidgets.QLabel() status_label.setObjectName("Status_Label") - status_label.setPixmap(self.status_icons[0]) + status_label.setPixmap(self.status_icons["notrecording"]) lo.addWidget(status_label) self.centralWidget().layout().addLayout(lo) - def _setup_subproc(self): - # Define and start processes - # will only start processes when settings are received - self._record_wrapper = ProcessWrapper('Depth_Process') - self._record_process_running = False - - self._features_wrapper = ProcessWrapper('Features_Process') - self._features_process_running = False + def _setup_pubsub(self, zmq_ctrl_port=60001, zmq_depth_port=60002): + context = zmq.Context() + + self._depth_sock = context.socket(zmq.SUB) + self._depth_sock.connect(f"tcp://localhost:{zmq_depth_port}") + self._depth_sock.setsockopt_string(zmq.SUBSCRIBE, "depth_status") + + self._features_sock = context.socket(zmq.PUB) + self._features_sock.bind(f"tcp://*:{zmq_ctrl_port}") + + def _pub_settings(self): + # Sanitize some settings for serialization. + _proc_settings = {} + for k, v in self._procedure_settings.items(): + if hasattr(v, "isoformat"): + _proc_settings[k] = v.isoformat() + elif hasattr(v, "tolist"): + _proc_settings[k] = v.tolist() + else: + _proc_settings[k] = v + + send_dict = { + "features": self._features_settings, + "procedure": _proc_settings, + "buffer": self._buffer_settings, + "subject": {**self._subject_settings, "birthday": self._subject_settings["birthday"].isoformat()} + } + self._features_sock.send_string("feature_settings " + json.dumps(send_dict)) def _do_modal_settings(self): win = SettingsDialog(self._subject_settings, @@ -125,74 +151,42 @@ def _do_modal_settings(self): result = win.exec_() if result == QtWidgets.QDialog.Accepted: win.update_settings() + self._pub_settings() else: return False - def _on_features_process_clicked(self): - # kill - if self._features_process_running: - self.run_feature_process(False) - else: - # if we terminate and re-start the processes, we need to re-enable the shared memory - self._features_wrapper.manage_shared_memory() - - # re-send the settings - self._features_wrapper.send_settings(self.features_settings) - - # re-start the worker - self.run_feature_process(True) - - def _on_record_clicked(self): - # kill - if self._record_wrapper.is_running(): - self.run_record_process(False) - self._run_recording(False) - else: - # if we terminate and re-start the processes, we need to re-enable the shared memory - self._record_wrapper.manage_shared_memory() - - # re-send the settings - self._record_wrapper.send_settings(self.depth_settings) - + def toggle_recording(self, on_off: Optional[bool] = None): + on_off = on_off if on_off is not None else not self._b_recording + if on_off: + if self._b_recording: + # Wants on but already recording. Stop then start again. + self.toggle_recording(False) + time.sleep(0.100) + self._pub_settings() # re-send the settings # start nsp recording - if self._run_recording(True) == 0: - # re-start the worker - self.run_record_process(True) - - def run_record_process(self, on_off: bool): - # start process - if on_off and not self._record_wrapper.is_running(): - self._record_wrapper.start_worker() + self._run_recording(True) else: - self._record_wrapper.kill_worker() - - def run_feature_process(self, on_off: bool): - if on_off and not self._features_process_running: - self._features_wrapper.start_worker() - self._features_process_running = True - else: - self._features_wrapper.kill_worker() - self._features_process_running = False + self._run_recording(False) def _run_recording(self, on_off: bool): # TODO: Use settings import os - f_name, m_name, l_name = self.parse_patient_name(self.subject_settings['name']) - file_info = {'filename': os.path.normpath(os.path.join(BASEPATH, - self.subject_settings['id'], - self.procedure_settings['date'].strftime('%m%d%y') + '_' + - self.subject_settings['id'] + '_' + - self.procedure_settings['target_name'] + '_' + - self.procedure_settings['recording_config'])), - 'comment': self.subject_settings['NSP_comment'], - 'patient_info': {'ID': self.subject_settings['id'], + f_name, m_name, l_name = self.parse_patient_name(self._subject_settings['name']) + file_info = {'filename': os.path.normpath(os.path.join(self._recording_path, + self._subject_settings['id'], + self._procedure_settings['date'].strftime('%m%d%y') + '_' + + self._subject_settings['id'] + '_' + + self._procedure_settings['target_name'] + '_' + + self._procedure_settings['recording_config'])), + 'comment': self._subject_settings['NSP_comment'], + 'patient_info': {'ID': self._subject_settings['id'], # if only single name, returned in l_name 'firstname': f_name if f_name else l_name, 'middlename': m_name, # TODO: implement MiddleName 'lastname': l_name, - 'DOBMonth': self.subject_settings['birthday'].month, - 'DOBDay': self.subject_settings['birthday'].day, - 'DOBYear': self.subject_settings['birthday'].year + 'DOBMonth': self._subject_settings['birthday'].month, + 'DOBDay': self._subject_settings['birthday'].day, + 'DOBYear': self._subject_settings['birthday'].year }} return self._data_source.set_recording_state(on_off, file_info) @@ -215,20 +209,32 @@ def parse_patient_name(full_name): return f_name, m_name, l_name def update(self): - status_label = self.findChild(QtWidgets.QLabel, "Status_Label") - record_pb = self.findChild(QtWidgets.QPushButton, "Record_PushButton") - features_pb = self.findChild(QtWidgets.QPushButton, "Features_PushButton") - - status_label.setPixmap(self.status_icons[self._record_wrapper.worker_status()]) - - rec_facecolor = "green" if self._record_wrapper.is_running() else "red" - record_pb.setStyleSheet("QPushButton { color: white; " - f"background-color : {rec_facecolor}; " - f"border-color : {rec_facecolor}; " - "border-width: 2px}") - - feat_facecolor = "green" if self._features_wrapper.is_running() else "red" - features_pb.setStyleSheet("QPushButton { color: white; " - f"background-color : {feat_facecolor}; " - f"border-color : {feat_facecolor}; " - "border-width: 2px}") + try: + # Check for an update from the depth process + received_msg = self._depth_sock.recv_string(flags=zmq.NOBLOCK)[len("depth_status")+1:] + + if received_msg == "startup": + # Depth processor has (re)started since we last published our settings. Publish again. + self._pub_settings() + + # Update the status + status_label = self.findChild(QtWidgets.QLabel, "Status_Label") + status_label.setPixmap(self.status_icons[received_msg]) + + # Change the color of the recording button. + record_pb = self.findChild(QtWidgets.QPushButton, "Record_PushButton") + rec_facecolor_map = { + "notrecording": "gray", + "recording": "red", + "accumulating": "yellow", + "done": "blue" + } + if received_msg in rec_facecolor_map: + rec_facecolor = rec_facecolor_map[received_msg] + record_pb.setStyleSheet("QPushButton { color: white; " + f"background-color : {rec_facecolor}; " + f"border-color : {rec_facecolor}; " + "border-width: 2px}") + except zmq.ZMQError: + received_msg = None + diff --git a/open_mer/dbsgui/widgets/SettingsDialog.py b/open_mer/dbsgui/widgets/SettingsDialog.py index eb2937b..4d4637a 100644 --- a/open_mer/dbsgui/widgets/SettingsDialog.py +++ b/open_mer/dbsgui/widgets/SettingsDialog.py @@ -258,7 +258,8 @@ def procedure_selection_change(self): self.update_procedure() def update_settings_from_db(self, idx): - self.procedure_settings.update(DBWrapper().load_procedure_details(idx, exclude=['subject', 'procedure_id'])) + res_dict = dict({"procedure_id": idx}, **DBWrapper().load_procedure_details(idx, exclude=['subject', 'procedure_id'])) + self.procedure_settings.update(res_dict) def update_procedure(self): self.target_name.setText(self.read_dict_value('target_name')) @@ -330,15 +331,19 @@ def __init__(self, buffer_settings): # Settings self.buffer_settings = buffer_settings - if not self.buffer_settings: - self.buffer_settings['buffer_length'] = '{:.3f}'.format(BUFFERLENGTH) - self.buffer_settings['sample_length'] = '{:.3f}'.format(SAMPLELENGTH) - self.buffer_settings['delay_buffer'] = '{:.3f}'.format(DELAYBUFFER) - self.buffer_settings['overwrite_depth'] = OVERWRITEDEPTH - self.buffer_settings['electrode_settings'] = {} - + # Fill in missing values with defaults. + buffer_defaults = { + "buffer_length": "{:.3f}".format(BUFFERLENGTH), + "sample_length": "{:.3f}".format(SAMPLELENGTH), + "delay_buffer": "{:.3f}".format(DELAYBUFFER), + "overwrite_depth": OVERWRITEDEPTH, + "electrode_settings": {} + } + for k, def_v in buffer_defaults.items(): + self.buffer_settings[k] = self.buffer_settings.get(k, def_v) + + # Create widgets and populate with values from settings. self.buffer_widgets = {} - buffer_layout = QGridLayout(self) row = -1 if 'electrode_settings' in self.buffer_settings.keys(): @@ -402,12 +407,12 @@ def __init__(self, features_settings): self.feature_categories = DBWrapper().all_features.keys() self.features_settings = features_settings if not self.features_settings: - self.features_settings['features'] = {} + self.features_settings = {} # Check if default values are defined for cat in self.feature_categories: # defaults to true, compute all features - self.features_settings['features'][cat] = True + self.features_settings[cat] = True self.features_widgets = {} @@ -418,12 +423,11 @@ def __init__(self, features_settings): self.all_features.setChecked(False) self.all_features.clicked.connect(self.toggle_all) features_layout.addWidget(self.all_features, 0, 0, 1, 1) - if 'features' in self.features_settings.keys(): - for idx, (label, sett) in enumerate(self.features_settings['features'].items()): - self.features_widgets[label] = QCheckBox(label) - self.features_widgets[label].setChecked(sett) - self.features_widgets[label].clicked.connect(self.toggle) - features_layout.addWidget(self.features_widgets[label], idx+1, 0, 1, 1) + for idx, (label, sett) in enumerate(self.features_settings.items()): + self.features_widgets[label] = QCheckBox(label) + self.features_widgets[label].setChecked(sett) + self.features_widgets[label].clicked.connect(self.toggle) + features_layout.addWidget(self.features_widgets[label], idx+1, 0, 1, 1) def toggle_all(self): for label, sett in self.features_widgets.items(): @@ -435,7 +439,7 @@ def toggle(self): def to_dict(self): for key, value in self.features_widgets.items(): - self.features_settings['features'][key] = value.isChecked() + self.features_settings[key] = value.isChecked() class SettingsDialog(QDialog): diff --git a/open_mer/feature_plots/FeaturePlotWidgets.py b/open_mer/feature_plots/FeaturePlotWidgets.py index b675fec..cd342b4 100644 --- a/open_mer/feature_plots/FeaturePlotWidgets.py +++ b/open_mer/feature_plots/FeaturePlotWidgets.py @@ -97,7 +97,7 @@ def configure_plot(self): self.plot.setTitle(title=self.plot_config['title'], **{'color': 'w', 'size': '16pt'}) self.plot.hideButtons() - if self.plot_config['image_plot']: + if self.plot_config['image_plot'] and False: self.img = pg.ImageItem() self.plot.addItem(self.img) @@ -376,7 +376,7 @@ def __init__(self, plot_config, *args, **kwargs): raw_sett = {**DEFAULTPLOT, 'x_axis': False, # Is Visible 'y_axis': False, - 'x_range': self.plot_config['x_range'], # None for auto-scale, list otherwise + 'x_range': [self.plot_config["x_start"], self.plot_config["x_stop"]], # self.plot_config['x_range'], # None for auto-scale, list otherwise 'y_range': [-self.plot_config['y_range'], self.plot_config['y_range']], 'auto_scale': False, 'interactive': False, diff --git a/open_mer/resources/config/DepthGUI.ini b/open_mer/resources/config/DepthGUI.ini index f2d4a32..9d7189b 100644 --- a/open_mer/resources/config/DepthGUI.ini +++ b/open_mer/resources/config/DepthGUI.ini @@ -13,7 +13,7 @@ offset=0 [depth-source-serial] class=FHCSerial baudrate=19200 -com_port=COM6 +com_port=COM5 scale_factor=0.001 [depth-mirror] diff --git a/open_mer/resources/config/FeaturesGUI.ini b/open_mer/resources/config/FeaturesGUI.ini index d612f59..b41df9e 100644 --- a/open_mer/resources/config/FeaturesGUI.ini +++ b/open_mer/resources/config/FeaturesGUI.ini @@ -15,14 +15,15 @@ get_comments=true buffer_parameter\comment_length=10 [buffer] -buffer_length=6.0 -sample_length=4.0 -delay_buffer=0.5 -overwrite_depth=true -threshold=true -validity=90.0 highpass=true +[features] +Raw=true +Mapping=true +STN=true +LFP=true +Spikes=true + [plot] x_start=-4000 x_stop=120000 diff --git a/open_mer/resources/config/ProcessGUI.ini b/open_mer/resources/config/ProcessGUI.ini index 0ddf35b..76c1328 100644 --- a/open_mer/resources/config/ProcessGUI.ini +++ b/open_mer/resources/config/ProcessGUI.ini @@ -2,13 +2,17 @@ fullScreen=false maximized=false frameless=true -size=@Size(600 680) -pos=@Point(1320 400) +size=@Size(600 60) +pos=@Point(1320 250) bg_color=#404040 [data-source] class=CerebusDataSource basepath=C:\\Recordings +sampling_group=30000 +get_continuous=false +get_events=false +get_comments=false [segment-source] class=SERF diff --git a/setup.cfg b/setup.cfg index 32ceb86..330f3c8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ install_requires = pyfftw mysqlclient django + pyzmq quantities neo @ git+https://github.com/NeuralEnsemble/python-neo.git pylsl @@ -51,6 +52,7 @@ gui_scripts = dbs-sweep = open_mer.scripts.SweepGUI:main dbs-waveform = open_mer.scripts.WaveformGUI:main dbs-raster = open_mer.scripts.RasterGUI:main + dbs-process = open_mer.scripts.ProcessGUI:main dbs-features = open_mer.scripts.FeaturesGUI:main dbs-mapping = open_mer.scripts.MappingGUI:main dbs-comments = open_mer.scripts.CommentsGUI:main From 2d8fcf7e611655ea449dd9942d5ec73dbb4d8129 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 17 Aug 2023 21:34:15 -0400 Subject: [PATCH 30/65] First attempt at channel_select pub/sub. --- open_mer/dbsgui/features.py | 31 ++++++++++++++------ open_mer/dbsgui/sweep.py | 56 ++++++++++++++++--------------------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/open_mer/dbsgui/features.py b/open_mer/dbsgui/features.py index 0f3d37c..2557b3d 100644 --- a/open_mer/dbsgui/features.py +++ b/open_mer/dbsgui/features.py @@ -1,4 +1,7 @@ +import json from pathlib import Path + +import zmq from qtpy import QtCore, QtWidgets from serf.tools.db_wrap import DBWrapper, ProcessWrapper from ..settings import defaults, locate_ini @@ -11,13 +14,15 @@ class FeaturesGUI(QtWidgets.QMainWindow): ((-2, 'depth_status_delay'), (-1, 'depth_status_in_use'), (1, 'depth_status_done'), (0, 'depth_status_off')) } - def __init__(self, ini_file: str = None, **kwargs): + def __init__(self, ini_file: str = None, zmq_chan_port=60003, **kwargs): """ - FeaturesGUI is the most complicated of all applications in this package. - - Has a GUI to configure many different settings. - - Sets up a sub-process (Depth_Process in serf package) to capture data from Central and store in the database - - Sets up a sub-process (Features_Process in serf package) to compute features on new entries in the database - - Visualizes raw segments and features from the database + - Visualizes raw segments and calculated features from the database + - Widgets to navigate the visualizations + - Subscribes to zmq messages to know which features to grab + - Subscribes to zmq messages to know which channels to show + - For semi-realtime application, you should be running Depth_Process and Features_Process (from serf package) + in background. + Args: ini_file: **kwargs: @@ -32,8 +37,15 @@ def __init__(self, ini_file: str = None, **kwargs): self._chan_labels = [] self._db = DBWrapper() - + self._zmq_chan_port = 60003 # TODO: Load from ini file. self._restore_from_settings(ini_file) + + # Subscribe to channel-change notifications + context = zmq.Context() + self._chan_sock = context.socket(zmq.SUB) + self._chan_sock.connect(f"tcp://localhost:{self._zmq_chan_port}") + self._chan_sock.setsockopt_string(zmq.SUBSCRIBE, "channel_select") + self._setup_ui() def closeEvent(self, *args, **kwargs): @@ -233,7 +245,10 @@ def on_sweep_clicked(self): def update(self): if self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox").isChecked(): - pass # TODO: Read from zeromq sweep sub + received_msg = self._chan_sock.recv_string(flags=zmq.NOBLOCK)[len("channel_select") + 1:] + if received_msg: + chan_settings = json.loads(received_msg) + # ^ dict with k,v_type "channel":int, "range":[float, float], "highpass":bool # features plot plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index dbc8c1c..72d9703 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -1,8 +1,10 @@ +import json import numpy as np from scipy import signal import pyaudio from qtpy import QtCore, QtWidgets import pyqtgraph as pg +import zmq from .utilities.pyqtgraph import parse_color_str, make_qcolor, get_colormap from .widgets.custom import CustomWidget, CustomGUI @@ -41,7 +43,7 @@ def do_plot_update(self): class SweepWidget(CustomWidget): - def __init__(self, *args, **kwargs): + def __init__(self, *args, zmq_chan_port=60003, **kwargs): """ *args typically contains a data_source dict with entries for 'srate', 'channel_names', 'chan_states' **kwargs typically contains dicts for configuring the plotter, including 'filter', 'plot', and 'theme'. @@ -49,9 +51,10 @@ def __init__(self, *args, **kwargs): self._monitor_group = None # QtWidgets.QButtonGroup(parent=self) self.plot_config = {} self.segmented_series = {} # Will contain one array of curves for each line/channel label. - # add a shared memory object to track the currently monitored channel - # - Used by features/depth - self.monitored_shared_mem = QtCore.QSharedMemory() + + context = zmq.Context() + self._chanselect_sock = context.socket(zmq.PUB) + self._chanselect_sock.bind(f"tcp://*:{zmq_chan_port}") super().__init__(*args, **kwargs) self.refresh_axes() # Even though super __init__ calls this, extra refresh is intentional @@ -59,13 +62,7 @@ def __init__(self, *args, **kwargs): self.pya_stream = None self.audio = {} self.reset_audio() - - # monitored_shared_mem needs to store 3 numbers: range, channel_id and do_hp. - # The first is a float64, so make them all 64 bit. - # 3 * 64bit = 192 bits = 24 bytes. QtCore.QSharedMemory allocates an entire page of 4096 bytes. - self.monitored_shared_mem.setKey("MonitoredChannelMemory") - self.monitored_shared_mem.create(24) - self.update_shared_memory() + self.publish_chanselect() def keyPressEvent(self, e): valid_keys = [QtCore.Qt.Key_0, QtCore.Qt.Key_1, QtCore.Qt.Key_2, QtCore.Qt.Key_3, QtCore.Qt.Key_4, QtCore.Qt.Key_5, QtCore.Qt.Key_6, QtCore.Qt.Key_7, QtCore.Qt.Key_8, @@ -154,7 +151,7 @@ def create_control_panel(self): def update_monitor_channel(self, chan_state, spike_only): # Let other processes know we've changed the monitor channel self.parent()._data_source.update_monitor(chan_state, spike_only=spike_only) - self.update_shared_memory() + self.publish_chanselect() def on_spk_aud_changed(self, state): current_button_id = self._monitor_group.checkedId() @@ -164,7 +161,7 @@ def on_spk_aud_changed(self, state): def on_hp_filter_changed(self, state): self.plot_config['do_hp'] = state == QtCore.Qt.Checked - self.update_shared_memory() + self.publish_chanselect() def on_ln_filter_changed(self, state): self.plot_config['do_ln'] = state == QtCore.Qt.Checked @@ -172,7 +169,7 @@ def on_ln_filter_changed(self, state): def on_range_edit_editingFinished(self): self.plot_config['y_range'] = np.float64(self.range_edit.text()) self.refresh_axes() - self.update_shared_memory() + self.publish_chanselect() def on_monitor_group_clicked(self, button): self.reset_audio() @@ -201,24 +198,19 @@ def on_monitor_group_clicked(self, button): spk_aud_cb = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") self.update_monitor_channel(chan_state, spk_aud_cb.checkState() == QtCore.Qt.Checked) - def update_shared_memory(self): - # updates only the memory section needed - if self.monitored_shared_mem.isAttached(): - # send data to shared memory object - self.monitored_shared_mem.lock() - chan_labels = [_['name'] for _ in self.chan_states] - if self.audio['chan_label'] in ['silence', None]: - curr_channel = np.float64(0) - else: - curr_channel = np.float64(chan_labels.index(self.audio['chan_label']) + 1) # 0 == None - - curr_range = self.plot_config['y_range'] - - curr_hp = np.float64(self.plot_config['do_hp']) - - to_write = np.array([curr_channel, curr_range, curr_hp], dtype=np.float64).tobytes() - self.monitored_shared_mem.data()[-len(to_write):] = memoryview(to_write) - self.monitored_shared_mem.unlock() + def publish_chanselect(self): + chan_labels = [_['name'] for _ in self.chan_states] + if self.audio['chan_label'] in ['silence', None]: + curr_channel = 0 + else: + curr_channel = chan_labels.index(self.audio['chan_label']) + 1 # 0 == None + curr_range = list(self.plot_config['y_range']) + curr_hp = self.plot_config['do_hp'] + self._chanselect_sock.send_string("channel_select " + json.dumps({ + "channel": curr_channel, + "range": curr_range, + "highpass": curr_hp + })) def on_thresh_line_moved(self, inf_line): new_thresh = None From 26794d06b00ce620de17cad02564cc9479fe62f5 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 18 Aug 2023 00:06:02 -0400 Subject: [PATCH 31/65] Fix FHC DDU settings in DepthGUI --- open_mer/depth_source/fhc.py | 5 +++-- open_mer/resources/config/DepthGUI.ini | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/open_mer/depth_source/fhc.py b/open_mer/depth_source/fhc.py index fe30f7e..bda1fc8 100644 --- a/open_mer/depth_source/fhc.py +++ b/open_mer/depth_source/fhc.py @@ -10,11 +10,13 @@ class FHCSerial(MerDepthSource): def __init__(self, scoped_settings: QtCore.QSettings): - super().__init__(scoped_settings) + scoped_settings.beginGroup("depth-source-serial") self._baudrate = parse_ini_try_numeric(scoped_settings, 'baudrate') or 19200 self._com_port = scoped_settings.value("com_port") self.ser = serial.Serial(timeout=1) self._is_v2 = False + super().__init__(scoped_settings) + scoped_settings.endGroup() @property def is_v2(self): @@ -27,7 +29,6 @@ def is_v2(self, value): self.doubleSpinBox_offset.setValue(60.00 if value else 0.00) def do_open(self): - super().do_open() self.ser.baudrate = self._baudrate if self._com_port not in serial.tools.list_ports.comports(): print(f"Port {self._com_port} not found in list of comports.") diff --git a/open_mer/resources/config/DepthGUI.ini b/open_mer/resources/config/DepthGUI.ini index 9d7189b..dd2bc46 100644 --- a/open_mer/resources/config/DepthGUI.ini +++ b/open_mer/resources/config/DepthGUI.ini @@ -7,6 +7,7 @@ pos=@Point(1320 0) bg_color=#404040 [depth-source] +;class=FHCSerial class=CBSDKPlayback offset=0 From aa5e149cc07a074fb418e401409a9759e74230df Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 18 Aug 2023 03:36:12 -0400 Subject: [PATCH 32/65] doc update --- docs/introduction.md | 6 ++++-- docs/preparing-distribution.md | 2 ++ docs/troubleshooting.md | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/introduction.md b/docs/introduction.md index c8a52a5..552a4e2 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -16,7 +16,7 @@ Here is the list of equipment we use. It may be possible to use this software wi ## Software -NeuroportDBS is a Suite of 6 different applications for visualizing signals in real-time: +NeuroportDBS is a Suite of applications for visualizing signals in real-time: ![Image of vis apps](https://github.com/SachsLab/OpenMER/blob/master/vis_apps_screenshot.PNG?raw=true) @@ -24,7 +24,9 @@ NeuroportDBS is a Suite of 6 different applications for visualizing signals in r * *RasterGUI* - Plots threshold crossing events in a raster plot, with spike rate displayed in the corner (up to 8 sec history) * *WaveformGUI* - Plots the waveforms of the last N threshold crossing events. * *DDUGUI* visualizes the depth readout from the drive (including adjustable offset value), and sends that depth to other consumers (e.g., the Blackrock NSP as a Comment; as a [labstreaminglayer](https://github.com/sccn/labstreaminglayer) outlet). -* *FeaturesGUI* is a much fuller application than the others. It monitors the signals and the depths, then for every new depth it stores a 4-sec segment to a database, and for each segment it calculates a set of predefined features. The depth history of raw segments or features are plotted and updated automatically. The database interaction occurs via a Django app called [SERF](https://github.com/cboulay/SERF) backed by a MySQL database. +* *ProcessGUI* - Set patient and procedure info, and it has widgets to control recording state. Further, the recording button changes colour depending on what a background database process is doing (monitoring, accumulating, etc). +* *FeaturesGUI* plots the depth history of raw segments or features from the database and it is updated automatically. The database interaction occurs via a Django app called [SERF](https://github.com/cboulay/SERF) backed by a MySQL database. + * In addition to the MySQL database, 2 SERF applications must be running: *serf-cbacquire* and *serf-procfeatures*. * *CommentGUI* (not shown) is for simple text entry widget to send arbitrary comments to the Blackrock NSP. We also use a GUI application we developed called [*CereStimDBS*](https://github.com/CerebusOSS/CereStimDBS) for controlling the Blackrock CereStim96 in a convenient manner for DBS surgeries. diff --git a/docs/preparing-distribution.md b/docs/preparing-distribution.md index 29aac7b..9c8cbd0 100644 --- a/docs/preparing-distribution.md +++ b/docs/preparing-distribution.md @@ -23,6 +23,8 @@ Create a folder somewhere you have write access. The location should have at lea ```shell script @echo off call "%~dp0env_for_icons.bat" + start "" "%WINPYDIR%\Scripts\serf-cbacquire.exe" /command:%1 /B + start "" "%WINPYDIR%\Scripts\serf-procfeatures.exe" /command:%1 /B start "" "%WINPYDIR%\Scripts\dbs-sweep.exe" /command:%1 /B start "" "%WINPYDIR%\Scripts\dbs-raster.exe" /command:%1 /B start "" "%WINPYDIR%\Scripts\dbs-waveform.exe" /command:%1 /B diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index de4000f..6584600 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,5 +1,24 @@ ## Connectivity +### ZeroMQ Ports + +We use ZeroMQ sockets for inter-process communication using the pub-sub pattern. The following table lists the sockets' +ports, topics, and notes about the accompanying messages. + +| Publisher | Port | Topic | Message | Subscribers | +|----------------------|-------|--------------------|------------------------------------------------------|----------------------| +| ProcessGUI | 60001 | procedure_settings | dict of settings dicts "procedure" and | FeaturesGUI | +| Depth_Process (SERF) | 60002 | snippet_status | startup; notrecording; recording; accumulating; done | ProcessGUI | +| SweepGUI | 60003 | channel_select | dict with channel, range, highpass | FeaturesGUI | +| FeaturesGUI | 60004 | features | refresh | FeaturesGUI | +| DepthGUI | 60005 | ddu | float of depth | Depth_Process (SERF) | + +### LSL + +| Origin | Stream Name | Stream Type | Content | Inlets | +|----------|-----------------|-------------|-------------------------|----------------------| +| DepthGUi | electrode_depth | depth | 1 float32 of elec depth | Depth_Process (SERF) | + ### Blackrock NSP If Central is running then these tools should attempt to connect to Central's shared memory, and the network settings are irrelevant. If Central is not running then you'll have to make sure the network settings are correct, and this may depend on how your PC and NSP are connected. From 0aff8af13522bcf8bd08369b11e4d2345b6edd74 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 18 Aug 2023 03:36:24 -0400 Subject: [PATCH 33/65] ini update --- open_mer/resources/config/FeaturesGUI.ini | 12 ++++++------ open_mer/resources/config/ProcessGUI.ini | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/open_mer/resources/config/FeaturesGUI.ini b/open_mer/resources/config/FeaturesGUI.ini index b41df9e..b2db1d3 100644 --- a/open_mer/resources/config/FeaturesGUI.ini +++ b/open_mer/resources/config/FeaturesGUI.ini @@ -2,8 +2,8 @@ fullScreen=false maximized=false frameless=true -size=@Size(600 680) -pos=@Point(1320 400) +size=@Size(600 780) +pos=@Point(1320 300) bg_color=#404040 [data-source] @@ -19,10 +19,10 @@ highpass=true [features] Raw=true -Mapping=true -STN=true -LFP=true -Spikes=true +Mapping=false +STN=false +LFP=false +Spikes=false [plot] x_start=-4000 diff --git a/open_mer/resources/config/ProcessGUI.ini b/open_mer/resources/config/ProcessGUI.ini index 76c1328..228927d 100644 --- a/open_mer/resources/config/ProcessGUI.ini +++ b/open_mer/resources/config/ProcessGUI.ini @@ -2,7 +2,7 @@ fullScreen=false maximized=false frameless=true -size=@Size(600 60) +size=@Size(600 50) pos=@Point(1320 250) bg_color=#404040 From 6298011fbe683cf0b940c8d64256756afdb0cdc5 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 18 Aug 2023 03:37:40 -0400 Subject: [PATCH 34/65] more specific naming; comment out unused code. --- open_mer/dbsgui/widgets/SettingsDialog.py | 278 +++++++++++----------- 1 file changed, 140 insertions(+), 138 deletions(-) diff --git a/open_mer/dbsgui/widgets/SettingsDialog.py b/open_mer/dbsgui/widgets/SettingsDialog.py index 4d4637a..d59086e 100644 --- a/open_mer/dbsgui/widgets/SettingsDialog.py +++ b/open_mer/dbsgui/widgets/SettingsDialog.py @@ -17,8 +17,6 @@ class SubjectWidget(QWidget): def __init__(self, subject_settings): super(SubjectWidget, self).__init__() - self.subject_enums = DBWrapper().return_enums('subject') - subject_layout = QGridLayout(self) subject_layout.setColumnMinimumWidth(2, 60) @@ -37,9 +35,10 @@ def __init__(self, subject_settings): self.name_edit.setMaxLength(135) subject_layout.addWidget(self.name_edit, 1, 1, 1, 4) + _subj_enums = DBWrapper().return_enums('subject') subject_layout.addWidget(QLabel("Sex: "), 2, 0, 1, 1) self.sex_combo = QComboBox() - self.sex_combo.addItems(self.subject_enums['sex'] if 'sex' in self.subject_enums.keys() else []) + self.sex_combo.addItems(_subj_enums['sex'] if 'sex' in _subj_enums.keys() else []) self.sex_combo.setCurrentIndex(0) subject_layout.addWidget(self.sex_combo, 2, 1, 1, 1) @@ -57,9 +56,9 @@ def __init__(self, subject_settings): if not self.subject_settings: self.update_settings_from_db(-1) - self.update_subject() + self.update_subj_widgets_from_settings() - def update_subject(self): + def update_subj_widgets_from_settings(self): self.name_edit.setText(self.read_dict_value(self.subject_settings, 'name')) self.id_combo.setCurrentText(self.read_dict_value(self.subject_settings, 'id')) self.sex_combo.setCurrentText(self.read_dict_value(self.subject_settings, 'sex')) @@ -77,7 +76,7 @@ def update_settings_from_db(self, idx): def load_subject(self): # id is a unique and mandatory field self.check_subject() - self.update_subject() + self.update_subj_widgets_from_settings() def check_subject(self): # when changing the id in the combobox, can be modifying or entering an existing subject id. Check to load data @@ -207,7 +206,7 @@ def __init__(self, procedure_settings): row += 1 proc_layout.addWidget(QWidget(), row, 1, 1, 3) - self.update_procedure() + self.update_proc_widgets_from_settings() def check_all_procedures(self, subject_id, block): self.prev_proc.blockSignals(block) @@ -250,18 +249,18 @@ def change_subject(self, sub_id, block=False): self.check_all_procedures(sub_id, block) def procedure_selection_change(self): + id = -1 if self.prev_proc.currentIndex() > 0: - self.update_settings_from_db( - self.all_procedures[self.prev_proc.currentIndex()-1].procedure_id) - else: - self.update_settings_from_db(-1) - self.update_procedure() + ix = self.prev_proc.currentIndex() - 1 # -1 because first entry is always blank. + id = self.all_procedures[ix].procedure_id + self.update_settings_from_db(id) + self.update_proc_widgets_from_settings() def update_settings_from_db(self, idx): res_dict = dict({"procedure_id": idx}, **DBWrapper().load_procedure_details(idx, exclude=['subject', 'procedure_id'])) self.procedure_settings.update(res_dict) - def update_procedure(self): + def update_proc_widgets_from_settings(self): self.target_name.setText(self.read_dict_value('target_name')) self.type_combo.setCurrentText(self.read_dict_value('type')) self.rec_combo.setCurrentText(self.read_dict_value('recording_config')) @@ -325,133 +324,135 @@ def to_dict(self): self.procedure_settings['offset_size'] = float(self.offset_size.text()) -class BufferWidget(QWidget): - def __init__(self, buffer_settings): - super(BufferWidget, self).__init__() - - # Settings - self.buffer_settings = buffer_settings - # Fill in missing values with defaults. - buffer_defaults = { - "buffer_length": "{:.3f}".format(BUFFERLENGTH), - "sample_length": "{:.3f}".format(SAMPLELENGTH), - "delay_buffer": "{:.3f}".format(DELAYBUFFER), - "overwrite_depth": OVERWRITEDEPTH, - "electrode_settings": {} - } - for k, def_v in buffer_defaults.items(): - self.buffer_settings[k] = self.buffer_settings.get(k, def_v) - - # Create widgets and populate with values from settings. - self.buffer_widgets = {} - buffer_layout = QGridLayout(self) - row = -1 - if 'electrode_settings' in self.buffer_settings.keys(): - for label, sett in self.buffer_settings['electrode_settings'].items(): - row += 1 - buffer_layout.addWidget(QLabel(label), row, 0, 1, 1) - self.buffer_widgets[label] = {} - self.buffer_widgets[label]['chk_threshold'] = QCheckBox("Threshold") - self.buffer_widgets[label]['chk_threshold'].setChecked(bool(sett['threshold'])) - self.buffer_widgets[label]['edit_validity'] = QLineEdit() - self.buffer_widgets[label]['edit_validity'].setText(str(sett['validity'])) - - buffer_layout.addWidget(self.buffer_widgets[label]['chk_threshold'], row, 1, 1, 1) - buffer_layout.addWidget(QLabel('Validity Threshold (%)'), row, 2, 1, 1) - buffer_layout.addWidget(self.buffer_widgets[label]['edit_validity'], row, 3, 1, 1) - - row += 1 - buffer_layout.addWidget(QLabel("Depth buffer size (s): "), row, 0, 1, 1) - self.edit_buffer_length = QLineEdit(self.buffer_settings['buffer_length']) - self.edit_buffer_length.setInputMask("0.000") - self.edit_buffer_length.setFixedWidth(40) - buffer_layout.addWidget(self.edit_buffer_length, row, 1, 1, 1) - - row += 1 - buffer_layout.addWidget(QLabel("Depth samples size (s): "), row, 0, 1, 1) - self.edit_sample_length = QLineEdit(self.buffer_settings['sample_length']) - self.edit_sample_length.setInputMask("0.000") - self.edit_sample_length.setFixedWidth(40) - buffer_layout.addWidget(self.edit_sample_length, row, 1, 1, 1) - - row += 1 - buffer_layout.addWidget(QLabel("Delay depth recording (s): "), row, 0, 1, 1) - self.delay_buffer = QLineEdit(self.buffer_settings['delay_buffer']) - self.delay_buffer.setInputMask("0.000") - self.delay_buffer.setFixedWidth(40) - buffer_layout.addWidget(self.delay_buffer, row, 1, 1, 1) - - row += 1 - self.overwrite_depth = QCheckBox("Overwrite depth values") - self.overwrite_depth.setChecked(self.buffer_settings['overwrite_depth']) - buffer_layout.addWidget(self.overwrite_depth, row, 0, 1, 1) - - def to_dict(self): - # convert all fields to dictionary and return it - self.buffer_settings['buffer_length'] = self.edit_buffer_length.text() - self.buffer_settings['sample_length'] = self.edit_sample_length.text() - self.buffer_settings['delay_buffer'] = self.delay_buffer.text() - self.buffer_settings['overwrite_depth'] = self.overwrite_depth.isChecked() - - for key, value in self.buffer_widgets.items(): - self.buffer_settings['electrode_settings'][key] = {} - self.buffer_settings['electrode_settings'][key]['threshold'] = value['chk_threshold'].isChecked() - self.buffer_settings['electrode_settings'][key]['validity'] = float(value['edit_validity'].text()) - - -class FeaturesWidget(QWidget): - def __init__(self, features_settings): - super(FeaturesWidget, self).__init__() - - # Settings - self.feature_categories = DBWrapper().all_features.keys() - self.features_settings = features_settings - if not self.features_settings: - self.features_settings = {} - - # Check if default values are defined - for cat in self.feature_categories: - # defaults to true, compute all features - self.features_settings[cat] = True - - self.features_widgets = {} - - features_layout = QGridLayout(self) - - # Add an option to toggle all features - self.all_features = QCheckBox('All') - self.all_features.setChecked(False) - self.all_features.clicked.connect(self.toggle_all) - features_layout.addWidget(self.all_features, 0, 0, 1, 1) - for idx, (label, sett) in enumerate(self.features_settings.items()): - self.features_widgets[label] = QCheckBox(label) - self.features_widgets[label].setChecked(sett) - self.features_widgets[label].clicked.connect(self.toggle) - features_layout.addWidget(self.features_widgets[label], idx+1, 0, 1, 1) - - def toggle_all(self): - for label, sett in self.features_widgets.items(): - self.features_widgets[label].setChecked(self.all_features.isChecked()) - - def toggle(self): - if any([not x.isChecked() for x in self.features_widgets.values()]): - self.all_features.setChecked(False) - - def to_dict(self): - for key, value in self.features_widgets.items(): - self.features_settings[key] = value.isChecked() +# class BufferWidget(QWidget): +# def __init__(self, buffer_settings): +# super(BufferWidget, self).__init__() +# +# # Settings +# self.buffer_settings = buffer_settings +# # Fill in missing values with defaults. +# buffer_defaults = { +# "buffer_length": "{:.3f}".format(BUFFERLENGTH), +# "sample_length": "{:.3f}".format(SAMPLELENGTH), +# "delay_buffer": "{:.3f}".format(DELAYBUFFER), +# "overwrite_depth": OVERWRITEDEPTH, +# "electrode_settings": {} +# } +# for k, def_v in buffer_defaults.items(): +# self.buffer_settings[k] = self.buffer_settings.get(k, def_v) +# +# # Create widgets and populate with values from settings. +# self.buffer_widgets = {} +# buffer_layout = QGridLayout(self) +# row = -1 +# if 'electrode_settings' in self.buffer_settings.keys(): +# for label, sett in self.buffer_settings['electrode_settings'].items(): +# row += 1 +# buffer_layout.addWidget(QLabel(label), row, 0, 1, 1) +# self.buffer_widgets[label] = {} +# self.buffer_widgets[label]['chk_threshold'] = QCheckBox("Threshold") +# self.buffer_widgets[label]['chk_threshold'].setChecked(bool(sett['threshold'])) +# self.buffer_widgets[label]['edit_validity'] = QLineEdit() +# self.buffer_widgets[label]['edit_validity'].setText(str(sett['validity'])) +# +# buffer_layout.addWidget(self.buffer_widgets[label]['chk_threshold'], row, 1, 1, 1) +# buffer_layout.addWidget(QLabel('Validity Threshold (%)'), row, 2, 1, 1) +# buffer_layout.addWidget(self.buffer_widgets[label]['edit_validity'], row, 3, 1, 1) +# +# row += 1 +# buffer_layout.addWidget(QLabel("Depth buffer size (s): "), row, 0, 1, 1) +# self.edit_buffer_length = QLineEdit(self.buffer_settings['buffer_length']) +# self.edit_buffer_length.setInputMask("0.000") +# self.edit_buffer_length.setFixedWidth(40) +# buffer_layout.addWidget(self.edit_buffer_length, row, 1, 1, 1) +# +# row += 1 +# buffer_layout.addWidget(QLabel("Depth samples size (s): "), row, 0, 1, 1) +# self.edit_sample_length = QLineEdit(self.buffer_settings['sample_length']) +# self.edit_sample_length.setInputMask("0.000") +# self.edit_sample_length.setFixedWidth(40) +# buffer_layout.addWidget(self.edit_sample_length, row, 1, 1, 1) +# +# row += 1 +# buffer_layout.addWidget(QLabel("Delay depth recording (s): "), row, 0, 1, 1) +# self.delay_buffer = QLineEdit(self.buffer_settings['delay_buffer']) +# self.delay_buffer.setInputMask("0.000") +# self.delay_buffer.setFixedWidth(40) +# buffer_layout.addWidget(self.delay_buffer, row, 1, 1, 1) +# +# row += 1 +# self.overwrite_depth = QCheckBox("Overwrite depth values") +# self.overwrite_depth.setChecked(self.buffer_settings['overwrite_depth']) +# buffer_layout.addWidget(self.overwrite_depth, row, 0, 1, 1) +# +# def to_dict(self): +# # convert all fields to dictionary and return it +# self.buffer_settings['buffer_length'] = self.edit_buffer_length.text() +# self.buffer_settings['sample_length'] = self.edit_sample_length.text() +# self.buffer_settings['delay_buffer'] = self.delay_buffer.text() +# self.buffer_settings['overwrite_depth'] = self.overwrite_depth.isChecked() +# +# for key, value in self.buffer_widgets.items(): +# self.buffer_settings['electrode_settings'][key] = {} +# self.buffer_settings['electrode_settings'][key]['threshold'] = value['chk_threshold'].isChecked() +# self.buffer_settings['electrode_settings'][key]['validity'] = float(value['edit_validity'].text()) +# +# +# class FeaturesWidget(QWidget): +# def __init__(self, features_settings): +# super(FeaturesWidget, self).__init__() +# +# # Settings +# self.feature_categories = DBWrapper().all_features.keys() +# self.features_settings = features_settings +# if not self.features_settings: +# self.features_settings = {} +# +# # Check if default values are defined +# for cat in self.feature_categories: +# # defaults to true, compute all features +# self.features_settings[cat] = True +# +# self.features_widgets = {} +# +# features_layout = QGridLayout(self) +# +# # Add an option to toggle all features +# self.all_features = QCheckBox('All') +# self.all_features.setChecked(False) +# self.all_features.clicked.connect(self.toggle_all) +# features_layout.addWidget(self.all_features, 0, 0, 1, 1) +# for idx, (label, sett) in enumerate(self.features_settings.items()): +# self.features_widgets[label] = QCheckBox(label) +# self.features_widgets[label].setChecked(sett) +# self.features_widgets[label].clicked.connect(self.toggle) +# features_layout.addWidget(self.features_widgets[label], idx+1, 0, 1, 1) +# +# def toggle_all(self): +# for label, sett in self.features_widgets.items(): +# self.features_widgets[label].setChecked(self.all_features.isChecked()) +# +# def toggle(self): +# if any([not x.isChecked() for x in self.features_widgets.values()]): +# self.all_features.setChecked(False) +# +# def to_dict(self): +# for key, value in self.features_widgets.items(): +# self.features_settings[key] = value.isChecked() class SettingsDialog(QDialog): - def __init__(self, subject_settings, procedure_settings, buffer_settings, features_settings, parent=None): + def __init__(self, subject_settings, procedure_settings, + # buffer_settings, features_settings, + parent=None): super(SettingsDialog, self).__init__(parent) self.setWindowTitle("Enter settings.") # settings dicts self.subject_settings = subject_settings self.procedure_settings = procedure_settings - self.buffer_settings = buffer_settings - self.features_settings = features_settings + # self.buffer_settings = buffer_settings + # self.features_settings = features_settings # Widgets to show/edit parameters. self.settings_layout = QVBoxLayout(self) @@ -463,16 +464,17 @@ def __init__(self, subject_settings, procedure_settings, buffer_settings, featur self.proc_widget = ProcedureWidget(self.procedure_settings) tab_widget.addTab(self.proc_widget, 'Procedure') - self.buff_widget = BufferWidget(self.buffer_settings) - tab_widget.addTab(self.buff_widget, 'Buffer') - - self.feat_widget = FeaturesWidget(self.features_settings) - tab_widget.addTab(self.feat_widget, 'Features') + # self.buff_widget = BufferWidget(self.buffer_settings) + # tab_widget.addTab(self.buff_widget, 'Buffer') + # + # self.feat_widget = FeaturesWidget(self.features_settings) + # tab_widget.addTab(self.feat_widget, 'Features') self.settings_layout.addWidget(tab_widget) # signals self.subject_widget.subject_change.connect(self.proc_widget.change_subject) + # update procedures when re-opening settings window if 'subject_id' not in self.subject_settings.keys(): self.subject_widget.check_subject() @@ -490,5 +492,5 @@ def __init__(self, subject_settings, procedure_settings, buffer_settings, featur def update_settings(self): self.subject_widget.to_dict() self.proc_widget.to_dict() - self.buff_widget.to_dict() - self.feat_widget.to_dict() + # self.buff_widget.to_dict() + # self.feat_widget.to_dict() From 45d1a6d8ec1e4754854158521155a1cc69e663b7 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 18 Aug 2023 03:43:04 -0400 Subject: [PATCH 35/65] Many fixes for feature plots. --- open_mer/dbsgui/depth.py | 15 +- open_mer/dbsgui/features.py | 152 ++++++++++++++----- open_mer/dbsgui/process.py | 70 ++++++--- open_mer/dbsgui/sweep.py | 6 +- open_mer/feature_plots/FeaturePlotWidgets.py | 2 +- 5 files changed, 177 insertions(+), 68 deletions(-) diff --git a/open_mer/dbsgui/depth.py b/open_mer/dbsgui/depth.py index 0595953..31b57af 100644 --- a/open_mer/dbsgui/depth.py +++ b/open_mer/dbsgui/depth.py @@ -1,5 +1,6 @@ -from qtpy import QtCore, QtWidgets from pathlib import Path +from qtpy import QtCore, QtWidgets +import zmq import pylsl from ..settings import defaults from ..depth_source import CBSDKPlayback @@ -37,6 +38,10 @@ def __init__(self, ini_file=None): self.restore_from_settings() self.setup_ui() + context = zmq.Context() + self._depth_sock = context.socket(zmq.PUB) + self._depth_sock.bind(f"tcp://*:{60005}") # TODO: Get port from settings + def restore_from_settings(self): settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) @@ -60,7 +65,7 @@ def restore_from_settings(self): settings.endGroup() settings.beginGroup("depth-mirror") - self._mirror['lsl'] = bool(settings.value("lsl_mirror", False)) + self._mirror['lsl'] = bool(settings.value("lsl_mirror", False)) # TODO: Remove self._mirror['nsp'] = bool(settings.value("lsl_mirror", False)) settings.endGroup() @@ -116,6 +121,7 @@ def setup_ui(self): h_layout.addWidget(cb) h_layout.addSpacing(5) + # TODO: Remove cb = QtWidgets.QCheckBox("LSL") cb.setChecked(self._mirror['lsl']) cb.clicked.connect(self.on_mirror_LSL_clicked) @@ -163,6 +169,7 @@ def setup_ui(self): self.plot_widget.setLayout(v_layout) + # TODO: Remove def on_mirror_LSL_clicked(self, state): if state > 0: outlet_info = pylsl.StreamInfo(name='electrode_depth', type='depth', channel_count=1, @@ -216,6 +223,10 @@ def update(self): if self._depth_stream is not None and new_value: self._depth_stream.push_sample([value]) + # Publish on ZeroMQ + if new_value: + self._depth_sock.send_string(f"ddu {value}") + def send(self): self.display_string = None # make sure the update function runs self.update() diff --git a/open_mer/dbsgui/features.py b/open_mer/dbsgui/features.py index 2557b3d..08b08dc 100644 --- a/open_mer/dbsgui/features.py +++ b/open_mer/dbsgui/features.py @@ -2,9 +2,9 @@ from pathlib import Path import zmq -from qtpy import QtCore, QtWidgets +from qtpy import QtCore, QtWidgets, QtGui from serf.tools.db_wrap import DBWrapper, ProcessWrapper -from ..settings import defaults, locate_ini +from ..settings import defaults, locate_ini, parse_ini_try_numeric from ..feature_plots import * @@ -14,7 +14,7 @@ class FeaturesGUI(QtWidgets.QMainWindow): ((-2, 'depth_status_delay'), (-1, 'depth_status_in_use'), (1, 'depth_status_done'), (0, 'depth_status_off')) } - def __init__(self, ini_file: str = None, zmq_chan_port=60003, **kwargs): + def __init__(self, ini_file: str = None, **kwargs): """ - Visualizes raw segments and calculated features from the database - Widgets to navigate the visualizations @@ -34,19 +34,18 @@ def __init__(self, ini_file: str = None, zmq_chan_port=60003, **kwargs): self._procedure_settings = {} self._buffer_settings = {} self._features_settings = {} - self._chan_labels = [] + self._chan_labels = set([]) self._db = DBWrapper() - self._zmq_chan_port = 60003 # TODO: Load from ini file. + # TODO: Load ports from ini file + self._zmq_ctrl_port = 60001 + self._zmq_chan_port = 60003 + self._zmq_feat_port = 60004 self._restore_from_settings(ini_file) - # Subscribe to channel-change notifications - context = zmq.Context() - self._chan_sock = context.socket(zmq.SUB) - self._chan_sock.connect(f"tcp://localhost:{self._zmq_chan_port}") - self._chan_sock.setsockopt_string(zmq.SUBSCRIBE, "channel_select") - + self._setup_pubsub() self._setup_ui() + self._features_sock.send_string("features refresh") def closeEvent(self, *args, **kwargs): super().closeEvent(*args, **kwargs) @@ -75,8 +74,6 @@ def _restore_from_settings(self, ini_file=None): self._plot_settings['x_stop'] = int(settings.value("x_stop", 120000)) self._plot_settings['y_range'] = int(settings.value("y_range", 250)) settings.endGroup() - self._plot_settings["color_iterator"] = -1 - self._plot_settings["image_plot"] = False settings.beginGroup("features") add_features = [] @@ -92,6 +89,50 @@ def _restore_from_settings(self, ini_file=None): # chk_threshold settings.endGroup() + self._plot_settings["color_iterator"] = -1 + self._plot_settings["image_plot"] = False + # theme + settings.beginGroup("theme") + self._plot_settings["theme"] = {} + for k in settings.allKeys(): + if k == 'colormap' or k.lower().startswith('pencolors'): + continue + self._plot_settings["theme"][k] = parse_ini_try_numeric(settings, k) + # theme > pencolors + self._plot_settings["theme"]['colormap'] = settings.value('colormap', 'custom') + if self._plot_settings["theme"]['colormap'] == "custom": + pencolors = [] + settings.beginGroup("pencolors") + for c_id in settings.childGroups(): + settings.beginGroup(c_id) + cname = settings.value("name", None) + if cname is not None: + cvalue = QtGui.QColor(cname) + else: + cvalue = settings.value("value", "#ffffff") + pencolors.append(cvalue) + settings.endGroup() + settings.endGroup() # pencolors + self._plot_settings["theme"]["pencolors"] = pencolors + settings.endGroup() # end theme + + def _setup_pubsub(self): + context = zmq.Context() + + # Subscribe to channel-change notifications + self._chan_sock = context.socket(zmq.SUB) + self._chan_sock.connect(f"tcp://localhost:{self._zmq_chan_port}") + self._chan_sock.setsockopt_string(zmq.SUBSCRIBE, "channel_select") + + # Subscribe to procedure set notifications -- react to procedure id + self._procedure_sock = context.socket(zmq.SUB) + self._procedure_sock.connect(f"tcp://localhost:{self._zmq_ctrl_port}") + self._procedure_sock.setsockopt_string(zmq.SUBSCRIBE, "procedure_settings") + + # Publish refresh notification -- on startup and when refresh is clicked + self._features_sock = context.socket(zmq.PUB) + self._features_sock.bind(f"tcp://*:{self._zmq_feat_port}") + def _setup_ui(self): main_widget = QtWidgets.QWidget(self) main_widget.setLayout(QtWidgets.QVBoxLayout()) @@ -152,7 +193,7 @@ def _setup_control_panel(self): refresh_pb = QtWidgets.QPushButton("Refresh") refresh_pb.setObjectName("Refresh_PushButton") refresh_pb.setMaximumWidth(50) - # refresh_pb.clicked.connect(self.on_refresh_clicked) + refresh_pb.clicked.connect(self.on_refresh_clicked) lo_R.addWidget(refresh_pb) lo_R.addSpacing(10) @@ -182,7 +223,7 @@ def _reset_chan_select_items(self): chan_combo.addItem("None") chan_combo.addItems(self._chan_labels) chan_combo.blockSignals(False) - chan_combo.setCurrentIndex(0) + chan_combo.setCurrentIndex(0) # Triggers an emission --> reset_stack def _reset_widget_stack(self): plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") @@ -203,9 +244,11 @@ def _reset_widget_stack(self): } n_feats = len(self._features_settings['features']) - for chan_ix, chan_label in enumerate(["None"] + self._chan_labels): + my_theme = self._plot_settings["theme"] + for chan_ix, chan_label in enumerate({"None"}.union(self._chan_labels)): + self._plot_settings["color_iterator"] = (self._plot_settings["color_iterator"] + 1) % len(my_theme['pencolors']) self._widget_stack[chan_label] = {} - for feat_ix, feat_label in enumerate(self._features_settings['features']): + for feat_ix, feat_label in enumerate(self._features_settings["features"]): self._widget_stack[chan_label][feat_label] = [n_feats*chan_ix + feat_ix, 0] w_cls = plot_class_map[feat_label] if feat_label in plot_class_map else NullPlotWidget plot_stack.addWidget(w_cls(dict(self._plot_settings))) @@ -216,12 +259,16 @@ def reset_stack(self): chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") if plot_stack is not None and chan_combo is not None and feat_combo is not None: - plot_stack.setCurrentIndex(self._widget_stack[chan_combo.currentText()][feat_combo.currentText()][0]) + chan_key = chan_combo.currentText() + feat_key = feat_combo.currentText() + idx = self._widget_stack[chan_key][feat_key][0] + plot_stack.setCurrentIndex(idx) def handle_procedure_id(self, procedure_id): self._db.select_procedure(procedure_id) self._chan_labels = self._db.list_channel_labels() self._reset_chan_select_items() + self._reset_widget_stack() def manage_refresh(self): plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") @@ -240,48 +287,69 @@ def on_sweep_clicked(self): chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") hp_chk = self.findChild(QtWidgets.QCheckBox, "HP_CheckBox") range_edit = self.findChild(QtWidgets.QLineEdit, "Range_LineEdit") + b_enable = not sweep_control.isChecked() for _ in [chan_combo, hp_chk, range_edit]: - _.setEnabled(sweep_control.isChecked()) + _.setEnabled(b_enable) - def update(self): - if self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox").isChecked(): + def on_refresh_clicked(self): + self._features_sock.send_string("features refresh") + + def _check_subs(self): + try: received_msg = self._chan_sock.recv_string(flags=zmq.NOBLOCK)[len("channel_select") + 1:] - if received_msg: - chan_settings = json.loads(received_msg) - # ^ dict with k,v_type "channel":int, "range":[float, float], "highpass":bool + chan_settings = json.loads(received_msg) + # ^ dict with k,v_type "channel":int, "range":[float, float], "highpass":bool + # TODO: chan_combo set new idx + except zmq.ZMQError: + pass + + try: + received_msg = self._procedure_sock.recv_string(flags=zmq.NOBLOCK)[len("procedure_settings") + 1:] + procedure_settings = json.loads(received_msg) + # ^ dict with settings for "procedure", "subject" + if "procedure" in procedure_settings and "procedure_id" in procedure_settings["procedure"]: + self.handle_procedure_id(procedure_settings["procedure"]["procedure_id"]) + except zmq.ZMQError: + pass - # features plot + def update(self): plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") + b_sweep_sync = self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox").isChecked() - curr_chan_lbl = chan_combo.currentText() - curr_feat = feat_combo.currentText() - stack_item = self._widget_stack[curr_chan_lbl][feat_combo.currentText()] + self._check_subs() + curr_chan_lbl = chan_combo.currentText() + curr_widget = plot_stack.currentWidget() if curr_chan_lbl != 'None': + curr_feat = feat_combo.currentText() + stack_item = self._widget_stack[curr_chan_lbl][curr_feat] do_hp = self._plot_settings["highpass"] + y_range = self._plot_settings["y_range"] - if self.y_range != plot_stack.currentWidget().plot_config['y_range']\ - or do_hp != plot_stack.currentWidget().plot_config['do_hp']: - plot_stack.currentWidget().clear_plot() + # If widget values don't match stored values + if y_range != curr_widget.plot_config['y_range']\ + or do_hp != curr_widget.plot_config['highpass']: + # Clear plots and update widgets with stored settings + curr_widget.clear_plot() stack_item[1] = 0 - plot_stack.currentWidget().plot_config['do_hp'] = do_hp - plot_stack.currentWidget().plot_config['y_range'] = self.y_range + curr_widget.plot_config['highpass'] = do_hp + curr_widget.plot_config['y_range'] = y_range curr_datum = stack_item[1] if curr_feat == 'Raw': - all_data = DBWrapper().load_depth_data(chan_lbl=curr_chan_lbl, - gt=curr_datum, - do_hp=do_hp, - return_uV=True) + all_data = self._db.load_depth_data(chan_lbl=curr_chan_lbl, + gt=curr_datum, + do_hp=do_hp, + return_uV=True) elif curr_feat == 'Mapping': - all_data = DBWrapper().load_mapping_response(chan_lbl=curr_chan_lbl, - gt=curr_datum) - else: - all_data = DBWrapper().load_features_data(category=curr_feat, - chan_lbl=curr_chan_lbl, + all_data = self._db.load_mapping_response(chan_lbl=curr_chan_lbl, gt=curr_datum) + else: + all_data = self._db.load_features_data(category=curr_feat, + chan_lbl=curr_chan_lbl, + gt=curr_datum) if all_data: plot_stack.currentWidget().update_plot(dict(all_data)) stack_item[1] = max(all_data.keys()) diff --git a/open_mer/dbsgui/process.py b/open_mer/dbsgui/process.py index 7f92a59..cc42c44 100644 --- a/open_mer/dbsgui/process.py +++ b/open_mer/dbsgui/process.py @@ -4,7 +4,7 @@ import json import zmq from qtpy import QtCore, QtWidgets, QtGui -from serf.tools.db_wrap import DBWrapper, ProcessWrapper +from serf.tools.db_wrap import DBWrapper from ..settings import defaults, locate_ini from .widgets.SettingsDialog import SettingsDialog import open_mer.data_source @@ -114,17 +114,21 @@ def _setup_control_panel(self): self.centralWidget().layout().addLayout(lo) - def _setup_pubsub(self, zmq_ctrl_port=60001, zmq_depth_port=60002): + def _setup_pubsub(self, zmq_ctrl_port=60001, zmq_depth_port=60002, zmq_feat_port=60004): context = zmq.Context() self._depth_sock = context.socket(zmq.SUB) self._depth_sock.connect(f"tcp://localhost:{zmq_depth_port}") - self._depth_sock.setsockopt_string(zmq.SUBSCRIBE, "depth_status") + self._depth_sock.setsockopt_string(zmq.SUBSCRIBE, "snippet_status") - self._features_sock = context.socket(zmq.PUB) - self._features_sock.bind(f"tcp://*:{zmq_ctrl_port}") + self._features_sock = context.socket(zmq.SUB) + self._features_sock.connect(f"tcp://localhost:{zmq_feat_port}") + self._features_sock.setsockopt_string(zmq.SUBSCRIBE, "features") - def _pub_settings(self): + self._procedure_sock = context.socket(zmq.PUB) + self._procedure_sock.bind(f"tcp://*:{zmq_ctrl_port}") + + def _publish_settings(self): # Sanitize some settings for serialization. _proc_settings = {} for k, v in self._procedure_settings.items(): @@ -141,17 +145,34 @@ def _pub_settings(self): "buffer": self._buffer_settings, "subject": {**self._subject_settings, "birthday": self._subject_settings["birthday"].isoformat()} } - self._features_sock.send_string("feature_settings " + json.dumps(send_dict)) + # TODO: features and buffer settings should just be in ini files. They don't change frequently. + self._procedure_sock.send_string("procedure_settings " + json.dumps(send_dict)) def _do_modal_settings(self): win = SettingsDialog(self._subject_settings, self._procedure_settings, - self._buffer_settings, - self._features_settings) + # self._buffer_settings, + # self._features_settings + ) result = win.exec_() if result == QtWidgets.QDialog.Accepted: win.update_settings() - self._pub_settings() + + # Create or load subject + # Returns subject_id/-1 whether subject is properly created or not + sub_id = DBWrapper().load_or_create_subject(self._subject_settings) + + if sub_id == -1: + print("Subject not created.") + return False + else: + self._subject_settings['subject_id'] = sub_id + self._procedure_settings['subject_id'] = sub_id + tmp = DBWrapper() + proc_id = tmp.load_or_create_procedure(self._procedure_settings) + self._procedure_settings["procedure_id"] = proc_id + + self._publish_settings() else: return False @@ -162,7 +183,7 @@ def toggle_recording(self, on_off: Optional[bool] = None): # Wants on but already recording. Stop then start again. self.toggle_recording(False) time.sleep(0.100) - self._pub_settings() # re-send the settings + self._publish_settings() # re-send the settings # start nsp recording self._run_recording(True) else: @@ -209,21 +230,28 @@ def parse_patient_name(full_name): return f_name, m_name, l_name def update(self): + b_publish_settings = False + try: + received_msg = self._features_sock.recv_string(flags=zmq.NOBLOCK)[len("features") + 1:] + b_publish_settings |= received_msg == "refresh" + except zmq.ZMQError: + pass + try: # Check for an update from the depth process - received_msg = self._depth_sock.recv_string(flags=zmq.NOBLOCK)[len("depth_status")+1:] + received_msg = self._depth_sock.recv_string(flags=zmq.NOBLOCK)[len("snippet_status")+1:] - if received_msg == "startup": - # Depth processor has (re)started since we last published our settings. Publish again. - self._pub_settings() + # Depth processor has (re)started since we last published our settings. Publish again. + b_publish_settings |= received_msg == "startup" - # Update the status + # Update the status. TODO: Remove this or get the old code from FeaturesGUI. status_label = self.findChild(QtWidgets.QLabel, "Status_Label") status_label.setPixmap(self.status_icons[received_msg]) # Change the color of the recording button. record_pb = self.findChild(QtWidgets.QPushButton, "Record_PushButton") rec_facecolor_map = { + "startup": "orange", "notrecording": "gray", "recording": "red", "accumulating": "yellow", @@ -231,10 +259,14 @@ def update(self): } if received_msg in rec_facecolor_map: rec_facecolor = rec_facecolor_map[received_msg] - record_pb.setStyleSheet("QPushButton { color: white; " + else: + rec_facecolor = "gray" + record_pb.setStyleSheet("QPushButton { color: white; " f"background-color : {rec_facecolor}; " f"border-color : {rec_facecolor}; " "border-width: 2px}") except zmq.ZMQError: - received_msg = None - + pass + + if b_publish_settings: + self._publish_settings() diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index 72d9703..a91d250 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -204,12 +204,10 @@ def publish_chanselect(self): curr_channel = 0 else: curr_channel = chan_labels.index(self.audio['chan_label']) + 1 # 0 == None - curr_range = list(self.plot_config['y_range']) - curr_hp = self.plot_config['do_hp'] self._chanselect_sock.send_string("channel_select " + json.dumps({ "channel": curr_channel, - "range": curr_range, - "highpass": curr_hp + "range": self.plot_config['y_range'], + "highpass": self.plot_config['do_hp'] })) def on_thresh_line_moved(self, inf_line): diff --git a/open_mer/feature_plots/FeaturePlotWidgets.py b/open_mer/feature_plots/FeaturePlotWidgets.py index cd342b4..af7adfe 100644 --- a/open_mer/feature_plots/FeaturePlotWidgets.py +++ b/open_mer/feature_plots/FeaturePlotWidgets.py @@ -338,7 +338,7 @@ def __init__(self, plot_config, *args, **kwargs): # create GLW for the depth plot depth_sett = {**DEPTH, - 'pen_color': pen_colors[self.plot_config['color_iterator']]} + 'pen_color': self.pen_color} self.depth_plot = BasePlotWidget(depth_sett) self.layout.addWidget(self.depth_plot, 0, 0, NPLOTSRAW, 1) From 9e35eae6dc39cedff2a6a3d6c4c993cd8f187e14 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 18 Aug 2023 15:35:58 -0400 Subject: [PATCH 36/65] Fixup DepthGUI for StarDrive --- open_mer/dbsgui/depth.py | 4 ++-- open_mer/depth_source/base.py | 3 ++- open_mer/depth_source/fhc.py | 19 ++++++++++++------- open_mer/resources/config/DepthGUI.ini | 11 ++++------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/open_mer/dbsgui/depth.py b/open_mer/dbsgui/depth.py index 31b57af..52bb7f7 100644 --- a/open_mer/dbsgui/depth.py +++ b/open_mer/dbsgui/depth.py @@ -99,7 +99,7 @@ def setup_ui(self): self._doubleSpinBox_offset.setMaximum(100.00) self._doubleSpinBox_offset.setSingleStep(1.00) self._doubleSpinBox_offset.setDecimals(2) - self._doubleSpinBox_offset.setValue(0.00) + self._doubleSpinBox_offset.setValue(self._depth_source.offset) self._doubleSpinBox_offset.setFixedWidth(60) h_layout.addWidget(self._doubleSpinBox_offset) @@ -211,7 +211,7 @@ def update(self): if nsp_cb and nsp_cb.isChecked() and not isinstance(self._depth_source, CBSDKPlayback): cbsdk_conn = CbSdkConnection() if cbsdk_conn.is_connected: - if self.chk_NSP.isChecked() and self.chk_NSP.isEnabled() and new_value: + if new_value: cbsdk_conn.set_comments("DTT:" + display_string) else: # try connecting if not connected but button is active diff --git a/open_mer/depth_source/base.py b/open_mer/depth_source/base.py index b65f957..1352447 100644 --- a/open_mer/depth_source/base.py +++ b/open_mer/depth_source/base.py @@ -5,7 +5,8 @@ class MerDepthSource: def __init__(self, scoped_settings: QtCore.QSettings): # scale_factor should be 0.001 for FHC DDU V2, 1.0 otherwise. - self._scale_factor = parse_ini_try_numeric(scoped_settings, 'scale_factor') or None + self.scale_factor = scoped_settings.value("scale_factor", 1.0, type=float) + self.offset = scoped_settings.value("offset", 0.0, type=float) self.do_open() def do_open(self): diff --git a/open_mer/depth_source/fhc.py b/open_mer/depth_source/fhc.py index bda1fc8..9d0be3d 100644 --- a/open_mer/depth_source/fhc.py +++ b/open_mer/depth_source/fhc.py @@ -10,13 +10,13 @@ class FHCSerial(MerDepthSource): def __init__(self, scoped_settings: QtCore.QSettings): - scoped_settings.beginGroup("depth-source-serial") + scoped_settings.beginGroup("serial") self._baudrate = parse_ini_try_numeric(scoped_settings, 'baudrate') or 19200 self._com_port = scoped_settings.value("com_port") self.ser = serial.Serial(timeout=1) self._is_v2 = False - super().__init__(scoped_settings) scoped_settings.endGroup() + super().__init__(scoped_settings) @property def is_v2(self): @@ -25,19 +25,24 @@ def is_v2(self): @is_v2.setter def is_v2(self, value): self._is_v2 = value - self._scale_factor = 0.001 if value else 1.0 - self.doubleSpinBox_offset.setValue(60.00 if value else 0.00) + self.scale_factor = 0.001 if value else 1.0 + self.offset = 60.00 if value else 0.00 def do_open(self): self.ser.baudrate = self._baudrate - if self._com_port not in serial.tools.list_ports.comports(): + for port, desc, hwid in sorted(serial.tools.list_ports.comports()): + if port == self._com_port: + break + else: print(f"Port {self._com_port} not found in list of comports.") + return if not self.ser.is_open: self.ser.port = self._com_port try: self.ser.open() # TODO: Add error. - # Quiet transmission for a minute + # Silence transmission temporarily self.ser.write("AXON-\r".encode()) + # Request version information. self.ser.write("V\r".encode()) # readlines will capture until timeout pattern = "([0-9]+\.[0-9]+)" @@ -62,7 +67,7 @@ def update(self): in_str = self.ser.readline().decode('utf-8').strip() if in_str: try: - raw_value = float(in_str) * (self._scale_factor or 1.0) + raw_value = float(in_str) * (self.scale_factor or 1.0) return raw_value except ValueError: diff --git a/open_mer/resources/config/DepthGUI.ini b/open_mer/resources/config/DepthGUI.ini index dd2bc46..aac40df 100644 --- a/open_mer/resources/config/DepthGUI.ini +++ b/open_mer/resources/config/DepthGUI.ini @@ -7,15 +7,12 @@ pos=@Point(1320 0) bg_color=#404040 [depth-source] -;class=FHCSerial -class=CBSDKPlayback -offset=0 - -[depth-source-serial] class=FHCSerial -baudrate=19200 -com_port=COM5 +;class=CBSDKPlayback +offset=0 scale_factor=0.001 +serial\baudrate=19200 +serial\com_port=COM5 [depth-mirror] lsl_mirror=true From fc6c2239d7d7dcc3b855a7b3d7ca7ef3e8dde834 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 31 Aug 2023 14:59:28 -0400 Subject: [PATCH 37/65] pass correct object when monitor selected via keypress --- open_mer/dbsgui/sweep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index a91d250..687d5d3 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -81,7 +81,7 @@ def keyPressEvent(self, e): if new_button_id is not None: button = self._monitor_group.button(new_button_id) button.setChecked(True) - self.on_monitor_group_clicked(new_button_id) + self.on_monitor_group_clicked(button) def closeEvent(self, evnt): if self.pya_stream: From 577137c17326a8a36d532826bfb2a39a4ed96607 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 31 Aug 2023 16:50:39 -0400 Subject: [PATCH 38/65] Update features chanselect when sweep changes chan. --- open_mer/dbsgui/features.py | 7 ++++++- open_mer/dbsgui/sweep.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/open_mer/dbsgui/features.py b/open_mer/dbsgui/features.py index 08b08dc..f809969 100644 --- a/open_mer/dbsgui/features.py +++ b/open_mer/dbsgui/features.py @@ -299,7 +299,12 @@ def _check_subs(self): received_msg = self._chan_sock.recv_string(flags=zmq.NOBLOCK)[len("channel_select") + 1:] chan_settings = json.loads(received_msg) # ^ dict with k,v_type "channel":int, "range":[float, float], "highpass":bool - # TODO: chan_combo set new idx + sweep_control = self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox") + if sweep_control.isChecked(): + chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + avail_chans = [chan_combo.itemText(_) for _ in range(chan_combo.count())] + if chan_settings["label"] in avail_chans: + chan_combo.setCurrentIndex(avail_chans.index(chan_settings["label"])) except zmq.ZMQError: pass diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index 687d5d3..1225831 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -206,6 +206,7 @@ def publish_chanselect(self): curr_channel = chan_labels.index(self.audio['chan_label']) + 1 # 0 == None self._chanselect_sock.send_string("channel_select " + json.dumps({ "channel": curr_channel, + "label": self.audio["chan_label"] or "", "range": self.plot_config['y_range'], "highpass": self.plot_config['do_hp'] })) From 5db110d805239d7ac7f65b68143803fa0a0c8296 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 31 Aug 2023 17:14:13 -0400 Subject: [PATCH 39/65] Better use of space in feature plots --- open_mer/feature_plots/FeaturePlotWidgets.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/open_mer/feature_plots/FeaturePlotWidgets.py b/open_mer/feature_plots/FeaturePlotWidgets.py index af7adfe..23c5e74 100644 --- a/open_mer/feature_plots/FeaturePlotWidgets.py +++ b/open_mer/feature_plots/FeaturePlotWidgets.py @@ -335,6 +335,7 @@ def __init__(self, plot_config, *args, **kwargs): # Create and add GraphicsLayoutWidget self.layout = QGridLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.setSpacing(0) # create GLW for the depth plot depth_sett = {**DEPTH, @@ -364,6 +365,8 @@ def __init__(self, plot_config, *args, **kwargs): # Prepare plot data self.data_layout = QVBoxLayout() + self.data_layout.setContentsMargins(0, 0, 0, 0) + self.data_layout.setSpacing(0) self.layout.addLayout(self.data_layout, 0, 1, NPLOTSRAW, 5) self.layout.setColumnStretch(0, 1) self.layout.setColumnStretch(1, 5) @@ -391,7 +394,10 @@ def __init__(self, plot_config, *args, **kwargs): tmp_txt.setX(0) tmp_txt.setY(0) tmp_txt.setAnchor((0.5, 1)) - tmp_txt.setZValue(1) + tmp_txt.setZValue(100) + _font = QFont() + _font.setPointSizeF(8.5) + tmp_txt.setFont(_font) tmp.plot.addItem(tmp_txt) self.data_texts.append(tmp_txt) From 45017f2be5c9febc2d84bb4eb70702c49b594e64 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 22 Sep 2023 00:16:49 -0400 Subject: [PATCH 40/65] Add script to copy ini files to home dir. --- open_mer/scripts/ResetUserSettings.py | 11 +++++++++++ open_mer/settings/reset.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 open_mer/scripts/ResetUserSettings.py create mode 100644 open_mer/settings/reset.py diff --git a/open_mer/scripts/ResetUserSettings.py b/open_mer/scripts/ResetUserSettings.py new file mode 100644 index 0000000..a6103fc --- /dev/null +++ b/open_mer/scripts/ResetUserSettings.py @@ -0,0 +1,11 @@ +from open_mer.settings.reset import copy_ini_resources_from_package_to_home + + +def main(): + # We have to call something in the package so it knows its package name. + # (Here, when called directly, __package__ is None) + copy_ini_resources_from_package_to_home() + + +if __name__ == '__main__': + main() diff --git a/open_mer/settings/reset.py b/open_mer/settings/reset.py new file mode 100644 index 0000000..f721bc2 --- /dev/null +++ b/open_mer/settings/reset.py @@ -0,0 +1,20 @@ +from pathlib import Path +import importlib.resources as pkg_resources +from shutil import copyfile + + +def copy_ini_resources_from_package_to_home(): + """ + Copies ini files from this package's resource directory to the user's home/.__package__ directory. + """ + root_pkg = __package__.split(".")[0] + dest_root = Path.home() / f".{root_pkg}" + dest_root.mkdir(exist_ok=True) + with pkg_resources.path(f"{root_pkg}.resources", "config") as path: + for resource_file in path.iterdir(): + if not resource_file.name.lower().endswith(".ini"): + continue + destination_path = dest_root / resource_file.name + if resource_file.is_file(): + print(f"Copying {resource_file} to {destination_path}") + copyfile(resource_file, destination_path) From 968191e5861a6e912881d376d50c9c7b4fba406e Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 22 Sep 2023 17:22:32 -0400 Subject: [PATCH 41/65] Moved ini files from config to settings --- open_mer/dbsgui/depth.py | 2 +- open_mer/dbsgui/process.py | 2 +- open_mer/resources/config/nsp_default.ccf | 125791 +++++++++++++++ .../{config => settings}/CbSdkConnection.ini | 0 .../{config => settings}/DepthGUI.ini | 1 - .../{config => settings}/FeaturesGUI.ini | 21 - .../{config => settings}/MappingGUI.ini | 1 - .../{config => settings}/ProcessGUI.ini | 1 - .../{config => settings}/RasterGUI.ini | 9 - open_mer/resources/settings/Style.ini | 14 + .../{config => settings}/SweepGUI.ini | 12 +- open_mer/settings/__init__.py | 2 +- open_mer/settings/reset.py | 2 +- setup.cfg | 3 +- 14 files changed, 125812 insertions(+), 49 deletions(-) create mode 100644 open_mer/resources/config/nsp_default.ccf rename open_mer/resources/{config => settings}/CbSdkConnection.ini (100%) rename open_mer/resources/{config => settings}/DepthGUI.ini (94%) rename open_mer/resources/{config => settings}/FeaturesGUI.ini (52%) rename open_mer/resources/{config => settings}/MappingGUI.ini (94%) rename open_mer/resources/{config => settings}/ProcessGUI.ini (94%) rename open_mer/resources/{config => settings}/RasterGUI.ini (62%) create mode 100644 open_mer/resources/settings/Style.ini rename open_mer/resources/{config => settings}/SweepGUI.ini (72%) diff --git a/open_mer/dbsgui/depth.py b/open_mer/dbsgui/depth.py index 52bb7f7..4fe9fda 100644 --- a/open_mer/dbsgui/depth.py +++ b/open_mer/dbsgui/depth.py @@ -30,7 +30,7 @@ def __init__(self, ini_file=None): self._settings_path = ini_path else: # Use default ini that ships with module. - self._settings_path = Path(__file__).parents[1] / 'resources' / 'config' / ini_path.name + self._settings_path = Path(__file__).parents[1] / "resources" / "settings" / ini_path.name self.display_string = None self._depth_stream = None diff --git a/open_mer/dbsgui/process.py b/open_mer/dbsgui/process.py index cc42c44..5f587fa 100644 --- a/open_mer/dbsgui/process.py +++ b/open_mer/dbsgui/process.py @@ -64,7 +64,7 @@ def _restore_from_settings(self, ini_file=None): settings.endGroup() def _save_settings(self): - if self._settings_path.parents[0] == 'config' and self._settings_path.parents[1] == 'resources': + if self._settings_path.parents[0] == "settings" and self._settings_path.parents[1] == "resources": # If this was loaded with the shipped settings, then write a new one in ~/.open_mer home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) self._settings_path = home_dir / '.open_mer' / self._settings_path.name diff --git a/open_mer/resources/config/nsp_default.ccf b/open_mer/resources/config/nsp_default.ccf new file mode 100644 index 0000000..ba447c3 --- /dev/null +++ b/open_mer/resources/config/nsp_default.ccf @@ -0,0 +1,125791 @@ + + + + + + 32768 + 78 + 166 + 0 + 1 + 1 + 1 + 1 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 257 + 65793 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 5 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 2 + 1 + 1 + 2 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 257 + 65793 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 5 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 3 + 1 + 1 + 3 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 257 + 65793 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 5 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 4 + 1 + 1 + 4 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 5 + 1 + 1 + 5 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 6 + 1 + 1 + 6 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 7 + 1 + 1 + 7 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 8 + 1 + 1 + 8 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 9 + 1 + 1 + 9 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 10 + 1 + 1 + 10 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 11 + 1 + 1 + 11 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 12 + 1 + 1 + 12 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 13 + 1 + 1 + 13 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 14 + 1 + 1 + 14 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 15 + 1 + 1 + 15 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 16 + 1 + 1 + 16 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 17 + 1 + 1 + 17 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 18 + 1 + 1 + 18 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 19 + 1 + 1 + 19 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 20 + 1 + 1 + 20 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 21 + 1 + 1 + 21 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 22 + 1 + 1 + 22 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 23 + 1 + 1 + 23 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 24 + 1 + 1 + 24 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 25 + 1 + 1 + 25 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 26 + 1 + 1 + 26 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 27 + 1 + 1 + 27 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 28 + 1 + 1 + 28 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 29 + 1 + 1 + 29 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 30 + 1 + 1 + 30 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 31 + 1 + 1 + 31 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 32 + 1 + 1 + 32 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 33 + 1 + 2 + 1 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 34 + 1 + 2 + 2 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 35 + 1 + 2 + 3 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 36 + 1 + 2 + 4 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 37 + 1 + 2 + 5 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 38 + 1 + 2 + 6 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 39 + 1 + 2 + 7 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 40 + 1 + 2 + 8 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 41 + 1 + 2 + 9 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 42 + 1 + 2 + 10 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 43 + 1 + 2 + 11 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 44 + 1 + 2 + 12 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 45 + 1 + 2 + 13 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 46 + 1 + 2 + 14 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 47 + 1 + 2 + 15 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 48 + 1 + 2 + 16 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 49 + 1 + 2 + 17 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 50 + 1 + 2 + 18 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 51 + 1 + 2 + 19 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 52 + 1 + 2 + 20 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 53 + 1 + 2 + 21 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 54 + 1 + 2 + 22 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 55 + 1 + 2 + 23 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 56 + 1 + 2 + 24 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 57 + 1 + 2 + 25 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 58 + 1 + 2 + 26 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 59 + 1 + 2 + 27 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 60 + 1 + 2 + 28 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 61 + 1 + 2 + 29 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 62 + 1 + 2 + 30 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 63 + 1 + 2 + 31 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 64 + 1 + 2 + 32 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 65 + 1 + 3 + 1 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 66 + 1 + 3 + 2 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 67 + 1 + 3 + 3 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 68 + 1 + 3 + 4 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 69 + 1 + 3 + 5 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 70 + 1 + 3 + 6 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 71 + 1 + 3 + 7 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 72 + 1 + 3 + 8 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 73 + 1 + 3 + 9 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 74 + 1 + 3 + 10 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 75 + 1 + 3 + 11 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 76 + 1 + 3 + 12 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 77 + 1 + 3 + 13 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 78 + 1 + 3 + 14 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 79 + 1 + 3 + 15 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 80 + 1 + 3 + 16 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 81 + 1 + 3 + 17 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 82 + 1 + 3 + 18 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 83 + 1 + 3 + 19 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 84 + 1 + 3 + 20 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 85 + 1 + 3 + 21 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 86 + 1 + 3 + 22 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 87 + 1 + 3 + 23 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 88 + 1 + 3 + 24 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 89 + 1 + 3 + 25 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 90 + 1 + 3 + 26 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 91 + 1 + 3 + 27 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 92 + 1 + 3 + 28 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 93 + 1 + 3 + 29 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 94 + 1 + 3 + 30 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 95 + 1 + 3 + 31 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 96 + 1 + 3 + 32 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 97 + 1 + 4 + 1 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 98 + 1 + 4 + 2 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 99 + 1 + 4 + 3 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 100 + 1 + 4 + 4 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 101 + 1 + 4 + 5 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 102 + 1 + 4 + 6 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 103 + 1 + 4 + 7 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 104 + 1 + 4 + 8 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 105 + 1 + 4 + 9 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 106 + 1 + 4 + 10 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 107 + 1 + 4 + 11 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 108 + 1 + 4 + 12 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 109 + 1 + 4 + 13 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 110 + 1 + 4 + 14 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 111 + 1 + 4 + 15 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 112 + 1 + 4 + 16 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 113 + 1 + 4 + 17 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 114 + 1 + 4 + 18 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 115 + 1 + 4 + 19 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 116 + 1 + 4 + 20 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 117 + 1 + 4 + 21 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 118 + 1 + 4 + 22 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 119 + 1 + 4 + 23 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 120 + 1 + 4 + 24 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 121 + 1 + 4 + 25 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 122 + 1 + 4 + 26 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 123 + 1 + 4 + 27 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 124 + 1 + 4 + 28 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 125 + 1 + 4 + 29 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 126 + 1 + 4 + 30 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 127 + 1 + 4 + 31 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 128 + 1 + 4 + 32 + + 263 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + -32764 + 32764 + -8191 + 8191 + 1 + uV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 300 + 1 + 1 + 7500000 + 3 + 257 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 65792 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -2047 + 2047 + + + 2 + 1023 + + -255 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 129 + 1 + 5 + 1 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 130 + 1 + 5 + 2 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 131 + 1 + 5 + 3 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 132 + 1 + 5 + 4 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 133 + 1 + 5 + 5 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 134 + 1 + 5 + 6 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 135 + 1 + 5 + 7 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 136 + 1 + 5 + 8 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 137 + 1 + 5 + 9 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 138 + 1 + 5 + 10 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 139 + 1 + 5 + 11 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 140 + 1 + 5 + 12 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 141 + 1 + 5 + 13 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 142 + 1 + 5 + 14 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 143 + 1 + 5 + 15 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 78 + 166 + 0 + 144 + 1 + 5 + 16 + + 259 + 0 + 0 + 0 + 8055 + 2093071 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 256 + + 0 + + 0 + 0 + 0 + + + 10000 + 2047 + + + 0 + 0 + -32767 + 32767 + + + 0 + 32767 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 1 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 2 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 3 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 4 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 145 + 1 + 6 + 1 + + 515 + 0 + 0 + 1732 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 0 + + 0 + + 0 + 1 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 146 + 1 + 6 + 2 + + 515 + 0 + 0 + 1732 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 0 + + 0 + + 0 + 1 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 147 + 1 + 6 + 3 + + 515 + 0 + 0 + 1732 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 0 + + 0 + + 0 + 1 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 148 + 1 + 6 + 4 + + 515 + 0 + 0 + 1732 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -5000 + 5000 + 1 + mV + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 0 + + 0 + + 0 + 1 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 69 + 166 + 0 + 149 + 1 + 7 + 1 + + 515 + 0 + 0 + 1735 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -1500 + 1500 + 1 + mV + + + 0 + 0 + 0 + 0 + 1 + + + + -2047 + 2047 + -511 + 511 + 1 + uV + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 64 + 0 + 0 + + 0 + + 0 + 1 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 150 + 1 + 7 + 2 + + 515 + 0 + 0 + 1735 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -1500 + 1500 + 1 + mV + + + 0 + 0 + 0 + 0 + 0 + + + + -32767 + 32767 + -1500 + 1500 + 1 + mV + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 + 0 + + 0 + + 0 + 1 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 151 + 1 + 8 + 1 + + 1027 + 0 + 13312 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 5120 + 0 + 0 + 0 + + 0 + + 0 + 0 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 152 + 1 + 9 + 1 + + 1027 + 0 + 32 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 0 + 32 + 0 + 0 + 0 + + 0 + + 0 + 0 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 153 + 1 + 10 + 1 + + 2051 + 1057947904 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 1056964864 + 0 + 0 + 0 + 0 + + 0 + + 0 + 0 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 154 + 1 + 10 + 2 + + 2051 + 1057947904 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 1056964864 + 0 + 0 + 0 + 0 + + 0 + + 0 + 0 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 155 + 1 + 10 + 3 + + 2051 + 1057947904 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 1056964864 + 0 + 0 + 0 + 0 + + 0 + + 0 + 0 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+ + 32768 + 64 + 166 + 0 + 156 + 1 + 10 + 4 + + 2051 + 1057947904 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + + + 1056964864 + 0 + 0 + 0 + 0 + + 0 + + 0 + 0 + 0 + + + 0 + 0 + + + 0 + 0 + 0 + 0 + + + 0 + 0 + + 0 + 0 + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + 0 + + 0 + 0 + + + + + 0 + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + 0 + 0 + 0 + +
+
+ + + 32768 + 85 + 12 + 0 + 300 + 5 + 0 + + 9 + 125 + 0.8 + 0.94 + 0.5 + 0.5 + 0.016 + + 250 + 0 + + + + 32768 + 84 + 13 + 0 + 1 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 2 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 3 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 4 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 5 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 6 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 7 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 8 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 9 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 10 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 11 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 12 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 13 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 14 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 15 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 16 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 17 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 18 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 19 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 20 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 21 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 22 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 23 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 24 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 25 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 26 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 27 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 28 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 29 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 30 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 31 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 32 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 33 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 34 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 35 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 36 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 37 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 38 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 39 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 40 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 41 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 42 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 43 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 44 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 45 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 46 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 47 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 48 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 49 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 50 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 51 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 52 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 53 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 54 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 55 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 56 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 57 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 58 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 59 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 60 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 61 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 62 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 63 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 64 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 65 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 66 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 67 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 68 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 69 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 70 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 71 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 72 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 73 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 74 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 75 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 76 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 77 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 78 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 79 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 80 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 81 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 82 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 83 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 84 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 85 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 86 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 87 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 88 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 89 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 90 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 91 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 92 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 93 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 94 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 95 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 96 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 97 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 98 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 99 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 100 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 101 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 102 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 103 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 104 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 105 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 106 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 107 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 108 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 109 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 110 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 111 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 112 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 113 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 114 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 115 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 116 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 117 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 118 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 119 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 120 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 121 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 122 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 123 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 124 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 125 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 126 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 127 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 128 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 129 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 130 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 131 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 132 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 133 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 134 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 135 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 136 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 137 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 138 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 139 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 140 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 141 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 142 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 143 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+ + 32768 + 84 + 13 + 0 + 144 +
+ 0 + 0 + 0 +
+ + + 50 + 0 + 0 + + + 0 + 50 + 0 + + + 0 + 0 + 50 + + +
+
+ + 32768 + 82 + 2 + 0 + 2.2 + 1.55 + + + 32768 + 83 + 2 + 0 + 272 + 30 + + + 32768 + 87 + 6 + 0 + + 1 + 0 + 99 + + + 0 + 4 + 99 + + +
+ + 32768 + 145 + 6 + 0 + 30000 + + 48 + 10 + + 0 + 50 + 0 + + + + 32768 + 40 + 3 + 0 + 60 + + 1 + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + 32768 + 39 + 248 + 0 + 1 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 2 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 3 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 4 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 5 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 6 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 7 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 8 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 9 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 10 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 11 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 12 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 13 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 14 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 15 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 16 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 17 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 18 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 19 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 20 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 21 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 22 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 23 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 24 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 25 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 26 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 27 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 28 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 29 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 30 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 31 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 32 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 33 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 34 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 35 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 36 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 37 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 38 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 39 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 40 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 41 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 42 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 43 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 44 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 45 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 46 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 47 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 48 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 49 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 50 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 51 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 52 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 53 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 54 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 55 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 56 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 57 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 58 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 59 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 60 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 61 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 62 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 63 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 64 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 65 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 66 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 67 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 68 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 69 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 70 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 71 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+ + 32768 + 39 + 248 + 0 + 72 + + + + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+ + + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+ + 0 +
+ 0 + 0 + 0 +
+ + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 + 0 + + + 0 + 0 +
+
+
+ 0 + 0 + + 0 + 0 + 0 + 0 + +
+
+ + + 32768 + 37 + 5 + 0 + 0 + 0 + 5e-12 + + 129 + 130 + + + + + + 4.1 + 7.6.1 + 7.06.00.00 + 128-Channel player with Experiment I/O + 12 + + DESKTOP-KLO548U + 2023-09-22T15:05:27 + +
diff --git a/open_mer/resources/config/CbSdkConnection.ini b/open_mer/resources/settings/CbSdkConnection.ini similarity index 100% rename from open_mer/resources/config/CbSdkConnection.ini rename to open_mer/resources/settings/CbSdkConnection.ini diff --git a/open_mer/resources/config/DepthGUI.ini b/open_mer/resources/settings/DepthGUI.ini similarity index 94% rename from open_mer/resources/config/DepthGUI.ini rename to open_mer/resources/settings/DepthGUI.ini index aac40df..9f505b6 100644 --- a/open_mer/resources/config/DepthGUI.ini +++ b/open_mer/resources/settings/DepthGUI.ini @@ -4,7 +4,6 @@ maximized=false frameless=true size=@Size(600 250) pos=@Point(1320 0) -bg_color=#404040 [depth-source] class=FHCSerial diff --git a/open_mer/resources/config/FeaturesGUI.ini b/open_mer/resources/settings/FeaturesGUI.ini similarity index 52% rename from open_mer/resources/config/FeaturesGUI.ini rename to open_mer/resources/settings/FeaturesGUI.ini index b2db1d3..4e05d30 100644 --- a/open_mer/resources/config/FeaturesGUI.ini +++ b/open_mer/resources/settings/FeaturesGUI.ini @@ -4,7 +4,6 @@ maximized=false frameless=true size=@Size(600 780) pos=@Point(1320 300) -bg_color=#404040 [data-source] class=CerebusDataSource @@ -33,23 +32,3 @@ n_segments = 20 spk_aud=true lock_threshold=true unit_scaling=0.25 - -[theme] -bgcolor=black -labelcolor_active=yellow -labelsize_active=15 -labelsize_inactive=11 -threshcolor=yellow -threshwidth=1 -linewidth=1 -colormap=custom -pencolors\0\name=cyan -pencolors\1\value=#00ff00 -pencolors\2\name=magenta -pencolors\3\name=red -pencolors\4\name=yellow -pencolors\5\name=white - -[data-source-LSL-example] -class=LSLDataSource -identifier="{\"name\": \"MyAudioStream\", \"type\": \"audio\"}" \ No newline at end of file diff --git a/open_mer/resources/config/MappingGUI.ini b/open_mer/resources/settings/MappingGUI.ini similarity index 94% rename from open_mer/resources/config/MappingGUI.ini rename to open_mer/resources/settings/MappingGUI.ini index ff05091..0d5e021 100644 --- a/open_mer/resources/config/MappingGUI.ini +++ b/open_mer/resources/settings/MappingGUI.ini @@ -4,7 +4,6 @@ maximized=false frameless=true size=@Size(100 1080) pos=@Point(1220 0) -bg_color=#404040 [data-source] class=CerebusDataSource diff --git a/open_mer/resources/config/ProcessGUI.ini b/open_mer/resources/settings/ProcessGUI.ini similarity index 94% rename from open_mer/resources/config/ProcessGUI.ini rename to open_mer/resources/settings/ProcessGUI.ini index 228927d..0fa6736 100644 --- a/open_mer/resources/config/ProcessGUI.ini +++ b/open_mer/resources/settings/ProcessGUI.ini @@ -4,7 +4,6 @@ maximized=false frameless=true size=@Size(600 50) pos=@Point(1320 250) -bg_color=#404040 [data-source] class=CerebusDataSource diff --git a/open_mer/resources/config/RasterGUI.ini b/open_mer/resources/settings/RasterGUI.ini similarity index 62% rename from open_mer/resources/config/RasterGUI.ini rename to open_mer/resources/settings/RasterGUI.ini index 6079e00..8af5c0b 100644 --- a/open_mer/resources/config/RasterGUI.ini +++ b/open_mer/resources/settings/RasterGUI.ini @@ -4,7 +4,6 @@ maximized=false frameless=true size=@Size(300 1080) pos=@Point(620 0) -bg_color=#404040 [data-source] class=CerebusDataSource @@ -19,13 +18,5 @@ x_range=0.5 y_range=8 [theme] -bgcolor=black frate_size=24 linewidth=1 -colormap=custom -pencolors\0\name=cyan -pencolors\1\value=#00ff00 -pencolors\2\name=magenta -pencolors\3\name=red -pencolors\4\name=yellow -pencolors\5\name=white diff --git a/open_mer/resources/settings/Style.ini b/open_mer/resources/settings/Style.ini new file mode 100644 index 0000000..a7bb867 --- /dev/null +++ b/open_mer/resources/settings/Style.ini @@ -0,0 +1,14 @@ +[MainWindow] +frameless=true +bg_color=#404040 + +[theme] +bgcolor=black +linewidth=1 +colormap=custom +pencolors\0\name=cyan +pencolors\1\value=#00ff00 +pencolors\2\name=magenta +pencolors\3\name=red +pencolors\4\name=yellow +pencolors\5\name=white diff --git a/open_mer/resources/config/SweepGUI.ini b/open_mer/resources/settings/SweepGUI.ini similarity index 72% rename from open_mer/resources/config/SweepGUI.ini rename to open_mer/resources/settings/SweepGUI.ini index d7a2a73..cb9dfc0 100644 --- a/open_mer/resources/config/SweepGUI.ini +++ b/open_mer/resources/settings/SweepGUI.ini @@ -1,10 +1,8 @@ [MainWindow] fullScreen=false maximized=false -frameless=true size=@Size(620 1080) -pos=@Point(0 0) -bg_color=#404040 +pos=@Point(0 40) [data-source] class=CerebusDataSource @@ -29,20 +27,12 @@ lock_threshold=true unit_scaling=0.25 [theme] -bgcolor=black labelcolor_active=yellow labelsize_active=15 labelsize_inactive=11 threshcolor=yellow threshwidth=1 linewidth=1 -colormap=custom -pencolors\0\name=cyan -pencolors\1\value=#00ff00 -pencolors\2\name=magenta -pencolors\3\name=red -pencolors\4\name=yellow -pencolors\5\name=white [data-source-LSL-example] class=LSLDataSource diff --git a/open_mer/settings/__init__.py b/open_mer/settings/__init__.py index 1dc2624..bbd08b1 100644 --- a/open_mer/settings/__init__.py +++ b/open_mer/settings/__init__.py @@ -32,6 +32,6 @@ def locate_ini(ini_name): _settings_path = ini_path else: # Use default ini that ships with module. - _settings_path = Path(__file__).parents[1] / 'resources' / 'config' / ini_path.name + _settings_path = Path(__file__).parents[1] / "resources" / "settings" / ini_path.name return _settings_path diff --git a/open_mer/settings/reset.py b/open_mer/settings/reset.py index f721bc2..ce60970 100644 --- a/open_mer/settings/reset.py +++ b/open_mer/settings/reset.py @@ -10,7 +10,7 @@ def copy_ini_resources_from_package_to_home(): root_pkg = __package__.split(".")[0] dest_root = Path.home() / f".{root_pkg}" dest_root.mkdir(exist_ok=True) - with pkg_resources.path(f"{root_pkg}.resources", "config") as path: + with pkg_resources.path(f"{root_pkg}.resources", "settings") as path: for resource_file in path.iterdir(): if not resource_file.name.lower().endswith(".ini"): continue diff --git a/setup.cfg b/setup.cfg index 330f3c8..1a4b094 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,8 @@ exclude = [options.package_data] "open_mer/resources/icons" = "*.png" -"open_mer/resources/config" = "*.ini" +"open_mer/resources/settings" = "*.ini" +"open_mer/resources/config" = "*.ccf" [options.entry_points] # https://setuptools.pypa.io/en/latest/userguide/entry_point.html gui_scripts = From 58ac2a697233c1ec2b7bc84b9134059c44d50864 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 22 Sep 2023 17:23:51 -0400 Subject: [PATCH 42/65] Rearrange ini parsing in widgets. --- open_mer/dbsgui/raster.py | 34 +++++- open_mer/dbsgui/sweep.py | 49 +++++++- open_mer/dbsgui/waveform.py | 50 ++++++-- open_mer/dbsgui/widgets/custom.py | 184 ++++++++++++++++-------------- 4 files changed, 219 insertions(+), 98 deletions(-) diff --git a/open_mer/dbsgui/raster.py b/open_mer/dbsgui/raster.py index c3e61a1..3046a00 100644 --- a/open_mer/dbsgui/raster.py +++ b/open_mer/dbsgui/raster.py @@ -16,9 +16,41 @@ def __init__(self): def widget_cls(self): return RasterWidget + def parse_settings(self): + settings_paths = [self._settings_paths["base"]] + if "custom" in self._settings_paths: + settings_paths.extend(self._settings_paths["custom"]) + + if "plot" not in self._plot_config: + self._plot_config["plot"] = {} + + if "theme" not in self._plot_config: + self._plot_config["theme"] = {} + + for ini_path in settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + settings.beginGroup("plot") + k_t = {"x_range": float, "y_range": int} + keys = settings.allKeys() + for k, t in k_t.items(): + if k in keys: + self._plot_config["plot"][k] = settings.value(k, type=t) + settings.endGroup() + + settings.beginGroup("theme") + keys = settings.allKeys() + k_t = {"frate_size": int} + for k, t in k_t.items(): + if k in keys: + self._plot_config["theme"][k] = settings.value(k, type=t) + settings.endGroup() + + super().parse_settings() + def on_plot_closed(self): self._plot_widget = None - self._data_source.disconnect_requested() + # self._data_source.disconnect_requested() def do_plot_update(self): ev_timestamps = self._data_source.get_event_data() diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index 1225831..8176673 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -21,12 +21,42 @@ def __init__(self): def widget_cls(self): return SweepWidget + def parse_settings(self): + settings_paths = [self._settings_paths["base"]] + if "custom" in self._settings_paths: + settings_paths.extend(self._settings_paths["custom"]) + + if "plot" not in self._plot_config: + self._plot_config["plot"] = {} + + for ini_path in settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + settings.beginGroup("plot") + k_t = {"x_range": float, "y_range": float, "downsample": int, "n_segments": int, + "spk_aud": bool, "lock_threshold": bool, "unit_scaling": float} + keys = settings.allKeys() + for k, t in k_t.items(): + if k in keys: + self._plot_config["plot"][k] = settings.value(k, type=t) + settings.endGroup() + + settings.beginGroup("theme") + keys = settings.allKeys() + k_t = {"threshcolor": str, "threshwidth": int} + for k, t in k_t.items(): + if k in keys: + self._plot_config["theme"][k] = settings.value(k, type=t) + settings.endGroup() + + super().parse_settings() + def on_plot_closed(self): if self._plot_widget is not None and self._plot_widget.awaiting_close: del self._plot_widget self._plot_widget = None - if not self._plot_widget: - self._data_source.disconnect_requested() + # if not self._plot_widget: + # self._data_source.disconnect_requested() def do_plot_update(self): cont_data = self._data_source.get_continuous_data() @@ -52,11 +82,12 @@ def __init__(self, *args, zmq_chan_port=60003, **kwargs): self.plot_config = {} self.segmented_series = {} # Will contain one array of curves for each line/channel label. - context = zmq.Context() - self._chanselect_sock = context.socket(zmq.PUB) + self._chanselect_context = zmq.Context() + self._chanselect_sock = self._chanselect_context.socket(zmq.PUB) self._chanselect_sock.bind(f"tcp://*:{zmq_chan_port}") super().__init__(*args, **kwargs) + self.refresh_axes() # Even though super __init__ calls this, extra refresh is intentional self.pya_manager = pyaudio.PyAudio() self.pya_stream = None @@ -89,6 +120,11 @@ def closeEvent(self, evnt): self.pya_stream.stop_stream() self.pya_stream.close() self.pya_manager.terminate() + + self._chanselect_sock.setsockopt(zmq.LINGER, 0) + self._chanselect_sock.close() + self._chanselect_context.term() + super().closeEvent(evnt) def create_control_panel(self): @@ -324,7 +360,10 @@ def add_series(self, chan_state): } def refresh_axes(self): - last_sample_ix = int(np.mod(get_now_time(), self.plot_config['x_range']) * self.samplingRate) + _t = get_now_time() + if _t is None: + return + last_sample_ix = int(np.mod(_t, self.plot_config["x_range"]) * self.samplingRate) state_names = [_['name'] for _ in self.chan_states] for line_label in self.segmented_series: ss_info = self.segmented_series[line_label] diff --git a/open_mer/dbsgui/waveform.py b/open_mer/dbsgui/waveform.py index 9f206a7..b920a9a 100644 --- a/open_mer/dbsgui/waveform.py +++ b/open_mer/dbsgui/waveform.py @@ -1,5 +1,5 @@ import numpy as np -from qtpy import QtWidgets +from qtpy import QtCore, QtWidgets import pyqtgraph as pg from .utilities.pyqtgraph import parse_color_str, get_colormap from .widgets.custom import CustomGUI, CustomWidget @@ -15,9 +15,43 @@ def __init__(self): def widget_cls(self): return WaveformWidget + def parse_settings(self): + settings_paths = [self._settings_paths["base"]] + if "custom" in self._settings_paths: + settings_paths.extend(self._settings_paths["custom"]) + + if "plot" not in self._plot_config: + self._plot_config["plot"] = {} + + if "theme" not in self._plot_config: + self._plot_config["theme"] = {} + + for ini_path in settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + settings.beginGroup("plot") + k_t = {"x_start": int, "x_stop": int, "y_range": int, + "n_waveforms": int, "unit_scaling": float} + keys = settings.allKeys() + for k, t in k_t.items(): + if k in keys: + self._plot_config["plot"][k] = settings.value(k, type=t) + settings.endGroup() + + settings.beginGroup("theme") + keys = settings.allKeys() + k_t = {"frate_size": int} + for k, t in k_t.items(): + if k in keys: + self._plot_config["theme"][k] = settings.value(k, type=t) + settings.endGroup() + + super().parse_settings() + def on_plot_closed(self): + del self._plot_widget self._plot_widget = None - self._data_source.disconnect_requested() + # self._data_source.disconnect_requested() def do_plot_update(self): for label in self._plot_widget.wf_info: @@ -68,11 +102,11 @@ def create_control_panel(self): def create_plots(self, plot={}, theme={}): # Collect PlotWidget configuration - self.plot_config['theme'] = theme - self.plot_config['x_range'] = (plot.get('x_start', -300), plot.get('x_stop', 1140)) - self.plot_config['y_range'] = plot.get('y_range', 250) - self.plot_config['n_waveforms'] = plot.get('n_waveforms', 200) - self.plot_config['unit_scaling'] = plot.get('unit_scaling', 0.25) + self.plot_config["theme"] = theme + self.plot_config["x_range"] = (plot["x_start"], plot["x_stop"]) + self.plot_config["y_range"] = plot["y_range"] + self.plot_config["n_waveforms"] = plot["n_waveforms"] + self.plot_config["unit_scaling"] = plot["unit_scaling"] # Update widget values without triggering signals prev_state = self.range_edit.blockSignals(True) @@ -161,4 +195,4 @@ def update(self, line_label, data): data_items = self.wf_info[line_label]['plot'].listDataItems() if len(data_items) > self.plot_config['n_waveforms']: for di in data_items[:-self.plot_config['n_waveforms']]: - self.wf_info[line_label]['plot'].removeItem(di) \ No newline at end of file + self.wf_info[line_label]['plot'].removeItem(di) diff --git a/open_mer/dbsgui/widgets/custom.py b/open_mer/dbsgui/widgets/custom.py index c56989a..8afe0e5 100644 --- a/open_mer/dbsgui/widgets/custom.py +++ b/open_mer/dbsgui/widgets/custom.py @@ -1,9 +1,7 @@ -import time from pathlib import Path +import importlib.resources as pkg_resources from qtpy import QtWidgets, QtCore, QtGui -# Import settings -from ...settings import defaults, parse_ini_try_numeric import open_mer.data_source @@ -12,96 +10,112 @@ class CustomGUI(QtWidgets.QMainWindow): This application is for monitoring continuous activity from a MER data source. """ - def __init__(self, ini_file=None): + def __init__(self): super().__init__() # Infer path to ini - ini_name = ini_file if ini_file is not None else (type(self).__name__ + '.ini') - ini_path = Path(ini_name) - if ini_path.exists(): - self._settings_path = ini_path - else: - # Try home / .open_mer first - home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) - ini_path = home_dir / '.open_mer' / ini_path.name - if ini_path.exists(): - self._settings_path = ini_path - else: - # Use default ini that ships with module. - self._settings_path = Path(__file__).parents[2] / 'resources' / 'config' / ini_path.name + root_pkg = __package__.split(".")[0] + ini_name = type(self).__name__ + '.ini' + + self._settings_paths: dict[str, Path] = {} + with pkg_resources.path(f"{root_pkg}.resources", "settings") as base_default: + ini_default = base_default / ini_name + self._settings_paths["base"] = ini_default + + base_custom = Path.home() / f".{root_pkg}" + ini_custom = base_custom / ini_name + if ini_custom.exists(): + self._settings_paths["custom"] = ini_custom self._plot_widget = None - self._plot_config = None self._data_source = None - self.restore_from_settings() + self._plot_config = {} + self.parse_settings() + self.try_reset_widget() self.show() def __del__(self): - # CbSdkConnection().disconnect() No need to disconnect because the instance will do so automatically. - pass - - def restore_from_settings(self): - # Should be overridden in child class, but likely calling this super at top of override. - settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) - - # Restore size and position. - default_dims = defaults.WINDOWDIMS_DICT[type(self).__name__] - settings.beginGroup("MainWindow") - self.move(settings.value("pos", QtCore.QPoint(default_dims[0], default_dims[1]))) - size_xy = settings.value("size", QtCore.QSize(default_dims[2], default_dims[3])) - self.resize(size_xy) - self.setMaximumWidth(size_xy.width()) - if settings.value("fullScreen", 'false') == 'true': - self.showFullScreen() - elif settings.value("maximized", 'false') == 'true': - self.showMaximized() - if settings.value("frameless", 'false') == 'true': - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - settings.endGroup() - - # Infer data source from ini file, setup data source - settings.beginGroup("data-source") - src_cls = getattr(open_mer.data_source, settings.value("class")) - # Get the _data_source. Note this might trigger on_source_connected before child - # finishes parsing settings. - _data_source = src_cls(scoped_settings=settings, on_connect_cb=self.on_source_connected) - settings.endGroup() - - plot_config = {} - for group_name in settings.childGroups(): - if group_name.lower() in ['mainwindow', 'theme'] or group_name.lower().startswith('data-source'): - continue - plot_config[group_name] = {} - settings.beginGroup(group_name) - for k in settings.allKeys(): - plot_config[group_name][k] = parse_ini_try_numeric(settings, k) - settings.endGroup() + self._data_source.disconnect_requested() + + def parse_settings(self): + """ + Parse ini files and populate ._plot_config. + Note that some settings (MainWindow) will be applied immediately and won't be stored in _plot_config. + + This method should usually be followed by .try_reset_widget() + """ + # Collect names of ini files in reverse importance. + settings_paths = [self._settings_paths["base"].parent / "Style.ini", self._settings_paths["base"]] + if "custom" in self._settings_paths: + settings_paths += [self._settings_paths["custom"].parent / "Style.ini", self._settings_paths["custom"]] # theme - settings.beginGroup("theme") - plot_config['theme'] = {} - for k in settings.allKeys(): - if k == 'colormap' or k.lower().startswith('pencolors'): - continue - plot_config['theme'][k] = parse_ini_try_numeric(settings, k) - # theme > pencolors - plot_config['theme']['colormap'] = settings.value('colormap', 'custom') - if plot_config['theme']['colormap'] == "custom": - pencolors = [] - settings.beginGroup("pencolors") - for c_id in settings.childGroups(): - settings.beginGroup(c_id) - cname = settings.value("name", None) - if cname is not None: - cvalue = QtGui.QColor(cname) - else: - cvalue = settings.value("value", "#ffffff") - pencolors.append(cvalue) - settings.endGroup() - settings.endGroup() # pencolors - plot_config['theme']['pencolors'] = pencolors - settings.endGroup() # end theme - self.plot_config = plot_config # Triggers setter --> self.try_reset_widget() + if "theme" not in self._plot_config: + self._plot_config["theme"] = {"colormap": None} + + for ini_path in settings_paths: + + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + # Apply MainWindow settings immediately. + settings.beginGroup("MainWindow") + keys: list = settings.allKeys() + if "pos" in keys: + self.move(settings.value("pos", type=QtCore.QPoint)) + if "size" in keys: + size_xy: QtCore.QPoint = settings.value("size", type=QtCore.QPoint) + self.resize(size_xy) + self.setMaximumWidth(size_xy.width()) + if "fullScreen" in keys and settings.value("fullScreen", type=bool): + self.showFullScreen() + elif "maximized" in keys and settings.value("maximized", type=bool): + self.showMaximized() + if "frameless" in keys and settings.value("frameless", type=bool): + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + settings.endGroup() + + # Immediately initiate connection with the data source. Connection outcome will be handled in the callback. + settings.beginGroup("data-source") + if "class" in settings.allKeys() and self._data_source is None: + # Infer data source from ini file, setup data source + src_cls = getattr(open_mer.data_source, settings.value("class", type=str)) + # Get the _data_source. Note this might trigger on_source_connected before child + # finishes parsing settings. + _data_source = src_cls(scoped_settings=settings, on_connect_cb=self.on_source_connected) + settings.endGroup() + + settings.beginGroup("theme") + k_t = { + "labelcolor_active": str, "labelsize_active": int, + "labelcolor_inactive": str, "labelsize_inactive": int, + "linewidth": int + } + keys = settings.allKeys() + for k, t in k_t.items(): + if k in keys: + self._plot_config["theme"][k] = settings.value(k, type=t) + + # theme > pencolors + if "colormap" in settings.allKeys(): + self._plot_config["theme"]["colormap"] = settings.value("colormap", "custom", type=str) + self._plot_config["theme"].pop("pencolors", None) + + if self._plot_config["theme"]["colormap"] == "custom": + settings.beginGroup("pencolors") + chan_ids = [int(_) for _ in settings.childGroups()] + if "pencolors" not in self._plot_config["theme"]: + self._plot_config["theme"]["pencolors"] = [None] * (max(chan_ids) + 1) + for c_id in chan_ids: + settings.beginGroup(str(c_id)) + if "name" in settings.allKeys(): + name = settings.value("name", type=str) + self._plot_config["theme"]["pencolors"][c_id] = QtGui.QColor(name) + else: + color_hex = settings.value("value", defaultValue="#ffffff", type=str) + self._plot_config["theme"]["pencolors"][c_id] = QtGui.QColor(color_hex) + settings.endGroup() + settings.endGroup() # pencolors + settings.endGroup() # theme @QtCore.Slot(QtCore.QObject) def on_source_connected(self, data_source): @@ -120,10 +134,12 @@ def do_plot_update(self): # abc.abstractmethod not possible because ABC does not work with Qt-derived classes, so raise error instead. raise NotImplementedError("This method must be overridden by sub-class.") + def on_plot_closed(self): + raise NotImplementedError("This method must be overridden by sub-class.") + def try_reset_widget(self): if self._plot_widget is not None: - # TODO: Close existing self._plot_widget - print("TODO: Close existing self._plot_widget") + self._plot_widget.close() if self.plot_config is not None and self.data_source is not None: src_dict = self.data_source.data_stats self._plot_widget = self.widget_cls(src_dict, **self.plot_config) From 1ae7d2d780ae96025b1056f84cd90e77a5022e7b Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 22 Sep 2023 17:24:19 -0400 Subject: [PATCH 43/65] Add try-except around GUI application exec. --- open_mer/scripts/RasterGUI.py | 7 +++++-- open_mer/scripts/SweepGUI.py | 7 +++++-- open_mer/scripts/WaveformGUI.py | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/open_mer/scripts/RasterGUI.py b/open_mer/scripts/RasterGUI.py index e3bd2c9..cf7286f 100644 --- a/open_mer/scripts/RasterGUI.py +++ b/open_mer/scripts/RasterGUI.py @@ -10,8 +10,11 @@ def main(): timer.timeout.connect(aw.update) timer.start(1) - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() + try: + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + except KeyboardInterrupt: + pass if __name__ == '__main__': diff --git a/open_mer/scripts/SweepGUI.py b/open_mer/scripts/SweepGUI.py index e8791de..0cafc04 100644 --- a/open_mer/scripts/SweepGUI.py +++ b/open_mer/scripts/SweepGUI.py @@ -11,8 +11,11 @@ def main(): timer.timeout.connect(aw.update) timer.start(1) - if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() + try: + if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + except KeyboardInterrupt: + pass if __name__ == '__main__': diff --git a/open_mer/scripts/WaveformGUI.py b/open_mer/scripts/WaveformGUI.py index 885006e..0bf5146 100644 --- a/open_mer/scripts/WaveformGUI.py +++ b/open_mer/scripts/WaveformGUI.py @@ -10,8 +10,11 @@ def main(): timer.timeout.connect(aw.update) timer.start(1) - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() + try: + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtWidgets.QApplication.instance().exec_() + except KeyboardInterrupt: + pass if __name__ == '__main__': From 3fdd31f92a45387a8cd5ccc9c56878b91f9bc710 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 22 Sep 2023 17:25:25 -0400 Subject: [PATCH 44/65] forgotten file in move from config to settings --- open_mer/resources/{config => settings}/WaveformGUI.ini | 2 -- 1 file changed, 2 deletions(-) rename open_mer/resources/{config => settings}/WaveformGUI.ini (92%) diff --git a/open_mer/resources/config/WaveformGUI.ini b/open_mer/resources/settings/WaveformGUI.ini similarity index 92% rename from open_mer/resources/config/WaveformGUI.ini rename to open_mer/resources/settings/WaveformGUI.ini index f6bbf8c..b7a9e77 100644 --- a/open_mer/resources/config/WaveformGUI.ini +++ b/open_mer/resources/settings/WaveformGUI.ini @@ -4,7 +4,6 @@ maximized=false frameless=true size=@Size(300 1080) pos=@Point(920 0) -bg_color=#404040 [data-source] class=CerebusDataSource @@ -22,7 +21,6 @@ n_waveforms=50 unit_scaling=0.25 [theme] -bgcolor=black frate_size=24 linewidth=1 colormap=hsv From 34969ea2233df531ed890cb5ec015698ad38566a Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 22 Sep 2023 17:30:18 -0400 Subject: [PATCH 45/65] Refactor docs for development (especially macOS). --- docs/for-developers.md | 43 ++++++++++++++++++++---- docs/getting-started.md | 25 ++++---------- docs/introduction.md | 10 ++---- docs/preparing-distribution.md | 5 ++- docs/settings.md | 61 ++++++++++++++++++++++++++++++++++ docs/usage-instructions.md | 46 ++++++++++++++----------- 6 files changed, 136 insertions(+), 54 deletions(-) create mode 100644 docs/settings.md diff --git a/docs/for-developers.md b/docs/for-developers.md index 1dc6979..28624cf 100644 --- a/docs/for-developers.md +++ b/docs/for-developers.md @@ -16,7 +16,7 @@ Run `mkdocs gh-deploy` to build the documentation, commit to the `gh-deploy` bra ### Autogenerated Documentation -The /docs/neuroport_dbs folder can hold stubs to tell the [mkdocstrings plugin](https://github.com/mkdocstrings/mkdocstrings) to build the API documentation from the docstrings in the library code itself. Currently this is empty. If any stubs are added then it's necessary to build the documentation from a Python environment that has the package installed. A stub takes the form +The /docs/neuroport_dbs folder can hold stubs to tell the [mkdocstrings plugin](https://github.com/mkdocstrings/mkdocstrings) to build the API documentation from the docstrings in the library code itself. Currently, this is empty. If any stubs are added then it's necessary to build the documentation from a Python environment that has the package installed. A stub takes the form ``` # Title @@ -33,15 +33,46 @@ If you build the docs locally then you'll also get the /site directory, but this TODO +## Development Environment +### Blackrock Neuroport -## For experts who want to use their existing environment +When using Blackrock hardware, the following tools and SDKs are needed. -We assume you know how to work with conda environments and that you have a MySQL database server running and configured to your liking. +The Blackrock NSP has its own [NeuroPort Central Suite](https://www.blackrockmicro.com/technical-support/software-downloads/) to manage the configuration of the device and to store data. However, its data visualization capabilities are rather limited and not suited for DBS MER. -* Install the Python packages from the table above. -* Adapt the instructions at [Segmented Electrophys Recordings and Features Database (SERF)](https://github.com/cboulay/SERF) to prepare the database server for these tools. -* If you have a hybrid distribution/system-MySQL environment (i.e., your name is Guillaume) then you may also wish to use some of the MySQL DB config settings from above. +The NSP data stream is accessible via an open source API [CereLink](https://github.com/CerebusOSS/CereLink) which includes a Python interface called `cerebus.cbpy`. These are maintained by Sachs Lab member Chadwick Boulay. Most of our OpenMER software is written in Python and depends on `cerebus.cbpy` and a custom [cerebuswrapper](https://github.com/SachsLab/cerebuswrapper) to communicate with the NSP. + +#### nPlayServer + +For development, it is useful to playback previously recorded data, without need of hardware or patients. + +**Windows** + +* Run "C:\Program Files (x86)\Blackrock Microsystems\NeuroPort Windows Suite\runNPlayAndCentral.bat" +* Select a recording to play back +* Use Central's hardware configuration tool to enable continuous recording and spike extraction on the recorded channels. +* Follow the general [Usage Instructions](./usage-instructions.md) with one modification: + * When running `dbs-ddu`, choose "cbsdk playback" from the dropdown menu to reuse the depths from the recording. The value might not update until the file plays back a change in depth. + +**Nix** + +* Blackrock does not distribute nPlayServer on macOS or Linux. However, it does exist. Contact Chad directly. +* Central is also unavailable on these platforms. Use `pycbsdk` to quickly change the hardware configuration. + +### Playback XDF file + +If you have a correctly formatted file, it may be enough to use [XDFStreamer](https://github.com/labstreaminglayer/App-XDFStreamer). + +TODO: More instructions needed. + +### Dependencies + +We assume you know how to work with conda / mamba environments and that you have a MySQL database server running and configured to your liking. + +* Create an `openmer` conda environment. +* Install the Python packages from the [table](preparing-distribution.md#required-python-packages). +* Adapt the instructions at [Segmented Electrophys Recordings and Features Database (SERF)](https://github.com/cboulay/SERF) to prepare the database server for your development environment. ## Future goal - installable package diff --git a/docs/getting-started.md b/docs/getting-started.md index 711e108..e76a990 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,12 +1,12 @@ -The PC which runs our software is directly connected to the acquisition system but it is never connected to the internet. Thus, we copy everything we need to a thumb drive which we then copy to the clinical PC and install. +The PC which runs our software is directly connected to the acquisition system but it is never connected to the internet. Thus, we create a portable install on a thumb drive which we then copy to the clinical PC. -> For development or testing on an internet-connected computer, please look at the [For Developers](./for-developers.md) documentation. +> For development or testing on an internet-connected computer, or a non-Windows computer, please look at the [For Developers](./for-developers.md) documentation. ## Installation ### Distribution -Copy the `` folder from the thumb drive to the instrument PC. Be sure to choose a location with lots of disk space because many recording segments will be stored within this folder. +Copy the `` folder from the thumb drive to the instrument PC. Be sure to choose a location with lots of disk space because many recording segments will be stored within the database located in this folder. > If you do not have the `` folder then follow the [Preparing Distribution](./preparing-distribution.md) instructions to create it. @@ -21,10 +21,7 @@ The `` folder is ready to use as-is. However, with some additional #### Settings files -Copy all of the .ini files from `\\\Lib\site-packages\open_mer\resources\config` -to %HOME%\.open_mer\. - -You can then edit these settings files to change some parameters. CbSkConnection.ini can be particularly important if not using Central. +See [Settings](settings.md) for more information. ## Using OpenMER @@ -32,16 +29,6 @@ See [Usage Instructions](./usage-instructions.md) ## Test Environment - Without Hardware -Testing without the hardware is also possible using a signal generator source or a data playback source (see below for example). - -### Emulate Blackrock NSP - -* Run "C:\Program Files (x86)\Blackrock Microsystems\NeuroPort Windows Suite\runNPlayAndCentral.bat" -* Select a recording to play back -* Use Central's hardware configuration tool to enable continuous recording and spike extraction on the recorded channels. -* Follow the general [Usage Instructions](./usage-instructions.md) with one modification: - * When running `dbs-ddu`, choose "cbsdk playback" from the dropdown menu to reuse the depths from the recording. The value might not update until the file plays back a change in depth. - -### Playback XDF file +Testing without the hardware is also possible using a signal generator source or a data playback source. -More instructions are needed. If you have a correctly formatted file, it may be enough to use [XDFStreamer](https://github.com/labstreaminglayer/App-XDFStreamer). +See the [For Developers](for-developers.md) documentation for additional information. diff --git a/docs/introduction.md b/docs/introduction.md index 552a4e2..fe6bcf1 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -16,7 +16,7 @@ Here is the list of equipment we use. It may be possible to use this software wi ## Software -NeuroportDBS is a Suite of applications for visualizing signals in real-time: +OpenMER is a Suite of applications for visualizing signals in real-time: ![Image of vis apps](https://github.com/SachsLab/OpenMER/blob/master/vis_apps_screenshot.PNG?raw=true) @@ -31,10 +31,4 @@ NeuroportDBS is a Suite of applications for visualizing signals in real-time: We also use a GUI application we developed called [*CereStimDBS*](https://github.com/CerebusOSS/CereStimDBS) for controlling the Blackrock CereStim96 in a convenient manner for DBS surgeries. -### Dependencies - -When using Blackrock hardware, the following tools and SDKs are needed. - -The Blackrock NSP has its own [NeuroPort Central Suite](https://www.blackrockmicro.com/technical-support/software-downloads/) to manage the configuration of the device and to store data. However, its data visualization capabilities are rather limited and not suited for DBS MER. - -The NSP data stream is accessible via an open source API [CereLink](https://github.com/CerebusOSS/CereLink) which includes a Python interface called `cerebus.cbpy`. These are maintained by Sachs Lab member Chadwick Boulay. Most of our Neuroport DBS software is written in Python and much of it uses `cerebus.cbpy` and a custom [cerebuswrapper](https://github.com/SachsLab/cerebuswrapper) to communicate with the NSP. +Please see [Getting Started](getting-started.md) documentation on how to set up OpenMER then look at the [Usage Instructions](usage-instructions.md) for simple OpenMER operating instructions. diff --git a/docs/preparing-distribution.md b/docs/preparing-distribution.md index 9c8cbd0..43114db 100644 --- a/docs/preparing-distribution.md +++ b/docs/preparing-distribution.md @@ -41,11 +41,14 @@ In your WinPython Command Prompt, try the following commands first. If they fail > ``` python.exe -m pip install --upgrade pip -pip install PySide6 pyFFTW mysqlclient Django quantities pylsl numpy scipy Cython pyaudio pyzmq +pip install Django quantities numpy scipy Cython pyFFTW mysqlclient +# Use `mamba install` for the above line on macOS. +pip install pylsl pyaudio PySide6 qtpy pyzmq pyqtgraph pip install git+https://github.com/NeuralEnsemble/python-neo.git pip install git+https://github.com/SachsLab/pytf.git pip install git+https://github.com/SachsLab/mspacman.git pip install https://github.com/CerebusOSS/CereLink/releases/download/v7.6.4/cerebus-0.4-cp311-cp311-win_amd64.whl +# pip install https://github.com/CerebusOSS/CereLink/releases/download/v7.6.4/cerebus-0.4-cp311-cp311-macosx_11_0_arm64.whl pip install git+https://github.com/CerebusOSS/cerebuswrapper.git pip install git+https://github.com/cboulay/SERF.git#subdirectory=python pip install git+https://github.com/SachsLab/OpenMER.git diff --git a/docs/settings.md b/docs/settings.md new file mode 100644 index 0000000..db5002b --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,61 @@ +## Settings Format + +Our settings use the QSettings ini format. + +## Modifying Settings + +The default settings are contained in files located within the open_mer Python package. + +The user must copy these files into their home directory then modify them there. +Copy all .ini files from `\\\Lib\site-packages\open_mer\resources\settings` to %HOME%\.open_mer\. +This can be done by running `python -m open_mer.scripts.ResetUserSettings`. + +> Warning: The ResetUserSettings script will wipe out any previously modified settings in the user's home directory. + +Settings in the home directory will take precedence over the settings in the open_mer package. + +### CbSdkConnection.ini + +If the OpenMER GUI ini files have `class=CerebusDataSource` in their `[data-source]` section, +then the CbSdkConnection.ini will be used to determine how to connect to the Cerebus (Blackrock Neuroport) data source. + +By default, every line is commented out and the cerebus.cbpy default settings are used. The default settings will +first attempt to use the shared memory created by Central, then attempt to connect directly to a legacy NSP located at +192.168.137.128 using port 51001. + +If neither is true (not on the same PC as Central, or not directly connected to legacy NSP), then modify the settings. + +* `client-addr=192.168.137.1` + * Set this to the IP address of the PC running OpenMER. + * Use `192.168.137.1` for the Blackrock Host PC. +* `client-port=51002` + * Set this to the port used to transmit control packets to the NSP. It is unlikely this will ever change from 51002. +* `inst-addr=192.168.137.128` + * Set this to the IP address of the NSP (or the PC running nPlayServer in bcast mode). + * Use `192.168.137.128` for legacy NSP + * Use `192.168.137.200` for digital Neuroport system (Gemini Hub) +* `inst-port=51001` + * Use `51001` for legacy NSP and `51002` for digital Neuroport system. +* `receive-buffer-size=8388608` + +### Style.ini + +Contains generic style settings that are expected to be consistent across applications, +such as MainWindow framing and line colors. + +Settings in Style.ini may be overwritten by settings in GUI-specific ini files. + +### DepthGUI.ini + +### FeaturesGUI.ini + +### MappingGUI.ini + +### ProcessGUI.ini + +### RasterGUI.ini + +### SweepGUI.ini + +### WaveformGUI.ini + diff --git a/docs/usage-instructions.md b/docs/usage-instructions.md index af07251..d4ce2d2 100644 --- a/docs/usage-instructions.md +++ b/docs/usage-instructions.md @@ -1,37 +1,43 @@ -First run the `mysqld` binary by double-clicking its shortcut. -Then do the same for the `NeuroportDBS` batch file. +* Run the MySQL daemon + * Win: `mysqld` (create a shortcut) + * macOS: `brew services start mysql` +* Run OpenMER Suite + * Win: Use the batch file + * macOS: No batch file yet. Additional details follow. -## Blackrock NSP +## Configure Neural Data Source -The NSP must be on. Central should be running. -* If you are going to run this on the same computer that is configuring the hardware and that might run Central, Central must be running first. Central will not start after the NeuroportDBS software is already running. -* If you are going to run this on a separate computer connected to the NSP over the network, then Central is not technically required except for the FeaturesGUI app. If the FeaturesGUI requirement for Central is problematic for you then please let us know and we will try to remove that dependency. -* If you do not have the NSP available then follow the instructions above to setup a test environment. +### Blackrock NSP -## DDU +* The NSP must be on. + * Can use nPlayServer instead for testing. +* Central should be running first if OpenMER and Central are on the same PC. -* Choose the COM port the depth digitizer is connected to then click Open. - * Choosing the wrong serial port may cause the application to hang. Force close then try again. If you are using the batch file to launch then this might mean closing all of the GUI applications and running the batch file again. - * You can probably identify the correct COM port in Windows Control Panel >Device Manager > Serial & LPT devices. -* By default, it will automatically stream the depth to both LSL and to the NSP (added to the .nev datafile as comments). You can change this behaviour by unchecking the boxes. -* If, like us, the depth readout isn't the same as your distance to target, then add an offset. - * For example, when using a StarDrive with NexFrame, and distance to target it 85.3 mm, and the StarDrive places the microelectrodes 60 mm toward target when the drive reads depth=0, the remaining distance to target is 25.3 mm, so we add an offset of `-25.3`. As the drive descends the microelectrodes, and the depth reading increases, the reported distance to target approaches 0 until passes the target then reports positive values. +## Raw data visualization -**For FHC motor controller V2**: It reports depth in um, so we have to scale the depth reading by 0.001 . The ability to detect which DDU is in use is not implemented in the DDU GUI so we manually edit DDUGUI.py to hard code the scaling. +SweepGUI -- RasterGUI -- WaveformGUI -## SweepGUI -- RasterGUI -- WaveformGUI +No special instructions. These should work as long as a recognized data source is available. -These 3 applications share the same simple instructions: First click "Connect" to open the NSP connection dialog then OK (assuming defaults are OK). Then click Add Plot to open the window. +### Sweep Plot Audio -The connection settings are ignored if Central is running on the same computer, because the default connection method first attempts to connect to Central's shared memory. +The SweepGUI has the ability to stream one of the visualized channels out over the computer's speaker system. You can select which channel is being streamed either by clicking on one of the radio buttons near the top or by using a number on the keyboard (0 for silence, 1-N for each visualized channel). Alternatively, you may use left-arrow and right-arrow for cycling through the channels, and Space selects silence. +We map a footpedal to right-arrow so the channels can be cycled without using hands. -### Sweep Plot Audio +## DDU -The SweepGUI has the ability to stream one of the visualized channels out over the computer's speaker system. You can select which channel is being streamed either by clicking on one of the radio buttons near the top or by using a number on the keyboard (0 for silence, 1-N for each visualized channel). For convenience when using a simple keyboard emulation (e.g. footpad), you may use left-arrow and right-arrow for cycling through the channels, and Space selects silence. +* Choose the COM port the depth digitizer is connected to then click Open. + * Choosing the wrong serial port may cause the application to hang. Force close then try again. + * You can probably identify the correct COM port in Windows Control Panel >Device Manager > Serial & LPT devices. +* By default, it will automatically stream the depth to both LSL and to the NSP (added to the .nev datafile as comments). You can change this behaviour by unchecking the boxes. +* Enter your distance to target +* If, like us, the depth readout isn't the same as your distance to target, then add an offset. + * It defaults to -60 mm if it detects the DDU version most commonly associated with the StarDrive. ## Features + Click connect, OK, Add Plot Then you're presented with a settings window. From b7995f018673afa26a4c3e1b1f850af3a94fc7e3 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 27 Sep 2023 16:01:52 -0400 Subject: [PATCH 46/65] Docs update --- docs/for-developers.md | 36 +++++++++++++++++++---- docs/getting-started.md | 20 +++++-------- docs/introduction.md | 21 ++++++++----- docs/preparing-distribution.md | 54 +++++++++++++++++++--------------- docs/settings.md | 11 ++++++- docs/troubleshooting.md | 27 +++-------------- docs/usage-instructions.md | 28 +++++++++++------- 7 files changed, 113 insertions(+), 84 deletions(-) diff --git a/docs/for-developers.md b/docs/for-developers.md index 28624cf..d648d8c 100644 --- a/docs/for-developers.md +++ b/docs/for-developers.md @@ -12,15 +12,17 @@ You will need to install several Python packages to maintain the documentation. The /docs/{top-level-section} folders contain a mix of .md and .ipynb documentation. The latter are converted to .md by the [mknotebooks plugin](https://github.com/greenape/mknotebooks/projects) during building. -Run `mkdocs gh-deploy` to build the documentation, commit to the `gh-deploy` branch, and push to GitHub. This will make the documentation available at https://SachsLab.github.io/NeuroportDBS/ +Run `mkdocs gh-deploy` to build the documentation, commit to the `gh-deploy` branch, and push to GitHub. This will make the documentation available at https://SachsLab.github.io/OpenMER/ ### Autogenerated Documentation -The /docs/neuroport_dbs folder can hold stubs to tell the [mkdocstrings plugin](https://github.com/mkdocstrings/mkdocstrings) to build the API documentation from the docstrings in the library code itself. Currently, this is empty. If any stubs are added then it's necessary to build the documentation from a Python environment that has the package installed. A stub takes the form +The /docs/open_mer folder can hold stubs to tell the [mkdocstrings plugin](https://github.com/mkdocstrings/mkdocstrings) to build the API documentation from the docstrings in the library code itself. Currently, this is empty. If any stubs are added then it's necessary to build the documentation from a Python environment that has the package installed. + +A stub takes the form ``` # Title -::: neuroport_dbs.module.name +::: open_mer.module.name ``` ### Testing the documentation locally @@ -33,6 +35,26 @@ If you build the docs locally then you'll also get the /site directory, but this TODO +## Interprocess Communication + +This section is referring to communication among the applications within the OpenMER suite, including mysqld and ~8 Python applications. This section is not referring to communication to/from the data sources. + +The applications all run independently of each other, but most of them work better in combination. To communicate information between applications we use [ZeroMQ](https://zeromq.org/). + +| Publisher | Port | Topic | Message | Subscribers | +|----------------------|-------|--------------------|--------------------------------------------------------|----------------------| +| ProcedureGUI | 60001 | procedure_settings | json of settings-dicts "procedure" and ?? | FeaturesGUI | +| Depth_Process (SERF) | 60002 | snippet_status | (startup, notrecording, recording, accumulating, done) | ProcedureGUI | +| SweepGUI | 60003 | channel_select | json with channel, range, highpass | FeaturesGUI | +| FeaturesGUI | 60004 | features | refresh | FeaturesGUI | +| DepthGUI | 60005 | ddu | float of depth | Depth_Process (SERF) | + +We also have one LSL stream coming from the DepthGUI. Old version of the SERF Depth_Process might still be using it but they should be migrated. + +| Stream Name | Stream Type | Content | Inlets | +|-----------------|-------------|-------------------------|--------------------------| +| electrode_depth | depth | 1 float32 of elec depth | old Depth_Process (SERF) | + ## Development Environment ### Blackrock Neuroport @@ -46,19 +68,21 @@ The NSP data stream is accessible via an open source API [CereLink](https://gith #### nPlayServer For development, it is useful to playback previously recorded data, without need of hardware or patients. +nPlayServer emulates a Blackrock Neuroport almost perfectly -- just with a different ip address and it plays back an old data file instead of provides new data. See below for platform-specific instructions. + +When using nPlayServer, you can follow the general [Usage Instructions](./usage-instructions.md) with one modification: +Modify the [DepthGUI settings](settings.md#depthguiini) to use "cbsdk playback" as its source. The depth value might not update until the file playback emits a change in depth. **Windows** * Run "C:\Program Files (x86)\Blackrock Microsystems\NeuroPort Windows Suite\runNPlayAndCentral.bat" * Select a recording to play back * Use Central's hardware configuration tool to enable continuous recording and spike extraction on the recorded channels. -* Follow the general [Usage Instructions](./usage-instructions.md) with one modification: - * When running `dbs-ddu`, choose "cbsdk playback" from the dropdown menu to reuse the depths from the recording. The value might not update until the file plays back a change in depth. **Nix** * Blackrock does not distribute nPlayServer on macOS or Linux. However, it does exist. Contact Chad directly. -* Central is also unavailable on these platforms. Use `pycbsdk` to quickly change the hardware configuration. +* Central is unavailable on these platforms. Use `pycbsdk` to quickly change the hardware configuration. ### Playback XDF file diff --git a/docs/getting-started.md b/docs/getting-started.md index e76a990..1fb0954 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,14 +1,16 @@ -The PC which runs our software is directly connected to the acquisition system but it is never connected to the internet. Thus, we create a portable install on a thumb drive which we then copy to the clinical PC. +The PC that runs our software is directly connected to the acquisition system but it is never connected to the internet. Thus, we create a portable install on a thumb drive which we then copy to the clinical PC. -> For development or testing on an internet-connected computer, or a non-Windows computer, please look at the [For Developers](./for-developers.md) documentation. +> For development or testing on an internet-connected computer, or a non-Windows computer, please look at the [For Developers](for-developers.md) documentation. ## Installation ### Distribution -Copy the `` folder from the thumb drive to the instrument PC. Be sure to choose a location with lots of disk space because many recording segments will be stored within the database located in this folder. +Copy the `` folder from the thumb drive to the instrument PC. -> If you do not have the `` folder then follow the [Preparing Distribution](./preparing-distribution.md) instructions to create it. +> Be sure to choose a destination with lots of disk space because many recording segments will be stored within the database located in this folder. + +> If you do not have the `` folder then follow the [Preparing Distribution](preparing-distribution.md) instructions to create it. ### Configure @@ -21,14 +23,8 @@ The `` folder is ready to use as-is. However, with some additional #### Settings files -See [Settings](settings.md) for more information. +OpenMER functionality can be modified via INI files. See [Settings](settings.md) for more information. ## Using OpenMER -See [Usage Instructions](./usage-instructions.md) - -## Test Environment - Without Hardware - -Testing without the hardware is also possible using a signal generator source or a data playback source. - -See the [For Developers](for-developers.md) documentation for additional information. +See [Usage Instructions](usage-instructions.md) diff --git a/docs/introduction.md b/docs/introduction.md index fe6bcf1..95d7dda 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -9,10 +9,10 @@ MER requires specialized equipment and software. While all-in-one medical device Here is the list of equipment we use. It may be possible to use this software with other equipment. * A variety of microelectrodes, but mostly [FHC Microtargeting Electrodes](https://www.fh-co.com/product-category/microtargeting/). -* We use the [Blackrock Neuroport](https://www.blackrockmicro.com/neuroscience-research-products/neural-data-acquisition-systems/neuroport-daq-system/) for signal acquisition, processing, and digitization. Other devices are possible. +* We use the [Blackrock Neuroport](https://www.blackrockmicro.com/neuroscience-research-products/neural-data-acquisition-systems/neuroport-daq-system/) for signal acquisition, processing, and digitization. Other devices are possible via [labstreaminglayer](https://labstreaminglayer.readthedocs.io/). * An [FHC Depth Digitizing Unit (DDU)](https://www.fh-co.com/product/microtargeting-controller-power-assist-system-2-0/) - they have similar units for the Nexdrive and STar Drive. * A drive compatible with the DDU, such as the [Medtronic Nexdrive](https://www.medicalexpo.com/prod/medtronic/product-70691-503248.html) or the [FHC STar Drive](https://www.fh-co.com/product/star-drive-motor-encoder-system/) -* The [Blackrock Cerestim](https://www.blackrockmicro.com/neuroscience-research-products/ephys-stimulation-systems/cerestim-96-neurostimulation-system/) for microstimulation. The Cerestim is not required. +* [Optional] A [Blackrock Cerestim](https://www.blackrockmicro.com/neuroscience-research-products/ephys-stimulation-systems/cerestim-96-neurostimulation-system/) for microstimulation. ## Software @@ -21,13 +21,18 @@ OpenMER is a Suite of applications for visualizing signals in real-time: ![Image of vis apps](https://github.com/SachsLab/OpenMER/blob/master/vis_apps_screenshot.PNG?raw=true) * *SweepGUI* - Plots continuous signals in sweeps, optional high-pass filter, and sonifies a channel. -* *RasterGUI* - Plots threshold crossing events in a raster plot, with spike rate displayed in the corner (up to 8 sec history) +* *RasterGUI* - Plots events (i.e., spikes) in a raster plot, with event rate displayed in the corner (up to 8 sec history) * *WaveformGUI* - Plots the waveforms of the last N threshold crossing events. -* *DDUGUI* visualizes the depth readout from the drive (including adjustable offset value), and sends that depth to other consumers (e.g., the Blackrock NSP as a Comment; as a [labstreaminglayer](https://github.com/sccn/labstreaminglayer) outlet). -* *ProcessGUI* - Set patient and procedure info, and it has widgets to control recording state. Further, the recording button changes colour depending on what a background database process is doing (monitoring, accumulating, etc). -* *FeaturesGUI* plots the depth history of raw segments or features from the database and it is updated automatically. The database interaction occurs via a Django app called [SERF](https://github.com/cboulay/SERF) backed by a MySQL database. - * In addition to the MySQL database, 2 SERF applications must be running: *serf-cbacquire* and *serf-procfeatures*. -* *CommentGUI* (not shown) is for simple text entry widget to send arbitrary comments to the Blackrock NSP. +* *DepthGUI* - Displays the depth readout from the drive (including optional offset), and sends that depth to other consumers. + * sent to Blackrock NSP as a Comment; + * pushed on a [labstreaminglayer](https://github.com/sccn/labstreaminglayer) outlet; + * broadcast over ZeroMQ (see [ZeroMQ Port list](for-developers.md#interprocess-communication)) +* *ProcedureGUI* - Sets patient and procedure info, and controls recording state. + * The recording button colour reflects a background database process (monitoring: blue, accumulating: red). +* *FeaturesGUI* - Plots the history of raw segments or features by depth. + * The database interaction occurs via a Django app called [SERF](https://github.com/cboulay/SERF) backed by a MySQL database. + * Requires running MySQL database, and 2 SERF applications: *serf-cbacquire* and *serf-procfeatures*. +* *CommentGUI* (not shown) - Send comments to the Blackrock NSP, with some widgets specialized for kinesthetic mapping. We also use a GUI application we developed called [*CereStimDBS*](https://github.com/CerebusOSS/CereStimDBS) for controlling the Blackrock CereStim96 in a convenient manner for DBS surgeries. diff --git a/docs/preparing-distribution.md b/docs/preparing-distribution.md index 43114db..e589b31 100644 --- a/docs/preparing-distribution.md +++ b/docs/preparing-distribution.md @@ -4,22 +4,23 @@ After completing these instructions to prepare the distribution, head back to [G ## Base -Create a folder somewhere you have write access. The location should have at least 2 GB disk space. Name the folder whatever you like. Throughout this documentation, we refer to the folder as ``. +Create a folder somewhere you have write access. The location should have at least 2 GB disk space. Name the folder whatever you like. Throughout this documentation, we refer to the folder as `OpenMER_Suite`. ## Python * Download the latest [WinPython release](https://winpython.github.io/#releases). * Get the one ending in `dot` as this excludes some unnecessary bloat. * These instructions were tested with WinPython64-3.11.4.0dot -* Run the WinPython self-extracting executable and choose the `` folder as the extraction location. This will create a `/` folder containing a full Python distribution with many useful packages installed. +* Run the WinPython self-extracting executable and choose the `OpenMER_Suite` folder as the extraction location. This will create a `OpenMER_Suite/` folder containing a full Python distribution with many useful packages installed. * The full list of installed packages can be found [here](https://github.com/winpython/winpython/blob/master/changelogs/). * [Edit the `\settings\winpython.ini` file](https://sourceforge.net/p/winpython/wiki/Environment/) and add the following line: `PATH = %WINPYDIR%\Lib\site-packages\PyQt5\Qt\bin;%PATH%` * TODO: Is this still necessary with PySide6? * In the WinPython folder, run "WinPython Command Prompt". This will open a Command Prompt with all the paths configured to use this new Python distribution. * Confirm with `echo %PATH%`. There should be many paths in the WinPython tree. * Uninstall PyQt5 (we will be installing PySide6): `pip uninstall pyqt5` + * TODO: Maybe no longer necessary with the `dot` WinPython? * Install all the Python packages according to the [Required Python Packages](#required-python-packages) section below. -* Make a batch file `WPy64-3850\scripts\OpenMER.bat` with the following contents: +* Make a batch file `OpenMER_Suite\\Scripts\OpenMER.bat` with the following contents: ```shell script @echo off call "%~dp0env_for_icons.bat" @@ -29,24 +30,29 @@ Create a folder somewhere you have write access. The location should have at lea start "" "%WINPYDIR%\Scripts\dbs-raster.exe" /command:%1 /B start "" "%WINPYDIR%\Scripts\dbs-waveform.exe" /command:%1 /B start "" "%WINPYDIR%\Scripts\dbs-ddu.exe" /command:%1 /B + start "" "%WINPYDIR%\Scripts\dbs-procedure.exe" /command:%1 /B start "" "%WINPYDIR%\Scripts\dbs-features.exe" /command:%1 /B ``` + +> Note: Check OpenMER/setup.cfg `[options.entry_points]` for the correct names of the exe files. ### Required Python Packages In your WinPython Command Prompt, try the following commands first. If they fail, download the wheels matching the version in the table and install the wheels directly. -> A great place to find wheels is http://www.lfd.uci.edu/~gohlke/pythonlibs/. The second best place to find wheels is in pypi.org and clicking on the "Download files" button for a particular package. + +> A great place to find wheels for Windows is http://www.lfd.uci.edu/~gohlke/pythonlibs/. The second best place to find wheels is in pypi.org and clicking on the "Download files" button for a particular package. > Developers only: For each of the SachsLab packages (mspacman, cerebuswrapper, serf, **neuroport_dbs**), you have the option of cloning the repo then installing the package in-place for easier editing. Using the WinPython command prompt, run `pip install -e .` from within the cloned directory. -> + ``` python.exe -m pip install --upgrade pip +# macOS developers: use `mamba` instead of `pip` for the next 1 line only. pip install Django quantities numpy scipy Cython pyFFTW mysqlclient -# Use `mamba install` for the above line on macOS. pip install pylsl pyaudio PySide6 qtpy pyzmq pyqtgraph pip install git+https://github.com/NeuralEnsemble/python-neo.git pip install git+https://github.com/SachsLab/pytf.git pip install git+https://github.com/SachsLab/mspacman.git +# macOS developers: comment out the following line and uncomment the subsequent line pip install https://github.com/CerebusOSS/CereLink/releases/download/v7.6.4/cerebus-0.4-cp311-cp311-win_amd64.whl # pip install https://github.com/CerebusOSS/CereLink/releases/download/v7.6.4/cerebus-0.4-cp311-cp311-macosx_11_0_arm64.whl pip install git+https://github.com/CerebusOSS/cerebuswrapper.git @@ -54,22 +60,22 @@ pip install git+https://github.com/cboulay/SERF.git#subdirectory=python pip install git+https://github.com/SachsLab/OpenMER.git ``` -| Package | Version | Wheel | pip command | -|----------------|---------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------| -| pySide6 | 6.5.2 | | | -| qtpy | 2.4.0 | | https://github.com/spyder-ide/qtpy.git | -| pyFFTW | 0.13.1 | | | -| mysqlclient | 2.2.0 | | | -| Django | 3.11.4 | (Comes in WinPython) | | -| quantities | 0.14.1 | | | -| python-neo | 0.13.0 | | `pip install git+https://github.com/NeuralEnsemble/python-neo.git` | -| pylsl | 1.16.1 | | `pip install pylsl` | -| pytf | 0.1 | [Link](https://github.com/SachsLab/pytf/releases/download/v0.1/pytf-0.1-py2.py3-none-any.whl) | `pip install git+https://github.com/SachsLab/pytf.git` | -| mspacman | 0.1 | [Link](https://github.com/SachsLab/mspacman/releases/download/v0.1/mspacman-0.1-py2.py3-none-any.whl) | `pip install git+https://github.com/SachsLab/mspacman.git` | -| cerebus | 0.4 | | `pip install https://github.com/CerebusOSS/CereLink/releases/download/v7.6.4/cerebus-0.4-cp311-cp311-win_amd64.whl` | -| cerebuswrapper | 0.1 | [Link](https://github.com/SachsLab/cerebuswrapper/releases/download/v0.1/cerebuswrapper-0.1.0-py3-none-any.whl) | `pip install git+https://github.com/CerebusOSS/cerebuswrapper.git` | -| serf | 1.2 | | `pip install git+https://github.com/cboulay/SERF.git#subdirectory=python` | -| neurport_dbs | 1.0 | [Link](https://github.com/SachsLab/NeuroportDBS/releases/download/v1.0/neuroport_dbs-1.0.0-py3-none-any.whl) | `pip install git+https://github.com/SachsLab/OpenMER.git` | +| Package | Version | Wheel | pip command | +|----------------|------------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------| +| pySide6 | 6.5.2 | | | +| qtpy | 2.4.0 | | https://github.com/spyder-ide/qtpy.git | +| pyFFTW | 0.13.1 | | | +| mysqlclient | 2.2.0 | | | +| Django | 4.2.5 | | | +| quantities | 0.14.1 | | | +| python-neo | git tag 340b0221 | | `pip install git+https://github.com/NeuralEnsemble/python-neo.git` | +| pylsl | 1.16.1 | | `pip install pylsl` | +| pytf | 0.1 | [Link](https://github.com/SachsLab/pytf/releases/download/v0.1/pytf-0.1-py2.py3-none-any.whl) | `pip install git+https://github.com/SachsLab/pytf.git` | +| mspacman | 0.1 | [Link](https://github.com/SachsLab/mspacman/releases/download/v0.1/mspacman-0.1-py2.py3-none-any.whl) | `pip install git+https://github.com/SachsLab/mspacman.git` | +| cerebus | 0.4 | | `pip install https://github.com/CerebusOSS/CereLink/releases/download/v7.6.4/cerebus-0.4-cp311-cp311-win_amd64.whl` | +| cerebuswrapper | 0.1 | [Link](https://github.com/SachsLab/cerebuswrapper/releases/download/v0.1/cerebuswrapper-0.1.0-py3-none-any.whl) | `pip install git+https://github.com/CerebusOSS/cerebuswrapper.git` | +| serf | 1.2 | | `pip install git+https://github.com/cboulay/SERF.git#subdirectory=python` | +| open_mer | * | [Link](https://github.com/SachsLab/NeuroportDBS/releases/download/) | `pip install git+https://github.com/SachsLab/OpenMER.git` | ## MySQL @@ -77,7 +83,7 @@ pip install git+https://github.com/SachsLab/OpenMER.git * Tested with mysql-8.1.0-win64.zip * Don't choose the .msi * After selecting the zip, on the following page look for the "No thanks, just start my download" link. -* Extract the mysql zip into `` and rename the extracted folder to `mysql` +* Extract the mysql zip into `OpenMER_Suite` and rename the extracted folder to `mysql` * In the command prompt, `cd` into the `bin` subfolder of the unzipped mysql folder. * Create a mysql\data folder along with the base databases: `mysqld --initialize-insecure --console` * You can change the default data directory, username, and password. See the section below on [Configuring MySQL Database Server](#configuring-mysql-database-server) @@ -110,7 +116,7 @@ pip install git+https://github.com/SachsLab/OpenMER.git ``` [client] user = root - password = {password} + password = #port = 3306 #socket = /tmp/mysql.sock ``` \ No newline at end of file diff --git a/docs/settings.md b/docs/settings.md index db5002b..23433ba 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -14,6 +14,8 @@ This can be done by running `python -m open_mer.scripts.ResetUserSettings`. Settings in the home directory will take precedence over the settings in the open_mer package. +## Common Settings + ### CbSdkConnection.ini If the OpenMER GUI ini files have `class=CerebusDataSource` in their `[data-source]` section, @@ -45,13 +47,20 @@ such as MainWindow framing and line colors. Settings in Style.ini may be overwritten by settings in GUI-specific ini files. +## Application Settings + ### DepthGUI.ini +* Choose the COM port the depth digitizer is connected to then click Open. + * Choosing the wrong serial port may cause the application to hang. Force close then try again. + * You can probably identify the correct COM port in Windows Control Panel >Device Manager > Serial & LPT devices. +* By default, it will automatically stream the depth to both LSL and to the NSP (added to the .nev datafile as comments). You can change this behaviour by unchecking the boxes. + ### FeaturesGUI.ini ### MappingGUI.ini -### ProcessGUI.ini +### ProcedureGUI.ini ### RasterGUI.ini diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 6584600..a0652c5 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,25 +1,4 @@ -## Connectivity - -### ZeroMQ Ports - -We use ZeroMQ sockets for inter-process communication using the pub-sub pattern. The following table lists the sockets' -ports, topics, and notes about the accompanying messages. - -| Publisher | Port | Topic | Message | Subscribers | -|----------------------|-------|--------------------|------------------------------------------------------|----------------------| -| ProcessGUI | 60001 | procedure_settings | dict of settings dicts "procedure" and | FeaturesGUI | -| Depth_Process (SERF) | 60002 | snippet_status | startup; notrecording; recording; accumulating; done | ProcessGUI | -| SweepGUI | 60003 | channel_select | dict with channel, range, highpass | FeaturesGUI | -| FeaturesGUI | 60004 | features | refresh | FeaturesGUI | -| DepthGUI | 60005 | ddu | float of depth | Depth_Process (SERF) | - -### LSL - -| Origin | Stream Name | Stream Type | Content | Inlets | -|----------|-----------------|-------------|-------------------------|----------------------| -| DepthGUi | electrode_depth | depth | 1 float32 of elec depth | Depth_Process (SERF) | - -### Blackrock NSP +## Blackrock NSP If Central is running then these tools should attempt to connect to Central's shared memory, and the network settings are irrelevant. If Central is not running then you'll have to make sure the network settings are correct, and this may depend on how your PC and NSP are connected. @@ -31,6 +10,8 @@ The client (PC) port defaults to 51002 The client IP address is 192.168.137.1 on Windows, and Mac and Linux use netmasks: 255.255.255.255 on Mac, and 192.168.137.255 on Linux The NSP IP address is 192.168.137.128 and port 51001 +New digital NSPs (Gemini) have different settings and are not fully supported by OpenMER. + ## DDU numbers changing too fast! -Version 2 of FHC's DDU returns the depths in um, but the DDUGUI software is expecting numbers in mm. I have yet to figure out a reliable way to determine which drive is in use without crashing the serial port. The simple solution is to divide the value by 1000. For now, the way to do this is to uncomment a line in DDUGUI.py (search for "FHC DDU V2"). +Version 2 of FHC's DDU returns the depths in um, but the DepthGUI software is expecting numbers in mm. The DDU version should be automatically detected now. If you are still experiencing this issue then update OpenMER. diff --git a/docs/usage-instructions.md b/docs/usage-instructions.md index d4ce2d2..5e6c288 100644 --- a/docs/usage-instructions.md +++ b/docs/usage-instructions.md @@ -12,29 +12,37 @@ Additional details follow. ### Blackrock NSP * The NSP must be on. - * Can use nPlayServer instead for testing. + * Can use nPlayServer instead for testing. See the [relevant section in the developer docs](for-developers.md#nplayserver). * Central should be running first if OpenMER and Central are on the same PC. +### LSL Source + +It is also possible to use an LSL outlet as a data source. +Documentation for this feature is currently unavailable. + ## Raw data visualization SweepGUI -- RasterGUI -- WaveformGUI -No special instructions. These should work as long as a recognized data source is available. +No special instructions. These should work as long as a recognized data source is available and transmitting data. + +Check out the relevant sections in the [settings docs](settings.md). ### Sweep Plot Audio -The SweepGUI has the ability to stream one of the visualized channels out over the computer's speaker system. You can select which channel is being streamed either by clicking on one of the radio buttons near the top or by using a number on the keyboard (0 for silence, 1-N for each visualized channel). Alternatively, you may use left-arrow and right-arrow for cycling through the channels, and Space selects silence. -We map a footpedal to right-arrow so the channels can be cycled without using hands. +The SweepGUI has the ability to stream one of the visualized channels out over the computer's speaker system. You can select which channel is being streamed either by clicking on one of the radio buttons near the top or by using a number on the keyboard (0 for silence, 1-N for each visualized channel). Additionally, you may use left-arrow and right-arrow for cycling through the channels, and Spacebar selects silence. -## DDU +We map a USB footpedal to right-arrow so the channels can be cycled without using our hands. + +## Depth + +It is very important to modify the [DepthGUI settings](settings.md#depthguiini) to set the source of the depth information. -* Choose the COM port the depth digitizer is connected to then click Open. - * Choosing the wrong serial port may cause the application to hang. Force close then try again. - * You can probably identify the correct COM port in Windows Control Panel >Device Manager > Serial & LPT devices. -* By default, it will automatically stream the depth to both LSL and to the NSP (added to the .nev datafile as comments). You can change this behaviour by unchecking the boxes. * Enter your distance to target * If, like us, the depth readout isn't the same as your distance to target, then add an offset. - * It defaults to -60 mm if it detects the DDU version most commonly associated with the StarDrive. + * It defaults to -60 mm if it detects that the DDU is the version most commonly associated with the StarDrive. + +## Procedure ## Features From c372a7ef6593e9135f15e3879ed55763a3a3cef9 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 27 Sep 2023 16:02:08 -0400 Subject: [PATCH 47/65] Add script to monitor ZeroMQ --- scripts/MonitorZeroMQ.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 scripts/MonitorZeroMQ.py diff --git a/scripts/MonitorZeroMQ.py b/scripts/MonitorZeroMQ.py new file mode 100644 index 0000000..4b55bdf --- /dev/null +++ b/scripts/MonitorZeroMQ.py @@ -0,0 +1,30 @@ +import zmq + + +topic_port = { + "procedure_settings": 60001, + "snippet_status": 60002, + "channel_select": 60003, + "features": 60004, + "ddu": 60005, +} + +context = zmq.Context() +sub_socks = {} + +for topic, port in topic_port.items(): + sub_socks[topic] = context.socket(zmq.SUB) + sub_socks[topic].connect(f"tcp://localhost:{port}") + sub_socks[topic].setsockopt_string(zmq.SUBSCRIBE, topic) + + +try: + while True: + for topic, sock in sub_socks.items(): + try: + recv_msg = sock.recv_string(flags=zmq.NOBLOCK)[len(topic) + 1:] + print(topic, recv_msg) + except zmq.ZMQError: + pass +except KeyboardInterrupt: + pass From 1a779d4e85fd4b1ea630e58f99f64d89aa5586be Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 27 Sep 2023 16:02:37 -0400 Subject: [PATCH 48/65] Rename ProcessGUI to ProcedureGUI --- open_mer/dbsgui/{process.py => procedure.py} | 24 +------------------ .../{ProcessGUI.ini => ProcedureGUI.ini} | 0 .../{ProcessGUI.py => ProcedureGUI.py} | 6 ++--- open_mer/settings/defaults.py | 2 +- setup.cfg | 2 +- 5 files changed, 6 insertions(+), 28 deletions(-) rename open_mer/dbsgui/{process.py => procedure.py} (91%) rename open_mer/resources/settings/{ProcessGUI.ini => ProcedureGUI.ini} (100%) rename open_mer/scripts/{ProcessGUI.py => ProcedureGUI.py} (81%) diff --git a/open_mer/dbsgui/process.py b/open_mer/dbsgui/procedure.py similarity index 91% rename from open_mer/dbsgui/process.py rename to open_mer/dbsgui/procedure.py index 5f587fa..d1d363a 100644 --- a/open_mer/dbsgui/process.py +++ b/open_mer/dbsgui/procedure.py @@ -10,7 +10,7 @@ import open_mer.data_source -class ProcessGUI(QtWidgets.QMainWindow): +class ProcedureGUI(QtWidgets.QMainWindow): def __init__(self, ini_file: str = None, **kwargs): super().__init__(**kwargs) @@ -33,7 +33,6 @@ def __init__(self, ini_file: str = None, **kwargs): def closeEvent(self, *args, **kwargs): self.toggle_recording(False) - self._save_settings() def _restore_from_settings(self, ini_file=None): # Infer path to ini @@ -63,27 +62,6 @@ def _restore_from_settings(self, ini_file=None): self._recording_path = settings.value("basepath", defaults.BASEPATH, type=str) settings.endGroup() - def _save_settings(self): - if self._settings_path.parents[0] == "settings" and self._settings_path.parents[1] == "resources": - # If this was loaded with the shipped settings, then write a new one in ~/.open_mer - home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) - self._settings_path = home_dir / '.open_mer' / self._settings_path.name - - settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) - - # Save MainWindow geometry. - settings.beginGroup("MainWindow") - settings.setValue("fullScreen", self.isFullScreen()) - settings.setValue("maximized", self.isMaximized()) - if not self.isFullScreen() and not self.isMaximized(): - settings.setValue("size", self.size()) - settings.setValue("pos", self.pos()) - settings.endGroup() - - # TODO: Save more settings. - - settings.sync() - def _setup_ui(self): main_widget = QtWidgets.QWidget() main_widget.setLayout(QtWidgets.QVBoxLayout()) diff --git a/open_mer/resources/settings/ProcessGUI.ini b/open_mer/resources/settings/ProcedureGUI.ini similarity index 100% rename from open_mer/resources/settings/ProcessGUI.ini rename to open_mer/resources/settings/ProcedureGUI.ini diff --git a/open_mer/scripts/ProcessGUI.py b/open_mer/scripts/ProcedureGUI.py similarity index 81% rename from open_mer/scripts/ProcessGUI.py rename to open_mer/scripts/ProcedureGUI.py index ef6b5c8..1be145c 100644 --- a/open_mer/scripts/ProcessGUI.py +++ b/open_mer/scripts/ProcedureGUI.py @@ -1,12 +1,12 @@ import sys import argparse from qtpy import QtCore, QtWidgets -from open_mer.dbsgui.process import ProcessGUI +from open_mer.dbsgui.procedure import ProcedureGUI def main(**kwargs): app = QtWidgets.QApplication(sys.argv) - window = ProcessGUI(**kwargs) + window = ProcedureGUI(**kwargs) window.show() timer = QtCore.QTimer() timer.timeout.connect(window.update) @@ -17,7 +17,7 @@ def main(**kwargs): if __name__ == '__main__': - parser = argparse.ArgumentParser(prog="ProcessGUI", + parser = argparse.ArgumentParser(prog="ProcedureGUI", description="Manage subprocesses to store segments and compute features.") parser.add_argument('-i', '--ini_file', nargs='?', help="Path to ini settings file.") args = parser.parse_args() diff --git a/open_mer/settings/defaults.py b/open_mer/settings/defaults.py index f44bbbf..3210f45 100644 --- a/open_mer/settings/defaults.py +++ b/open_mer/settings/defaults.py @@ -32,7 +32,7 @@ WINDOWDIMS_DICT = { 'SweepGUI': WINDOWDIMS_SWEEP, 'RasterGUI': WINDOWDIMS_RASTER, 'WaveformGUI': WINDOWDIMS_WAVEFORMS, 'DepthGUI': WINDOWDIMS_DEPTH, - 'ProcessGUI': WINDOWDIMS_SUBPROC, + 'ProcedureGUI': WINDOWDIMS_SUBPROC, 'FeaturesGUI': WINDOWDIMS_FEATURES, 'MappingGUI': WINDOWDIMS_MAPPING } diff --git a/setup.cfg b/setup.cfg index 1a4b094..191294c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,7 +53,7 @@ gui_scripts = dbs-sweep = open_mer.scripts.SweepGUI:main dbs-waveform = open_mer.scripts.WaveformGUI:main dbs-raster = open_mer.scripts.RasterGUI:main - dbs-process = open_mer.scripts.ProcessGUI:main + dbs-procedure = open_mer.scripts.ProcedureGUI:main dbs-features = open_mer.scripts.FeaturesGUI:main dbs-mapping = open_mer.scripts.MappingGUI:main dbs-comments = open_mer.scripts.CommentsGUI:main From de5983a5adfefcc7eac96976612af643d93bbc04 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 28 Sep 2023 01:07:16 -0400 Subject: [PATCH 49/65] Major refactor: * pull out IPC into own config * ini_window base for common ini handling * start to bring data logic out of widgets --- docs/for-developers.md | 4 +- docs/settings.md | 8 +- open_mer/data_source/cerebus.py | 59 ++- open_mer/dbsgui/mapping.py | 32 +- open_mer/dbsgui/raster.py | 163 +++--- open_mer/dbsgui/sweep.py | 530 ++++++++++--------- open_mer/dbsgui/waveform.py | 184 +++---- open_mer/dbsgui/widgets/custom.py | 267 ++++------ open_mer/dbsgui/widgets/ini_window.py | 125 +++++ open_mer/resources/settings/IPC.ini | 11 + open_mer/resources/settings/ProcedureGUI.ini | 2 +- open_mer/resources/settings/RasterGUI.ini | 2 +- open_mer/resources/settings/SweepGUI.ini | 4 +- open_mer/resources/settings/WaveformGUI.ini | 6 +- 14 files changed, 741 insertions(+), 656 deletions(-) create mode 100644 open_mer/dbsgui/widgets/ini_window.py create mode 100644 open_mer/resources/settings/IPC.ini diff --git a/docs/for-developers.md b/docs/for-developers.md index d648d8c..863a0fb 100644 --- a/docs/for-developers.md +++ b/docs/for-developers.md @@ -43,8 +43,8 @@ The applications all run independently of each other, but most of them work bett | Publisher | Port | Topic | Message | Subscribers | |----------------------|-------|--------------------|--------------------------------------------------------|----------------------| -| ProcedureGUI | 60001 | procedure_settings | json of settings-dicts "procedure" and ?? | FeaturesGUI | -| Depth_Process (SERF) | 60002 | snippet_status | (startup, notrecording, recording, accumulating, done) | ProcedureGUI | +| ProcedureGUI | 60001 | procedure_settings | json of settings-dicts "procedure" and ?? | FeaturesGUI | +| Depth_Process (SERF) | 60002 | snippet_status | (startup, notrecording, recording, accumulating, done) | ProcedureGUI | | SweepGUI | 60003 | channel_select | json with channel, range, highpass | FeaturesGUI | | FeaturesGUI | 60004 | features | refresh | FeaturesGUI | | DepthGUI | 60005 | ddu | float of depth | Depth_Process (SERF) | diff --git a/docs/settings.md b/docs/settings.md index 23433ba..f38a6d5 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -21,21 +21,21 @@ Settings in the home directory will take precedence over the settings in the ope If the OpenMER GUI ini files have `class=CerebusDataSource` in their `[data-source]` section, then the CbSdkConnection.ini will be used to determine how to connect to the Cerebus (Blackrock Neuroport) data source. -By default, every line is commented out and the cerebus.cbpy default settings are used. The default settings will -first attempt to use the shared memory created by Central, then attempt to connect directly to a legacy NSP located at -192.168.137.128 using port 51001. +By default, every line is commented out and the cerebus.cbpy default settings are used. The default settings will first attempt to use the shared memory created by Central, then attempt to connect directly to a legacy NSP located at 192.168.137.128 using port 51001. -If neither is true (not on the same PC as Central, or not directly connected to legacy NSP), then modify the settings. +If neither is true (not on the same PC as Central, nor directly connected to legacy NSP), then modify the settings. * `client-addr=192.168.137.1` * Set this to the IP address of the PC running OpenMER. * Use `192.168.137.1` for the Blackrock Host PC. + * Use `127.0.0.1` for local nPlayServer (without Central) and not in bcast mode. * `client-port=51002` * Set this to the port used to transmit control packets to the NSP. It is unlikely this will ever change from 51002. * `inst-addr=192.168.137.128` * Set this to the IP address of the NSP (or the PC running nPlayServer in bcast mode). * Use `192.168.137.128` for legacy NSP * Use `192.168.137.200` for digital Neuroport system (Gemini Hub) + * Use `127.0.0.1` for local nPlayServer (without Central) and not in bcast mode. * `inst-port=51001` * Use `51001` for legacy NSP and `51002` for digital Neuroport system. * `receive-buffer-size=8388608` diff --git a/open_mer/data_source/cerebus.py b/open_mer/data_source/cerebus.py index 7ad6ea0..fa14c63 100644 --- a/open_mer/data_source/cerebus.py +++ b/open_mer/data_source/cerebus.py @@ -13,42 +13,61 @@ class CerebusDataSource(IDataSource): - def __init__(self, scoped_settings: QtCore.QSettings, **kwargs): + def __init__(self, settings_path: Path, **kwargs): super().__init__(**kwargs) # Sets on_connect_cb self._cbsdk_conn = CbSdkConnection() - cbsdk_settings_path = Path(scoped_settings.fileName()).parents[0] / "CbSdkConnection.ini" + conn_params = self._cbsdk_conn.con_params.copy() + + # Get connection-level settings from common CbSdkConnection.ini + cbsdk_settings_path = settings_path.parents[0] / "CbSdkConnection.ini" conn_settings = QtCore.QSettings(str(cbsdk_settings_path), QtCore.QSettings.IniFormat) conn_settings.beginGroup("conn-params") - conn_params = self._cbsdk_conn.con_params.copy() - for key, orig_value in conn_params.items(): - new_value = conn_settings.value(key, orig_value) - if key in ["inst-port", "client-port", "receive-buffer-size"]: - new_value = int(new_value) - conn_params[key] = new_value + for k, t in { + "client-addr": str, + "client-port": int, + "inst-addr": str, + "inst-port": int, + "receive-buffer-size": int + }.items(): + conn_params[k] = conn_settings.value(k, defaultValue=conn_params[k], type=t) conn_settings.endGroup() - self._cbsdk_conn.con_params = conn_params + + # Create a connection with default settings. This allows us to read config below. result = self._cbsdk_conn.connect() if result != 0: raise ConnectionError("Could not connect to CerebusDataSource: {}".format(result)) + conn_config = {} - for key in scoped_settings.allKeys(): - if key in ['basepath', 'class', 'sampling_group']: - continue - split_key = key.split('/') - if len(split_key) > 1: + scoped_settings = QtCore.QSettings(str(settings_path), QtCore.QSettings.IniFormat) + scoped_settings.beginGroup("data-source") + # instance = 0, reset = True, buffer_parameter = None, range_parameter = None, + # get_events = False, get_continuous = False, get_comments = False + for k, t in { + "get_continuous": bool, + "get_events": bool, + "get_comments": bool, + "buffer_parameter\\comment_length": int + }.items(): + if "\\" in k: + split_key = k.split("\\") if split_key[0] not in conn_config: conn_config[split_key[0]] = {} - conn_config[split_key[0]][split_key[1]] = parse_ini_try_numeric(scoped_settings, key) + if split_key[0] in scoped_settings.childGroups(): + scoped_settings.beginGroup(split_key[0]) + conn_config[split_key[0]][split_key[1]] = scoped_settings.value(split_key[1], type=t) + scoped_settings.endGroup() else: - conn_config[key] = parse_ini_try_numeric(scoped_settings, key) + if k in scoped_settings.allKeys(): + conn_config[k] = scoped_settings.value(k, type=t) + _sampling_rate = scoped_settings.value("sampling_rate", defaultValue=30_000, type=int) + scoped_settings.endGroup() - # get_events, get_comments, get_continuous, buffer_parameter: comment_length - self._cbsdk_conn.cbsdk_config = conn_config - self._group_ix = SAMPLINGGROUPS.index(scoped_settings.value("sampling_group", type=str)) + self._cbsdk_conn.cbsdk_config = conn_config # This will trigger cbsdkConnection to update trial config + self._group_ix = SAMPLINGGROUPS.index(str(_sampling_rate)) self._group_info = self._decode_group_info(self._cbsdk_conn.get_group_config(self._group_ix)) - # self.wf_config = self.cbsdk_conn.get_sys_config() # {'spklength': 48, 'spkpretrig': 10, 'sysfreq': 30000} + # self._wf_config = self._cbsdk_conn.get_sys_config() # {'spklength': 48, 'spkpretrig': 10, 'sysfreq': 30000} if self._on_connect_cb is not None: self._on_connect_cb(self) diff --git a/open_mer/dbsgui/mapping.py b/open_mer/dbsgui/mapping.py index 594a4eb..6f315fe 100644 --- a/open_mer/dbsgui/mapping.py +++ b/open_mer/dbsgui/mapping.py @@ -6,24 +6,6 @@ from ..settings.defaults import MAPPINGSTIMULI -class MappingGUI(CustomGUI): - - def __init__(self): - super(MappingGUI, self).__init__() - self.setWindowTitle('MappingGUI') - - @CustomGUI.widget_cls.getter - def widget_cls(self): - return MappingWidget - - def on_plot_closed(self): - self._plot_widget = None - self._data_source.disconnect_requested() - - def do_plot_update(self): - pass - - class MappingWidget(CustomWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -218,3 +200,17 @@ def refresh_axes(self): def clear(self): pass + + +class MappingGUI(CustomGUI): + widget_cls = MappingWidget + + def __init__(self): + self._plot_widget: MappingWidget | None = None # This will get updated in super init but it helps type hints + super(MappingGUI, self).__init__() + self.setWindowTitle('MappingGUI') + + def do_plot_update(self): + pass + + diff --git a/open_mer/dbsgui/raster.py b/open_mer/dbsgui/raster.py index 3046a00..861fc1c 100644 --- a/open_mer/dbsgui/raster.py +++ b/open_mer/dbsgui/raster.py @@ -6,121 +6,55 @@ from ..data_source import get_now_time -class RasterGUI(CustomGUI): - - def __init__(self): - super(RasterGUI, self).__init__() - self.setWindowTitle('RasterGUI') - - @CustomGUI.widget_cls.getter - def widget_cls(self): - return RasterWidget - - def parse_settings(self): - settings_paths = [self._settings_paths["base"]] - if "custom" in self._settings_paths: - settings_paths.extend(self._settings_paths["custom"]) - - if "plot" not in self._plot_config: - self._plot_config["plot"] = {} - - if "theme" not in self._plot_config: - self._plot_config["theme"] = {} - - for ini_path in settings_paths: - settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) - - settings.beginGroup("plot") - k_t = {"x_range": float, "y_range": int} - keys = settings.allKeys() - for k, t in k_t.items(): - if k in keys: - self._plot_config["plot"][k] = settings.value(k, type=t) - settings.endGroup() - - settings.beginGroup("theme") - keys = settings.allKeys() - k_t = {"frate_size": int} - for k, t in k_t.items(): - if k in keys: - self._plot_config["theme"][k] = settings.value(k, type=t) - settings.endGroup() - - super().parse_settings() - - def on_plot_closed(self): - self._plot_widget = None - # self._data_source.disconnect_requested() - - def do_plot_update(self): - ev_timestamps = self._data_source.get_event_data() - ev_chan_ids = [x[0] for x in ev_timestamps] - for chan_label in self._plot_widget.rasters: - ri = self._plot_widget.rasters[chan_label] - if ri['chan_id'] in ev_chan_ids: - data = ev_timestamps[ev_chan_ids.index(ri['chan_id'])][1]['timestamps'] - else: - data = [[], ] - self._plot_widget.update(chan_label, data) - - comments = self._data_source.get_comments() - if comments: - self._plot_widget.parse_comments(comments) - - class RasterWidget(CustomWidget): frate_changed = QtCore.Signal(str, float) - def __init__(self, *args, **kwargs): + def __init__(self, *args, theme={}, plot={}): self.DTT = None - self.plot_config = {} - super().__init__(*args, **kwargs) + self._plot_config = {} + super().__init__(*args, theme=theme, plot=plot) - def create_plots(self, plot={}, theme={}): - self.plot_config['theme'] = theme - self.plot_config['x_range'] = plot.get('x_range', 0.5) - self.plot_config['y_range'] = plot.get('y_range', 8) - self.plot_config['color_iterator'] = -1 + def _parse_config(self, theme={}, plot={}): + super()._parse_config(theme=theme) + plot = plot["plot"] + self._plot_config["x_range"] = plot.get("x_range", 0.5) + self._plot_config["y_range"] = plot.get("y_range", 8) + def create_plots(self): # Create and add GraphicsLayoutWidget glw = pg.GraphicsLayoutWidget(parent=self) # glw.useOpenGL(True) - if 'bgcolor' in self.plot_config['theme']: - glw.setBackground(parse_color_str(self.plot_config['theme']['bgcolor'])) + if 'bgcolor' in self._theme: + glw.setBackground(parse_color_str(self._theme['bgcolor'])) self.layout().addWidget(glw) - cmap = self.plot_config['theme']['colormap'] - if cmap != 'custom': - self.plot_config['theme']['pencolors'] = get_colormap(self.plot_config['theme']['colormap'], - len(self.chan_states)) - self.rasters = {} # Will contain one dictionary for each line/channel label. for chan_state in self.chan_states: self.add_series(chan_state) def add_series(self, chan_state): - my_theme = self.plot_config['theme'] + my_theme = self._theme glw = self.findChild(pg.GraphicsLayoutWidget) new_plot = glw.addPlot(row=len(self.rasters), col=0, enableMenu=False) new_plot.setMouseEnabled(x=False, y=False) # Appearance settings - self.plot_config['color_iterator'] = (self.plot_config['color_iterator'] + 1) % len(my_theme['pencolors']) - pen_color = QtGui.QColor(my_theme['pencolors'][self.plot_config['color_iterator']]) - pen = pg.mkPen(pen_color, width=my_theme.get('linewidth', 1)) + self._theme["color_iterator"] = (self._theme["color_iterator"] + 1) % len(my_theme["pencolors"]) + pen_color = QtGui.QColor(my_theme["pencolors"][self._theme["color_iterator"]]) + pen = pg.mkPen(pen_color, width=my_theme.get("linewidth", 1)) # Create PlotCurveItem for latest spikes (bottom row) and slower-updating old spikes (upper rows) pcis = [] for pci_ix in range(2): - pci = pg.PlotCurveItem(parent=new_plot, connect='pairs') + pci = pg.PlotCurveItem(parent=new_plot, connect="pairs") pci.setPen(pen) new_plot.addItem(pci) pcis.append(pci) # Create text for displaying firing rate. Placeholder text is channel label. - frate_annotation = pg.TextItem(text=chan_state['name'], + frate_annotation = pg.TextItem(text=chan_state["name"], color=(255, 255, 255)) - frate_annotation.setPos(0, self.plot_config['y_range']) + frate_annotation.setPos(0, self._plot_config["y_range"]) my_font = QtGui.QFont() - my_font.setPointSize(my_theme.get('frate_size', 24)) + my_font.setPointSize(my_theme.get("frate_size", 24)) frate_annotation.setFont(my_font) new_plot.addItem(frate_annotation) # Store information @@ -135,11 +69,11 @@ def add_series(self, chan_state): self.clear() def refresh_axes(self): - self.x_lim = int(self.plot_config['x_range'] * self.samplingRate) + self.x_lim = int(self._plot_config['x_range'] * self.samplingRate) for rs_key in self.rasters: plot = self.rasters[rs_key]['plot'] - plot.setXRange(0, self.plot_config['x_range'] * self.samplingRate) - plot.setYRange(-0.05, self.plot_config['y_range'] + 0.05) + plot.setXRange(0, self._plot_config['x_range'] * self.samplingRate) + plot.setYRange(-0.05, self._plot_config['y_range'] + 0.05) plot.hideAxis('bottom') plot.hideAxis('left') @@ -153,7 +87,7 @@ def clear(self): rs['latest_timestamps'] = np.empty(0, dtype=np.uint32) # Bottom row rs['count'] = 0 rs['start_time'] = start_time - rs['r0_tmin'] = start_time - (start_time % (self.plot_config['x_range'] * self.samplingRate)) + rs['r0_tmin'] = start_time - (start_time % (self._plot_config['x_range'] * self.samplingRate)) rs['last_spike_time'] = start_time self.modify_frate(key, 0) @@ -206,7 +140,7 @@ def update(self, line_label, data): rs['latest_timestamps'] = rs['latest_timestamps'][np.logical_not(b_move_old)] # Remove spikes from rs['old_timestamps'] that are outside the plot_range - new_tmin = new_r0_tmin - self.x_lim * (self.plot_config['y_range'] - 1) + new_tmin = new_r0_tmin - self.x_lim * (self._plot_config['y_range'] - 1) b_drop_old = rs['old_timestamps'] < new_tmin if np.any(b_drop_old): rs['old_timestamps'] = rs['old_timestamps'][np.logical_not(b_drop_old)] @@ -225,7 +159,7 @@ def update(self, line_label, data): x_vals = rs['old_timestamps'] % self.x_lim y_vals = 1.1 * np.ones_like(x_vals) # second-from-bottom starts at y=1.1 # If a spike is older than row_ix, += 1 - for row_ix in range(1, self.plot_config['y_range']): + for row_ix in range(1, self._plot_config['y_range']): row_cutoff = new_r0_tmin - (row_ix * self.x_lim) y_vals[rs['old_timestamps'] < row_cutoff] += 1 x_vals = np.repeat(x_vals, 2) @@ -244,4 +178,49 @@ def update(self, line_label, data): samples_elapsed = max(now_time, rs['last_spike_time']) - rs['start_time'] if samples_elapsed > 0: frate = rs['count'] * self.samplingRate / samples_elapsed - self.modify_frate(line_label, frate) \ No newline at end of file + self.modify_frate(line_label, frate) + + +class RasterGUI(CustomGUI): + widget_cls = RasterWidget + + def __init__(self): + self._plot_widget: RasterWidget | None = None # This will get updated in super init but it helps type hints + super(RasterGUI, self).__init__() + self.setWindowTitle('RasterGUI') + + def parse_settings(self): + super().parse_settings() + + if "plot" not in self._plot_settings: + self._plot_settings["plot"] = {} + + for ini_path in self._settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + settings.beginGroup("plot") + for k, t in {"x_range": float, "y_range": int}.items(): + if k in settings.allKeys(): + self._plot_settings["plot"][k] = settings.value(k, type=t) + settings.endGroup() + + settings.beginGroup("theme") + for k, t in {"frate_size": int}.items(): + if k in settings.allKeys(): + self._theme_settings[k] = settings.value(k, type=t) + settings.endGroup() + + def do_plot_update(self): + ev_timestamps = self._data_source.get_event_data() + ev_chan_ids = [x[0] for x in ev_timestamps] + for chan_label in self._plot_widget.rasters: + ri = self._plot_widget.rasters[chan_label] + if ri['chan_id'] in ev_chan_ids: + data = ev_timestamps[ev_chan_ids.index(ri['chan_id'])][1]['timestamps'] + else: + data = [[], ] + self._plot_widget.update(chan_label, data) + + comments = self._data_source.get_comments() + if comments: + self._plot_widget.parse_comments(comments) diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index 8176673..4a0ff2d 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -6,97 +6,60 @@ import pyqtgraph as pg import zmq -from .utilities.pyqtgraph import parse_color_str, make_qcolor, get_colormap +from .utilities.pyqtgraph import parse_color_str, make_qcolor from .widgets.custom import CustomWidget, CustomGUI from ..data_source import get_now_time -class SweepGUI(CustomGUI): - - def __init__(self): - super(SweepGUI, self).__init__() - self.setWindowTitle('SweepGUI') - - @CustomGUI.widget_cls.getter - def widget_cls(self): - return SweepWidget - - def parse_settings(self): - settings_paths = [self._settings_paths["base"]] - if "custom" in self._settings_paths: - settings_paths.extend(self._settings_paths["custom"]) - - if "plot" not in self._plot_config: - self._plot_config["plot"] = {} - - for ini_path in settings_paths: - settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) - - settings.beginGroup("plot") - k_t = {"x_range": float, "y_range": float, "downsample": int, "n_segments": int, - "spk_aud": bool, "lock_threshold": bool, "unit_scaling": float} - keys = settings.allKeys() - for k, t in k_t.items(): - if k in keys: - self._plot_config["plot"][k] = settings.value(k, type=t) - settings.endGroup() - - settings.beginGroup("theme") - keys = settings.allKeys() - k_t = {"threshcolor": str, "threshwidth": int} - for k, t in k_t.items(): - if k in keys: - self._plot_config["theme"][k] = settings.value(k, type=t) - settings.endGroup() - - super().parse_settings() - - def on_plot_closed(self): - if self._plot_widget is not None and self._plot_widget.awaiting_close: - del self._plot_widget - self._plot_widget = None - # if not self._plot_widget: - # self._data_source.disconnect_requested() - - def do_plot_update(self): - cont_data = self._data_source.get_continuous_data() - if cont_data is not None: - cont_chan_ids = [x[0] for x in cont_data] - chart_chan_ids = [x['src'] for x in self._plot_widget.chan_states] - match_chans = list(set(cont_chan_ids) & set(chart_chan_ids)) - for chan_id in match_chans: - ch_ix = cont_chan_ids.index(chan_id) - data = cont_data[ch_ix][1] - label = self._plot_widget.chan_states[ch_ix]['name'] - self._plot_widget.update(label, data, ch_state=self._plot_widget.chan_states[ch_ix]) +# TODO: Continue decoupling effort to get all data and audio processing into SweepGUI and +# only plotting in SweepWidget. Search and fix all self.parent() and some self._plot_widget. +# Fixes will likely require more signals and slots. class SweepWidget(CustomWidget): + chanselect_updated = QtCore.Signal() - def __init__(self, *args, zmq_chan_port=60003, **kwargs): + def __init__(self, *args, theme={}, plot={}): """ - *args typically contains a data_source dict with entries for 'srate', 'channel_names', 'chan_states' - **kwargs typically contains dicts for configuring the plotter, including 'filter', 'plot', and 'theme'. + *args is passed directly to super. + **kwargs is passed (via super) directly to _parse_config. Optionally includes dicts for theme and plot. """ self._monitor_group = None # QtWidgets.QButtonGroup(parent=self) - self.plot_config = {} + self._plot_config = {} self.segmented_series = {} # Will contain one array of curves for each line/channel label. - self._chanselect_context = zmq.Context() - self._chanselect_sock = self._chanselect_context.socket(zmq.PUB) - self._chanselect_sock.bind(f"tcp://*:{zmq_chan_port}") - - super().__init__(*args, **kwargs) + super().__init__(*args, theme=theme, plot=plot) self.refresh_axes() # Even though super __init__ calls this, extra refresh is intentional - self.pya_manager = pyaudio.PyAudio() - self.pya_stream = None - self.audio = {} - self.reset_audio() - self.publish_chanselect() + self.chanselect_updated.emit() + + def _parse_config(self, theme={}, plot={}, alt_loc=False): + super()._parse_config(theme=theme) + + filter = plot.get("filter", {}) + plot = plot["plot"] + # Collect PlotWidget configuration + self._plot_config["downsample"] = plot.get("downsample", 1) + self._plot_config["x_range"] = plot.get("x_range", 1.0) + self._plot_config["y_range"] = plot.get("y_range", 250) + self._plot_config["n_segments"] = plot.get("n_segments", 20) + # Default scaling of 0.25 -- Data are 16-bit integers from -8192 uV to +8192 uV. We want plot scales in uV. + self._plot_config["unit_scaling"] = plot.get("unit_scaling", 0.25) + self._plot_config["alt_loc"] = alt_loc + self._plot_config["do_hp"] = filter.get("order", 4) > 0 + if self._plot_config["do_hp"]: + self._plot_config["hp_sos"] = signal.butter(filter.get("order", 4), + 2 * filter.get("cutoff", 250) / self.samplingRate, + btype=filter.get("btype", "highpass"), + output=filter.get("output", "sos")) + self._plot_config["do_ln"] = bool(plot.get("do_ln", True)) + self._plot_config["ln_filt"] = None # TODO: comb filter coeffs + self._plot_config["spk_aud"] = bool(plot.get('spk_aud', True)) + self._plot_config["lock_threshold"] = bool(plot.get('lock_threshold', False)) def keyPressEvent(self, e): - valid_keys = [QtCore.Qt.Key_0, QtCore.Qt.Key_1, QtCore.Qt.Key_2, QtCore.Qt.Key_3, QtCore.Qt.Key_4, QtCore.Qt.Key_5, QtCore.Qt.Key_6, QtCore.Qt.Key_7, QtCore.Qt.Key_8, + valid_keys = [QtCore.Qt.Key_0, QtCore.Qt.Key_1, QtCore.Qt.Key_2, QtCore.Qt.Key_3, QtCore.Qt.Key_4, + QtCore.Qt.Key_5, QtCore.Qt.Key_6, QtCore.Qt.Key_7, QtCore.Qt.Key_8, QtCore.Qt.Key_9][:len(self.chan_states) + 1] current_button_id = self._monitor_group.checkedId() new_button_id = None @@ -114,29 +77,17 @@ def keyPressEvent(self, e): button.setChecked(True) self.on_monitor_group_clicked(button) - def closeEvent(self, evnt): - if self.pya_stream: - if self.pya_stream.is_active(): - self.pya_stream.stop_stream() - self.pya_stream.close() - self.pya_manager.terminate() - - self._chanselect_sock.setsockopt(zmq.LINGER, 0) - self._chanselect_sock.close() - self._chanselect_context.term() - - super().closeEvent(evnt) - def create_control_panel(self): # Create control panel # +/- range cntrl_layout = QtWidgets.QHBoxLayout() cntrl_layout.addWidget(QtWidgets.QLabel("+/- ")) - self.range_edit = QtWidgets.QLineEdit("{:.2f}".format(250)) # Value overridden in create_plots - self.range_edit.editingFinished.connect(self.on_range_edit_editingFinished) - self.range_edit.setMinimumHeight(23) - self.range_edit.setMaximumWidth(80) - cntrl_layout.addWidget(self.range_edit) + range_edit = QtWidgets.QLineEdit("{:.2f}".format(250)) # Value overridden in create_plots + range_edit.setObjectName("range_LineEdit") + range_edit.editingFinished.connect(self.on_range_edit_editingFinished) + range_edit.setMinimumHeight(23) + range_edit.setMaximumWidth(80) + cntrl_layout.addWidget(range_edit) # buttons for audio monitoring cntrl_layout.addStretch(1) cntrl_layout.addWidget(QtWidgets.QLabel("Monitor: ")) @@ -187,175 +138,147 @@ def create_control_panel(self): def update_monitor_channel(self, chan_state, spike_only): # Let other processes know we've changed the monitor channel self.parent()._data_source.update_monitor(chan_state, spike_only=spike_only) - self.publish_chanselect() + self.chanselect_updated.emit() def on_spk_aud_changed(self, state): current_button_id = self._monitor_group.checkedId() if current_button_id > 0: chan_state = self.chan_states[current_button_id - 1] - self.update_monitor_channel(chan_state, state == QtCore.Qt.Checked) + self.update_monitor_channel(chan_state, state == QtCore.Qt.Checked.value) def on_hp_filter_changed(self, state): - self.plot_config['do_hp'] = state == QtCore.Qt.Checked - self.publish_chanselect() + self._plot_config["do_hp"] = state == QtCore.Qt.Checked.value + for ch_label, ss_info in self.segmented_series.items(): + ss_info["hp_zi"] = None + self.chanselect_updated.emit() def on_ln_filter_changed(self, state): - self.plot_config['do_ln'] = state == QtCore.Qt.Checked + self._plot_config["do_ln"] = state == QtCore.Qt.Checked.value def on_range_edit_editingFinished(self): - self.plot_config['y_range'] = np.float64(self.range_edit.text()) + _range_edit = self.findChild(QtWidgets.QLineEdit, name="range_LineEdit") + self._plot_config["y_range"] = np.float64(_range_edit.text()) self.refresh_axes() - self.publish_chanselect() + self.chanselect_updated.emit() def on_monitor_group_clicked(self, button): - self.reset_audio() - this_label = '' - if button.text() == 'None': - self.audio['chan_label'] = 'silence' - chan_state = {'src': 0} + self.parent().reset_audio() + this_label = "" + if button.text() == "None": + self.parent().audio["chan_label"] = "silence" + chan_state = {"src": 0} else: this_label = button.text() button_idx = self.labels.index(this_label) - self.audio['chan_label'] = this_label + self.parent().audio["chan_label"] = this_label chan_state = self.chan_states[button_idx] # Reset plot titles - active_kwargs = {'color': parse_color_str(self.plot_config['theme'].get('labelcolor_active', 'yellow')), - 'size': str(self.plot_config['theme'].get('labelsize_active', 15)) + 'pt'} - inactive_kwargs = {'color': self.plot_config['theme'].get('labelcolor_inactive', None), - 'size': str(self.plot_config['theme'].get('labelsize_inactive', 11)) + 'pt'} - if inactive_kwargs['color'] is not None: - inactive_kwargs['color'] = parse_color_str(inactive_kwargs['color']) + active_kwargs = { + "color": parse_color_str(self._theme.get("labelcolor_active", "yellow")), + "size": str(self._theme.get("labelsize_active", 15)) + "pt" + } + inactive_kwargs = { + "color": self._theme.get("labelcolor_inactive", None), + "size": str(self._theme.get("labelsize_inactive", 11)) + "pt" + } + if inactive_kwargs["color"] is not None: + inactive_kwargs["color"] = parse_color_str(inactive_kwargs["color"]) for label in self.labels: - plot_item = self.segmented_series[label]['plot'] + plot_item = self.segmented_series[label]["plot"] label_kwargs = active_kwargs if label == this_label else inactive_kwargs plot_item.setTitle(title=plot_item.titleLabel.text, **label_kwargs) - spk_aud_cb = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") + spk_aud_cb: QtWidgets.QCheckBox = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") self.update_monitor_channel(chan_state, spk_aud_cb.checkState() == QtCore.Qt.Checked) - def publish_chanselect(self): - chan_labels = [_['name'] for _ in self.chan_states] - if self.audio['chan_label'] in ['silence', None]: - curr_channel = 0 - else: - curr_channel = chan_labels.index(self.audio['chan_label']) + 1 # 0 == None - self._chanselect_sock.send_string("channel_select " + json.dumps({ - "channel": curr_channel, - "label": self.audio["chan_label"] or "", - "range": self.plot_config['y_range'], - "highpass": self.plot_config['do_hp'] - })) - def on_thresh_line_moved(self, inf_line): new_thresh = None for line_label in self.segmented_series: ss_info = self.segmented_series[line_label] - if ss_info['thresh_line'] == inf_line: - new_thresh = int(inf_line.getYPos() / self.plot_config['unit_scaling']) + if ss_info["thresh_line"] == inf_line: + new_thresh = int(inf_line.getYPos() / self._plot_config["unit_scaling"]) # Let other processes know we've changed the threshold line. self.parent()._data_source.update_threshold(ss_info, new_thresh) - # TODO: shared mem? - lkthr_cb = self.findChild(QtWidgets.QCheckBox, name="lkthr_CheckBox") - if new_thresh is not None and lkthr_cb.checkState() == QtCore.Qt.Checked: + + # If we the "lock thresholds" buttton is checked then update the rest of the lines + lkthr_cb: QtWidgets.QCheckBox = self.findChild(QtWidgets.QCheckBox, name="lkthr_CheckBox") + if new_thresh is not None and lkthr_cb.checkState() == QtCore.Qt.Checked.value: for line_label in self.segmented_series: ss_info = self.segmented_series[line_label] - if ss_info['thresh_line'] != inf_line: - ss_info['thresh_line'].setPos(new_thresh * self.plot_config['unit_scaling']) + if ss_info["thresh_line"] != inf_line: + ss_info["thresh_line"].setPos(new_thresh * self._plot_config["unit_scaling"]) self.parent()._data_source.update_threshold(ss_info, new_thresh) - def create_plots(self, plot={}, filter={}, theme={}, alt_loc=False): - # Collect PlotWidget configuration - self.plot_config['theme'] = theme - self.plot_config['downsample'] = plot.get('downsample', 1) - self.plot_config['x_range'] = plot.get('x_range', 1.0) - self.plot_config['y_range'] = plot.get('y_range', 250) + def create_plots(self): # Update the range_edit text, but block its signal when doing so - prev_state = self.range_edit.blockSignals(True) - self.range_edit.setText(str(self.plot_config['y_range'])) - self.range_edit.blockSignals(prev_state) - self.plot_config['n_segments'] = plot.get('n_segments', 20) - # Default scaling of 0.25 -- Data are 16-bit integers from -8192 uV to +8192 uV. We want plot scales in uV. - self.plot_config['unit_scaling'] = plot.get('unit_scaling', 0.25) - self.plot_config['alt_loc'] = alt_loc - self.plot_config['color_iterator'] = -1 - self.plot_config['do_hp'] = filter.get('order', 4) > 0 - if self.plot_config['do_hp']: - self.plot_config['hp_sos'] = signal.butter(filter.get('order', 4), - 2 * filter.get('cutoff', 250) / self.samplingRate, - btype=filter.get('btype', 'highpass'), - output=filter.get('output', 'sos')) - hp_filt_cb = self.findChild(QtWidgets.QCheckBox, name="hpfilt_CheckBox") - - hp_filt_cb.setCheckState(QtCore.Qt.Checked if self.plot_config['do_hp'] else QtCore.Qt.Unchecked) - - ln_filt_cb = self.findChild(QtWidgets.QCheckBox, name="lnfilt_CheckBox") - self.plot_config['do_ln'] = bool(plot.get('do_ln', True)) - ln_filt_cb.setCheckState(QtCore.Qt.Checked if self.plot_config['do_ln'] else QtCore.Qt.Unchecked) - self.plot_config['ln_filt'] = None # TODO: comb filter coeffs - - spk_aud_cb = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") - spk_aud_cb.setCheckState(QtCore.Qt.Checked if bool(plot.get('spk_aud', True)) else QtCore.Qt.Unchecked) - - thrlk_cb = self.findChild(QtWidgets.QCheckBox, name="lkthr_CheckBox") - thrlk_cb.setCheckState(QtCore.Qt.Checked if bool(plot.get('lock_threshold', False)) else QtCore.Qt.Unchecked) + range_edit: QtWidgets.QLineEdit = self.findChild(QtWidgets.QLineEdit, name="range_LineEdit") + prev_state = range_edit.blockSignals(True) + range_edit.setText(str(self._plot_config["y_range"])) + range_edit.blockSignals(prev_state) + + hp_filt_cb: QtWidgets.QCheckBox = self.findChild(QtWidgets.QCheckBox, name="hpfilt_CheckBox") + + hp_filt_cb.setCheckState(QtCore.Qt.Checked if self._plot_config["do_hp"] else QtCore.Qt.Unchecked) + + ln_filt_cb: QtWidgets.QCheckBox = self.findChild(QtWidgets.QCheckBox, name="lnfilt_CheckBox") + ln_filt_cb.setCheckState(QtCore.Qt.Checked if self._plot_config["do_ln"] else QtCore.Qt.Unchecked) + + spk_aud_cb: QtWidgets.QCheckBox = self.findChild(QtWidgets.QCheckBox, name="spkaud_CheckBox") + spk_aud_cb.setCheckState(QtCore.Qt.Checked if self._plot_config["spk_aud"] else QtCore.Qt.Unchecked) + + thrlk_cb: QtWidgets.QCheckBox = self.findChild(QtWidgets.QCheckBox, name="lkthr_CheckBox") + thrlk_cb.setCheckState(QtCore.Qt.Checked if self._plot_config["lock_threshold"] else QtCore.Qt.Unchecked) # Create and add GraphicsLayoutWidget glw = pg.GraphicsLayoutWidget(parent=self) # show=False, size=None, title=None, border=None # glw.useOpenGL(True) # Actually seems slower. - if 'bgcolor' in self.plot_config['theme']: - glw.setBackground(parse_color_str(self.plot_config['theme']['bgcolor'])) + if "bgcolor" in self._theme: + glw.setBackground(parse_color_str(self._theme["bgcolor"])) self.layout().addWidget(glw) - cmap = self.plot_config['theme']['colormap'] - if cmap != 'custom': - self.plot_config['theme']['pencolors'] = get_colormap(self.plot_config['theme']['colormap'], - len(self.chan_states)) - # Add add a plot with a series of many curve segments for each line. for chan_state in self.chan_states: self.add_series(chan_state) def add_series(self, chan_state): - my_theme = self.plot_config['theme'] - # Plot for this channel glw = self.findChild(pg.GraphicsLayoutWidget) new_plot = glw.addPlot(row=len(self.segmented_series), col=0, - title=chan_state['name'], enableMenu=False) + title=chan_state["name"], enableMenu=False) new_plot.setMouseEnabled(x=False, y=False) # Prepare plot - self.plot_config['color_iterator'] = (self.plot_config['color_iterator'] + 1) % len(my_theme['pencolors']) - pen_color = make_qcolor(my_theme['pencolors'][self.plot_config['color_iterator']]) - pen = pg.mkPen(pen_color, width=my_theme.get('linewidth', 1)) + self._theme["color_iterator"] = (self._theme["color_iterator"] + 1) % len(self._theme["pencolors"]) + pen_color = make_qcolor(self._theme["pencolors"][self._theme["color_iterator"]]) + pen = pg.mkPen(pen_color, width=self._theme.get('linewidth', 1)) samples_per_segment = int( - np.ceil(self.plot_config['x_range'] * self.samplingRate / self.plot_config['n_segments'])) - for ix in range(self.plot_config['n_segments']): - if ix < (self.plot_config['n_segments'] - 1): + np.ceil(self._plot_config["x_range"] * self.samplingRate / self._plot_config["n_segments"])) + for ix in range(self._plot_config["n_segments"]): + if ix < (self._plot_config["n_segments"] - 1): seg_x = np.arange(ix * samples_per_segment, (ix + 1) * samples_per_segment, dtype=np.int16) else: # Last segment might not be full length. seg_x = np.arange(ix * samples_per_segment, - int(self.plot_config['x_range'] * self.samplingRate), dtype=np.int16) - seg_x = seg_x[::self.plot_config['downsample']] + int(self._plot_config["x_range"] * self.samplingRate), dtype=np.int16) + seg_x = seg_x[::self._plot_config["downsample"]] c = new_plot.plot(parent=new_plot, pen=pen) # PlotDataItem c.setData(x=seg_x, y=np.zeros_like(seg_x)) # Pre-fill. # Add threshold line - thresh_color = make_qcolor(my_theme.get('threshcolor', 'yellow')) - pen = pg.mkPen(color=thresh_color, width=my_theme.get('threshwidth', 1)) + thresh_color = make_qcolor(self._theme.get('threshcolor', 'yellow')) + pen = pg.mkPen(color=thresh_color, width=self._theme.get('threshwidth', 1)) thresh_line = pg.InfiniteLine(angle=0, pen=pen, movable=True, label="{value:.0f}", labelOpts={'position': 0.05}) thresh_line.sigPositionChangeFinished.connect(self.on_thresh_line_moved) new_plot.addItem(thresh_line) - self.segmented_series[chan_state['name']] = { + self.segmented_series[chan_state["name"]] = { 'chan_id': chan_state['src'], 'line_ix': len(self.segmented_series), - 'plot': new_plot, + "plot": new_plot, 'last_sample_ix': -1, - 'thresh_line': thresh_line, - 'hp_zi': signal.sosfilt_zi(self.plot_config['hp_sos']), + "thresh_line": thresh_line, + 'hp_zi': None, 'ln_zi': None } @@ -363,59 +286,29 @@ def refresh_axes(self): _t = get_now_time() if _t is None: return - last_sample_ix = int(np.mod(_t, self.plot_config["x_range"]) * self.samplingRate) - state_names = [_['name'] for _ in self.chan_states] + last_sample_ix = int(np.mod(_t, self._plot_config["x_range"]) * self.samplingRate) + state_names = [_["name"] for _ in self.chan_states] for line_label in self.segmented_series: ss_info = self.segmented_series[line_label] chan_state = self.chan_states[state_names.index(line_label)] # Fixup axes - plot = ss_info['plot'] - plot.setXRange(0, self.plot_config['x_range'] * self.samplingRate) - plot.setYRange(-self.plot_config['y_range'], self.plot_config['y_range']) + plot = ss_info["plot"] + plot.setXRange(0, self._plot_config["x_range"] * self.samplingRate) + plot.setYRange(-self._plot_config["y_range"], self._plot_config["y_range"]) plot.hideAxis('bottom') plot.hideAxis('left') # Reset data - for seg_ix in range(self.plot_config['n_segments']): + for seg_ix in range(self._plot_config["n_segments"]): pci = plot.dataItems[seg_ix] old_x, old_y = pci.getData() pci.setData(x=old_x, y=np.zeros_like(old_x)) ss_info['last_sample_ix'] = last_sample_ix - gain = chan_state['gain'] if 'gain' in chan_state else self.plot_config['unit_scaling'] - if 'spkthrlevel' in chan_state: - ss_info['thresh_line'].setValue(chan_state['spkthrlevel'] * gain) - - def reset_audio(self): - if self.pya_stream: - if self.pya_stream.is_active(): - self.pya_stream.stop_stream() - self.pya_stream.close() - frames_per_buffer = 1 << (int(0.030*self.samplingRate) - 1).bit_length() - self.audio['buffer'] = np.zeros(frames_per_buffer, dtype=np.int16) - self.audio['write_ix'] = 0 - self.audio['read_ix'] = 0 - self.audio['chan_label'] = None - self.pya_stream = self.pya_manager.open(format=pyaudio.paInt16, - channels=1, - rate=self.samplingRate, - output=True, - frames_per_buffer=frames_per_buffer, - stream_callback=self.pyaudio_callback) - - def pyaudio_callback(self, - in_data, # recorded data if input=True; else None - frame_count, # number of frames. 1024. - time_info, # dictionary - status_flags): # PaCallbackFlags - # time_info: {'input_buffer_adc_time': ??, 'current_time': ??, 'output_buffer_dac_time': ??} - # status_flags: https://people.csail.mit.edu/hubert/pyaudio/docs/#pacallbackflags - read_indices = (np.arange(frame_count) + self.audio['read_ix']) % self.audio['buffer'].shape[0] - out_data = self.audio['buffer'][read_indices].tobytes() - self.audio['read_ix'] = (self.audio['read_ix'] + frame_count) % self.audio['buffer'].shape[0] - flag = pyaudio.paContinue - return out_data, flag + gain = chan_state["gain"] if "gain" in chan_state else self._plot_config["unit_scaling"] + if "spkthrlevel" in chan_state: + ss_info["thresh_line"].setValue(chan_state["spkthrlevel"] * gain) def update(self, line_label, data, ch_state=None): """ @@ -426,24 +319,12 @@ def update(self, line_label, data, ch_state=None): """ ss_info = self.segmented_series[line_label] n_in = data.shape[0] - gain = ch_state['gain'] if ch_state is not None and 'gain' in ch_state else self.plot_config['unit_scaling'] - data = data * gain - if self.plot_config['do_hp']: - data, ss_info['hp_zi'] = signal.sosfilt(self.plot_config['hp_sos'], data, zi=ss_info['hp_zi']) - if self.plot_config['do_ln']: - pass # TODO: Line noise / comb filter - if self.pya_stream: - if 'chan_label' in self.audio and self.audio['chan_label']: - if self.audio['chan_label'] == line_label: - write_indices = (np.arange(data.shape[0]) + self.audio['write_ix']) % self.audio['buffer'].shape[0] - self.audio['buffer'][write_indices] = (np.copy(data) * (2**15 / self.plot_config['y_range'])).astype(np.int16) - self.audio['write_ix'] = (self.audio['write_ix'] + data.shape[0]) % self.audio['buffer'].shape[0] # Assume new samples are consecutively added to old samples (i.e., no lost samples) sample_indices = np.arange(n_in, dtype=np.int32) + ss_info['last_sample_ix'] # Wrap sample indices around our plotting limit - n_plot_samples = int(self.plot_config['x_range'] * self.samplingRate) + n_plot_samples = int(self._plot_config["x_range"] * self.samplingRate) sample_indices = np.int32(np.mod(sample_indices, n_plot_samples)) # If the data length is longer than one sweep then the indices will overlap. Trim to last n_plot_samples @@ -452,15 +333,15 @@ def update(self, line_label, data, ch_state=None): data = data[-n_plot_samples:] # Go through each plotting segment and replace data with new data as needed. - for pci in ss_info['plot'].dataItems: + for pci in ss_info["plot"].dataItems: old_x, old_y = pci.getData() x_lims = [old_x[0], old_x[-1]] - x_lims[1] += (self.plot_config['downsample'] - 1) + x_lims[1] += (self._plot_config["downsample"] - 1) data_bool = np.logical_and(sample_indices >= x_lims[0], sample_indices <= x_lims[-1]) if np.where(data_bool)[0].size > 0: new_x, new_y = sample_indices[data_bool], data[data_bool] - if self.plot_config['downsample'] > 1: - _dsfac = self.plot_config['downsample'] + if self._plot_config["downsample"] > 1: + _dsfac = self._plot_config["downsample"] new_x = new_x[::_dsfac] - (new_x[0] % _dsfac) + (old_x[0] % _dsfac) new_y = new_y[::_dsfac] old_bool = np.in1d(old_x, new_x, assume_unique=True) @@ -469,4 +350,157 @@ def update(self, line_label, data, ch_state=None): # old_y[np.where(old_bool)[0][-1]+1:] = 0 # Uncomment to zero out the end of the last seg. pci.setData(x=old_x, y=old_y) # Store last_sample_ix for next iteration. - self.segmented_series[line_label]['last_sample_ix'] = sample_indices[-1] \ No newline at end of file + self.segmented_series[line_label]['last_sample_ix'] = sample_indices[-1] + + +class SweepGUI(CustomGUI): + widget_cls = SweepWidget + + def __init__(self): + self._plot_widget: SweepWidget | None = None # This will get updated in super init but it helps type hints + super(SweepGUI, self).__init__() + self.setWindowTitle('SweepGUI') + + self.pya_manager = pyaudio.PyAudio() + self.pya_stream = None + self.audio = {} + self.reset_audio() + + def __del__(self): + if self.pya_stream: + if self.pya_stream.is_active(): + self.pya_stream.stop_stream() + self.pya_stream.close() + self.pya_manager.terminate() + super().__del__() + + def parse_settings(self): + # Specialization of ini parsing + super().parse_settings() + + if "plot" not in self._plot_settings: + self._plot_settings["plot"] = {} + + if "filter" not in self._plot_settings: + self._plot_settings["filter"] = {} + + for ini_path in self._settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + settings.beginGroup("plot") + for k, t in { + "x_range": float, "y_range": float, "downsample": int, "n_segments": int, + "spk_aud": bool, "lock_threshold": bool, "unit_scaling": float + }.items(): + if k in settings.allKeys(): + self._plot_settings["plot"][k] = settings.value(k, type=t) + settings.endGroup() + + settings.beginGroup("filter") + for k, t in {"order": int, "cutoff": int, "btype": str, "output": str}.items(): + if k in settings.allKeys(): + self._plot_settings["filter"][k] = settings.value(k, type=t) + settings.endGroup() + + settings.beginGroup("theme") + for k, t in {"threshcolor": str, "threshwidth": int}.items(): + if k in settings.allKeys(): + self._theme_settings[k] = settings.value(k, type=t) + settings.endGroup() + + def _setup_ipc(self): + self._chanselect_context = zmq.Context() + self._chanselect_sock = self._chanselect_context.socket(zmq.PUB) + self._chanselect_sock.bind(f"tcp://*:{self._ipc_settings['channel_select']}") + + def _cleanup_ipc(self): + self._chanselect_sock.setsockopt(zmq.LINGER, 0) + self._chanselect_sock.close() + self._chanselect_context.term() + + def try_reset_widget(self): + super().try_reset_widget() + self._plot_widget.chanselect_updated.connect(self.publish_chanselect) + + def publish_chanselect(self): + # TODO: Move chan_states and audio from _plot_widget into self + chan_labels = [_["name"] for _ in self._plot_widget.chan_states] + if self.audio["chan_label"] in ["silence", None]: + curr_channel = 0 + else: + curr_channel = chan_labels.index(self.audio["chan_label"]) + 1 # 0 == None + topic_msg = "channel_select " + json.dumps({ + "channel": curr_channel, + "label": self.audio["chan_label"] or "", + "range": self._plot_widget.plot_config["y_range"], + "highpass": self._plot_widget.plot_config["do_hp"] + }) + self._chanselect_sock.send_string(topic_msg) + + def reset_audio(self): + if self.pya_stream: + if self.pya_stream.is_active(): + self.pya_stream.stop_stream() + self.pya_stream.close() + frames_per_buffer = 1 << (int(0.030 * self._plot_widget.samplingRate) - 1).bit_length() + self.audio["buffer"] = np.zeros(frames_per_buffer, dtype=np.int16) + self.audio["write_ix"] = 0 + self.audio["read_ix"] = 0 + self.audio["chan_label"] = None + self.pya_stream = self.pya_manager.open(format=pyaudio.paInt16, + channels=1, + rate=self._plot_widget.samplingRate, + output=True, + frames_per_buffer=frames_per_buffer, + stream_callback=self.pyaudio_callback) + + def pyaudio_callback(self, + in_data, # recorded data if input=True; else None + frame_count, # number of frames. 1024. + time_info, # dictionary + status_flags): # PaCallbackFlags + # time_info: {'input_buffer_adc_time': ??, 'current_time': ??, 'output_buffer_dac_time': ??} + # status_flags: https://people.csail.mit.edu/hubert/pyaudio/docs/#pacallbackflags + read_indices = (np.arange(frame_count) + self.audio["read_ix"]) % self.audio["buffer"].shape[0] + out_data = self.audio["buffer"][read_indices].tobytes() + self.audio["read_ix"] = (self.audio["read_ix"] + frame_count) % self.audio["buffer"].shape[0] + flag = pyaudio.paContinue + return out_data, flag + + def do_plot_update(self): + cont_data = self._data_source.get_continuous_data() + if cont_data is not None: + src_chan_ids = [x[0] for x in cont_data] + chart_chan_ids = [x["src"] for x in self._plot_widget.chan_states] + match_chans = list(set(src_chan_ids) & set(chart_chan_ids)) + for chan_id in match_chans: + src_ix = src_chan_ids.index(chan_id) + chart_ix = chart_chan_ids.index(chan_id) + chan_state = self._plot_widget.chan_states[chart_ix] + data = cont_data[src_ix][1] + + proc_data = self._postproc_data(chan_state["name"], data, ch_state=chan_state) + self._sonify_data(chan_state["name"], proc_data) + self._plot_widget.update(chan_state["name"], proc_data, ch_state=chan_state) + + def _postproc_data(self, label, data, ch_state=None): + # TODO: Store channel state and filter state in this class, not the plot widget + ss_info = self._plot_widget.segmented_series[label] + gain = ch_state["gain"] if ch_state is not None and "gain" in ch_state else self._plot_widget.plot_config["unit_scaling"] + data = data * gain + if self._plot_widget.plot_config["do_hp"]: + if ss_info["hp_zi"] is None: + ss_info["hp_zi"] = signal.sosfilt_zi(self._plot_widget.plot_config["hp_sos"]) + data, ss_info["hp_zi"] = signal.sosfilt(self._plot_widget.plot_config["hp_sos"], data, zi=ss_info["hp_zi"]) + if self._plot_widget.plot_config["do_ln"]: + pass # TODO: Line noise / comb filter + return data + + def _sonify_data(self, label, data): + if self.pya_stream: + if "chan_label" in self.audio and self.audio["chan_label"]: + if self.audio["chan_label"] == label: + write_indices = (np.arange(data.shape[0]) + self.audio["write_ix"]) % self.audio["buffer"].shape[0] + self.audio["buffer"][write_indices] = ( + np.copy(data) * (2 ** 15 / self._plot_widget.plot_config["y_range"])).astype(np.int16) + self.audio["write_ix"] = (self.audio["write_ix"] + data.shape[0]) % self.audio["buffer"].shape[0] diff --git a/open_mer/dbsgui/waveform.py b/open_mer/dbsgui/waveform.py index b920a9a..a3ff43d 100644 --- a/open_mer/dbsgui/waveform.py +++ b/open_mer/dbsgui/waveform.py @@ -1,95 +1,47 @@ import numpy as np from qtpy import QtCore, QtWidgets import pyqtgraph as pg -from .utilities.pyqtgraph import parse_color_str, get_colormap -from .widgets.custom import CustomGUI, CustomWidget - - -class WaveformGUI(CustomGUI): - - def __init__(self): - super().__init__() - self.setWindowTitle('WaveformGUI') - - @CustomGUI.widget_cls.getter - def widget_cls(self): - return WaveformWidget - - def parse_settings(self): - settings_paths = [self._settings_paths["base"]] - if "custom" in self._settings_paths: - settings_paths.extend(self._settings_paths["custom"]) - - if "plot" not in self._plot_config: - self._plot_config["plot"] = {} - - if "theme" not in self._plot_config: - self._plot_config["theme"] = {} - - for ini_path in settings_paths: - settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) - - settings.beginGroup("plot") - k_t = {"x_start": int, "x_stop": int, "y_range": int, - "n_waveforms": int, "unit_scaling": float} - keys = settings.allKeys() - for k, t in k_t.items(): - if k in keys: - self._plot_config["plot"][k] = settings.value(k, type=t) - settings.endGroup() - - settings.beginGroup("theme") - keys = settings.allKeys() - k_t = {"frate_size": int} - for k, t in k_t.items(): - if k in keys: - self._plot_config["theme"][k] = settings.value(k, type=t) - settings.endGroup() - - super().parse_settings() - - def on_plot_closed(self): - del self._plot_widget - self._plot_widget = None - # self._data_source.disconnect_requested() - - def do_plot_update(self): - for label in self._plot_widget.wf_info: - this_info = self._plot_widget.wf_info[label] - temp_wfs, unit_ids = self._data_source.get_waveforms(this_info) - self._plot_widget.update(label, [temp_wfs, unit_ids]) - comments = self._data_source.get_comments() - if comments: - self._plot_widget.parse_comments(comments) +from .utilities.pyqtgraph import parse_color_str +from .widgets.custom import CustomGUI, CustomWidget class WaveformWidget(CustomWidget): - def __init__(self, *args, **kwargs): + def __init__(self, *args, theme={}, plot={}): self.DTT = None self.plot_config = {} - super().__init__(*args, **kwargs) + super().__init__(*args, theme=theme, plot=plot) + + def _parse_config(self, theme={}, plot={}): + super()._parse_config(theme=theme) + plot = plot["plot"] + self.plot_config["x_range"] = (plot["x_start"], plot["x_stop"]) + self.plot_config["y_range"] = plot["y_range"] + self.plot_config["n_waveforms"] = plot["n_waveforms"] + self.plot_config["unit_scaling"] = plot["unit_scaling"] def create_control_panel(self): # Create control panel cntrl_layout = QtWidgets.QHBoxLayout() # +/- amplitude range cntrl_layout.addWidget(QtWidgets.QLabel("+/- ")) - self.range_edit = QtWidgets.QLineEdit("{:.2f}".format(250)) # Value overridden in create_plots - self.range_edit.setMaximumWidth(80) - self.range_edit.setMinimumHeight(23) - self.range_edit.editingFinished.connect(self.on_range_edit_editingFinished) - cntrl_layout.addWidget(self.range_edit) + range_edit = QtWidgets.QLineEdit("{:.2f}".format(250)) # Value overridden in create_plots + range_edit.setObjectName("range_LineEdit") + range_edit.setMinimumHeight(23) + range_edit.setMaximumWidth(80) + range_edit.editingFinished.connect(self.on_range_edit_editingFinished) + cntrl_layout.addWidget(range_edit) cntrl_layout.addStretch() # N Spikes cntrl_layout.addWidget(QtWidgets.QLabel("N Spikes ")) - self.n_spikes_edit = QtWidgets.QLineEdit("{}".format(100)) # Value overridden in create_plots - self.n_spikes_edit.setMaximumWidth(80) - self.n_spikes_edit.setMinimumHeight(23) - self.n_spikes_edit.editingFinished.connect(self.on_n_spikes_edit_editingFinished) - cntrl_layout.addWidget(self.n_spikes_edit) + n_spikes_edit = QtWidgets.QLineEdit("{}".format(100)) # Value overridden in create_plots + n_spikes_edit.setObjectName("n_spikes_LineEdit") + n_spikes_edit.setMaximumWidth(80) + n_spikes_edit.setMinimumHeight(23) + n_spikes_edit.editingFinished.connect(self.on_n_spikes_edit_editingFinished) + cntrl_layout.addWidget(n_spikes_edit) cntrl_layout.addStretch() # Clear button @@ -100,33 +52,23 @@ def create_control_panel(self): # Finish self.layout().addLayout(cntrl_layout) - def create_plots(self, plot={}, theme={}): - # Collect PlotWidget configuration - self.plot_config["theme"] = theme - self.plot_config["x_range"] = (plot["x_start"], plot["x_stop"]) - self.plot_config["y_range"] = plot["y_range"] - self.plot_config["n_waveforms"] = plot["n_waveforms"] - self.plot_config["unit_scaling"] = plot["unit_scaling"] - + def create_plots(self): # Update widget values without triggering signals - prev_state = self.range_edit.blockSignals(True) - self.range_edit.setText(str(self.plot_config['y_range'])) - self.range_edit.blockSignals(prev_state) - prev_state = self.n_spikes_edit.blockSignals(True) - self.n_spikes_edit.setText(str(self.plot_config['n_waveforms'])) - self.n_spikes_edit.blockSignals(prev_state) + range_edit: QtWidgets.QLineEdit = self.findChild(QtWidgets.QLineEdit, name="range_LineEdit") + prev_state = range_edit.blockSignals(True) + range_edit.setText(str(self.plot_config['y_range'])) + range_edit.blockSignals(prev_state) + + n_spikes_edit: QtWidgets.QLineEdit = self.findChild(QtWidgets.QLineEdit, name="n_spikes_LineEdit") + prev_state = n_spikes_edit.blockSignals(True) + n_spikes_edit.setText(str(self.plot_config['n_waveforms'])) + n_spikes_edit.blockSignals(prev_state) # Create and add GraphicsLayoutWidget glw = pg.GraphicsLayoutWidget(parent=self) # self.glw.useOpenGL(True) - if 'bgcolor' in self.plot_config['theme']: - glw.setBackground(parse_color_str(self.plot_config['theme']['bgcolor'])) - - cmap = self.plot_config['theme']['colormap'] - if cmap != 'custom': - cmap_colors = get_colormap(self.plot_config['theme']['colormap'], - plot.get('n_colors', 6)) - self.plot_config['theme']['pencolors'] = np.vstack((255 * np.ones_like(cmap_colors[0]), cmap_colors)) + if "bgcolor" in self._theme: + glw.setBackground(parse_color_str(self._theme["bgcolor"])) self.layout().addWidget(glw) self.wf_info = {} # Will contain one dictionary for each line/channel label. @@ -157,11 +99,13 @@ def clear(self): self.wf_info[line_label]['plot'].clear() def on_range_edit_editingFinished(self): - self.plot_config['y_range'] = float(self.range_edit.text()) + range_edit: QtWidgets.QLineEdit = self.findChild(QtWidgets.QLineEdit, name="range_LineEdit") + self.plot_config['y_range'] = float(range_edit.text()) self.refresh_axes() def on_n_spikes_edit_editingFinished(self): - self.plot_config['n_waveforms'] = int(self.n_spikes_edit.text()) + n_spikes_edit: QtWidgets.QLineEdit = self.findChild(QtWidgets.QLineEdit, name="n_spikes_LineEdit") + self.plot_config['n_waveforms'] = int(n_spikes_edit.text()) self.refresh_axes() def parse_comments(self, comments): @@ -188,7 +132,7 @@ def update(self, line_label, data): for ix in range(wfs.shape[0]): if np.sum(np.nonzero(wfs[ix])) > 0: c = pg.PlotCurveItem() - pen_color = self.plot_config['theme']['pencolors'][unit_ids[ix]] + pen_color = self._theme['pencolors'][unit_ids[ix]] c.setPen(pen_color) self.wf_info[line_label]['plot'].addItem(c) c.setData(x=x, y=self.plot_config['unit_scaling']*wfs[ix]) @@ -196,3 +140,49 @@ def update(self, line_label, data): if len(data_items) > self.plot_config['n_waveforms']: for di in data_items[:-self.plot_config['n_waveforms']]: self.wf_info[line_label]['plot'].removeItem(di) + + +class WaveformGUI(CustomGUI): + widget_cls = WaveformWidget + + def __init__(self): + self._plot_widget: WaveformWidget | None = None # This will get updated in super init but it helps type hints + super().__init__() + self.setWindowTitle("WaveformGUI") + + def parse_settings(self): + super().parse_settings() + + if "plot" not in self._plot_settings: + self._plot_settings["plot"] = {} + + for ini_path in self._settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + settings.beginGroup("plot") + for k, t in { + "x_start": int, "x_stop": int, "y_range": int, + "n_waveforms": int, "unit_scaling": float + }.items(): + if k in settings.allKeys(): + self._plot_settings["plot"][k] = settings.value(k, type=t) + settings.endGroup() + + settings.beginGroup("theme") + for k, t in {"frate_size": int}.items(): + if k in settings.allKeys(): + self._theme_settings[k] = settings.value(k, type=t) + settings.endGroup() + + super().parse_settings() + + def do_plot_update(self): + for label in self._plot_widget.wf_info: + this_info = self._plot_widget.wf_info[label] + temp_wfs, unit_ids = self._data_source.get_waveforms(this_info) + self._plot_widget.update(label, [temp_wfs, unit_ids]) + + comments = self._data_source.get_comments() + if comments: + self._plot_widget.parse_comments(comments) + diff --git a/open_mer/dbsgui/widgets/custom.py b/open_mer/dbsgui/widgets/custom.py index 8afe0e5..279be5b 100644 --- a/open_mer/dbsgui/widgets/custom.py +++ b/open_mer/dbsgui/widgets/custom.py @@ -1,168 +1,8 @@ -from pathlib import Path -import importlib.resources as pkg_resources -from qtpy import QtWidgets, QtCore, QtGui +from qtpy import QtWidgets, QtCore import open_mer.data_source - - -class CustomGUI(QtWidgets.QMainWindow): - """ - This application is for monitoring continuous activity from a MER data source. - """ - - def __init__(self): - super().__init__() - - # Infer path to ini - root_pkg = __package__.split(".")[0] - ini_name = type(self).__name__ + '.ini' - - self._settings_paths: dict[str, Path] = {} - with pkg_resources.path(f"{root_pkg}.resources", "settings") as base_default: - ini_default = base_default / ini_name - self._settings_paths["base"] = ini_default - - base_custom = Path.home() / f".{root_pkg}" - ini_custom = base_custom / ini_name - if ini_custom.exists(): - self._settings_paths["custom"] = ini_custom - - self._plot_widget = None - self._data_source = None - self._plot_config = {} - self.parse_settings() - self.try_reset_widget() - self.show() - - def __del__(self): - self._data_source.disconnect_requested() - - def parse_settings(self): - """ - Parse ini files and populate ._plot_config. - Note that some settings (MainWindow) will be applied immediately and won't be stored in _plot_config. - - This method should usually be followed by .try_reset_widget() - """ - # Collect names of ini files in reverse importance. - settings_paths = [self._settings_paths["base"].parent / "Style.ini", self._settings_paths["base"]] - if "custom" in self._settings_paths: - settings_paths += [self._settings_paths["custom"].parent / "Style.ini", self._settings_paths["custom"]] - - # theme - if "theme" not in self._plot_config: - self._plot_config["theme"] = {"colormap": None} - - for ini_path in settings_paths: - - settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) - - # Apply MainWindow settings immediately. - settings.beginGroup("MainWindow") - keys: list = settings.allKeys() - if "pos" in keys: - self.move(settings.value("pos", type=QtCore.QPoint)) - if "size" in keys: - size_xy: QtCore.QPoint = settings.value("size", type=QtCore.QPoint) - self.resize(size_xy) - self.setMaximumWidth(size_xy.width()) - if "fullScreen" in keys and settings.value("fullScreen", type=bool): - self.showFullScreen() - elif "maximized" in keys and settings.value("maximized", type=bool): - self.showMaximized() - if "frameless" in keys and settings.value("frameless", type=bool): - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - settings.endGroup() - - # Immediately initiate connection with the data source. Connection outcome will be handled in the callback. - settings.beginGroup("data-source") - if "class" in settings.allKeys() and self._data_source is None: - # Infer data source from ini file, setup data source - src_cls = getattr(open_mer.data_source, settings.value("class", type=str)) - # Get the _data_source. Note this might trigger on_source_connected before child - # finishes parsing settings. - _data_source = src_cls(scoped_settings=settings, on_connect_cb=self.on_source_connected) - settings.endGroup() - - settings.beginGroup("theme") - k_t = { - "labelcolor_active": str, "labelsize_active": int, - "labelcolor_inactive": str, "labelsize_inactive": int, - "linewidth": int - } - keys = settings.allKeys() - for k, t in k_t.items(): - if k in keys: - self._plot_config["theme"][k] = settings.value(k, type=t) - - # theme > pencolors - if "colormap" in settings.allKeys(): - self._plot_config["theme"]["colormap"] = settings.value("colormap", "custom", type=str) - self._plot_config["theme"].pop("pencolors", None) - - if self._plot_config["theme"]["colormap"] == "custom": - settings.beginGroup("pencolors") - chan_ids = [int(_) for _ in settings.childGroups()] - if "pencolors" not in self._plot_config["theme"]: - self._plot_config["theme"]["pencolors"] = [None] * (max(chan_ids) + 1) - for c_id in chan_ids: - settings.beginGroup(str(c_id)) - if "name" in settings.allKeys(): - name = settings.value("name", type=str) - self._plot_config["theme"]["pencolors"][c_id] = QtGui.QColor(name) - else: - color_hex = settings.value("value", defaultValue="#ffffff", type=str) - self._plot_config["theme"]["pencolors"][c_id] = QtGui.QColor(color_hex) - settings.endGroup() - settings.endGroup() # pencolors - settings.endGroup() # theme - - @QtCore.Slot(QtCore.QObject) - def on_source_connected(self, data_source): - self.data_source = data_source # Triggers setter --> self.try_reset_widget() - - @property - def widget_cls(self): - return NotImplemented # Child class must override this attribute - - def update(self): - super().update() - if self.data_source.is_connected and self._plot_widget: - self.do_plot_update() - - def do_plot_update(self): - # abc.abstractmethod not possible because ABC does not work with Qt-derived classes, so raise error instead. - raise NotImplementedError("This method must be overridden by sub-class.") - - def on_plot_closed(self): - raise NotImplementedError("This method must be overridden by sub-class.") - - def try_reset_widget(self): - if self._plot_widget is not None: - self._plot_widget.close() - if self.plot_config is not None and self.data_source is not None: - src_dict = self.data_source.data_stats - self._plot_widget = self.widget_cls(src_dict, **self.plot_config) - self._plot_widget.was_closed.connect(self.on_plot_closed) - self.setCentralWidget(self._plot_widget) - - @property - def data_source(self): - return self._data_source - - @data_source.setter - def data_source(self, value): - self._data_source = value - self.try_reset_widget() - - @property - def plot_config(self): - return self._plot_config - - @plot_config.setter - def plot_config(self, value): - self._plot_config = value - self.try_reset_widget() +from .ini_window import IniWindow +from ..utilities.pyqtgraph import get_colormap class CustomWidget(QtWidgets.QWidget): @@ -177,9 +17,12 @@ def __init__(self, source_dict, **kwargs): # Init member variables self.awaiting_close = False - self.labels = source_dict['channel_names'] - self.chan_states = source_dict['chan_states'] - self.samplingRate = source_dict['srate'] + self.labels = source_dict["channel_names"] + self.chan_states = source_dict["chan_states"] + self.samplingRate = source_dict["srate"] + + self._theme = {} + self._parse_config(**kwargs) # Create UI elements plot_layout = QtWidgets.QVBoxLayout() @@ -187,9 +30,16 @@ def __init__(self, source_dict, **kwargs): plot_layout.setSpacing(0) self.setLayout(plot_layout) self.create_control_panel() - self.create_plots(**kwargs) + self.create_plots() self.refresh_axes() + def _parse_config(self, **kwargs): + self._theme = kwargs["theme"] + cmap = self._theme["colormap"] + if cmap != "custom": + self._theme["pencolors"] = get_colormap(self._theme["colormap"], len(self.chan_states)) + self._theme["color_iterator"] = -1 + def create_control_panel(self): cntrl_layout = QtWidgets.QHBoxLayout() clear_button = QtWidgets.QPushButton("Clear") @@ -198,7 +48,7 @@ def create_control_panel(self): cntrl_layout.addWidget(clear_button) self.layout().addLayout(cntrl_layout) - def create_plots(self, **kwargs): + def create_plots(self): raise TypeError("Must be implemented by sub-class.") def refresh_axes(self): @@ -211,3 +61,84 @@ def closeEvent(self, evnt): super().closeEvent(evnt) self.awaiting_close = True self.was_closed.emit() + + +class CustomGUI(IniWindow): + """ + This application is for monitoring continuous activity from a MER data source. + """ + widget_cls = CustomWidget + + def __init__(self): + self._plot_settings = {} + self._source_settings = {} + self._data_source = None + self._plot_widget: CustomWidget | None = None + # parent IniWindow init will read settings files and setup IPC + super().__init__() + self._init_connection() + # self.try_reset_widget() is auto-triggered from previous line if connection is successful. + self.show() + + def __del__(self): + if self._data_source is not None: + self._data_source.disconnect_requested() + super().__del__() + + def parse_settings(self): + super().parse_settings() + for ini_path in self._settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + # Store / update connection settings which will be triggered after all ini processing + settings.beginGroup("data-source") + if "class" in settings.allKeys() and self._data_source is None: + # Infer data source from ini file + src_cls = getattr(open_mer.data_source, str(settings.value("class", type=str))) + self._source_settings["class"] = src_cls + self._source_settings["settings_path"] = ini_path + settings.endGroup() + + def _init_connection(self): + if "class" in self._source_settings and self._source_settings["class"] is not None: + _data_source = self._source_settings["class"]( + settings_path=self._source_settings["settings_path"], + on_connect_cb=self.on_source_connected + ) + + @QtCore.Slot(QtCore.QObject) + def on_source_connected(self, data_source): + self.data_source = data_source # Triggers setter --> self.try_reset_widget() + + @property + def data_source(self): + return self._data_source + + @data_source.setter + def data_source(self, value): + self._data_source = value + self.try_reset_widget() + + def update(self): + super().update() + if self.data_source.is_connected and self._plot_widget: + self.do_plot_update() + + def do_plot_update(self): + # abc.abstractmethod not possible because ABC does not work with Qt-derived classes, so raise error instead. + raise NotImplementedError("This method must be overridden by sub-class.") + + def on_plot_closed(self, force: bool = False): + if self._plot_widget is not None and (force or self._plot_widget.awaiting_close): + del self._plot_widget + self._plot_widget = None + + def try_reset_widget(self): + self.on_plot_closed(force=True) + if self.data_source is not None: + src_dict = self.data_source.data_stats + self._plot_widget = self.__class__.widget_cls(src_dict, + theme=self._theme_settings, + plot=self._plot_settings) + self._plot_widget.was_closed.connect(self.on_plot_closed) + self.setCentralWidget(self._plot_widget) diff --git a/open_mer/dbsgui/widgets/ini_window.py b/open_mer/dbsgui/widgets/ini_window.py new file mode 100644 index 0000000..5a2e729 --- /dev/null +++ b/open_mer/dbsgui/widgets/ini_window.py @@ -0,0 +1,125 @@ +from pathlib import Path +import importlib.resources as pkg_resources +from qtpy import QtWidgets, QtCore, QtGui + + +class IniWindow(QtWidgets.QMainWindow): + """ + A MainWindow that parses multiple QSettings ini files. + .show() must be manually or by sub-class after adding more widgets + (presumably using settings). + """ + def __init__(self): + super().__init__() + self._theme_settings = {"colormap": ""} + self._ipc_settings = {} + self._settings_paths: [Path] = [] + + self._build_ini_paths() + self.parse_settings() + self._setup_ipc() + + def __del__(self): + self._cleanup_ipc() + + def _build_ini_paths(self): + self._settings_paths: [Path] = [] + + # Infer paths to ini files + root_pkg = __package__.split(".")[0] + ini_name = type(self).__name__ + '.ini' + with pkg_resources.path(f"{root_pkg}.resources", "settings") as pkg_path: + pkg_path = pkg_path + home_path = Path.home() / f".{root_pkg}" + + # Add paths to settings files in ascending priority + self._settings_paths.append(pkg_path / "Style.ini") + self._settings_paths.append(pkg_path / "IPC.ini") + self._settings_paths.append(pkg_path / ini_name) + self._settings_paths.append(home_path / "Style.ini") + self._settings_paths.append(home_path / "IPC.ini") + self._settings_paths.append(home_path / ini_name) + + def parse_settings(self): + """ + Parse ini files and populate ._plot_settings. + Note that some settings (MainWindow) will be applied immediately and won't be stored in _plot_settings. + Other settings, especially those that are widget-specific, will be stored in _plot_settings. + Data source settings will be stored in _source_settings for later use by ._init_connection() + This method should usually be followed by .try_reset_widget() + """ + for ini_path in self._settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + # Apply MainWindow settings immediately. + settings.beginGroup("MainWindow") + keys: list = settings.allKeys() + if "pos" in keys: + self.move(settings.value("pos", type=QtCore.QPoint)) + if "size" in keys: + size_xy: QtCore.QSize = settings.value("size", type=QtCore.QSize) + self.resize(size_xy) + self.setMaximumWidth(size_xy.width()) + if "fullScreen" in keys and settings.value("fullScreen", type=bool): + self.showFullScreen() + elif "maximized" in keys and settings.value("maximized", type=bool): + self.showMaximized() + if "frameless" in keys and settings.value("frameless", type=bool): + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + settings.endGroup() + + # theme + settings.beginGroup("theme") + # > labels and lines + for k, t in { + "labelcolor_active": str, "labelsize_active": int, + "labelcolor_inactive": str, "labelsize_inactive": int, + "linewidth": int + }.items(): + if k in settings.allKeys(): + self._theme_settings[k] = settings.value(k, type=t) + # > pencolors + if "colormap" in settings.allKeys(): + self._theme_settings["colormap"] = settings.value("colormap", defaultValue="custom", type=str) + self._theme_settings.pop("pencolors", None) + if self._theme_settings["colormap"] == "custom": + settings.beginGroup("pencolors") + chan_ids = [int(_) for _ in settings.childGroups()] + if "pencolors" not in self._theme_settings: + self._theme_settings["pencolors"] = [None] * (max(chan_ids) + 1) + for c_id in chan_ids: + settings.beginGroup(str(c_id)) + if "name" in settings.allKeys(): + name = settings.value("name", type=str) + self._theme_settings["pencolors"][c_id] = QtGui.QColor(name) + else: + color_hex = settings.value("value", defaultValue="#ffffff", type=str) + self._theme_settings["pencolors"][c_id] = QtGui.QColor(color_hex) + settings.endGroup() + settings.endGroup() + settings.endGroup() + + # Store / update IPC settings which will be used by subclasses only + settings.beginGroup("ZeroMQ") + sock_ids = [int(_) for _ in settings.childGroups()] + sock_ids.sort() + for sock_id in sock_ids: + settings.beginGroup(str(sock_id)) + self._ipc_settings[settings.value("topic", type=str)] = settings.value("port", type=int) + settings.endGroup() + settings.endGroup() + + def _setup_ipc(self): + # Optional method to setup inter-process communication (mostly ZeroMQ pub/sub) + # e.g.: + # self._ipc_context = zmq.Context() + # self._ipc_sock = self._ipc_context.socket(zmq.PUB) + # self._ipc_sock.bind(f"tcp://*:{self._ipc_settings[topic]}") + # self._ipc_sock.setsockopt_string(zmq.SUBSCRIBE, topic) + pass + + def _cleanup_ipc(self): + # self._ipc_sock.setsockopt(zmq.LINGER, 0) + # self._ipc_sock.close() + # self._ipc_context.term() + pass diff --git a/open_mer/resources/settings/IPC.ini b/open_mer/resources/settings/IPC.ini new file mode 100644 index 0000000..f31e8a3 --- /dev/null +++ b/open_mer/resources/settings/IPC.ini @@ -0,0 +1,11 @@ +[ZeroMQ] +0\port=60001 +0\topic=procedure_settings +1\port=60002 +1\topic=snippet_status +2\port=60003 +2\topic=channel_select +3\port=60004 +3\topic=features +4\port=60005 +4\topic=ddu diff --git a/open_mer/resources/settings/ProcedureGUI.ini b/open_mer/resources/settings/ProcedureGUI.ini index 0fa6736..a803150 100644 --- a/open_mer/resources/settings/ProcedureGUI.ini +++ b/open_mer/resources/settings/ProcedureGUI.ini @@ -8,7 +8,7 @@ pos=@Point(1320 250) [data-source] class=CerebusDataSource basepath=C:\\Recordings -sampling_group=30000 +sampling_rate=30000 get_continuous=false get_events=false get_comments=false diff --git a/open_mer/resources/settings/RasterGUI.ini b/open_mer/resources/settings/RasterGUI.ini index 8af5c0b..a344678 100644 --- a/open_mer/resources/settings/RasterGUI.ini +++ b/open_mer/resources/settings/RasterGUI.ini @@ -7,7 +7,7 @@ pos=@Point(620 0) [data-source] class=CerebusDataSource -sampling_group=30000 +sampling_rate=30000 get_continuous=false get_events=true get_comments=true diff --git a/open_mer/resources/settings/SweepGUI.ini b/open_mer/resources/settings/SweepGUI.ini index cb9dfc0..67304c9 100644 --- a/open_mer/resources/settings/SweepGUI.ini +++ b/open_mer/resources/settings/SweepGUI.ini @@ -2,11 +2,11 @@ fullScreen=false maximized=false size=@Size(620 1080) -pos=@Point(0 40) +pos=@Point(0 0) [data-source] class=CerebusDataSource -sampling_group=30000 +sampling_rate=30000 get_continuous=true get_events=false get_comments=false diff --git a/open_mer/resources/settings/WaveformGUI.ini b/open_mer/resources/settings/WaveformGUI.ini index b7a9e77..14b3bbb 100644 --- a/open_mer/resources/settings/WaveformGUI.ini +++ b/open_mer/resources/settings/WaveformGUI.ini @@ -7,7 +7,7 @@ pos=@Point(920 0) [data-source] class=CerebusDataSource -sampling_group=30000 +sampling_rate=30000 get_continuous=false get_events=false get_comments=true @@ -23,5 +23,5 @@ unit_scaling=0.25 [theme] frate_size=24 linewidth=1 -colormap=hsv -n_colors=4 +colormap=custom +pencolors\0\name=#ffffff From 6e1f0320609ff12472fb180c2c81057bd47ca8db Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 29 Sep 2023 02:22:34 -0400 Subject: [PATCH 50/65] Continue major refactor: * Fix most applications * Depth_Process and Feature_Process migrated here from SERF --- docs/for-developers.md | 14 +- docs/preparing-distribution.md | 2 +- open_mer/data_source/cerebus.py | 3 +- open_mer/data_source/lsl.py | 2 + open_mer/dbsgui/depth.py | 330 ++++++------ open_mer/dbsgui/features.py | 514 +++++++++++-------- open_mer/dbsgui/procedure.py | 240 +++++---- open_mer/dbsgui/raster.py | 2 +- open_mer/dbsgui/sweep.py | 2 +- open_mer/dbsgui/waveform.py | 2 +- open_mer/dbsgui/widgets/SettingsDialog.py | 255 +++------ open_mer/dbsgui/widgets/ini_window.py | 11 +- open_mer/depth_source/base.py | 6 +- open_mer/depth_source/cerebus.py | 9 +- open_mer/depth_source/fhc.py | 27 +- open_mer/feature_plots/FeaturePlotWidgets.py | 117 ++--- open_mer/resources/settings/DepthGUI.ini | 4 +- open_mer/resources/settings/FeaturesGUI.ini | 34 +- open_mer/scripts/Depth_Process.py | 382 ++++++++++++++ open_mer/scripts/FeaturesGUI.py | 11 +- open_mer/scripts/Features_Process.py | 102 ++++ open_mer/scripts/ProcedureGUI.py | 12 +- setup.cfg | 2 + 23 files changed, 1237 insertions(+), 846 deletions(-) create mode 100644 open_mer/scripts/Depth_Process.py create mode 100644 open_mer/scripts/Features_Process.py diff --git a/docs/for-developers.md b/docs/for-developers.md index 863a0fb..509e1cb 100644 --- a/docs/for-developers.md +++ b/docs/for-developers.md @@ -41,13 +41,13 @@ This section is referring to communication among the applications within the Ope The applications all run independently of each other, but most of them work better in combination. To communicate information between applications we use [ZeroMQ](https://zeromq.org/). -| Publisher | Port | Topic | Message | Subscribers | -|----------------------|-------|--------------------|--------------------------------------------------------|----------------------| -| ProcedureGUI | 60001 | procedure_settings | json of settings-dicts "procedure" and ?? | FeaturesGUI | -| Depth_Process (SERF) | 60002 | snippet_status | (startup, notrecording, recording, accumulating, done) | ProcedureGUI | -| SweepGUI | 60003 | channel_select | json with channel, range, highpass | FeaturesGUI | -| FeaturesGUI | 60004 | features | refresh | FeaturesGUI | -| DepthGUI | 60005 | ddu | float of depth | Depth_Process (SERF) | +| Publisher | Port | Topic | Message | Subscribers | +|----------------|-------|--------------------|--------------------------------------------------------|----------------| +| ProcedureGUI | 60001 | procedure_settings | json of settings-dicts "procedure" and ?? | FeaturesGUI | +| Depth_Process | 60002 | snippet_status | (startup, notrecording, recording, accumulating, done) | ProcedureGUI | +| SweepGUI | 60003 | channel_select | json with channel, range, highpass | FeaturesGUI | +| FeaturesGUI | 60004 | features | refresh | ProcedureGUI | +| DepthGUI | 60005 | ddu | float of depth | Depth_Process | We also have one LSL stream coming from the DepthGUI. Old version of the SERF Depth_Process might still be using it but they should be migrated. diff --git a/docs/preparing-distribution.md b/docs/preparing-distribution.md index e589b31..6c182c3 100644 --- a/docs/preparing-distribution.md +++ b/docs/preparing-distribution.md @@ -48,7 +48,7 @@ In your WinPython Command Prompt, try the following commands first. If they fail python.exe -m pip install --upgrade pip # macOS developers: use `mamba` instead of `pip` for the next 1 line only. pip install Django quantities numpy scipy Cython pyFFTW mysqlclient -pip install pylsl pyaudio PySide6 qtpy pyzmq pyqtgraph +pip install pylsl pyaudio PySide6 qtpy pyzmq pyqtgraph pyserial pip install git+https://github.com/NeuralEnsemble/python-neo.git pip install git+https://github.com/SachsLab/pytf.git pip install git+https://github.com/SachsLab/mspacman.git diff --git a/open_mer/data_source/cerebus.py b/open_mer/data_source/cerebus.py index fa14c63..f0ebbd1 100644 --- a/open_mer/data_source/cerebus.py +++ b/open_mer/data_source/cerebus.py @@ -1,7 +1,8 @@ from pathlib import Path + from qtpy import QtCore + from .interface import IDataSource -from ..settings import parse_ini_try_numeric try: from cerebuswrapper import CbSdkConnection except ModuleNotFoundError as e: diff --git a/open_mer/data_source/lsl.py b/open_mer/data_source/lsl.py index 1ea95df..f883474 100644 --- a/open_mer/data_source/lsl.py +++ b/open_mer/data_source/lsl.py @@ -1,7 +1,9 @@ from typing import Union, Tuple + from qtpy import QtCore import numpy as np import pylsl + from .interface import IDataSource diff --git a/open_mer/dbsgui/depth.py b/open_mer/dbsgui/depth.py index 4fe9fda..fda6ed8 100644 --- a/open_mer/dbsgui/depth.py +++ b/open_mer/dbsgui/depth.py @@ -1,8 +1,6 @@ -from pathlib import Path from qtpy import QtCore, QtWidgets import zmq -import pylsl -from ..settings import defaults +from .widgets.ini_window import IniWindow from ..depth_source import CBSDKPlayback try: from cerebuswrapper import CbSdkConnection @@ -12,65 +10,105 @@ import open_mer.depth_source -class DepthGUI(QtWidgets.QMainWindow): +class MyLCD(QtWidgets.QLCDNumber): + lcd_dbl_clicked = QtCore.Signal() + + def mouseDoubleClickEvent(self, event): + # event is a PySide6.QtGui.QMouseEvent + self.lcd_dbl_clicked.emit() - def __init__(self, ini_file=None): - super().__init__() - # Infer path to ini - ini_name = ini_file if ini_file is not None else (type(self).__name__ + '.ini') - ini_path = Path(ini_name) - if ini_path.exists(): - self._settings_path = ini_path - else: - # Try home / .open_mer first - home_dir = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.HomeLocation)) - ini_path = home_dir / '.open_mer' / ini_path.name - if ini_path.exists(): - self._settings_path = ini_path - else: - # Use default ini that ships with module. - self._settings_path = Path(__file__).parents[1] / "resources" / "settings" / ini_path.name +class DepthGUI(IniWindow): + def __init__(self): + self._depth_sock = None + self._data_settings = {"source": {"class": None, "serial": {}}, "mirror": {"lsl": None, "nsp": None}} + self._depth_source = None + super().__init__() self.display_string = None self._depth_stream = None - self._mirror = {'lsl': None, 'nsp': None} - self.restore_from_settings() + self._init_connection() self.setup_ui() - context = zmq.Context() - self._depth_sock = context.socket(zmq.PUB) - self._depth_sock.bind(f"tcp://*:{60005}") # TODO: Get port from settings - - def restore_from_settings(self): - settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) - - # Restore size and position. - default_dims = defaults.WINDOWDIMS_DICT[type(self).__name__] - settings.beginGroup("MainWindow") - self.resize(settings.value("size", QtCore.QSize(default_dims[2], default_dims[3]))) - self.move(settings.value("pos", QtCore.QPoint(default_dims[0], default_dims[1]))) - if settings.value("fullScreen", 'false') == 'true': - self.showFullScreen() - elif settings.value("maximized", 'false') == 'true': - self.showMaximized() - if settings.value("frameless", 'false') == 'true': - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - settings.endGroup() - - # Infer depth source from ini file, setup data source - settings.beginGroup("depth-source") - src_cls = getattr(open_mer.depth_source, settings.value("class", "CBSDKPlayback")) - self._depth_source = src_cls(scoped_settings=settings) - settings.endGroup() - - settings.beginGroup("depth-mirror") - self._mirror['lsl'] = bool(settings.value("lsl_mirror", False)) # TODO: Remove - self._mirror['nsp'] = bool(settings.value("lsl_mirror", False)) - settings.endGroup() + def parse_settings(self): + # Handles MainWindow geometry and collects self._theme_settings and self._ipc_settings + super().parse_settings() + # Get custom settings + for ini_path in self._settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + settings.beginGroup("depth-source") + for k, t in { + "class": str, + }.items(): + if k in settings.allKeys(): + if k == "class": + src_cls = getattr(open_mer.depth_source, settings.value(k, type=t)) + self._data_settings["source"][k] = src_cls + else: + self._data_settings["source"][k] = settings.value(k, type=t) + settings.beginGroup("serial") + for k, t in { + "baudrate": int, "com_port": str + }.items(): + if k in settings.allKeys(): + self._data_settings["source"]["serial"][k] = settings.value(k, type=t) + settings.endGroup() # serial + settings.endGroup() # depth-source + + settings.beginGroup("depth-mirror") + for k, t in { + "lsl_mirror": bool, "nsp_mirror": bool + }.items(): + if k in settings.allKeys(): + self._data_settings["mirror"][k] = settings.value(k, type=t) + settings.endGroup() # depth-mirror + + def _setup_ipc(self): + self._depth_context = zmq.Context() + self._depth_sock = self._depth_context.socket(zmq.PUB) + self._depth_sock.bind(f"tcp://*:{self._ipc_settings['ddu']}") + + if self._data_settings["mirror"]["lsl"]: + import pylsl + outlet_info = pylsl.StreamInfo(name='electrode_depth', type='depth', channel_count=1, + nominal_srate=pylsl.IRREGULAR_RATE, channel_format=pylsl.cf_float32, + source_id='depth1214') + self._depth_stream = pylsl.StreamOutlet(outlet_info) + + src_cls = self._data_settings["source"]["class"] + if src_cls is not None and src_cls != open_mer.depth_source.CBSDKPlayback: + # We have a data source that is not CBSDKPlayback (e.g., serial, or some other) + # so we want to send the NSP comments about our depths so they get saved in the .nev file. + CbSdkConnection().connect() + CbSdkConnection().cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} + # cbsdk_conn.set_comments("DTT:" + display_string) + + def _cleanup_ipc(self): + self._depth_sock.setsockopt(zmq.LINGER, 0) + self._depth_sock.close() + self._depth_context.term() + + if self._depth_stream is not None: + del self._depth_stream + self._depth_stream = None + + src_cls = self._data_settings["source"]["class"] + if src_cls is not None and src_cls != open_mer.depth_source.CBSDKPlayback: + CbSdkConnection().disconnect() + + def _init_connection(self): + if self._data_settings["source"]["class"] is not None: + self._depth_source = self._data_settings["source"]["class"]() + + def __del__(self): + if self._depth_source is not None: + self._depth_source.do_close() + self._depth_source = None + super().__del__() def setup_ui(self): - self.setWindowTitle('Neuroport DBS - Electrodes Depth') + self.setWindowTitle("DDU") self.show() self.plot_widget = QtWidgets.QWidget() self.setCentralWidget(self.plot_widget) @@ -79,154 +117,98 @@ def setup_ui(self): v_layout = QtWidgets.QVBoxLayout() v_layout.setSpacing(0) v_layout.setContentsMargins(10, 0, 10, 10) + h_layout = QtWidgets.QHBoxLayout() h_layout.addWidget(QtWidgets.QLabel("DTT: ")) - self._doubleSpinBox_DTT = QtWidgets.QDoubleSpinBox() - self._doubleSpinBox_DTT = QtWidgets.QDoubleSpinBox() - self._doubleSpinBox_DTT.setMinimum(-100.00) - self._doubleSpinBox_DTT.setMaximum(100.00) - self._doubleSpinBox_DTT.setSingleStep(1.00) - self._doubleSpinBox_DTT.setDecimals(2) - self._doubleSpinBox_DTT.setValue(0.00) - self._doubleSpinBox_DTT.setFixedWidth(60) - h_layout.addWidget(self._doubleSpinBox_DTT) + dtt_spinbox = QtWidgets.QDoubleSpinBox() + dtt_spinbox.setObjectName("DTT_doubleSpinBox") + dtt_spinbox.setMinimum(-100.00) + dtt_spinbox.setMaximum(100.00) + dtt_spinbox.setSingleStep(1.00) + dtt_spinbox.setDecimals(2) + dtt_spinbox.setValue(0.00) + dtt_spinbox.setFixedWidth(80) + h_layout.addWidget(dtt_spinbox) # Manual offset added to the depth before display and mirroring h_layout.addWidget(QtWidgets.QLabel("Offset: ")) - self._doubleSpinBox_offset = QtWidgets.QDoubleSpinBox() - self._doubleSpinBox_offset.setMinimum(-100.00) - self._doubleSpinBox_offset.setMaximum(100.00) - self._doubleSpinBox_offset.setSingleStep(1.00) - self._doubleSpinBox_offset.setDecimals(2) - self._doubleSpinBox_offset.setValue(self._depth_source.offset) - self._doubleSpinBox_offset.setFixedWidth(60) - h_layout.addWidget(self._doubleSpinBox_offset) + offset_spinbox = QtWidgets.QDoubleSpinBox() + offset_spinbox.setObjectName("offset_doubleSpinBox") + offset_spinbox.setMinimum(-100.00) + offset_spinbox.setMaximum(100.00) + offset_spinbox.setSingleStep(1.00) + offset_spinbox.setDecimals(2) + offset_spinbox.setValue(self._depth_source.offset) + offset_spinbox.setFixedWidth(80) + h_layout.addWidget(offset_spinbox) h_layout.addStretch() - - # Widgets to manage mirroring the resulting value (including scale and offset) to other outputs - h_layout.addWidget(QtWidgets.QLabel("Stream to :")) - - cb = QtWidgets.QCheckBox("NSP") - cb.setObjectName("NSP_CheckBox") - if isinstance(self._depth_source, CBSDKPlayback): - cb.setChecked(False) - cb.setEnabled(False) - else: - cb.setChecked(self._mirror['nsp']) - cb.setEnabled(True) - cb.clicked.connect(self.on_mirror_NSP_clicked) - cb.click() - h_layout.addWidget(cb) - h_layout.addSpacing(5) - - # TODO: Remove - cb = QtWidgets.QCheckBox("LSL") - cb.setChecked(self._mirror['lsl']) - cb.clicked.connect(self.on_mirror_LSL_clicked) - cb.click() - h_layout.addWidget(cb) - h_layout.addSpacing(5) - - # Manual close button because window has no frame. - quit_btn = QtWidgets.QPushButton('X') - quit_btn.setMaximumWidth(20) - quit_btn.clicked.connect(QtWidgets.QApplication.instance().quit) - - quit_btn.setStyleSheet("QPushButton { color: white; " - "background-color : red; " - "border-color : red; " - "border-width: 2px}") - - h_layout.addWidget(quit_btn) - v_layout.addLayout(h_layout) # add a frame for the LCD numbers - self.lcd_frame = QtWidgets.QFrame() - self.lcd_frame.setFrameShape(QtWidgets.QFrame.Shape.Box) + lcd_frame = QtWidgets.QFrame() + lcd_frame.setFrameShape(QtWidgets.QFrame.Shape.Box) lcd_layout = QtWidgets.QGridLayout() - self.lcd_frame.setLayout(lcd_layout) + lcd_frame.setLayout(lcd_layout) # RAW reading from DDU - self.raw_ddu = QtWidgets.QLCDNumber() - self.raw_ddu.setDigitCount(7) - self.raw_ddu.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.raw_ddu.setSmallDecimalPoint(True) - self.raw_ddu.setFixedHeight(50) - self.raw_ddu.display("{0:.3f}".format(0)) - lcd_layout.addWidget(self.raw_ddu, 0, 3, 2, 3) - - # TODO: Use custom class and reimplement self.offset_ddu.mouseDoubleClickEvent(), then git rid of "Send!" - self.offset_ddu = QtWidgets.QLCDNumber() - self.offset_ddu.setDigitCount(7) - self.offset_ddu.setFixedHeight(150) - self.offset_ddu.display("{0:.3f}".format(-10)) - self.offset_ddu.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - lcd_layout.addWidget(self.offset_ddu, 2, 0, 5, 6) - v_layout.addWidget(self.lcd_frame) + raw_ddu_lcd = QtWidgets.QLCDNumber() + raw_ddu_lcd.setObjectName("raw_ddu_LCD") + raw_ddu_lcd.setDigitCount(7) + raw_ddu_lcd.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + raw_ddu_lcd.setSmallDecimalPoint(True) + raw_ddu_lcd.setFixedHeight(50) + raw_ddu_lcd.display("{0:.3f}".format(0)) + lcd_layout.addWidget(raw_ddu_lcd, 0, 3, 2, 3) + + offset_lcd = MyLCD() + offset_lcd.setObjectName("offset_LCD") + offset_lcd.setDigitCount(7) + offset_lcd.setFixedHeight(150) + offset_lcd.display("{0:.3f}".format(-10)) + offset_lcd.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + offset_lcd.lcd_dbl_clicked.connect(self.publish) + lcd_layout.addWidget(offset_lcd, 2, 0, 5, 6) + + v_layout.addWidget(lcd_frame) self.plot_widget.setLayout(v_layout) - # TODO: Remove - def on_mirror_LSL_clicked(self, state): - if state > 0: - outlet_info = pylsl.StreamInfo(name='electrode_depth', type='depth', channel_count=1, - nominal_srate=pylsl.IRREGULAR_RATE, channel_format=pylsl.cf_float32, - source_id='depth1214') - self._depth_stream = pylsl.StreamOutlet(outlet_info) - else: - self._depth_stream = None - - def on_mirror_NSP_clicked(self, state): - if not isinstance(self._depth_source, CBSDKPlayback): - if state > 0: - CbSdkConnection().connect() - CbSdkConnection().cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} - # cbsdk_conn.set_comments("DTT:" + display_string) - else: - CbSdkConnection().disconnect() - - def _do_close(self, from_port): - self._depth_source.do_close() - def update(self): - # Added new_value handling for playback if we ever want to post-process depth - # on previously recorded sessions. - new_value = False - value = self._depth_source.update() if value is not None: - self.raw_ddu.display("{0:.3f}".format(value)) - value += self._doubleSpinBox_DTT.value() + self._doubleSpinBox_offset.value() + # Find widgets + raw_ddu_lcd: QtWidgets.QLCDNumber = self.findChild(QtWidgets.QLCDNumber, "raw_ddu_LCD") + offset_lcd: MyLCD = self.findChild(MyLCD, "offset_LCD") + dtt_spinbox: QtWidgets.QDoubleSpinBox = self.findChild(QtWidgets.QDoubleSpinBox, "DTT_doubleSpinBox") + offset_spinbox: QtWidgets.QDoubleSpinBox = self.findChild(QtWidgets.QDoubleSpinBox, "offset_doubleSpinBox") + + # Update displays for raw and corrected values + raw_ddu_lcd.display("{0:.3f}".format(value)) + value += dtt_spinbox.value() + offset_spinbox.value() display_string = "{0:.3f}".format(value) + offset_lcd.display(display_string) + + # If value is new, store and publish if display_string != self.display_string: - new_value = True self.display_string = display_string - self.offset_ddu.display(display_string) + self.publish() + def publish(self): # Push to NSP (only if this is not NSP playback) - nsp_cb = self.findChild(QtWidgets.QCheckBox, "NSP_CheckBox") - if nsp_cb and nsp_cb.isChecked() and not isinstance(self._depth_source, CBSDKPlayback): + src_cls = self._data_settings["source"]["class"] + if src_cls is not None and src_cls != open_mer.depth_source.CBSDKPlayback: cbsdk_conn = CbSdkConnection() - if cbsdk_conn.is_connected: - if new_value: - cbsdk_conn.set_comments("DTT:" + display_string) - else: - # try connecting if not connected but button is active - if self.chk_NSP.isChecked() and self.chk_NSP.isEnabled(): - cbsdk_conn.connect() - cbsdk_conn.cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} + if not cbsdk_conn.is_connected: + cbsdk_conn.connect() + cbsdk_conn.cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} + if not cbsdk_conn.is_connected: + cbsdk_conn.set_comments("DTT:" + self.display_string) # Push to LSL - if self._depth_stream is not None and new_value: - self._depth_stream.push_sample([value]) + if self._depth_stream is not None: + self._depth_stream.push_sample([self.display_string]) # Publish on ZeroMQ - if new_value: - self._depth_sock.send_string(f"ddu {value}") - - def send(self): - self.display_string = None # make sure the update function runs - self.update() + self._depth_sock.send_string(f"ddu {self.display_string}") diff --git a/open_mer/dbsgui/features.py b/open_mer/dbsgui/features.py index f809969..3fce90e 100644 --- a/open_mer/dbsgui/features.py +++ b/open_mer/dbsgui/features.py @@ -1,184 +1,90 @@ import json -from pathlib import Path +import importlib.resources as pkg_resources import zmq from qtpy import QtCore, QtWidgets, QtGui -from serf.tools.db_wrap import DBWrapper, ProcessWrapper -from ..settings import defaults, locate_ini, parse_ini_try_numeric -from ..feature_plots import * - - -class FeaturesGUI(QtWidgets.QMainWindow): - status_icons = { - k: Path(__file__).parents[0] / 'resources' / 'icons' / (v + '.png') for k, v in - ((-2, 'depth_status_delay'), (-1, 'depth_status_in_use'), (1, 'depth_status_done'), (0, 'depth_status_off')) - } - - def __init__(self, ini_file: str = None, **kwargs): - """ - - Visualizes raw segments and calculated features from the database - - Widgets to navigate the visualizations - - Subscribes to zmq messages to know which features to grab - - Subscribes to zmq messages to know which channels to show - - For semi-realtime application, you should be running Depth_Process and Features_Process (from serf package) - in background. - - Args: - ini_file: - **kwargs: - """ - super().__init__(**kwargs) +from serf.tools.db_wrap import DBWrapper - self._plot_settings = {} - self._subject_settings = {} - self._procedure_settings = {} - self._buffer_settings = {} - self._features_settings = {} - self._chan_labels = set([]) - - self._db = DBWrapper() - # TODO: Load ports from ini file - self._zmq_ctrl_port = 60001 - self._zmq_chan_port = 60003 - self._zmq_feat_port = 60004 - self._restore_from_settings(ini_file) - - self._setup_pubsub() - self._setup_ui() - self._features_sock.send_string("features refresh") - - def closeEvent(self, *args, **kwargs): - super().closeEvent(*args, **kwargs) - - def _restore_from_settings(self, ini_file=None): - ini_name = ini_file if ini_file is not None else (type(self).__name__ + '.ini') - settings_path = locate_ini(ini_name) - settings = QtCore.QSettings(str(settings_path), QtCore.QSettings.IniFormat) - default_dims = defaults.WINDOWDIMS_DICT[type(self).__name__] - - settings.beginGroup("MainWindow") - self.move(settings.value("pos", QtCore.QPoint(default_dims[0], default_dims[1]))) - size_xy = settings.value("size", QtCore.QSize(default_dims[2], default_dims[3])) - self.resize(size_xy) - self.setMaximumWidth(size_xy.width()) - if settings.value("fullScreen", 'false') == 'true': - self.showFullScreen() - elif settings.value("maximized", 'false') == 'true': - self.showMaximized() - if settings.value("frameless", 'false') == 'true': - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - settings.endGroup() - - settings.beginGroup("plot") - self._plot_settings['x_start'] = int(settings.value("x_start", -4000)) - self._plot_settings['x_stop'] = int(settings.value("x_stop", 120000)) - self._plot_settings['y_range'] = int(settings.value("y_range", 250)) - settings.endGroup() - - settings.beginGroup("features") - add_features = [] - for feat_grp in settings.childKeys(): - b_add = settings.value(feat_grp, True, type=bool) - if b_add: - add_features.append(feat_grp) - self._features_settings["features"] = add_features - settings.endGroup() - - settings.beginGroup("buffer") - self._plot_settings["highpass"] = settings.value("highpass", "true") == "true" - # chk_threshold - settings.endGroup() - - self._plot_settings["color_iterator"] = -1 - self._plot_settings["image_plot"] = False - # theme - settings.beginGroup("theme") - self._plot_settings["theme"] = {} - for k in settings.allKeys(): - if k == 'colormap' or k.lower().startswith('pencolors'): - continue - self._plot_settings["theme"][k] = parse_ini_try_numeric(settings, k) - # theme > pencolors - self._plot_settings["theme"]['colormap'] = settings.value('colormap', 'custom') - if self._plot_settings["theme"]['colormap'] == "custom": - pencolors = [] - settings.beginGroup("pencolors") - for c_id in settings.childGroups(): - settings.beginGroup(c_id) - cname = settings.value("name", None) - if cname is not None: - cvalue = QtGui.QColor(cname) - else: - cvalue = settings.value("value", "#ffffff") - pencolors.append(cvalue) - settings.endGroup() - settings.endGroup() # pencolors - self._plot_settings["theme"]["pencolors"] = pencolors - settings.endGroup() # end theme - - def _setup_pubsub(self): - context = zmq.Context() - - # Subscribe to channel-change notifications - self._chan_sock = context.socket(zmq.SUB) - self._chan_sock.connect(f"tcp://localhost:{self._zmq_chan_port}") - self._chan_sock.setsockopt_string(zmq.SUBSCRIBE, "channel_select") - - # Subscribe to procedure set notifications -- react to procedure id - self._procedure_sock = context.socket(zmq.SUB) - self._procedure_sock.connect(f"tcp://localhost:{self._zmq_ctrl_port}") - self._procedure_sock.setsockopt_string(zmq.SUBSCRIBE, "procedure_settings") - - # Publish refresh notification -- on startup and when refresh is clicked - self._features_sock = context.socket(zmq.PUB) - self._features_sock.bind(f"tcp://*:{self._zmq_feat_port}") - - def _setup_ui(self): - main_widget = QtWidgets.QWidget(self) - main_widget.setLayout(QtWidgets.QVBoxLayout()) - self.setCentralWidget(main_widget) - - self._setup_control_panel() - self._reset_widget_stack() +from ..feature_plots import * +from .widgets.custom import CustomWidget, CustomGUI + + +class FeatureStackWidget(CustomWidget): + + def __init__(self, *args, theme={}, plot={}, features={}, procedure={}): + with pkg_resources.path(f"{__package__.split('.')[0]}.resources", "icons") as res_icons_path: + res_icons_path = res_icons_path + self.status_icons = { + k: QtGui.QPixmap(str(res_icons_path / (v + '.png'))) + for k, v in { + "accumulating": "depth_status_delay", + "recording": "depth_status_in_use", + "done": "depth_status_done", + "notrecording": "depth_status_off" + }.items() + } + self._plot_config = {} + self._features = {} + # self._theme = {} created by super + super().__init__(*args, theme=theme, plot=plot, features=features, procedure=procedure) + self.reset_feat_select_items() + self.reset_chan_select_items() + + def _parse_config(self, theme={}, plot={}, features={}, procedure={}): + super()._parse_config(theme=theme) + filter = plot.get("filter", {}) + self._plot_config = plot["plot"].copy() + self._plot_config["highpass"] = filter.get("highpass", True) + # This next 2 lines convert our dict from {ind: {name: enabled}} to {ind: name)} only for enabled + self._features = {k: [(_k, _v) for _k, _v in v.items()][0] for k, v in features.items()} + self._features = {k: v[0] for k, v in self._features.items() if v[1]} + self._procedure = procedure.copy() + + def create_control_panel(self): + # | chan_select | feature_select | | | + # |----------------------------------| status icon | refresh | + # | y-range | highpass | match sweep | | | - def _setup_control_panel(self): - # Top row lo_L1 = QtWidgets.QHBoxLayout() + # Channel select lo_L1.addWidget(QtWidgets.QLabel("Electrode: ", alignment=QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight)) - chan_select_cb = QtWidgets.QComboBox(self.centralWidget()) + chan_select_cb = QtWidgets.QComboBox() chan_select_cb.setObjectName("ChanSelect_ComboBox") chan_select_cb.setMinimumWidth(70) chan_select_cb.setEnabled(False) - chan_select_cb.currentIndexChanged.connect(lambda idx: self.reset_stack()) - self._reset_chan_select_items() + chan_select_cb.addItem("None") + chan_select_cb.currentIndexChanged.connect(self.refresh_axes) lo_L1.addWidget(chan_select_cb) + # Feature select lo_L1.addSpacing(20) lo_L1.addWidget(QtWidgets.QLabel("Feature set: ", alignment=QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight)) - feat_select_cb = QtWidgets.QComboBox(self.centralWidget()) + feat_select_cb = QtWidgets.QComboBox() feat_select_cb.setObjectName("FeatureSelect_ComboBox") feat_select_cb.setMinimumWidth(60) - self._reset_feat_select_items() - feat_select_cb.currentIndexChanged.connect(lambda idx: self.reset_stack()) + feat_select_cb.currentIndexChanged.connect(self.refresh_axes) lo_L1.addWidget(feat_select_cb) - # Second row + # Second row - left: y-range, highpass, match sweep; right: refresh lo_L2 = QtWidgets.QHBoxLayout() lo_L2.addSpacing(10) + # Range Edit lo_L2.addWidget(QtWidgets.QLabel("+/- ", alignment=QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight)) - range_edit = QtWidgets.QLineEdit("{:.2f}".format(self._plot_settings['y_range'])) + range_edit = QtWidgets.QLineEdit("{:.2f}".format(self._plot_config['y_range'])) range_edit.setObjectName("Range_LineEdit") range_edit.setMaximumWidth(50) range_edit.editingFinished.connect(self.on_range_edited) lo_L2.addWidget(range_edit) + # HP lo_L2.addSpacing(30) hp_chk = QtWidgets.QCheckBox("HP") hp_chk.setObjectName("HP_CheckBox") - hp_chk.setChecked(self._plot_settings["highpass"]) + hp_chk.setChecked(self._plot_config["highpass"]) lo_L2.addWidget(hp_chk) + # Match Sweep lo_L2.addSpacing(30) sweep_chk = QtWidgets.QCheckBox("Match SweepGUI") @@ -189,14 +95,21 @@ def _setup_control_panel(self): lo_L2.addWidget(sweep_chk) lo_R = QtWidgets.QHBoxLayout() + + # Status Label + status_label = QtWidgets.QLabel() + status_label.setObjectName("Status_Label") + status_label.setPixmap(self.status_icons["notrecording"]) + lo_R.addWidget(status_label) + lo_R.addSpacing(10) + # Refresh button refresh_pb = QtWidgets.QPushButton("Refresh") refresh_pb.setObjectName("Refresh_PushButton") refresh_pb.setMaximumWidth(50) - refresh_pb.clicked.connect(self.on_refresh_clicked) lo_R.addWidget(refresh_pb) - lo_R.addSpacing(10) + # Combine layouts lo_L = QtWidgets.QVBoxLayout() lo_L.addLayout(lo_L1) lo_L.addSpacing(5) @@ -206,105 +119,236 @@ def _setup_control_panel(self): lo.addLayout(lo_L) lo.addStretch() lo.addLayout(lo_R) - self.centralWidget().layout().addLayout(lo) + self.layout().addLayout(lo) - def _reset_feat_select_items(self): - feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") - feat_combo.blockSignals(True) - feat_combo.clear() - feat_combo.addItems(self._features_settings['features']) - feat_combo.blockSignals(False) - feat_combo.setCurrentIndex(0) + def on_range_edited(self): + range_edit = self.findChild(QtWidgets.QLineEdit, "Range_LineEdit") + self._plot_config["y_range"] = float(range_edit.text()) - def _reset_chan_select_items(self): - chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") - chan_combo.blockSignals(True) - chan_combo.clear() - chan_combo.addItem("None") - chan_combo.addItems(self._chan_labels) - chan_combo.blockSignals(False) - chan_combo.setCurrentIndex(0) # Triggers an emission --> reset_stack + def on_sweep_clicked(self): + sweep_control: QtWidgets.QCheckBox = self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox") + chan_combo: QtWidgets.QComboBox = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + hp_chk: QtWidgets.QCheckBox = self.findChild(QtWidgets.QCheckBox, "HP_CheckBox") + range_edit: QtWidgets.QLineEdit = self.findChild(QtWidgets.QLineEdit, "Range_LineEdit") + b_enable = not sweep_control.isChecked() + for _ in [chan_combo, hp_chk, range_edit]: + _.setEnabled(b_enable) - def _reset_widget_stack(self): + def create_plots(self): plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") if plot_stack is None: plot_stack = QtWidgets.QStackedWidget() plot_stack.setObjectName("Plot_Stack") - self.centralWidget().layout().addWidget(plot_stack) + self.layout().addWidget(plot_stack) + # Clear old widgets from the stack to_delete = [plot_stack.widget(_) for _ in range(plot_stack.count())] for wid in to_delete: plot_stack.removeWidget(wid) wid.deleteLater() - self._widget_stack = {} + self._widget_stack = {} # dict of dicts. Outer key is channel label. Inner key is feature label. plot_class_map = { 'Raw': RawPlots, 'Mapping': MappingPlots, 'STN': STNPlots, 'LFP': LFPPlots, 'Spikes': SpikePlots, None: NullPlotWidget } - n_feats = len(self._features_settings['features']) - my_theme = self._plot_settings["theme"] - for chan_ix, chan_label in enumerate({"None"}.union(self._chan_labels)): - self._plot_settings["color_iterator"] = (self._plot_settings["color_iterator"] + 1) % len(my_theme['pencolors']) + n_feats = len(self._features) + chan_labels = self._procedure.get("chan_labels", []) + ["None"] + self._theme["color_iterator"] = -1 + for chan_ix, chan_label in enumerate(chan_labels): + self._theme["color_iterator"] = (self._theme["color_iterator"] + 1) % len(self._theme["pencolors"]) + pcolor = self._theme["pencolors"][self._theme["color_iterator"]] self._widget_stack[chan_label] = {} - for feat_ix, feat_label in enumerate(self._features_settings["features"]): - self._widget_stack[chan_label][feat_label] = [n_feats*chan_ix + feat_ix, 0] - w_cls = plot_class_map[feat_label] if feat_label in plot_class_map else NullPlotWidget - plot_stack.addWidget(w_cls(dict(self._plot_settings))) + for feat_ix, f in self._features.items(): + self._widget_stack[chan_label][f] = [n_feats * chan_ix + feat_ix, 0] + w_cls = plot_class_map[f] if f in plot_class_map else NullPlotWidget + plot_stack.addWidget(w_cls({**self._plot_config, "pen_color": pcolor})) plot_stack.setCurrentIndex(0) - def reset_stack(self): - plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") - chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") - feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") - if plot_stack is not None and chan_combo is not None and feat_combo is not None: - chan_key = chan_combo.currentText() - feat_key = feat_combo.currentText() + def refresh_axes(self, ix: int = 0): + # We ignore ix from single-widget interaction events. + # We instead retrieve the key from a combination of widgets. + plot_stack: QtWidgets.QStackedWidget = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") + chan_combo: QtWidgets.QComboBox = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + feat_combo: QtWidgets.QComboBox = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") + chan_key = chan_combo.currentText() + feat_key = feat_combo.currentText() + if chan_key and feat_key: idx = self._widget_stack[chan_key][feat_key][0] plot_stack.setCurrentIndex(idx) - def handle_procedure_id(self, procedure_id): - self._db.select_procedure(procedure_id) - self._chan_labels = self._db.list_channel_labels() - self._reset_chan_select_items() - self._reset_widget_stack() + def clear(self): + pass + + def reset_feat_select_items(self): + feat_combo: QtWidgets.QComboBox = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") + feat_combo.blockSignals(True) + feat_combo.clear() + feat_combo.addItems(list(self._features.values())) + feat_combo.blockSignals(False) + feat_combo.setCurrentIndex(0) + + def reset_chan_select_items(self): + chan_labels = self._procedure.get("chan_labels", []) + ["None"] + chan_combo: QtWidgets.QComboBox = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + chan_combo.blockSignals(True) + chan_combo.clear() + chan_combo.addItems(chan_labels) + chan_combo.blockSignals(False) + chan_combo.setCurrentIndex(0) # Triggers an emission --> refresh_axes + + def handle_ipc_channel_select(self, chan_settings: dict): + sweep_control: QtWidgets.QCheckBox = self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox") + if sweep_control.isChecked(): + chan_combo: QtWidgets.QComboBox = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + avail_chans = [chan_combo.itemText(_) for _ in range(chan_combo.count())] + if chan_settings["label"] in avail_chans: + chan_combo.setCurrentIndex(avail_chans.index(chan_settings["label"])) + + +class FeaturesGUI(CustomGUI): + widget_cls = FeatureStackWidget + + def __init__(self): + """ + - Visualizes raw segments and calculated features from the database + - Widgets to navigate the visualizations + - Subscribes to zmq messages to know which features to grab + - Subscribes to zmq messages to know which channels to show + - For semi-realtime application, you should be running Depth_Process and Features_Process (from serf package) + in background. + """ + self._plot_settings = {} + self._subject_settings = {} + self._procedure_settings = {} + self._features_settings = {} + self._chan_labels = [] + self._plot_widget: FeatureStackWidget | None = None # This will get updated in super init but it helps type hints + self._db = DBWrapper() + super().__init__() + # Try to get the current procedure. + self._features_sock.send_string("features refresh") + + def parse_settings(self): + super().parse_settings() + + if "plot" not in self._plot_settings: + self._plot_settings["plot"] = {} + + if "filter" not in self._plot_settings: + self._plot_settings["filter"] = {} + + for ini_path in self._settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + settings.beginGroup("plot") + for k, t in {"x_start": int, "x_stop": int, "y_range": int, "image_plot": bool}.items(): + if k in settings.allKeys(): + self._plot_settings["plot"][k] = settings.value(k, type=t) + settings.endGroup() + + settings.beginGroup("buffer") + if "highpass" in settings.allKeys(): + self._plot_settings["filter"]["highpass"] = settings.value("highpass", type=bool) + settings.endGroup() + + settings.beginGroup("features") + feat_inds = [int(_) for _ in settings.childGroups()] + feat_inds.sort() + for f_ind in feat_inds: + settings.beginGroup(str(f_ind)) + self._features_settings[f_ind] = {settings.value("name", type=str): settings.value("enable", type=bool)} + settings.endGroup() + settings.endGroup() # features + + settings.beginGroup("theme") + for k in settings.allKeys(): + self._theme_settings[k] = settings.value(k, type=str) + settings.endGroup() # status + + def _setup_ipc(self): + self._zmq_context = zmq.Context() + + # Subscribe to channel-change notifications -- so we can update which channel we are viewing + self._chan_sock = self._zmq_context.socket(zmq.SUB) + self._chan_sock.connect(f"tcp://localhost:{self._ipc_settings['channel_select']}") + self._chan_sock.setsockopt_string(zmq.SUBSCRIBE, "channel_select") + + # Subscribe to procedure set notifications -- load data + self._procedure_sock = self._zmq_context.socket(zmq.SUB) + self._procedure_sock.connect(f"tcp://localhost:{self._ipc_settings['procedure_settings']}") + self._procedure_sock.setsockopt_string(zmq.SUBSCRIBE, "procedure_settings") + + # Subscribe to snippet status -- update icon and trigger plot update + self._snippet_sock = self._zmq_context.socket(zmq.SUB) + self._snippet_sock.connect(f"tcp://localhost:{self._ipc_settings['snippet_status']}") + self._snippet_sock.setsockopt_string(zmq.SUBSCRIBE, "snippet_status") + + # Publish refresh notification -- on startup and when refresh is clicked + self._features_sock = self._zmq_context.socket(zmq.PUB) + self._features_sock.bind(f"tcp://*:{self._ipc_settings['features']}") + + def _cleanup_ipc(self): + for _sock in [self._chan_sock, self._procedure_sock, self._snippet_sock, self._features_sock]: + _sock.setsockopt(zmq.LINGER, 0) + _sock.close() + self._zmq_context.term() + + def try_reset_widget(self): + # Do not call super().try_reset_widget(), because it expects a data source and here we don't care + # if cerebus is running + self.on_plot_closed(force=True) + source_dict = { + "channel_names": self._chan_labels, + "chan_states": None, + "srate": None + } + # srate = int(SAMPLINGGROUPS[self._group_ix]) + # extra = {} + # + # # self._chan_states = pd.DataFrame(columns=['name', 'src', 'unit', 'type', 'pos']) + # for ch_ix, ch_dict in enumerate(self._group_info): + # chan_names.append(ch_dict['label']) + # ch_info = self._cbsdk_conn.get_channel_info(ch_dict['chan']) + # chan_states.append({ + # 'name': ch_dict['label'], + # 'src': ch_dict['chan'], + # 'unit': ch_dict['unit'], + # 'gain': ch_dict['gain'], + # 'spkthrlevel': ch_info['spkthrlevel'] + # }) + self._plot_widget = self.__class__.widget_cls( + source_dict, + theme=self._theme_settings, + plot=self._plot_settings, + features=self._features_settings, + procedure={**self._procedure_settings, "chan_labels": self._chan_labels} + ) + self._plot_widget.was_closed.connect(self.on_plot_closed) + self.setCentralWidget(self._plot_widget) + + # After creating _plot_widget, update its widgets + refresh_pb: QtWidgets.QPushButton = self._plot_widget.findChild(QtWidgets.QPushButton, "Refresh_PushButton") + refresh_pb.clicked.connect(self.on_refresh_clicked) def manage_refresh(self): - plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") - chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") - feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") + plot_stack: QtWidgets.QStackedWidget = self._plot_widget.findChild(QtWidgets.QStackedWidget, "Plot_Stack") + chan_combo: QtWidgets.QComboBox = self._plot_widget.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + feat_combo: QtWidgets.QComboBox = self._plot_widget.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") stack_item = self._widget_stack[chan_combo.currentText()][feat_combo.currentText()] plot_stack.widget(stack_item[0]).clear_plot() stack_item[1] = 0 - def on_range_edited(self): - range_edit = self.findChild(QtWidgets.QLineEdit, "Range_LineEdit") - self._plot_settings["y_range"] = float(range_edit.text()) - - def on_sweep_clicked(self): - sweep_control = self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox") - chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") - hp_chk = self.findChild(QtWidgets.QCheckBox, "HP_CheckBox") - range_edit = self.findChild(QtWidgets.QLineEdit, "Range_LineEdit") - b_enable = not sweep_control.isChecked() - for _ in [chan_combo, hp_chk, range_edit]: - _.setEnabled(b_enable) - def on_refresh_clicked(self): self._features_sock.send_string("features refresh") - def _check_subs(self): + def _check_ipc(self): try: received_msg = self._chan_sock.recv_string(flags=zmq.NOBLOCK)[len("channel_select") + 1:] chan_settings = json.loads(received_msg) # ^ dict with k,v_type "channel":int, "range":[float, float], "highpass":bool - sweep_control = self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox") - if sweep_control.isChecked(): - chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") - avail_chans = [chan_combo.itemText(_) for _ in range(chan_combo.count())] - if chan_settings["label"] in avail_chans: - chan_combo.setCurrentIndex(avail_chans.index(chan_settings["label"])) + self._plot_widget.handle_ipc_channel_select(chan_settings) except zmq.ZMQError: pass @@ -313,25 +357,46 @@ def _check_subs(self): procedure_settings = json.loads(received_msg) # ^ dict with settings for "procedure", "subject" if "procedure" in procedure_settings and "procedure_id" in procedure_settings["procedure"]: - self.handle_procedure_id(procedure_settings["procedure"]["procedure_id"]) + procedure_id = procedure_settings["procedure"]["procedure_id"] + if procedure_id != self._db.current_procedure or len(self._chan_labels) == 0: + self._db.select_procedure(procedure_id) + self._chan_labels = self._db.list_channel_labels() + self.try_reset_widget() except zmq.ZMQError: pass - def update(self): - plot_stack = self.findChild(QtWidgets.QStackedWidget, "Plot_Stack") - chan_combo = self.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") - feat_combo = self.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") - b_sweep_sync = self.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox").isChecked() + try: + received_msg = self._snippet_sock.recv_string(flags=zmq.NOBLOCK)[len("snippet_status") + 1:] - self._check_subs() + # Depth processor has (re)started. Maybe trigger a refresh? + # received_msg == "startup" + + # Update the status. + status_label = self._plot_widget.findChild(QtWidgets.QLabel, "Status_Label") + if received_msg in self._plot_widget.status_icons: + status_label.setPixmap(self._plot_widget.status_icons[received_msg]) + except zmq.ZMQError: + pass + + def do_plot_update(self): + self._check_ipc() + + # TODO: Maintain state of current channel label, current feature, last_id, do_hp + + plot_stack: QtWidgets.QStackedWidget = self._plot_widget.findChild(QtWidgets.QStackedWidget, "Plot_Stack") + chan_combo: QtWidgets.QComboBox = self._plot_widget.findChild(QtWidgets.QComboBox, "ChanSelect_ComboBox") + feat_combo: QtWidgets.QComboBox = self._plot_widget.findChild(QtWidgets.QComboBox, "FeatureSelect_ComboBox") + sweep_cb: QtWidgets.QCheckBox = self._plot_widget.findChild(QtWidgets.QCheckBox, "Sweep_CheckBox") - curr_chan_lbl = chan_combo.currentText() curr_widget = plot_stack.currentWidget() + curr_chan_lbl = chan_combo.currentText() if chan_combo.count() > 0 else "None" + b_sweep_sync = sweep_cb.isChecked() + if curr_chan_lbl != 'None': curr_feat = feat_combo.currentText() - stack_item = self._widget_stack[curr_chan_lbl][curr_feat] - do_hp = self._plot_settings["highpass"] - y_range = self._plot_settings["y_range"] + stack_item = self._plot_widget._widget_stack[curr_chan_lbl][curr_feat] + do_hp = self._plot_settings["filter"]["highpass"] + y_range = self._plot_settings["plot"]["y_range"] # If widget values don't match stored values if y_range != curr_widget.plot_config['y_range']\ @@ -348,7 +413,8 @@ def update(self): gt=curr_datum, do_hp=do_hp, return_uV=True) - elif curr_feat == 'Mapping': + elif False and curr_feat == 'Mapping': + # Currently not working all_data = self._db.load_mapping_response(chan_lbl=curr_chan_lbl, gt=curr_datum) else: diff --git a/open_mer/dbsgui/procedure.py b/open_mer/dbsgui/procedure.py index d1d363a..cc23310 100644 --- a/open_mer/dbsgui/procedure.py +++ b/open_mer/dbsgui/procedure.py @@ -1,81 +1,110 @@ -from pathlib import Path from typing import Optional import time + import json import zmq -from qtpy import QtCore, QtWidgets, QtGui +from qtpy import QtCore, QtWidgets from serf.tools.db_wrap import DBWrapper -from ..settings import defaults, locate_ini + +from .widgets.ini_window import IniWindow from .widgets.SettingsDialog import SettingsDialog import open_mer.data_source -class ProcedureGUI(QtWidgets.QMainWindow): +class ProcedureGUI(IniWindow): - def __init__(self, ini_file: str = None, **kwargs): - super().__init__(**kwargs) - self.status_icons = { - k: QtGui.QPixmap(str(Path(__file__).parents[0] / 'resources' / 'icons' / (v + '.png'))) for k, v in - {"accumulating": "depth_status_delay", "recording": "depth_status_in_use", "done": "depth_status_done", "notrecording": "depth_status_off"}.items() - } - self._b_recording = False + def __init__(self): self._data_source = None - self._restore_from_settings(ini_file) + self._source_settings = {} + + super().__init__() + + # TODO: It doesn't really have icons anymore. This code should move to FeaturesGUI + + self._b_recording = False + + self._init_connection() self._setup_ui() - self._setup_pubsub() self._subject_settings = {} self._procedure_settings = {} - self._buffer_settings = {} - self._features_settings = {} + self.show() self._do_modal_settings() - def closeEvent(self, *args, **kwargs): - self.toggle_recording(False) - - def _restore_from_settings(self, ini_file=None): - # Infer path to ini - ini_name = ini_file if ini_file is not None else (type(self).__name__ + '.ini') - self._settings_path = locate_ini(ini_name) - settings = QtCore.QSettings(str(self._settings_path), QtCore.QSettings.IniFormat) - default_dims = defaults.WINDOWDIMS_DICT[type(self).__name__] - settings.beginGroup("MainWindow") - self.move(settings.value("pos", QtCore.QPoint(default_dims[0], default_dims[1]))) - size_xy = settings.value("size", QtCore.QSize(default_dims[2], default_dims[3])) - self.resize(size_xy) - self.setMaximumWidth(size_xy.width()) - if settings.value("fullScreen", 'false') == 'true': - self.showFullScreen() - elif settings.value("maximized", 'false') == 'true': - self.showMaximized() - if settings.value("frameless", 'false') == 'true': - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - settings.endGroup() - - # Infer data source from ini file, setup data source - settings.beginGroup("data-source") - src_cls = getattr(open_mer.data_source, settings.value("class")) - # Get the _data_source. Note this might trigger on_source_connected before child - # finishes parsing settings. - self._data_source = src_cls(scoped_settings=settings) - self._recording_path = settings.value("basepath", defaults.BASEPATH, type=str) - settings.endGroup() + def parse_settings(self): + # Handles MainWindow geometry and collects self._theme_settings and self._ipc_settings + super().parse_settings() + # Get custom settings + for ini_path in self._settings_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + # Store / update connection settings which will be triggered after all ini processing + settings.beginGroup("data-source") + for k, t in { + "class": str, + "basepath": str, + }.items(): + if k in settings.allKeys(): + if k == "class": + src_cls = getattr(open_mer.data_source, str(settings.value(k, type=t))) + self._source_settings[k] = src_cls + self._source_settings["settings_path"] = ini_path + else: + self._source_settings[k] = settings.value(k, type=t) + settings.endGroup() # data-source + + def _setup_ipc(self): + self._sock_context = zmq.Context() + + self._features_sock = self._sock_context.socket(zmq.SUB) + self._features_sock.connect(f"tcp://localhost:{self._ipc_settings['features']}") + self._features_sock.setsockopt_string(zmq.SUBSCRIBE, "features") + + self._snippet_sock = self._sock_context.socket(zmq.SUB) + self._snippet_sock.connect(f"tcp://localhost:{self._ipc_settings['snippet_status']}") + self._snippet_sock.setsockopt_string(zmq.SUBSCRIBE, "snippet_status") + + self._procedure_sock = self._sock_context.socket(zmq.PUB) + self._procedure_sock.bind(f"tcp://*:{self._ipc_settings['procedure_settings']}") + + def _cleanup_ipc(self): + for _sock in [self._features_sock, self._procedure_sock]: + _sock.setsockopt(zmq.LINGER, 0) + _sock.close() + self._sock_context.term() + + @QtCore.Slot(QtCore.QObject) + def _on_source_connected(self, data_source): + self._data_source = data_source + + def _init_connection(self): + if "class" in self._source_settings and self._source_settings["class"] is not None: + _data_source = self._source_settings["class"]( + settings_path=self._source_settings["settings_path"], + on_connect_cb=self._on_source_connected + ) + + def __del__(self): + if self._data_source is not None: + self._data_source.set_recording_state(False, {"filename": None}) + self._data_source.disconnect_requested() + super().__del__() def _setup_ui(self): + self.setWindowTitle("OpenMER Procedure") main_widget = QtWidgets.QWidget() - main_widget.setLayout(QtWidgets.QVBoxLayout()) self.setCentralWidget(main_widget) - self._setup_control_panel() - def _setup_control_panel(self): lo = QtWidgets.QHBoxLayout() + # Settings button settings_pb = QtWidgets.QPushButton("Settings") settings_pb.setObjectName("Settings_PushButton") settings_pb.setMaximumWidth(50) settings_pb.clicked.connect(lambda state: self._do_modal_settings()) lo.addWidget(settings_pb) + # Record button lo.addSpacing(5) record_pb = QtWidgets.QPushButton("Record") @@ -83,28 +112,8 @@ def _setup_control_panel(self): record_pb.setMaximumWidth(50) record_pb.clicked.connect(lambda: self.toggle_recording(None)) lo.addWidget(record_pb) - # Status Label - lo.addSpacing(20) - status_label = QtWidgets.QLabel() - status_label.setObjectName("Status_Label") - status_label.setPixmap(self.status_icons["notrecording"]) - lo.addWidget(status_label) - - self.centralWidget().layout().addLayout(lo) - - def _setup_pubsub(self, zmq_ctrl_port=60001, zmq_depth_port=60002, zmq_feat_port=60004): - context = zmq.Context() - self._depth_sock = context.socket(zmq.SUB) - self._depth_sock.connect(f"tcp://localhost:{zmq_depth_port}") - self._depth_sock.setsockopt_string(zmq.SUBSCRIBE, "snippet_status") - - self._features_sock = context.socket(zmq.SUB) - self._features_sock.connect(f"tcp://localhost:{zmq_feat_port}") - self._features_sock.setsockopt_string(zmq.SUBSCRIBE, "features") - - self._procedure_sock = context.socket(zmq.PUB) - self._procedure_sock.bind(f"tcp://*:{zmq_ctrl_port}") + main_widget.setLayout(lo) def _publish_settings(self): # Sanitize some settings for serialization. @@ -118,25 +127,20 @@ def _publish_settings(self): _proc_settings[k] = v send_dict = { - "features": self._features_settings, "procedure": _proc_settings, - "buffer": self._buffer_settings, - "subject": {**self._subject_settings, "birthday": self._subject_settings["birthday"].isoformat()} + "subject": {**self._subject_settings, "birthday": self._subject_settings["birthday"].isoformat()}, + "recording": {"state": self._b_recording} } - # TODO: features and buffer settings should just be in ini files. They don't change frequently. self._procedure_sock.send_string("procedure_settings " + json.dumps(send_dict)) def _do_modal_settings(self): - win = SettingsDialog(self._subject_settings, - self._procedure_settings, - # self._buffer_settings, - # self._features_settings - ) + win = SettingsDialog(self._subject_settings, self._procedure_settings) result = win.exec_() if result == QtWidgets.QDialog.Accepted: + # Update self._subject_settings and self._procedure_settings from values in the dialog win.update_settings() - # Create or load subject + # Load or create subject # Returns subject_id/-1 whether subject is properly created or not sub_id = DBWrapper().load_or_create_subject(self._subject_settings) @@ -144,10 +148,10 @@ def _do_modal_settings(self): print("Subject not created.") return False else: - self._subject_settings['subject_id'] = sub_id - self._procedure_settings['subject_id'] = sub_id - tmp = DBWrapper() - proc_id = tmp.load_or_create_procedure(self._procedure_settings) + # Update settings with the true subject and procedure ids from the database + self._subject_settings["subject_id"] = sub_id + self._procedure_settings["subject_id"] = sub_id + proc_id = DBWrapper().load_or_create_procedure(self._procedure_settings) self._procedure_settings["procedure_id"] = proc_id self._publish_settings() @@ -155,28 +159,31 @@ def _do_modal_settings(self): return False def toggle_recording(self, on_off: Optional[bool] = None): + if self._data_source is None: + return + on_off = on_off if on_off is not None else not self._b_recording if on_off: if self._b_recording: # Wants on but already recording. Stop then start again. self.toggle_recording(False) time.sleep(0.100) + self._b_recording = True self._publish_settings() # re-send the settings - # start nsp recording - self._run_recording(True) else: - self._run_recording(False) + self._b_recording = False + self._run_recording(self._b_recording) + + record_pb: QtWidgets.QPushButton = self.findChild(QtWidgets.QPushButton, "Record_PushButton") + rec_facecolor = "red" if self._b_recording else "gray" + record_pb.setStyleSheet("QPushButton { color: white; " + f"background-color : {rec_facecolor}; " + f"border-color : {rec_facecolor}; " + "border-width: 2px}") def _run_recording(self, on_off: bool): - # TODO: Use settings - import os f_name, m_name, l_name = self.parse_patient_name(self._subject_settings['name']) - file_info = {'filename': os.path.normpath(os.path.join(self._recording_path, - self._subject_settings['id'], - self._procedure_settings['date'].strftime('%m%d%y') + '_' + - self._subject_settings['id'] + '_' + - self._procedure_settings['target_name'] + '_' + - self._procedure_settings['recording_config'])), + file_info = {'filename': self._get_filename(), 'comment': self._subject_settings['NSP_comment'], 'patient_info': {'ID': self._subject_settings['id'], # if only single name, returned in l_name @@ -207,8 +214,23 @@ def parse_patient_name(full_name): l_name = str.join(' ', names[l_idx:]) return f_name, m_name, l_name + def _get_filename(self): + import os + return os.path.normpath(os.path.join( + self._source_settings["basepath"], + self._subject_settings["id"], + "_".join([ + self._procedure_settings["date"].strftime('%m%d%y'), + self._subject_settings["id"], + self._procedure_settings["target_name"], + self._procedure_settings["recording_config"] + ]) + )) + def update(self): b_publish_settings = False + + # Fetch any new features try: received_msg = self._features_sock.recv_string(flags=zmq.NOBLOCK)[len("features") + 1:] b_publish_settings |= received_msg == "refresh" @@ -216,35 +238,11 @@ def update(self): pass try: - # Check for an update from the depth process - received_msg = self._depth_sock.recv_string(flags=zmq.NOBLOCK)[len("snippet_status")+1:] - - # Depth processor has (re)started since we last published our settings. Publish again. - b_publish_settings |= received_msg == "startup" - - # Update the status. TODO: Remove this or get the old code from FeaturesGUI. - status_label = self.findChild(QtWidgets.QLabel, "Status_Label") - status_label.setPixmap(self.status_icons[received_msg]) - - # Change the color of the recording button. - record_pb = self.findChild(QtWidgets.QPushButton, "Record_PushButton") - rec_facecolor_map = { - "startup": "orange", - "notrecording": "gray", - "recording": "red", - "accumulating": "yellow", - "done": "blue" - } - if received_msg in rec_facecolor_map: - rec_facecolor = rec_facecolor_map[received_msg] - else: - rec_facecolor = "gray" - record_pb.setStyleSheet("QPushButton { color: white; " - f"background-color : {rec_facecolor}; " - f"border-color : {rec_facecolor}; " - "border-width: 2px}") + received_msg = self._snippet_sock.recv_string(flags=zmq.NOBLOCK)[len("snippet_status") + 1:] + b_publish_settings |= received_msg == "refresh" except zmq.ZMQError: pass + # Fetch the status of the snippet process if b_publish_settings: self._publish_settings() diff --git a/open_mer/dbsgui/raster.py b/open_mer/dbsgui/raster.py index 861fc1c..4ab3201 100644 --- a/open_mer/dbsgui/raster.py +++ b/open_mer/dbsgui/raster.py @@ -187,7 +187,7 @@ class RasterGUI(CustomGUI): def __init__(self): self._plot_widget: RasterWidget | None = None # This will get updated in super init but it helps type hints super(RasterGUI, self).__init__() - self.setWindowTitle('RasterGUI') + self.setWindowTitle("Raster") def parse_settings(self): super().parse_settings() diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index 4a0ff2d..9736e1c 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -359,7 +359,7 @@ class SweepGUI(CustomGUI): def __init__(self): self._plot_widget: SweepWidget | None = None # This will get updated in super init but it helps type hints super(SweepGUI, self).__init__() - self.setWindowTitle('SweepGUI') + self.setWindowTitle("Sweep") self.pya_manager = pyaudio.PyAudio() self.pya_stream = None diff --git a/open_mer/dbsgui/waveform.py b/open_mer/dbsgui/waveform.py index a3ff43d..2bc0f82 100644 --- a/open_mer/dbsgui/waveform.py +++ b/open_mer/dbsgui/waveform.py @@ -148,7 +148,7 @@ class WaveformGUI(CustomGUI): def __init__(self): self._plot_widget: WaveformWidget | None = None # This will get updated in super init but it helps type hints super().__init__() - self.setWindowTitle("WaveformGUI") + self.setWindowTitle("Waveform") def parse_settings(self): super().parse_settings() diff --git a/open_mer/dbsgui/widgets/SettingsDialog.py b/open_mer/dbsgui/widgets/SettingsDialog.py index d59086e..91ad350 100644 --- a/open_mer/dbsgui/widgets/SettingsDialog.py +++ b/open_mer/dbsgui/widgets/SettingsDialog.py @@ -7,27 +7,33 @@ from qtpy.QtGui import QRegularExpressionValidator from serf.tools.db_wrap import DBWrapper -# Settings -from open_mer.settings.defaults import BUFFERLENGTH, SAMPLELENGTH, DELAYBUFFER, OVERWRITEDEPTH - class SubjectWidget(QWidget): subject_change = Signal(int) - def __init__(self, subject_settings): - super(SubjectWidget, self).__init__() + def __init__(self, subject_settings, parent=None): + super().__init__(parent=parent) + + self._setup_ui() + + # Subject Settings + self.subject_settings = subject_settings + if not self.subject_settings: + self.update_settings_from_db(-1) + + self.update_subj_widgets_from_settings() + def _setup_ui(self): subject_layout = QGridLayout(self) subject_layout.setColumnMinimumWidth(2, 60) subject_layout.addWidget(QLabel("Id: "), 0, 0, 1, 1) self.id_combo = QComboBox() self.id_combo.setEditable(True) - self.id_combo.addItem('') + self.id_combo.addItem("") self.id_combo.addItems(DBWrapper().list_all_subjects()) - self.id_combo.currentIndexChanged.connect(self.load_subject) + self.id_combo.currentIndexChanged.connect(self.load_subject) # Which transitively calls check_subject self.id_combo.lineEdit().editingFinished.connect(self.check_subject) - subject_layout.addWidget(self.id_combo, 0, 1, 1, 4) subject_layout.addWidget(QLabel("Name: "), 1, 0, 1, 1) @@ -35,10 +41,10 @@ def __init__(self, subject_settings): self.name_edit.setMaxLength(135) subject_layout.addWidget(self.name_edit, 1, 1, 1, 4) - _subj_enums = DBWrapper().return_enums('subject') + _subj_enums = DBWrapper().return_enums("subject") subject_layout.addWidget(QLabel("Sex: "), 2, 0, 1, 1) self.sex_combo = QComboBox() - self.sex_combo.addItems(_subj_enums['sex'] if 'sex' in _subj_enums.keys() else []) + self.sex_combo.addItems(_subj_enums["sex"] if "sex" in _subj_enums.keys() else []) self.sex_combo.setCurrentIndex(0) subject_layout.addWidget(self.sex_combo, 2, 1, 1, 1) @@ -51,20 +57,13 @@ def __init__(self, subject_settings): self.file_comment.setMaximumHeight(150) subject_layout.addWidget(self.file_comment, 4, 1, 1, 4) - # Subject Settings - self.subject_settings = subject_settings - if not self.subject_settings: - self.update_settings_from_db(-1) - - self.update_subj_widgets_from_settings() - def update_subj_widgets_from_settings(self): - self.name_edit.setText(self.read_dict_value(self.subject_settings, 'name')) - self.id_combo.setCurrentText(self.read_dict_value(self.subject_settings, 'id')) - self.sex_combo.setCurrentText(self.read_dict_value(self.subject_settings, 'sex')) - dob = self.read_dict_value(self.subject_settings, 'birthday') - if dob not in [None, '']: - q_dob = QDate.fromString(dob, 'yyyy-MM-d') + self.name_edit.setText(self.subject_settings.get("name", "")) + self.id_combo.setCurrentText(self.subject_settings.get("id", "")) + self.sex_combo.setCurrentText(self.subject_settings.get("sex", "")) + dob = self.subject_settings.get("birthday", "") + if dob not in [None, ""]: + q_dob = QDate.fromString(dob.isoformat() if hasattr(dob, "year") else dob, "yyyy-MM-d") self.dob_calendar.setSelectedDate(q_dob) else: self.dob_calendar.setSelectedDate(QDate.currentDate()) @@ -82,23 +81,19 @@ def check_subject(self): # when changing the id in the combobox, can be modifying or entering an existing subject id. Check to load data # if so. curr_id = self.id_combo.currentText() - if curr_id != '': + if curr_id != "": self.update_settings_from_db(curr_id) - self.subject_change.emit(self.subject_settings['subject_id']) + self.subject_change.emit(self.subject_settings["subject_id"]) else: self.update_settings_from_db(-1) self.subject_change.emit(-1) - @staticmethod - def read_dict_value(dictionary, value): - return str(dictionary[value]) if value in dictionary.keys() else '' - def to_dict(self): - self.subject_settings['id'] = self.id_combo.currentText() - self.subject_settings['name'] = self.name_edit.text() - self.subject_settings['sex'] = self.sex_combo.currentText() - self.subject_settings['birthday'] = self.dob_calendar.selectedDate().toPython() - self.subject_settings['NSP_comment'] = self.file_comment.toPlainText() + self.subject_settings["id"] = self.id_combo.currentText() + self.subject_settings["name"] = self.name_edit.text() + self.subject_settings["sex"] = self.sex_combo.currentText() + self.subject_settings["birthday"] = self.dob_calendar.selectedDate().toPython() + self.subject_settings["NSP_comment"] = self.file_comment.toPlainText() class ProcedureWidget(QWidget): @@ -112,7 +107,7 @@ def __init__(self, procedure_settings): if not self.procedure_settings: self.update_settings_from_db(-1) - self.proc_enums = DBWrapper().return_enums('procedure') + self.proc_enums = DBWrapper().return_enums("procedure") proc_layout = QGridLayout(self) row = 0 @@ -261,198 +256,74 @@ def update_settings_from_db(self, idx): self.procedure_settings.update(res_dict) def update_proc_widgets_from_settings(self): - self.target_name.setText(self.read_dict_value('target_name')) - self.type_combo.setCurrentText(self.read_dict_value('type')) - self.rec_combo.setCurrentText(self.read_dict_value('recording_config')) - self.electrode_combo.setCurrentText(self.read_dict_value('electrode_config')) - self.medic_combo.setCurrentText(self.read_dict_value('medication_status')) - self.offset_size.setText(str(self.read_dict_value('offset_size'))) - self.offset_direction_combo.setCurrentText(self.read_dict_value('offset_direction')) - entry = self.read_dict_value('entry') + self.target_name.setText(self.procedure_settings.get("target_name", "")) + self.type_combo.setCurrentText(self.procedure_settings.get("type", "")) + self.rec_combo.setCurrentText(self.procedure_settings.get("recording_config", "")) + self.electrode_combo.setCurrentText(self.procedure_settings.get("electrode_config", "")) + self.medic_combo.setCurrentText(self.procedure_settings.get("medication_status", "")) + self.offset_size.setText(str(self.procedure_settings.get("offset_size", None))) + self.offset_direction_combo.setCurrentText(self.procedure_settings.get("offset_direction", "")) + entry = self.procedure_settings.get("entry", None) if entry is None: entry = [0., 0., 0.] self.entry_x.setText(str(entry[0])) self.entry_y.setText(str(entry[1])) self.entry_z.setText(str(entry[2])) - target = self.read_dict_value('target') + target = self.procedure_settings.get("target", None) if target is None: target = [0., 0., 0.] self.target_x.setText(str(target[0])) self.target_y.setText(str(target[1])) self.target_z.setText(str(target[2])) - ddt = self.read_dict_value('distance_to_target') + ddt = self.procedure_settings.get("distance_to_target", None) if ddt is None: ddt = 0.000 self.dist_to_target.setText(str(ddt)) # self.update_dist_to_target() - a = self.read_dict_value('a') + a = self.procedure_settings.get("a", None) if a is None: a = [0., 0., 0.] self.a_x.setText(str(a[0])) self.a_y.setText(str(a[1])) self.a_z.setText(str(a[2])) - e = self.read_dict_value('e') + e = self.procedure_settings.get("e", None) if e is None: e = [0., 0., 0.] self.e_x.setText(str(e[0])) self.e_y.setText(str(e[1])) self.e_z.setText(str(e[2])) - def read_dict_value(self, value): - return self.procedure_settings[value] if value in self.procedure_settings.keys() else None - def to_dict(self): - self.procedure_settings['type'] = self.type_combo.currentText() - self.procedure_settings['a'] = np.array([float(self.a_x.text()), + self.procedure_settings["type"] = self.type_combo.currentText() + self.procedure_settings["a"] = np.array([float(self.a_x.text()), float(self.a_y.text()), float(self.a_z.text())], dtype=float) - self.procedure_settings['distance_to_target'] = float(self.dist_to_target.text()) - self.procedure_settings['e'] = np.array([float(self.e_x.text()), + self.procedure_settings["distance_to_target"] = float(self.dist_to_target.text()) + self.procedure_settings["e"] = np.array([float(self.e_x.text()), float(self.e_y.text()), float(self.e_z.text())], dtype=float) - self.procedure_settings['electrode_config'] = self.electrode_combo.currentText() - self.procedure_settings['entry'] = np.array([float(self.entry_x.text()), + self.procedure_settings["electrode_config"] = self.electrode_combo.currentText() + self.procedure_settings["entry"] = np.array([float(self.entry_x.text()), float(self.entry_y.text()), float(self.entry_z.text())], dtype=float) - self.procedure_settings['medication_status'] = self.medic_combo.currentText() - self.procedure_settings['target_name'] = self.target_name.text() - self.procedure_settings['recording_config'] = self.rec_combo.currentText() - self.procedure_settings['target'] = np.array([float(self.target_x.text()), + self.procedure_settings["medication_status"] = self.medic_combo.currentText() + self.procedure_settings["target_name"] = self.target_name.text() + self.procedure_settings["recording_config"] = self.rec_combo.currentText() + self.procedure_settings["target"] = np.array([float(self.target_x.text()), float(self.target_y.text()), float(self.target_z.text())], dtype=float) - self.procedure_settings['offset_direction'] = self.offset_direction_combo.currentText() - self.procedure_settings['offset_size'] = float(self.offset_size.text()) - - -# class BufferWidget(QWidget): -# def __init__(self, buffer_settings): -# super(BufferWidget, self).__init__() -# -# # Settings -# self.buffer_settings = buffer_settings -# # Fill in missing values with defaults. -# buffer_defaults = { -# "buffer_length": "{:.3f}".format(BUFFERLENGTH), -# "sample_length": "{:.3f}".format(SAMPLELENGTH), -# "delay_buffer": "{:.3f}".format(DELAYBUFFER), -# "overwrite_depth": OVERWRITEDEPTH, -# "electrode_settings": {} -# } -# for k, def_v in buffer_defaults.items(): -# self.buffer_settings[k] = self.buffer_settings.get(k, def_v) -# -# # Create widgets and populate with values from settings. -# self.buffer_widgets = {} -# buffer_layout = QGridLayout(self) -# row = -1 -# if 'electrode_settings' in self.buffer_settings.keys(): -# for label, sett in self.buffer_settings['electrode_settings'].items(): -# row += 1 -# buffer_layout.addWidget(QLabel(label), row, 0, 1, 1) -# self.buffer_widgets[label] = {} -# self.buffer_widgets[label]['chk_threshold'] = QCheckBox("Threshold") -# self.buffer_widgets[label]['chk_threshold'].setChecked(bool(sett['threshold'])) -# self.buffer_widgets[label]['edit_validity'] = QLineEdit() -# self.buffer_widgets[label]['edit_validity'].setText(str(sett['validity'])) -# -# buffer_layout.addWidget(self.buffer_widgets[label]['chk_threshold'], row, 1, 1, 1) -# buffer_layout.addWidget(QLabel('Validity Threshold (%)'), row, 2, 1, 1) -# buffer_layout.addWidget(self.buffer_widgets[label]['edit_validity'], row, 3, 1, 1) -# -# row += 1 -# buffer_layout.addWidget(QLabel("Depth buffer size (s): "), row, 0, 1, 1) -# self.edit_buffer_length = QLineEdit(self.buffer_settings['buffer_length']) -# self.edit_buffer_length.setInputMask("0.000") -# self.edit_buffer_length.setFixedWidth(40) -# buffer_layout.addWidget(self.edit_buffer_length, row, 1, 1, 1) -# -# row += 1 -# buffer_layout.addWidget(QLabel("Depth samples size (s): "), row, 0, 1, 1) -# self.edit_sample_length = QLineEdit(self.buffer_settings['sample_length']) -# self.edit_sample_length.setInputMask("0.000") -# self.edit_sample_length.setFixedWidth(40) -# buffer_layout.addWidget(self.edit_sample_length, row, 1, 1, 1) -# -# row += 1 -# buffer_layout.addWidget(QLabel("Delay depth recording (s): "), row, 0, 1, 1) -# self.delay_buffer = QLineEdit(self.buffer_settings['delay_buffer']) -# self.delay_buffer.setInputMask("0.000") -# self.delay_buffer.setFixedWidth(40) -# buffer_layout.addWidget(self.delay_buffer, row, 1, 1, 1) -# -# row += 1 -# self.overwrite_depth = QCheckBox("Overwrite depth values") -# self.overwrite_depth.setChecked(self.buffer_settings['overwrite_depth']) -# buffer_layout.addWidget(self.overwrite_depth, row, 0, 1, 1) -# -# def to_dict(self): -# # convert all fields to dictionary and return it -# self.buffer_settings['buffer_length'] = self.edit_buffer_length.text() -# self.buffer_settings['sample_length'] = self.edit_sample_length.text() -# self.buffer_settings['delay_buffer'] = self.delay_buffer.text() -# self.buffer_settings['overwrite_depth'] = self.overwrite_depth.isChecked() -# -# for key, value in self.buffer_widgets.items(): -# self.buffer_settings['electrode_settings'][key] = {} -# self.buffer_settings['electrode_settings'][key]['threshold'] = value['chk_threshold'].isChecked() -# self.buffer_settings['electrode_settings'][key]['validity'] = float(value['edit_validity'].text()) -# -# -# class FeaturesWidget(QWidget): -# def __init__(self, features_settings): -# super(FeaturesWidget, self).__init__() -# -# # Settings -# self.feature_categories = DBWrapper().all_features.keys() -# self.features_settings = features_settings -# if not self.features_settings: -# self.features_settings = {} -# -# # Check if default values are defined -# for cat in self.feature_categories: -# # defaults to true, compute all features -# self.features_settings[cat] = True -# -# self.features_widgets = {} -# -# features_layout = QGridLayout(self) -# -# # Add an option to toggle all features -# self.all_features = QCheckBox('All') -# self.all_features.setChecked(False) -# self.all_features.clicked.connect(self.toggle_all) -# features_layout.addWidget(self.all_features, 0, 0, 1, 1) -# for idx, (label, sett) in enumerate(self.features_settings.items()): -# self.features_widgets[label] = QCheckBox(label) -# self.features_widgets[label].setChecked(sett) -# self.features_widgets[label].clicked.connect(self.toggle) -# features_layout.addWidget(self.features_widgets[label], idx+1, 0, 1, 1) -# -# def toggle_all(self): -# for label, sett in self.features_widgets.items(): -# self.features_widgets[label].setChecked(self.all_features.isChecked()) -# -# def toggle(self): -# if any([not x.isChecked() for x in self.features_widgets.values()]): -# self.all_features.setChecked(False) -# -# def to_dict(self): -# for key, value in self.features_widgets.items(): -# self.features_settings[key] = value.isChecked() + self.procedure_settings["offset_direction"] = self.offset_direction_combo.currentText() + self.procedure_settings["offset_size"] = float(self.offset_size.text()) class SettingsDialog(QDialog): - def __init__(self, subject_settings, procedure_settings, - # buffer_settings, features_settings, - parent=None): + def __init__(self, subject_settings: dict, procedure_settings: dict, parent=None): super(SettingsDialog, self).__init__(parent) self.setWindowTitle("Enter settings.") - # settings dicts + # settings dicts - we will mutate the input dictionaries self.subject_settings = subject_settings self.procedure_settings = procedure_settings - # self.buffer_settings = buffer_settings - # self.features_settings = features_settings # Widgets to show/edit parameters. self.settings_layout = QVBoxLayout(self) @@ -464,22 +335,16 @@ def __init__(self, subject_settings, procedure_settings, self.proc_widget = ProcedureWidget(self.procedure_settings) tab_widget.addTab(self.proc_widget, 'Procedure') - # self.buff_widget = BufferWidget(self.buffer_settings) - # tab_widget.addTab(self.buff_widget, 'Buffer') - # - # self.feat_widget = FeaturesWidget(self.features_settings) - # tab_widget.addTab(self.feat_widget, 'Features') - self.settings_layout.addWidget(tab_widget) # signals self.subject_widget.subject_change.connect(self.proc_widget.change_subject) # update procedures when re-opening settings window - if 'subject_id' not in self.subject_settings.keys(): + if "subject_id" not in self.subject_settings.keys(): self.subject_widget.check_subject() - elif self.subject_settings['subject_id'] not in [None, '']: - self.proc_widget.change_subject(self.subject_settings['subject_id'], block=True) + elif self.subject_settings["subject_id"] not in [None, ""]: + self.proc_widget.change_subject(self.subject_settings["subject_id"], block=True) # OK and Cancel buttons buttons = QDialogButtonBox( @@ -492,5 +357,3 @@ def __init__(self, subject_settings, procedure_settings, def update_settings(self): self.subject_widget.to_dict() self.proc_widget.to_dict() - # self.buff_widget.to_dict() - # self.feat_widget.to_dict() diff --git a/open_mer/dbsgui/widgets/ini_window.py b/open_mer/dbsgui/widgets/ini_window.py index 5a2e729..bd5850d 100644 --- a/open_mer/dbsgui/widgets/ini_window.py +++ b/open_mer/dbsgui/widgets/ini_window.py @@ -1,5 +1,6 @@ from pathlib import Path import importlib.resources as pkg_resources + from qtpy import QtWidgets, QtCore, QtGui @@ -28,14 +29,14 @@ def _build_ini_paths(self): # Infer paths to ini files root_pkg = __package__.split(".")[0] ini_name = type(self).__name__ + '.ini' - with pkg_resources.path(f"{root_pkg}.resources", "settings") as pkg_path: - pkg_path = pkg_path + with pkg_resources.path(f"{root_pkg}.resources", "settings") as res_settings_path: + res_settings_path = res_settings_path home_path = Path.home() / f".{root_pkg}" # Add paths to settings files in ascending priority - self._settings_paths.append(pkg_path / "Style.ini") - self._settings_paths.append(pkg_path / "IPC.ini") - self._settings_paths.append(pkg_path / ini_name) + self._settings_paths.append(res_settings_path / "Style.ini") + self._settings_paths.append(res_settings_path / "IPC.ini") + self._settings_paths.append(res_settings_path / ini_name) self._settings_paths.append(home_path / "Style.ini") self._settings_paths.append(home_path / "IPC.ini") self._settings_paths.append(home_path / ini_name) diff --git a/open_mer/depth_source/base.py b/open_mer/depth_source/base.py index 1352447..e88679d 100644 --- a/open_mer/depth_source/base.py +++ b/open_mer/depth_source/base.py @@ -3,10 +3,10 @@ class MerDepthSource: - def __init__(self, scoped_settings: QtCore.QSettings): + def __init__(self, scale_factor=1.0, offset=0.0): # scale_factor should be 0.001 for FHC DDU V2, 1.0 otherwise. - self.scale_factor = scoped_settings.value("scale_factor", 1.0, type=float) - self.offset = scoped_settings.value("offset", 0.0, type=float) + self.scale_factor = scale_factor + self.offset = offset self.do_open() def do_open(self): diff --git a/open_mer/depth_source/cerebus.py b/open_mer/depth_source/cerebus.py index f2aa270..06a0f76 100644 --- a/open_mer/depth_source/cerebus.py +++ b/open_mer/depth_source/cerebus.py @@ -1,4 +1,3 @@ -from qtpy import QtCore from .base import MerDepthSource try: from cerebuswrapper import CbSdkConnection @@ -8,8 +7,8 @@ class CBSDKPlayback(MerDepthSource): - def __init__(self, scoped_settings: QtCore.QSettings): - super().__init__(scoped_settings) + def __init__(self, **kwargs): + super().__init__(**kwargs) def do_open(self): CbSdkConnection().connect() @@ -23,7 +22,7 @@ def do_open(self): def do_close(self): CbSdkConnection().disconnect() - def update(self): + def update(self) -> None | float: cbsdk_conn = CbSdkConnection() if cbsdk_conn.is_connected: comments = cbsdk_conn.get_comments() @@ -38,4 +37,4 @@ def update(self): if len(dtts) > 0: raw_value = dtts[-1] return raw_value - return None \ No newline at end of file + return None diff --git a/open_mer/depth_source/fhc.py b/open_mer/depth_source/fhc.py index 9d0be3d..c8c9697 100644 --- a/open_mer/depth_source/fhc.py +++ b/open_mer/depth_source/fhc.py @@ -1,22 +1,17 @@ import re - -from qtpy import QtCore -import serial -import serial.tools.list_ports +# serial imports happen in methods from .base import MerDepthSource -from ..settings import parse_ini_try_numeric class FHCSerial(MerDepthSource): - def __init__(self, scoped_settings: QtCore.QSettings): - scoped_settings.beginGroup("serial") - self._baudrate = parse_ini_try_numeric(scoped_settings, 'baudrate') or 19200 - self._com_port = scoped_settings.value("com_port") - self.ser = serial.Serial(timeout=1) + def __init__(self, serial={"baudrate": 19200, "com_port": "COM5"}, **kwargs): + import serial as pyserial + self._baudrate = serial["baudrate"] + self._com_port = serial["com_port"] + self.ser = pyserial.Serial(timeout=1) self._is_v2 = False - scoped_settings.endGroup() - super().__init__(scoped_settings) + super().__init__(**kwargs) @property def is_v2(self): @@ -29,8 +24,10 @@ def is_v2(self, value): self.offset = 60.00 if value else 0.00 def do_open(self): + import serial as pyserial + from serial.tools import list_ports self.ser.baudrate = self._baudrate - for port, desc, hwid in sorted(serial.tools.list_ports.comports()): + for port, desc, hwid in sorted(list_ports.comports()): if port == self._com_port: break else: @@ -39,7 +36,7 @@ def do_open(self): if not self.ser.is_open: self.ser.port = self._com_port try: - self.ser.open() # TODO: Add error. + self.ser.open() # TODO: Add error handling. # Silence transmission temporarily self.ser.write("AXON-\r".encode()) # Request version information. @@ -56,7 +53,7 @@ def do_open(self): # Resume transmission. self.ser.write("AXON+\r".encode()) _ = self.ser.readline() # Clear out the first response to AXON+ - except serial.serialutil.SerialException: + except pyserial.serialutil.SerialException: print("Could not open serial port") def do_close(self): diff --git a/open_mer/feature_plots/FeaturePlotWidgets.py b/open_mer/feature_plots/FeaturePlotWidgets.py index 23c5e74..a28bb2b 100644 --- a/open_mer/feature_plots/FeaturePlotWidgets.py +++ b/open_mer/feature_plots/FeaturePlotWidgets.py @@ -94,15 +94,15 @@ def clear_text_line(self): [x.setSymbolSize(6) for x in self.plot.curves] def configure_plot(self): - self.plot.setTitle(title=self.plot_config['title'], **{'color': 'w', 'size': '16pt'}) + self.plot.setTitle(title=self.plot_config["title"], **{"color": "w", "size": "16pt"}) self.plot.hideButtons() - if self.plot_config['image_plot'] and False: + if False and self.plot_config["image_plot"]: self.img = pg.ImageItem() self.plot.addItem(self.img) - pos = np.array(self.plot_config['c_lim']) - # color = np.array([[0, 0, 0, 255], self.plot_config['pen_color'].getRgb()], dtype=np.ubyte) + pos = np.array(self.plot_config["c_lim"]) + # color = np.array([[0, 0, 0, 255], self.plot_config["pen_color"].getRgb()], dtype=np.ubyte) # c_map = pg.ColorMap(pos, color) # lut = c_map.getLookupTable(pos[0], pos[1], 1024) @@ -116,7 +116,7 @@ def configure_plot(self): self.img.scale(1/1000, 1) self.img.setPos(DEPTHRANGE[0], 0) - if self.plot_config['marker_line'] is not None: + if self.plot_config["marker_line"] is not None: self.plot.addItem(pg.InfiniteLine(angle=0 if self.plot_config['swap_xy'] else 90, pos=self.plot_config['marker_line'], movable=False, @@ -127,25 +127,25 @@ def configure_plot(self): self.plot.scene().sigMouseClicked.connect(self.mouse_clicked) self.plot.vb.installEventFilter(self) - if self.plot_config['x_range']: - self.plot.setXRange(self.plot_config['x_range'][0], self.plot_config['x_range'][1], padding=0) - if self.plot_config['y_range']: - self.plot.setYRange(self.plot_config['y_range'][0], self.plot_config['y_range'][1], padding=0) - self.plot.setLabel('bottom', self.plot_config['x_label']) - self.plot.setLabel('left', self.plot_config['y_label']) - if not self.plot_config['x_axis']: - self.plot.hideAxis('bottom') - if not self.plot_config['y_axis']: - self.plot.hideAxis('left') + if self.plot_config["x_range"]: + self.plot.setXRange(self.plot_config["x_range"][0], self.plot_config["x_range"][1], padding=0) + if self.plot_config["y_range"]: + self.plot.setYRange(self.plot_config["y_range"][0], self.plot_config["y_range"][1], padding=0) + self.plot.setLabel("bottom", self.plot_config["x_label"]) + self.plot.setLabel("left", self.plot_config["y_label"]) + if not self.plot_config["x_axis"]: + self.plot.hideAxis("bottom") + if not self.plot_config["y_axis"]: + self.plot.hideAxis("left") font = QFont() font.setPixelSize(20) font.setBold(True) - self.plot.getAxis('bottom').setStyle(showValues=self.plot_config['x_ticks'], + self.plot.getAxis('bottom').setStyle(showValues=self.plot_config["x_ticks"], tickFont=font, tickTextOffset=10) self.plot.getAxis('bottom').setTextPen((255, 255, 255, 255)) - self.plot.getAxis('left').setStyle(showValues=self.plot_config['y_ticks'], + self.plot.getAxis('left').setStyle(showValues=self.plot_config["y_ticks"], tickFont=font) self.plot.getAxis('left').setTextPen((255, 255, 255, 255)) if self.plot_config['y_tick_labels']: @@ -243,23 +243,23 @@ def mouse_clicked(self, evt): if self.plot_config['auto_scale']: self.plot.autoRange(padding=0.05, items=self.plot.dataItems) if self.plot_config['x_range']: - self.plot.setXRange(self.plot_config['x_range'][0], self.plot_config['x_range'][1], + self.plot.setXRange(self.plot_config["x_range"][0], self.plot_config["x_range"][1], padding=0) else: - if self.plot_config['x_range']: - self.plot.setXRange(self.plot_config['x_range'][0], self.plot_config['x_range'][1], padding=0) - if self.plot_config['y_range']: - self.plot.setYRange(self.plot_config['y_range'][0], self.plot_config['y_range'][1], padding=0) + if self.plot_config["x_range"]: + self.plot.setXRange(self.plot_config["x_range"][0], self.plot_config["x_range"][1], padding=0) + if self.plot_config["y_range"]: + self.plot.setYRange(self.plot_config["y_range"][0], self.plot_config["y_range"][1], padding=0) def update_plot(self, all_data): if all_data is not None: # all_data is a dict {datum_id: [depth, np array of data]} for idx, data in all_data.items(): # append data - x = data[self.plot_config['x_name']] if self.plot_config['x_name'] in data else None + x = data[self.plot_config["x_name"]] if self.plot_config["x_name"] in data else None - if x not in [None, ''] and self.plot_config['y_name'] in data: - y = data[self.plot_config['y_name']] + if x not in [None, ''] and self.plot_config["y_name"] in data: + y = data[self.plot_config["y_name"]] # if depth data was overwritten, we need to delete previous points and errorbars if x in self.data.keys(): @@ -275,34 +275,34 @@ def update_plot(self, all_data): self.data[x] = y if y[2]: - symbol_brush = self.plot_config['pen_color'] + symbol_brush = self.plot_config["pen_color"] else: symbol_brush = None # post_processing receives the entire feature value array, typically: # [x, y, valid]. # should return an array with values to plot - if self.plot_config['post_processing'] and not self.plot_config['error_bars']: - y = self.plot_config['post_processing'](x, y) - elif self.plot_config['post_processing'] and self.plot_config['error_bars']: - y, eb = self.plot_config['post_processing'](x, y) - self.plot.addItem(pg.ErrorBarItem(x=[x], y=y, height=eb, pen=self.plot_config['pen_color'])) + if self.plot_config["post_processing"] and not self.plot_config["error_bars"]: + y = self.plot_config["post_processing"](x, y) + elif self.plot_config["post_processing"] and self.plot_config["error_bars"]: + y, eb = self.plot_config["post_processing"](x, y) + self.plot.addItem(pg.ErrorBarItem(x=[x], y=y, height=eb, pen=self.plot_config["pen_color"])) else: y = y[1] - if not self.plot_config['image_plot']: + if not self.plot_config["image_plot"]: # make sure x is a list the same size as y x = [x] * len(y) self.plot.plot(x=x, y=y, symbol='o', symbolSize=6, pen=None, - symbolBrush=symbol_brush, symbolPen=self.plot_config['pen_color']) + symbolBrush=symbol_brush, symbolPen=self.plot_config["pen_color"]) else: self.img.setImage(y, autoLevels=False) if self.plot_config['auto_scale']: self.plot.autoRange(padding=0.05, items=self.plot.dataItems) - if self.plot_config['x_range']: - self.plot.setXRange(self.plot_config['x_range'][0], self.plot_config['x_range'][1], + if self.plot_config["x_range"]: + self.plot.setXRange(self.plot_config["x_range"][0], self.plot_config["x_range"][1], padding=0) # self.plot.enableAutoRange(axis=pg.ViewBox.YAxis) @@ -330,7 +330,6 @@ def __init__(self, plot_config, *args, **kwargs): super(RawPlots, self).__init__(*args, **kwargs) self.plot_config = plot_config - self.pen_color = QColor(pen_colors[self.plot_config['color_iterator']]) # Create and add GraphicsLayoutWidget self.layout = QGridLayout(self) @@ -339,7 +338,7 @@ def __init__(self, plot_config, *args, **kwargs): # create GLW for the depth plot depth_sett = {**DEPTH, - 'pen_color': self.pen_color} + 'pen_color': self.plot_config["pen_color"]} self.depth_plot = BasePlotWidget(depth_sett) self.layout.addWidget(self.depth_plot, 0, 0, NPLOTSRAW, 1) @@ -380,7 +379,7 @@ def __init__(self, plot_config, *args, **kwargs): 'x_axis': False, # Is Visible 'y_axis': False, 'x_range': [self.plot_config["x_start"], self.plot_config["x_stop"]], # self.plot_config['x_range'], # None for auto-scale, list otherwise - 'y_range': [-self.plot_config['y_range'], self.plot_config['y_range']], + 'y_range': [-self.plot_config["y_range"], self.plot_config["y_range"]], 'auto_scale': False, 'interactive': False, 'marker_line': None, @@ -433,9 +432,9 @@ def update_plot(self, all_data): self.depth_pdi[depth_data[0]] = depth_data[1] # plot depth - symbol_brush = self.pen_color if depth_data[2] else None + symbol_brush = self.plot_config["pen_color"] if depth_data[2] else None self.depth_plot.plot.plot(x=[0], y=[depth_data[0]], symbol='o', symbolBrush=symbol_brush, - symbolPen=self.pen_color, symbolSize=6) + symbolPen=self.plot_config["pen_color"], symbolSize=6) if new_depth == DEPTHRANGE[0] and new_depth > depth_data[0]: new_depth = depth_data[0] @@ -464,7 +463,7 @@ def plot_depth_values(self): to_plot = self.depth_pdi[all_depths[idx]] # data if len(self.data_figures[-plot_idx].plot.dataItems) == 0: self.data_figures[-plot_idx].plot.addItem(pg.PlotDataItem(to_plot, - pen=self.pen_color, + pen=self.plot_config["pen_color"], autoDownsample=True)) else: self.data_figures[-plot_idx].plot.dataItems[0].setData(to_plot) @@ -475,7 +474,7 @@ def plot_depth_values(self): if len(self.data_figures[-plot_idx].plot.dataItems) > 0: self.data_figures[-plot_idx].plot.dataItems[0].setData([0]) - self.data_figures[-plot_idx].plot.setYRange(-self.plot_config['y_range'], self.plot_config['y_range']) + self.data_figures[-plot_idx].plot.setYRange(-self.plot_config["y_range"], self.plot_config["y_range"]) idx -= 1 plot_idx += 1 @@ -506,8 +505,6 @@ def __init__(self, plot_config, *args, **kwargs): super(STNPlots, self).__init__(*args, **kwargs) self.plot_config = plot_config - pen_color = QColor(pen_colors[plot_config['color_iterator']]) - # Create and add GraphicsLayoutWidget self.layout = QGridLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) @@ -516,7 +513,7 @@ def __init__(self, plot_config, *args, **kwargs): rms_sett = {**FEAT_VS_DEPTH, 'title': 'Noise RMS (' + u'\u03BC' + 'V)', 'y_name': 'NoiseRMS', - 'pen_color': pen_color} + 'pen_color': self.plot_config["pen_color"]} self.rms_plot = BasePlotWidget(rms_sett) self.layout.addWidget(self.rms_plot, 0, 0, 1, 1) @@ -524,7 +521,7 @@ def __init__(self, plot_config, *args, **kwargs): 'title': 'Beta power (dB)', 'y_name': 'BetaPower', 'post_processing': self.beta_process, - 'pen_color': pen_color} + 'pen_color': self.plot_config["pen_color"]} self.bp_plot = BasePlotWidget(beta_sett) self.layout.addWidget(self.bp_plot, 1, 0, 1, 1) # https://gist.github.com/beniwohli/765262 @@ -535,7 +532,7 @@ def __init__(self, plot_config, *args, **kwargs): 'x_ticks': True, 'post_processing': self.pac_process, 'error_bars': True, - 'pen_color': pen_color} + 'pen_color': self.plot_config["pen_color"]} self.pac_plot = BasePlotWidget(pac_sett) self.layout.addWidget(self.pac_plot, 2, 0, 1, 1) @@ -568,7 +565,6 @@ def __init__(self, plot_config, *args, **kwargs): super(LFPPlots, self).__init__(*args, **kwargs) self.plot_config = plot_config - self.pen_color = QColor(pen_colors[self.plot_config['color_iterator']]) # Create and add GraphicsLayoutWidget self.layout = QGridLayout(self) @@ -584,7 +580,7 @@ def __init__(self, plot_config, *args, **kwargs): 'y_tick_labels': [[(1, 4), (6, 8), (11, 16), (16, 32), (21, 64), (26, 128), (31, 256)]], 'c_lim': [50, 150], 'post_processing': self.spectrum_process, - 'pen_color': self.pen_color + 'pen_color': self.plot_config["pen_color"] } self.spectro_plot = BasePlotWidget(pwr_sett) self.layout.addWidget(self.spectro_plot, 0, 0, 1, 1) @@ -594,7 +590,7 @@ def __init__(self, plot_config, *args, **kwargs): 'title': 'Beta power (dB)', 'y_name': 'LFPSpectrumAndEpisodes', 'post_processing': self.beta_process, - 'pen_color': self.pen_color} + 'pen_color': self.plot_config["pen_color"]} self.bp_plot = BasePlotWidget(beta_sett) self.layout.addWidget(self.bp_plot, 1, 0, 1, 1) @@ -607,7 +603,7 @@ def __init__(self, plot_config, *args, **kwargs): 'y_tick_labels': [[(1, 4), (6, 8), (11, 16), (16, 32), (21, 64)]], 'c_lim': [0, 1], 'post_processing': self.episodes_process, - 'pen_color': self.pen_color + 'pen_color': self.plot_config["pen_color"] } self.episodes_plot = BasePlotWidget(pep_sett) self.layout.addWidget(self.episodes_plot, 2, 0, 1, 1) @@ -618,7 +614,7 @@ def __init__(self, plot_config, *args, **kwargs): 'title': 'Beta episodes', 'y_name': 'LFPSpectrumAndEpisodes', 'post_processing': self.beta_ep_process, - 'pen_color': self.pen_color} + 'pen_color': self.plot_config["pen_color"]} self.b_ep_plot = BasePlotWidget(beta_ep_sett) self.layout.addWidget(self.b_ep_plot, 3, 0, 1, 1) @@ -754,7 +750,6 @@ def __init__(self, plot_config, *args, **kwargs): super(SpikePlots, self).__init__(*args, **kwargs) self.plot_config = plot_config - self.pen_color = QColor(pen_colors[self.plot_config['color_iterator']]) # Create and add GraphicsLayoutWidget self.layout = QGridLayout(self) @@ -765,7 +760,7 @@ def __init__(self, plot_config, *args, **kwargs): 'title': 'Noise RMS (' + u'\u03BC' + 'V)', 'y_name': 'DBSSpikeFeatures', 'post_processing': lambda x,y : [y[1][0]], - 'pen_color': self.pen_color} + 'pen_color': self.plot_config["pen_color"]} self.rms_plot = BasePlotWidget(rms_sett) self.layout.addWidget(self.rms_plot, 0, 0, 1, 1) @@ -775,7 +770,7 @@ def __init__(self, plot_config, *args, **kwargs): 'title': 'Rate (Hz)', 'y_name': 'DBSSpikeFeatures', 'post_processing': lambda x, y: [y[1][1]], - 'pen_color': self.pen_color} + 'pen_color': self.plot_config["pen_color"]} self.rate_plot = BasePlotWidget(rate_sett) self.layout.addWidget(self.rate_plot, 1, 0, 1, 1) @@ -785,7 +780,7 @@ def __init__(self, plot_config, *args, **kwargs): 'title': 'Burst Index', 'y_name': 'DBSSpikeFeatures', 'post_processing': lambda x, y: [y[1][2]], - 'pen_color': self.pen_color} + 'pen_color': self.plot_config["pen_color"]} self.burst_plot = BasePlotWidget(burst_sett) self.layout.addWidget(self.burst_plot, 2, 0, 1, 1) @@ -796,7 +791,7 @@ def __init__(self, plot_config, *args, **kwargs): 'x_ticks': True, 'y_name': 'DBSSpikeFeatures', 'post_processing': lambda x, y: [y[1][3]], - 'pen_color': self.pen_color} + 'pen_color': self.plot_config["pen_color"]} self.ff_plot = BasePlotWidget(ff_sett) self.layout.addWidget(self.ff_plot, 3, 0, 1, 1) @@ -829,8 +824,6 @@ def __init__(self, plot_config, *args, **kwargs): super(MappingPlots, self).__init__(*args, **kwargs) self.plot_config = plot_config - pen_color = QColor(pen_colors[plot_config['color_iterator']]) - # Create and add GraphicsLayoutWidget self.layout = QGridLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) @@ -843,7 +836,7 @@ def __init__(self, plot_config, *args, **kwargs): 'y_tick_labels': [[(0, "No"), (1, "Un"), (2, "Fo"), (3, "Le"), (4, "Ha"), (5, "Ar"), (6, 'He')]], 'auto_scale': False, 'post_processing': self.fill_empty, - 'pen_color': pen_color} + 'pen_color': self.plot_config["pen_color"]} self.kin_plot = BasePlotWidget(kin_sett) self.add_dashed_lines(self.kin_plot.plot) self.layout.addWidget(self.kin_plot, 0, 0, 1, 1) @@ -855,7 +848,7 @@ def __init__(self, plot_config, *args, **kwargs): 'y_tick_labels': [[(0, "No"), (1, "Un"), (2, "Fo"), (3, "Le"), (4, "Ha"), (5, "Ar"), (6, 'He')]], 'auto_scale': False, 'post_processing': self.fill_empty, - 'pen_color': pen_color} + 'pen_color': self.plot_config["pen_color"]} self.tact_plot = BasePlotWidget(tact_sett) self.add_dashed_lines(self.tact_plot.plot) self.layout.addWidget(self.tact_plot, 1, 0, 1, 1) @@ -867,7 +860,7 @@ def __init__(self, plot_config, *args, **kwargs): 'y_tick_labels': [[(0, "No"), (1, "Un"), (2, "Fo"), (3, "Le"), (4, "Ha"), (5, "Ar"), (6, 'He')]], 'auto_scale': False, 'post_processing': self.fill_empty, - 'pen_color': pen_color} + 'pen_color': self.plot_config["pen_color"]} self.custom_plot = BasePlotWidget(custom_sett) self.add_dashed_lines(self.custom_plot.plot) self.layout.addWidget(self.custom_plot, 2, 0, 1, 1) diff --git a/open_mer/resources/settings/DepthGUI.ini b/open_mer/resources/settings/DepthGUI.ini index 9f505b6..96481d0 100644 --- a/open_mer/resources/settings/DepthGUI.ini +++ b/open_mer/resources/settings/DepthGUI.ini @@ -6,8 +6,8 @@ size=@Size(600 250) pos=@Point(1320 0) [depth-source] -class=FHCSerial -;class=CBSDKPlayback +;class=FHCSerial +class=CBSDKPlayback offset=0 scale_factor=0.001 serial\baudrate=19200 diff --git a/open_mer/resources/settings/FeaturesGUI.ini b/open_mer/resources/settings/FeaturesGUI.ini index 4e05d30..d0ba1c0 100644 --- a/open_mer/resources/settings/FeaturesGUI.ini +++ b/open_mer/resources/settings/FeaturesGUI.ini @@ -15,20 +15,34 @@ buffer_parameter\comment_length=10 [buffer] highpass=true +buffer_duration=6.0 +sample_duration=4.0 +delay_duration=0.5 +validity_threshold=0.9 +overwrite_depth=true +# electrode_settings= [features] -Raw=true -Mapping=false -STN=false -LFP=false -Spikes=false +0\name=Raw +0\enable=true +1\name=Mapping +1\enable=false +2\name=STN +2\enable=true +3\name=LFP +3\enable=true +4\name=Spikes +4\enable=true [plot] x_start=-4000 x_stop=120000 y_range=250 -downsample=1 -n_segments = 20 -spk_aud=true -lock_threshold=true -unit_scaling=0.25 +image_plot=false + +[theme] +startup=orange +notrecording=gray +recording=red +accumulating=yellow +done=blue diff --git a/open_mer/scripts/Depth_Process.py b/open_mer/scripts/Depth_Process.py new file mode 100644 index 0000000..a7d9031 --- /dev/null +++ b/open_mer/scripts/Depth_Process.py @@ -0,0 +1,382 @@ +from qtpy import QtCore +import importlib.resources as pkg_resources +from pathlib import Path +import time +import json + +import numpy as np +import zmq +from cerebuswrapper import CbSdkConnection +from django.utils import timezone +from serf.tools.db_wrap import DBWrapper + +from open_mer.data_source.cerebus import SAMPLINGGROUPS + + +class NSPBufferWorker: + + def __init__(self, ipc_settings, buffer_settings): + self._ipc_settings = ipc_settings + self._buffer_settings = buffer_settings + self.buffer = None + self.db_wrapper = DBWrapper() + self.procedure_id = None + + # cbSDK; connect using default parameters + self.cbsdk_conn = CbSdkConnection(simulate_ok=False) + self.cbsdk_conn.connect() + + self._setup_ipc() + + self._reset_group_info() + self.reset_buffer() + + self.start_time = timezone.now() + self._snippet_sock.send_string(f"snippet_status startup") + self.is_running = True + + def _setup_ipc(self): + self._ipc_context = zmq.Context() + + # procedure settings subscription + self._ctrl_sock = self._ipc_context.socket(zmq.SUB) + self._ctrl_sock.connect(f"tcp://localhost:{self._ipc_settings['procedure_settings']}") + self._ctrl_sock.setsockopt_string(zmq.SUBSCRIBE, "procedure_settings") + + # ddu depth subscription + self._ddu_sock = self._ipc_context.socket(zmq.SUB) + self._ddu_sock.connect(f"tcp://localhost:{self._ipc_settings['ddu']}") + self._ddu_sock.setsockopt_string(zmq.SUBSCRIBE, "ddu") + + # self status publisher + self._snippet_sock = self._ipc_context.socket(zmq.PUB) + self._snippet_sock.bind(f"tcp://*:{self._ipc_settings['snippet_status']}") + + def _reset_group_info(self): + self.sampling_rate = self._buffer_settings["sampling_group"] # TODO: Fix name in FeaturesGUI.ini + self.sampling_group = SAMPLINGGROUPS.index(str(self.sampling_rate)) + self.group_info = self.cbsdk_conn.get_group_config(self.sampling_group) + self.n_chan = len(self.group_info) + self.valid_electrodes = [_["chan"] for _ in self.group_info] + self.buffer_length = int(self.sampling_rate * self._buffer_settings["buffer_duration"]) + self.sample_length = int(self.sampling_rate * self._buffer_settings["sample_duration"]) + self.delay_length = int(self.sampling_rate * self._buffer_settings["delay_duration"]) + self.overwrite_depth = self._buffer_settings["overwrite_depth"] + # default values, might be overwritten by electrode_settings + self.validity_threshold = [self.sample_length * self._buffer_settings["validity_threshold"]] * self.n_chan + self.threshold = [False] * self.n_chan + + def reset_buffer(self): + self.buffer = np.zeros((self.n_chan, self.buffer_length), dtype=np.int16) + self.buffer_idx = 0 + # for each channel we will keep a bool array whether each sample point is valid or not + # when a condition is met to trigger sending the sample to the DB we will pick the window + # with highest validity count. + self.validity = np.zeros((self.n_chan, self.buffer_length), dtype=bool) + self.valid_idx = (0, 0) + + self.delay_counter = 0 + self.update_buffer_status = True + self.delay_done = False + + def process_settings(self, sett_dict): + # process inputs + sett_keys = list(sett_dict.keys()) + + if "procedure" in sett_keys and "procedure_id" in sett_dict["procedure"]: + self.reset_procedure(sett_dict["procedure"]["procedure_id"]) + + if "sampling_group_id" in sett_keys: + # TODO: buffer_settings has bad key name because of the error in FeaturesGUI.ini + self._buffer_settings["sampling_group"] = sett_dict["sampling_rate"] + # self._buffer_settings["validity_threshold"] = sett_dict["???"] + self._reset_group_info() + self.reset_buffer() + + if "buffer" in sett_keys and "electrode_settings" in sett_dict["buffer"]: + for ii, info in enumerate(self.group_info): + label = info["label"] + if label in sett_dict["buffer"]["electrode_settings"]: + el_sett = sett_dict["buffer"]["electrode_settings"][label] + self.threshold[ii] = bool(el_sett["threshold"]) + self.validity_threshold[ii] = float(el_sett["validity"]) / 100 * self.sample_length + + def reset_procedure(self, proc_id): + self.procedure_id = proc_id + self.db_wrapper.select_procedure(self.procedure_id) + + def clear_buffer(self): + if self.buffer is None: + self.reset_buffer() + self.buffer.fill(0) + self.buffer_idx = 0 + + self.validity.fill(False) + # list of tuples: (index of validity value, value) + # saves the index with largest validity across all channels + self.valid_idx = (0, 0) + self.delay_counter = 0 + + self.update_buffer_status = True + self.delay_done = False + # self.start_time = timezone.now() + + def wait_for_delay_end(self, data): + data_length = data[0][1].shape[0] + self.delay_counter += data_length + # check if we have accumulated enough data to end delay and start recording + if self.delay_counter <= self.delay_length: + return False + else: + # truncate the data to the first index over the delay period + start_idx = max(0, int(self.delay_length - self.delay_counter)) + for chan_idx, (chan, values) in enumerate(data): + data[chan_idx][1] = values[start_idx:] + + # now is for the last sample. subtract data length / SAMPLINGRATE to get time of first sample + self.start_time = timezone.now() + time_delta = timezone.timedelta(seconds=data[0][1].shape[0] / self.sampling_rate) + self.start_time -= time_delta + + self._snippet_sock.send_string(f"snippet_status recording") + return True + + def send_to_db(self, depth): + do_save = self.valid_idx[1] != 0 + # if we actually have a computed validity (i.e. segment is long enough) + if do_save: + # the info that needs to be sent the DB_wrapper is: + # Datum: + # - subject_id + # - is_good : to be determined by validity values + # - start_time / stop_time ? + # Datum Store: + # - channel_labels : from group_info + # - erp : actual data + # - n_channels and n_samples : determined by data size + # - x_vec: time ? + # DatumDetailValue: + # - detail_type: depth (fetch from DetailType + # - value: depth value + self.db_wrapper.save_depth_datum(depth=depth, + data=self.buffer[:, + self.valid_idx[0]:self.valid_idx[0]+self.sample_length], + is_good=np.array([x >= y for x, y in zip( + np.sum(self.validity[:, self.valid_idx[0]: + self.valid_idx[0] + self.sample_length], axis=1), + self.validity_threshold)], dtype=bool), + group_info=self.group_info, + start_time=self.start_time, + stop_time=timezone.now()) + self.update_buffer_status = False + return do_save + + @staticmethod + def validate_data_sample(data): + # TODO: implement other metrics + # SUPER IMPORTANT: when cbpy returns an int16 value, it can be -32768, however in numpy: + # np.abs(-32768) = -32768 for 16 bit integers since +32768 does not exist. + # We therefore can't use the absolute value for the threshold. + threshold = 30000 # arbitrarily set for now + validity = np.array([-threshold < x < threshold for x in data]) + + return validity + + def run_forever(self): + prev_status = None + current_depth = None + while self.is_running: + try: + received_msg = self._ctrl_sock.recv_string(flags=zmq.NOBLOCK)[len("procedure_settings") + 1:] + settings_dict = json.loads(received_msg) + # Check for kill signal + if "running" in settings_dict and not settings_dict["running"]: + self.is_running = False + continue + # Process remaining settings + self.process_settings(settings_dict) + except zmq.ZMQError: + pass + + # collect NSP data, regardless of recording status to keep cbsdk buffer empty + # data is a list of lists. + # 1st level is a list of channels + # 2nd level is a list [chan_id, np.array(data)] + data = self.cbsdk_conn.get_continuous_data() + # Only keep channels within our sampling group + data = [x for x in data if x[0] in self.valid_electrodes] + + # only process the NSP data if Central is recording + _status = "recording" if self.cbsdk_conn.get_recording_state() else "notrecording" + if _status == "recording" and data and current_depth: + + if not self.delay_done: + self.delay_done = self.wait_for_delay_end(data) + + # Only process if we are in a new depth, past the delay, and we didn't just send a snippet to the db. + if self.delay_done and self.update_buffer_status: + # all data segments should have the same length, so first check if we run out of buffer space + data_length = data[0][1].shape[0] + if (self.buffer_idx + data_length) >= self.buffer_length: + # if we run out of buffer space before data has been sent to the DB few things could have gone + # wrong: + # - data in buffer is not good enough + # - the new data chunk is larger than the difference between buffer and sample length + # (e.g. 6s buffer and 4s sample, if the current buffer has 3s of data and it receives a 4s + # long chunk then the buffer would overrun, and still not have enough data to send to DB. + # Although unlikely in real-life, it happened during debugging.) + + # trim data to only fill the buffer, discarding the rest + # TODO: is this the optimal solution? Slide buffer instead? + data_length = self.buffer_length - self.buffer_idx + + # continue to validate received data + for chan_idx, (chan, values) in enumerate(data): + + if data_length > 0: + # Validate data + valid = self.validate_data_sample(values[:data_length]) + + # append data to buffer + self.buffer[chan_idx, + self.buffer_idx:self.buffer_idx + data_length] = values[:data_length] + + self.validity[chan_idx, + self.buffer_idx:self.buffer_idx + data_length] = valid + + _status = "accumulating" + + # increment buffer index, all data segments should have same length, if they don't, will match + # the first channel + self.buffer_idx += data_length + + # check if data length > sample length + if self.buffer_idx >= self.sample_length: + + # compute total validity of last sample_length and if > threshold, send to DB + sample_idx = self.buffer_idx - self.sample_length + + temp_sum = [np.sum(x[sample_idx:self.buffer_idx]) for x in self.validity] + + # check if validity is better than previous sample, if so, store it + if np.sum(temp_sum) > self.valid_idx[1]: + self.valid_idx = (sample_idx, np.sum(temp_sum)) + + if all(x >= y for x, y in zip(temp_sum, self.validity_threshold)) or \ + self.buffer_idx >= self.buffer_length: + # We have accumulated enough data for this depth. Send to db! + stored = self.send_to_db(current_depth) + if stored: + _status = "done" + + # check for new depth + # At this point, the individual channels have either been sent to the DB or are still collecting waiting for + # either of the following conditions: acquire sufficient data (i.e. sample_length) or acquire sufficiently + # clean data (i.e. validity_threshold). If the channel is still acquiring data but has sufficiently long + # segments, we will send the cleanest segment to the DB (i.e. valid_idx). + new_depth = None + try: + received_msg = self._ddu_sock.recv_string(flags=zmq.NOBLOCK)[len("ddu") + 1:] + if received_msg: + new_depth = float(received_msg) + except zmq.ZMQError: + pass + + # If new depth value + if new_depth is not None and (new_depth != current_depth or self.overwrite_depth): + # We are moving on. If still updating the buffer, then we can check to see if we have + # enough valid samples -- though maybe not high quality -- and save the best available segment. + if self.update_buffer_status: + _ = self.send_to_db(current_depth) + + # New depth verified. Let's clear the buffer for accumulation again. + self.clear_buffer() + current_depth = new_depth + + if prev_status is None: + # First iteration -- triggers an update from the ProcedureGUI if running + self._snippet_sock.send_string(f"snippet_status refresh") + + # Optionally publish the recording status + # if status has changed, but not from done->recording as done implies recording, + # and we prefer to keep the "done" status until there's a new depth. + if _status != prev_status and not (prev_status == "done" and _status == "recording"): + self._snippet_sock.send_string(f"snippet_status {_status}") + prev_status = _status + + time.sleep(.010) + + +def build_ini_paths() -> list[Path]: + # Infer paths to ini files + res_settings_path = pkg_resources.files("open_mer.resources") / "settings" + home_path = Path.home() / ".open_mer" + + return [ + res_settings_path / "IPC.ini", + res_settings_path / "FeaturesGUI.ini", # for buffer settings + home_path / "IPC.ini", + home_path / "FeaturesGUI.ini" # for buffer settings + ] + + +def parse_ini(ini_paths) -> (dict, dict, dict): + ipc_settings = {} + buffer_settings = {} + feature_settings = {} + for ini_path in ini_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + # Store / update IPC settings which will be used by subclasses only + settings.beginGroup("ZeroMQ") + sock_ids = [int(_) for _ in settings.childGroups()] + sock_ids.sort() + for sock_id in sock_ids: + settings.beginGroup(str(sock_id)) + ipc_settings[settings.value("topic", type=str)] = settings.value("port", type=int) + settings.endGroup() + settings.endGroup() + + # Buffer + settings.beginGroup("buffer") + for k, t in { + "highpass": bool, + "buffer_duration": float, + "sample_duration": float, + "delay_duration": float, + "validity_threshold": float, + "overwrite_depth": bool, + # electrode_settings ?! + # chk_threshold ?! + }.items(): + if k in settings.allKeys(): + buffer_settings[k] = settings.value(k, type=t) + settings.endGroup() # buffer + + settings.beginGroup("data-source") + for k, t in {"sampling_group": int}.items(): + if k in settings.allKeys(): + buffer_settings[k] = settings.value(k, type=t) + settings.endGroup() + + # Features (used by Features_Process) + settings.beginGroup("features") + feat_inds = [int(_) for _ in settings.childGroups()] + feat_inds.sort() + for f_ind in feat_inds: + settings.beginGroup(str(f_ind)) + feature_settings[f_ind] = (settings.value("name", type=str), settings.value("enable", type=bool)) + settings.endGroup() + settings.endGroup() # features + return ipc_settings, buffer_settings, feature_settings + + +def main(): + ipc_settings, buffer_settings, feature_settings = parse_ini(build_ini_paths()) + + worker = NSPBufferWorker(ipc_settings, buffer_settings) + worker.run_forever() + + +if __name__ == '__main__': + main() diff --git a/open_mer/scripts/FeaturesGUI.py b/open_mer/scripts/FeaturesGUI.py index fa7e063..532490d 100644 --- a/open_mer/scripts/FeaturesGUI.py +++ b/open_mer/scripts/FeaturesGUI.py @@ -1,12 +1,11 @@ import sys -import argparse from qtpy import QtCore, QtWidgets from open_mer.dbsgui.features import FeaturesGUI -def main(**kwargs): +def main(): app = QtWidgets.QApplication(sys.argv) - window = FeaturesGUI(**kwargs) + window = FeaturesGUI() window.show() timer = QtCore.QTimer() timer.timeout.connect(window.update) @@ -17,8 +16,4 @@ def main(**kwargs): if __name__ == '__main__': - parser = argparse.ArgumentParser(prog="FeaturesGUI", - description="Visualize MER trajectory segments and features.") - parser.add_argument('-i', '--ini_file', nargs='?', help="Path to ini settings file.") - args = parser.parse_args() - main(**args.__dict__) + main() diff --git a/open_mer/scripts/Features_Process.py b/open_mer/scripts/Features_Process.py new file mode 100644 index 0000000..e0f2798 --- /dev/null +++ b/open_mer/scripts/Features_Process.py @@ -0,0 +1,102 @@ +import time +import json + +import zmq +from serf.tools.db_wrap import DBWrapper + +from open_mer.scripts.Depth_Process import build_ini_paths, parse_ini + + +class FeaturesWorker: + + def __init__(self, ipc_settings, buffer_settings, feature_settings): + self._ipc_settings = ipc_settings + self._buffer_settings = buffer_settings + self._feature_settings = feature_settings + + # DB wrapper + self.db_wrapper = DBWrapper() + + self._setup_ipc() + + self.procedure_id = None + self.all_datum_ids = [] + self.gt = 0 # fetch datum ids greater than this value + + feat_list = [v[0] for k, v in feature_settings.items() if v[1]] + self.reset_features(feat_list) + + self.is_running = True + + def _setup_ipc(self): + self._ipc_context = zmq.Context() + + # procedure settings subscription + self._ctrl_sock = self._ipc_context.socket(zmq.SUB) + self._ctrl_sock.connect(f"tcp://localhost:{self._ipc_settings['procedure_settings']}") + self._ctrl_sock.setsockopt_string(zmq.SUBSCRIBE, "procedure_settings") + + def process_settings(self, sett_dict): + # process inputs + if 'procedure' in sett_dict.keys(): + self.reset_procedure(sett_dict['procedure']) + + if 'features' in sett_dict.keys(): + self.reset_features(sett_dict['features']) + + def reset_procedure(self, proc_dict): + if "procedure_id" in proc_dict: + self.procedure_id = proc_dict["procedure_id"] + self.db_wrapper.select_procedure(self.procedure_id) + self.reset_datum() + + def reset_features(self, feats): + self.db_wrapper.select_features(feats) + self.reset_datum() + + def reset_datum(self): + # we will list all datum for the subject and all feature types that match the settings + self.all_datum_ids = [] + self.gt = 0 + + def run_forever(self): + while self.is_running: + try: + received_msg = self._ctrl_sock.recv_string(flags=zmq.NOBLOCK)[len("procedure_settings")+1:] + settings_dict = json.loads(received_msg) + if "running" in settings_dict and not settings_dict["running"]: + self.is_running = False + continue + self.process_settings(settings_dict) + except zmq.ZMQError: + pass + + new_datum = self.db_wrapper.list_all_datum_ids(gt=self.gt) + + if len(new_datum) > 0: + self.all_datum_ids += new_datum + + if len(self.all_datum_ids) > 0: + # get oldest data and check if all features have been computed + # in case we're stuck with a datum whose feature can't compute, we + # want to keep the largest datum id. + self.gt = max(self.all_datum_ids + [self.gt]) + d_id = self.all_datum_ids.pop(0) + if not self.db_wrapper.check_and_compute_features(d_id): + # re append at the end of the list? + self.all_datum_ids.append(d_id) + time.sleep(0.25) # test to slow down process to decrease HDD load + # time.sleep(0.1) + + +def main(): + ipc_settings, buffer_settings, feature_settings = parse_ini(build_ini_paths()) + worker = FeaturesWorker(ipc_settings, buffer_settings, feature_settings) + try: + worker.run_forever() + except KeyboardInterrupt: + worker.is_running = False + + +if __name__ == '__main__': + main() diff --git a/open_mer/scripts/ProcedureGUI.py b/open_mer/scripts/ProcedureGUI.py index 1be145c..1ebccc4 100644 --- a/open_mer/scripts/ProcedureGUI.py +++ b/open_mer/scripts/ProcedureGUI.py @@ -1,13 +1,11 @@ import sys -import argparse from qtpy import QtCore, QtWidgets from open_mer.dbsgui.procedure import ProcedureGUI -def main(**kwargs): +def main(): app = QtWidgets.QApplication(sys.argv) - window = ProcedureGUI(**kwargs) - window.show() + window = ProcedureGUI() timer = QtCore.QTimer() timer.timeout.connect(window.update) timer.start(100) @@ -17,8 +15,4 @@ def main(**kwargs): if __name__ == '__main__': - parser = argparse.ArgumentParser(prog="ProcedureGUI", - description="Manage subprocesses to store segments and compute features.") - parser.add_argument('-i', '--ini_file', nargs='?', help="Path to ini settings file.") - args = parser.parse_args() - main(**args.__dict__) + main() diff --git a/setup.cfg b/setup.cfg index 191294c..cafa9e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,3 +58,5 @@ gui_scripts = dbs-mapping = open_mer.scripts.MappingGUI:main dbs-comments = open_mer.scripts.CommentsGUI:main dbs-ddu = open_mer.scripts.DepthGUI:main + dbs-meracquire = open_mer.scripts.Depth_Process:main + dbs-procfeatures = open_mer.scripts.Features_Process:main From ded5c170399236fd5f595ba7aa219deaeb18886b Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 29 Sep 2023 02:42:06 -0400 Subject: [PATCH 51/65] DepthGUI.ini return default source to FHCSerial --- open_mer/resources/settings/DepthGUI.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/open_mer/resources/settings/DepthGUI.ini b/open_mer/resources/settings/DepthGUI.ini index 96481d0..9f505b6 100644 --- a/open_mer/resources/settings/DepthGUI.ini +++ b/open_mer/resources/settings/DepthGUI.ini @@ -6,8 +6,8 @@ size=@Size(600 250) pos=@Point(1320 0) [depth-source] -;class=FHCSerial -class=CBSDKPlayback +class=FHCSerial +;class=CBSDKPlayback offset=0 scale_factor=0.001 serial\baudrate=19200 From d09d573965b013913c67335e92f2883ddd653a35 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 29 Sep 2023 02:42:28 -0400 Subject: [PATCH 52/65] Add sleep to MonitorZeroMQ to reduce CPU usage. --- scripts/MonitorZeroMQ.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/MonitorZeroMQ.py b/scripts/MonitorZeroMQ.py index 4b55bdf..b1ace15 100644 --- a/scripts/MonitorZeroMQ.py +++ b/scripts/MonitorZeroMQ.py @@ -1,3 +1,5 @@ +import time + import zmq @@ -25,6 +27,6 @@ recv_msg = sock.recv_string(flags=zmq.NOBLOCK)[len(topic) + 1:] print(topic, recv_msg) except zmq.ZMQError: - pass + time.sleep(0.1) except KeyboardInterrupt: pass From dd31e43e5f6c9e5acd7a869668f92837f63e1fc4 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 29 Sep 2023 02:42:42 -0400 Subject: [PATCH 53/65] Fix reference to child widget settings. --- open_mer/dbsgui/sweep.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index 9736e1c..2e53378 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -432,8 +432,8 @@ def publish_chanselect(self): topic_msg = "channel_select " + json.dumps({ "channel": curr_channel, "label": self.audio["chan_label"] or "", - "range": self._plot_widget.plot_config["y_range"], - "highpass": self._plot_widget.plot_config["do_hp"] + "range": self._plot_widget._plot_config["y_range"], + "highpass": self._plot_widget._plot_config["do_hp"] }) self._chanselect_sock.send_string(topic_msg) @@ -486,13 +486,13 @@ def do_plot_update(self): def _postproc_data(self, label, data, ch_state=None): # TODO: Store channel state and filter state in this class, not the plot widget ss_info = self._plot_widget.segmented_series[label] - gain = ch_state["gain"] if ch_state is not None and "gain" in ch_state else self._plot_widget.plot_config["unit_scaling"] + gain = ch_state["gain"] if ch_state is not None and "gain" in ch_state else self._plot_widget._plot_config["unit_scaling"] data = data * gain - if self._plot_widget.plot_config["do_hp"]: + if self._plot_widget._plot_config["do_hp"]: if ss_info["hp_zi"] is None: - ss_info["hp_zi"] = signal.sosfilt_zi(self._plot_widget.plot_config["hp_sos"]) - data, ss_info["hp_zi"] = signal.sosfilt(self._plot_widget.plot_config["hp_sos"], data, zi=ss_info["hp_zi"]) - if self._plot_widget.plot_config["do_ln"]: + ss_info["hp_zi"] = signal.sosfilt_zi(self._plot_widget._plot_config["hp_sos"]) + data, ss_info["hp_zi"] = signal.sosfilt(self._plot_widget._plot_config["hp_sos"], data, zi=ss_info["hp_zi"]) + if self._plot_widget._plot_config["do_ln"]: pass # TODO: Line noise / comb filter return data @@ -502,5 +502,5 @@ def _sonify_data(self, label, data): if self.audio["chan_label"] == label: write_indices = (np.arange(data.shape[0]) + self.audio["write_ix"]) % self.audio["buffer"].shape[0] self.audio["buffer"][write_indices] = ( - np.copy(data) * (2 ** 15 / self._plot_widget.plot_config["y_range"])).astype(np.int16) + np.copy(data) * (2 ** 15 / self._plot_widget._plot_config["y_range"])).astype(np.int16) self.audio["write_ix"] = (self.audio["write_ix"] + data.shape[0]) % self.audio["buffer"].shape[0] From 9d0f15fbe5680fbc86e8bf373df6975386cb07c9 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 29 Sep 2023 02:43:29 -0400 Subject: [PATCH 54/65] update paths to snippet and features processes --- docs/preparing-distribution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/preparing-distribution.md b/docs/preparing-distribution.md index 6c182c3..431c918 100644 --- a/docs/preparing-distribution.md +++ b/docs/preparing-distribution.md @@ -24,8 +24,8 @@ Create a folder somewhere you have write access. The location should have at lea ```shell script @echo off call "%~dp0env_for_icons.bat" - start "" "%WINPYDIR%\Scripts\serf-cbacquire.exe" /command:%1 /B - start "" "%WINPYDIR%\Scripts\serf-procfeatures.exe" /command:%1 /B + start "" "%WINPYDIR%\Scripts\dbs-meracquire.exe" /command:%1 /B + start "" "%WINPYDIR%\Scripts\dbs-procfeatures.exe" /command:%1 /B start "" "%WINPYDIR%\Scripts\dbs-sweep.exe" /command:%1 /B start "" "%WINPYDIR%\Scripts\dbs-raster.exe" /command:%1 /B start "" "%WINPYDIR%\Scripts\dbs-waveform.exe" /command:%1 /B From 5a117944b3f22de6e13352f82b05d30c603a2a83 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 1 Nov 2023 23:53:14 -0400 Subject: [PATCH 55/65] Moved most of Depth_Process to dbsgui/process/trajectory.py Added some debug print statements. --- open_mer/dbsgui/process/__init__.py | 73 +++++ open_mer/dbsgui/process/trajectory.py | 306 +++++++++++++++++++++ open_mer/scripts/Depth_Process.py | 372 +------------------------- 3 files changed, 380 insertions(+), 371 deletions(-) create mode 100644 open_mer/dbsgui/process/__init__.py create mode 100644 open_mer/dbsgui/process/trajectory.py diff --git a/open_mer/dbsgui/process/__init__.py b/open_mer/dbsgui/process/__init__.py new file mode 100644 index 0000000..4ed5e12 --- /dev/null +++ b/open_mer/dbsgui/process/__init__.py @@ -0,0 +1,73 @@ +import importlib.resources as pkg_resources +from pathlib import Path + +from qtpy import QtCore + +from .trajectory import NSPBufferWorker + + +__all__ = ["build_ini_paths", "parse_ini", "NSPBufferWorker"] + + +def build_ini_paths() -> list[Path]: + # Infer paths to ini files + res_settings_path = pkg_resources.files("open_mer.resources") / "settings" + home_path = Path.home() / ".open_mer" + + return [ + res_settings_path / "IPC.ini", + res_settings_path / "FeaturesGUI.ini", # for buffer settings + home_path / "IPC.ini", + home_path / "FeaturesGUI.ini" # for buffer settings + ] + + +def parse_ini(ini_paths) -> (dict, dict, dict): + ipc_settings = {} + buffer_settings = {} + feature_settings = {} + for ini_path in ini_paths: + settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) + + # Store / update IPC settings which will be used by subclasses only + settings.beginGroup("ZeroMQ") + sock_ids = [int(_) for _ in settings.childGroups()] + sock_ids.sort() + for sock_id in sock_ids: + settings.beginGroup(str(sock_id)) + ipc_settings[settings.value("topic", type=str)] = settings.value("port", type=int) + settings.endGroup() + settings.endGroup() + + # Buffer + settings.beginGroup("buffer") + for k, t in { + "highpass": bool, + "buffer_duration": float, + "sample_duration": float, + "delay_duration": float, + "validity_threshold": float, + "overwrite_depth": bool, + # electrode_settings ?! + # chk_threshold ?! + }.items(): + if k in settings.allKeys(): + buffer_settings[k] = settings.value(k, type=t) + settings.endGroup() # buffer + + settings.beginGroup("data-source") + for k, t in {"sampling_group": int}.items(): + if k in settings.allKeys(): + buffer_settings[k] = settings.value(k, type=t) + settings.endGroup() + + # Features (used by Features_Process) + settings.beginGroup("features") + feat_inds = [int(_) for _ in settings.childGroups()] + feat_inds.sort() + for f_ind in feat_inds: + settings.beginGroup(str(f_ind)) + feature_settings[f_ind] = (settings.value("name", type=str), settings.value("enable", type=bool)) + settings.endGroup() + settings.endGroup() # features + return ipc_settings, buffer_settings, feature_settings diff --git a/open_mer/dbsgui/process/trajectory.py b/open_mer/dbsgui/process/trajectory.py new file mode 100644 index 0000000..eb4ced4 --- /dev/null +++ b/open_mer/dbsgui/process/trajectory.py @@ -0,0 +1,306 @@ +import time +import json + +import numpy as np +import zmq +from cerebuswrapper import CbSdkConnection +from django.utils import timezone +from serf.tools.db_wrap import DBWrapper + +from open_mer.data_source.cerebus import SAMPLINGGROUPS + + +class NSPBufferWorker: + + def __init__(self, ipc_settings, buffer_settings): + self._ipc_settings = ipc_settings + self._buffer_settings = buffer_settings + self.buffer = None + self.db_wrapper = DBWrapper() + self.procedure_id = None + + # cbSDK; connect using default parameters + self.cbsdk_conn = CbSdkConnection(simulate_ok=False) + self.cbsdk_conn.connect() + + self._setup_ipc() + + self._reset_group_info() + self.reset_buffer() + + self.start_time = timezone.now() + self._snippet_sock.send_string(f"snippet_status startup") + self.is_running = True + + def _setup_ipc(self): + self._ipc_context = zmq.Context() + + # procedure settings subscription + self._ctrl_sock = self._ipc_context.socket(zmq.SUB) + self._ctrl_sock.connect(f"tcp://localhost:{self._ipc_settings['procedure_settings']}") + self._ctrl_sock.setsockopt_string(zmq.SUBSCRIBE, "procedure_settings") + + # ddu depth subscription + self._ddu_sock = self._ipc_context.socket(zmq.SUB) + self._ddu_sock.connect(f"tcp://localhost:{self._ipc_settings['ddu']}") + self._ddu_sock.setsockopt_string(zmq.SUBSCRIBE, "ddu") + + # self status publisher + self._snippet_sock = self._ipc_context.socket(zmq.PUB) + self._snippet_sock.bind(f"tcp://*:{self._ipc_settings['snippet_status']}") + + def _reset_group_info(self): + self.sampling_rate = self._buffer_settings["sampling_group"] # TODO: Fix name in FeaturesGUI.ini + self.sampling_group = SAMPLINGGROUPS.index(str(self.sampling_rate)) + self.group_info = self.cbsdk_conn.get_group_config(self.sampling_group) + self.n_chan = len(self.group_info) + self.valid_electrodes = [_["chan"] for _ in self.group_info] + self.buffer_length = int(self.sampling_rate * self._buffer_settings["buffer_duration"]) + self.sample_length = int(self.sampling_rate * self._buffer_settings["sample_duration"]) + self.delay_length = int(self.sampling_rate * self._buffer_settings["delay_duration"]) + self.overwrite_depth = self._buffer_settings["overwrite_depth"] + # default values, might be overwritten by electrode_settings + self.validity_threshold = [self.sample_length * self._buffer_settings["validity_threshold"]] * self.n_chan + self.threshold = [False] * self.n_chan + + def reset_buffer(self): + self.buffer = np.zeros((self.n_chan, self.buffer_length), dtype=np.int16) + self.buffer_idx = 0 + # for each channel we will keep a bool array whether each sample point is valid or not + # when a condition is met to trigger sending the sample to the DB we will pick the window + # with highest validity count. + self.validity = np.zeros((self.n_chan, self.buffer_length), dtype=bool) + self.valid_idx = (0, 0) + + self.delay_counter = 0 + self.update_buffer_status = True + self.delay_done = False + + def process_settings(self, sett_dict): + # process inputs + sett_keys = list(sett_dict.keys()) + + if "procedure" in sett_keys and "procedure_id" in sett_dict["procedure"]: + self.reset_procedure(sett_dict["procedure"]["procedure_id"]) + + if "sampling_group_id" in sett_keys: + # TODO: buffer_settings has bad key name because of the error in FeaturesGUI.ini + self._buffer_settings["sampling_group"] = sett_dict["sampling_rate"] + # self._buffer_settings["validity_threshold"] = sett_dict["???"] + self._reset_group_info() + self.reset_buffer() + + if "buffer" in sett_keys and "electrode_settings" in sett_dict["buffer"]: + for ii, info in enumerate(self.group_info): + label = info["label"] + if label in sett_dict["buffer"]["electrode_settings"]: + el_sett = sett_dict["buffer"]["electrode_settings"][label] + self.threshold[ii] = bool(el_sett["threshold"]) + self.validity_threshold[ii] = float(el_sett["validity"]) / 100 * self.sample_length + + def reset_procedure(self, proc_id): + self.procedure_id = proc_id + self.db_wrapper.select_procedure(self.procedure_id) + print(f"Trajectory snippet updated procedure_id to {self.procedure_id}") + + def clear_buffer(self): + if self.buffer is None: + self.reset_buffer() + self.buffer.fill(0) + self.buffer_idx = 0 + + self.validity.fill(False) + # list of tuples: (index of validity value, value) + # saves the index with largest validity across all channels + self.valid_idx = (0, 0) + self.delay_counter = 0 + + self.update_buffer_status = True + self.delay_done = False + # self.start_time = timezone.now() + + def wait_for_delay_end(self, data): + data_length = data[0][1].shape[0] + self.delay_counter += data_length + # check if we have accumulated enough data to end delay and start recording + if self.delay_counter <= self.delay_length: + return False + else: + # truncate the data to the first index over the delay period + start_idx = max(0, int(self.delay_length - self.delay_counter)) + for chan_idx, (chan, values) in enumerate(data): + data[chan_idx][1] = values[start_idx:] + + # now is for the last sample. subtract data length / SAMPLINGRATE to get time of first sample + self.start_time = timezone.now() + time_delta = timezone.timedelta(seconds=data[0][1].shape[0] / self.sampling_rate) + self.start_time -= time_delta + + self._snippet_sock.send_string(f"snippet_status recording") + return True + + def send_to_db(self, depth): + do_save = self.valid_idx[1] != 0 + # if we actually have a computed validity (i.e. segment is long enough) + if do_save: + # the info that needs to be sent the DB_wrapper is: + # Datum: + # - subject_id + # - is_good : to be determined by validity values + # - start_time / stop_time ? + # Datum Store: + # - channel_labels : from group_info + # - erp : actual data + # - n_channels and n_samples : determined by data size + # - x_vec: time ? + # DatumDetailValue: + # - detail_type: depth (fetch from DetailType + # - value: depth value + print(f"Saving depth datum. group_info: {self.group_info}") + self.db_wrapper.save_depth_datum(depth=depth, + data=self.buffer[:, + self.valid_idx[0]:self.valid_idx[0]+self.sample_length], + is_good=np.array([x >= y for x, y in zip( + np.sum(self.validity[:, self.valid_idx[0]: + self.valid_idx[0] + self.sample_length], axis=1), + self.validity_threshold)], dtype=bool), + group_info=self.group_info, + start_time=self.start_time, + stop_time=timezone.now()) + self.update_buffer_status = False + return do_save + + @staticmethod + def validate_data_sample(data): + # TODO: implement other metrics + # SUPER IMPORTANT: when cbpy returns an int16 value, it can be -32768, however in numpy: + # np.abs(-32768) = -32768 for 16 bit integers since +32768 does not exist. + # We therefore can't use the absolute value for the threshold. + threshold = 30000 # arbitrarily set for now + validity = np.array([-threshold < x < threshold for x in data]) + + return validity + + def run_forever(self): + prev_status = None + current_depth = None + while self.is_running: + try: + received_msg = self._ctrl_sock.recv_string(flags=zmq.NOBLOCK)[len("procedure_settings") + 1:] + settings_dict = json.loads(received_msg) + # Check for kill signal + if "running" in settings_dict and not settings_dict["running"]: + self.is_running = False + continue + # Process remaining settings + self.process_settings(settings_dict) + except zmq.ZMQError: + pass + + # collect NSP data, regardless of recording status to keep cbsdk buffer empty + # data is a list of lists. + # 1st level is a list of channels + # 2nd level is a list [chan_id, np.array(data)] + data = self.cbsdk_conn.get_continuous_data() + # Only keep channels within our sampling group + data = [x for x in data if x[0] in self.valid_electrodes] + + # only process the NSP data if Central is recording + _status = "recording" if self.cbsdk_conn.get_recording_state() else "notrecording" + if _status == "recording" and data and current_depth: + + if not self.delay_done: + self.delay_done = self.wait_for_delay_end(data) + + # Only process if we are in a new depth, past the delay, and we didn't just send a snippet to the db. + if self.delay_done and self.update_buffer_status: + # all data segments should have the same length, so first check if we run out of buffer space + data_length = data[0][1].shape[0] + if (self.buffer_idx + data_length) >= self.buffer_length: + # if we run out of buffer space before data has been sent to the DB few things could have gone + # wrong: + # - data in buffer is not good enough + # - the new data chunk is larger than the difference between buffer and sample length + # (e.g. 6s buffer and 4s sample, if the current buffer has 3s of data and it receives a 4s + # long chunk then the buffer would overrun, and still not have enough data to send to DB. + # Although unlikely in real-life, it happened during debugging.) + + # trim data to only fill the buffer, discarding the rest + # TODO: is this the optimal solution? Slide buffer instead? + data_length = self.buffer_length - self.buffer_idx + + # continue to validate received data + for chan_idx, (chan, values) in enumerate(data): + + if data_length > 0: + # Validate data + valid = self.validate_data_sample(values[:data_length]) + + # append data to buffer + self.buffer[chan_idx, + self.buffer_idx:self.buffer_idx + data_length] = values[:data_length] + + self.validity[chan_idx, + self.buffer_idx:self.buffer_idx + data_length] = valid + + _status = "accumulating" + + # increment buffer index, all data segments should have same length, if they don't, will match + # the first channel + self.buffer_idx += data_length + + # check if data length > sample length + if self.buffer_idx >= self.sample_length: + + # compute total validity of last sample_length and if > threshold, send to DB + sample_idx = self.buffer_idx - self.sample_length + + temp_sum = [np.sum(x[sample_idx:self.buffer_idx]) for x in self.validity] + + # check if validity is better than previous sample, if so, store it + if np.sum(temp_sum) > self.valid_idx[1]: + self.valid_idx = (sample_idx, np.sum(temp_sum)) + + if all(x >= y for x, y in zip(temp_sum, self.validity_threshold)) or \ + self.buffer_idx >= self.buffer_length: + # We have accumulated enough data for this depth. Send to db! + stored = self.send_to_db(current_depth) + if stored: + _status = "done" + + # check for new depth + # At this point, the individual channels have either been sent to the DB or are still collecting waiting for + # either of the following conditions: acquire sufficient data (i.e. sample_length) or acquire sufficiently + # clean data (i.e. validity_threshold). If the channel is still acquiring data but has sufficiently long + # segments, we will send the cleanest segment to the DB (i.e. valid_idx). + new_depth = None + try: + received_msg = self._ddu_sock.recv_string(flags=zmq.NOBLOCK)[len("ddu") + 1:] + if received_msg: + new_depth = float(received_msg) + except zmq.ZMQError: + pass + + # If new depth value + if new_depth is not None and (new_depth != current_depth or self.overwrite_depth): + # We are moving on. If still updating the buffer, then we can check to see if we have + # enough valid samples -- though maybe not high quality -- and save the best available segment. + if self.update_buffer_status: + _ = self.send_to_db(current_depth) + + # New depth verified. Let's clear the buffer for accumulation again. + self.clear_buffer() + current_depth = new_depth + + if prev_status is None: + # First iteration -- triggers an update from the ProcedureGUI if running + self._snippet_sock.send_string(f"snippet_status refresh") + + # Optionally publish the recording status + # if status has changed, but not from done->recording as done implies recording, + # and we prefer to keep the "done" status until there's a new depth. + if _status != prev_status and not (prev_status == "done" and _status == "recording"): + self._snippet_sock.send_string(f"snippet_status {_status}") + prev_status = _status + + time.sleep(.010) diff --git a/open_mer/scripts/Depth_Process.py b/open_mer/scripts/Depth_Process.py index a7d9031..1611680 100644 --- a/open_mer/scripts/Depth_Process.py +++ b/open_mer/scripts/Depth_Process.py @@ -1,374 +1,4 @@ -from qtpy import QtCore -import importlib.resources as pkg_resources -from pathlib import Path -import time -import json - -import numpy as np -import zmq -from cerebuswrapper import CbSdkConnection -from django.utils import timezone -from serf.tools.db_wrap import DBWrapper - -from open_mer.data_source.cerebus import SAMPLINGGROUPS - - -class NSPBufferWorker: - - def __init__(self, ipc_settings, buffer_settings): - self._ipc_settings = ipc_settings - self._buffer_settings = buffer_settings - self.buffer = None - self.db_wrapper = DBWrapper() - self.procedure_id = None - - # cbSDK; connect using default parameters - self.cbsdk_conn = CbSdkConnection(simulate_ok=False) - self.cbsdk_conn.connect() - - self._setup_ipc() - - self._reset_group_info() - self.reset_buffer() - - self.start_time = timezone.now() - self._snippet_sock.send_string(f"snippet_status startup") - self.is_running = True - - def _setup_ipc(self): - self._ipc_context = zmq.Context() - - # procedure settings subscription - self._ctrl_sock = self._ipc_context.socket(zmq.SUB) - self._ctrl_sock.connect(f"tcp://localhost:{self._ipc_settings['procedure_settings']}") - self._ctrl_sock.setsockopt_string(zmq.SUBSCRIBE, "procedure_settings") - - # ddu depth subscription - self._ddu_sock = self._ipc_context.socket(zmq.SUB) - self._ddu_sock.connect(f"tcp://localhost:{self._ipc_settings['ddu']}") - self._ddu_sock.setsockopt_string(zmq.SUBSCRIBE, "ddu") - - # self status publisher - self._snippet_sock = self._ipc_context.socket(zmq.PUB) - self._snippet_sock.bind(f"tcp://*:{self._ipc_settings['snippet_status']}") - - def _reset_group_info(self): - self.sampling_rate = self._buffer_settings["sampling_group"] # TODO: Fix name in FeaturesGUI.ini - self.sampling_group = SAMPLINGGROUPS.index(str(self.sampling_rate)) - self.group_info = self.cbsdk_conn.get_group_config(self.sampling_group) - self.n_chan = len(self.group_info) - self.valid_electrodes = [_["chan"] for _ in self.group_info] - self.buffer_length = int(self.sampling_rate * self._buffer_settings["buffer_duration"]) - self.sample_length = int(self.sampling_rate * self._buffer_settings["sample_duration"]) - self.delay_length = int(self.sampling_rate * self._buffer_settings["delay_duration"]) - self.overwrite_depth = self._buffer_settings["overwrite_depth"] - # default values, might be overwritten by electrode_settings - self.validity_threshold = [self.sample_length * self._buffer_settings["validity_threshold"]] * self.n_chan - self.threshold = [False] * self.n_chan - - def reset_buffer(self): - self.buffer = np.zeros((self.n_chan, self.buffer_length), dtype=np.int16) - self.buffer_idx = 0 - # for each channel we will keep a bool array whether each sample point is valid or not - # when a condition is met to trigger sending the sample to the DB we will pick the window - # with highest validity count. - self.validity = np.zeros((self.n_chan, self.buffer_length), dtype=bool) - self.valid_idx = (0, 0) - - self.delay_counter = 0 - self.update_buffer_status = True - self.delay_done = False - - def process_settings(self, sett_dict): - # process inputs - sett_keys = list(sett_dict.keys()) - - if "procedure" in sett_keys and "procedure_id" in sett_dict["procedure"]: - self.reset_procedure(sett_dict["procedure"]["procedure_id"]) - - if "sampling_group_id" in sett_keys: - # TODO: buffer_settings has bad key name because of the error in FeaturesGUI.ini - self._buffer_settings["sampling_group"] = sett_dict["sampling_rate"] - # self._buffer_settings["validity_threshold"] = sett_dict["???"] - self._reset_group_info() - self.reset_buffer() - - if "buffer" in sett_keys and "electrode_settings" in sett_dict["buffer"]: - for ii, info in enumerate(self.group_info): - label = info["label"] - if label in sett_dict["buffer"]["electrode_settings"]: - el_sett = sett_dict["buffer"]["electrode_settings"][label] - self.threshold[ii] = bool(el_sett["threshold"]) - self.validity_threshold[ii] = float(el_sett["validity"]) / 100 * self.sample_length - - def reset_procedure(self, proc_id): - self.procedure_id = proc_id - self.db_wrapper.select_procedure(self.procedure_id) - - def clear_buffer(self): - if self.buffer is None: - self.reset_buffer() - self.buffer.fill(0) - self.buffer_idx = 0 - - self.validity.fill(False) - # list of tuples: (index of validity value, value) - # saves the index with largest validity across all channels - self.valid_idx = (0, 0) - self.delay_counter = 0 - - self.update_buffer_status = True - self.delay_done = False - # self.start_time = timezone.now() - - def wait_for_delay_end(self, data): - data_length = data[0][1].shape[0] - self.delay_counter += data_length - # check if we have accumulated enough data to end delay and start recording - if self.delay_counter <= self.delay_length: - return False - else: - # truncate the data to the first index over the delay period - start_idx = max(0, int(self.delay_length - self.delay_counter)) - for chan_idx, (chan, values) in enumerate(data): - data[chan_idx][1] = values[start_idx:] - - # now is for the last sample. subtract data length / SAMPLINGRATE to get time of first sample - self.start_time = timezone.now() - time_delta = timezone.timedelta(seconds=data[0][1].shape[0] / self.sampling_rate) - self.start_time -= time_delta - - self._snippet_sock.send_string(f"snippet_status recording") - return True - - def send_to_db(self, depth): - do_save = self.valid_idx[1] != 0 - # if we actually have a computed validity (i.e. segment is long enough) - if do_save: - # the info that needs to be sent the DB_wrapper is: - # Datum: - # - subject_id - # - is_good : to be determined by validity values - # - start_time / stop_time ? - # Datum Store: - # - channel_labels : from group_info - # - erp : actual data - # - n_channels and n_samples : determined by data size - # - x_vec: time ? - # DatumDetailValue: - # - detail_type: depth (fetch from DetailType - # - value: depth value - self.db_wrapper.save_depth_datum(depth=depth, - data=self.buffer[:, - self.valid_idx[0]:self.valid_idx[0]+self.sample_length], - is_good=np.array([x >= y for x, y in zip( - np.sum(self.validity[:, self.valid_idx[0]: - self.valid_idx[0] + self.sample_length], axis=1), - self.validity_threshold)], dtype=bool), - group_info=self.group_info, - start_time=self.start_time, - stop_time=timezone.now()) - self.update_buffer_status = False - return do_save - - @staticmethod - def validate_data_sample(data): - # TODO: implement other metrics - # SUPER IMPORTANT: when cbpy returns an int16 value, it can be -32768, however in numpy: - # np.abs(-32768) = -32768 for 16 bit integers since +32768 does not exist. - # We therefore can't use the absolute value for the threshold. - threshold = 30000 # arbitrarily set for now - validity = np.array([-threshold < x < threshold for x in data]) - - return validity - - def run_forever(self): - prev_status = None - current_depth = None - while self.is_running: - try: - received_msg = self._ctrl_sock.recv_string(flags=zmq.NOBLOCK)[len("procedure_settings") + 1:] - settings_dict = json.loads(received_msg) - # Check for kill signal - if "running" in settings_dict and not settings_dict["running"]: - self.is_running = False - continue - # Process remaining settings - self.process_settings(settings_dict) - except zmq.ZMQError: - pass - - # collect NSP data, regardless of recording status to keep cbsdk buffer empty - # data is a list of lists. - # 1st level is a list of channels - # 2nd level is a list [chan_id, np.array(data)] - data = self.cbsdk_conn.get_continuous_data() - # Only keep channels within our sampling group - data = [x for x in data if x[0] in self.valid_electrodes] - - # only process the NSP data if Central is recording - _status = "recording" if self.cbsdk_conn.get_recording_state() else "notrecording" - if _status == "recording" and data and current_depth: - - if not self.delay_done: - self.delay_done = self.wait_for_delay_end(data) - - # Only process if we are in a new depth, past the delay, and we didn't just send a snippet to the db. - if self.delay_done and self.update_buffer_status: - # all data segments should have the same length, so first check if we run out of buffer space - data_length = data[0][1].shape[0] - if (self.buffer_idx + data_length) >= self.buffer_length: - # if we run out of buffer space before data has been sent to the DB few things could have gone - # wrong: - # - data in buffer is not good enough - # - the new data chunk is larger than the difference between buffer and sample length - # (e.g. 6s buffer and 4s sample, if the current buffer has 3s of data and it receives a 4s - # long chunk then the buffer would overrun, and still not have enough data to send to DB. - # Although unlikely in real-life, it happened during debugging.) - - # trim data to only fill the buffer, discarding the rest - # TODO: is this the optimal solution? Slide buffer instead? - data_length = self.buffer_length - self.buffer_idx - - # continue to validate received data - for chan_idx, (chan, values) in enumerate(data): - - if data_length > 0: - # Validate data - valid = self.validate_data_sample(values[:data_length]) - - # append data to buffer - self.buffer[chan_idx, - self.buffer_idx:self.buffer_idx + data_length] = values[:data_length] - - self.validity[chan_idx, - self.buffer_idx:self.buffer_idx + data_length] = valid - - _status = "accumulating" - - # increment buffer index, all data segments should have same length, if they don't, will match - # the first channel - self.buffer_idx += data_length - - # check if data length > sample length - if self.buffer_idx >= self.sample_length: - - # compute total validity of last sample_length and if > threshold, send to DB - sample_idx = self.buffer_idx - self.sample_length - - temp_sum = [np.sum(x[sample_idx:self.buffer_idx]) for x in self.validity] - - # check if validity is better than previous sample, if so, store it - if np.sum(temp_sum) > self.valid_idx[1]: - self.valid_idx = (sample_idx, np.sum(temp_sum)) - - if all(x >= y for x, y in zip(temp_sum, self.validity_threshold)) or \ - self.buffer_idx >= self.buffer_length: - # We have accumulated enough data for this depth. Send to db! - stored = self.send_to_db(current_depth) - if stored: - _status = "done" - - # check for new depth - # At this point, the individual channels have either been sent to the DB or are still collecting waiting for - # either of the following conditions: acquire sufficient data (i.e. sample_length) or acquire sufficiently - # clean data (i.e. validity_threshold). If the channel is still acquiring data but has sufficiently long - # segments, we will send the cleanest segment to the DB (i.e. valid_idx). - new_depth = None - try: - received_msg = self._ddu_sock.recv_string(flags=zmq.NOBLOCK)[len("ddu") + 1:] - if received_msg: - new_depth = float(received_msg) - except zmq.ZMQError: - pass - - # If new depth value - if new_depth is not None and (new_depth != current_depth or self.overwrite_depth): - # We are moving on. If still updating the buffer, then we can check to see if we have - # enough valid samples -- though maybe not high quality -- and save the best available segment. - if self.update_buffer_status: - _ = self.send_to_db(current_depth) - - # New depth verified. Let's clear the buffer for accumulation again. - self.clear_buffer() - current_depth = new_depth - - if prev_status is None: - # First iteration -- triggers an update from the ProcedureGUI if running - self._snippet_sock.send_string(f"snippet_status refresh") - - # Optionally publish the recording status - # if status has changed, but not from done->recording as done implies recording, - # and we prefer to keep the "done" status until there's a new depth. - if _status != prev_status and not (prev_status == "done" and _status == "recording"): - self._snippet_sock.send_string(f"snippet_status {_status}") - prev_status = _status - - time.sleep(.010) - - -def build_ini_paths() -> list[Path]: - # Infer paths to ini files - res_settings_path = pkg_resources.files("open_mer.resources") / "settings" - home_path = Path.home() / ".open_mer" - - return [ - res_settings_path / "IPC.ini", - res_settings_path / "FeaturesGUI.ini", # for buffer settings - home_path / "IPC.ini", - home_path / "FeaturesGUI.ini" # for buffer settings - ] - - -def parse_ini(ini_paths) -> (dict, dict, dict): - ipc_settings = {} - buffer_settings = {} - feature_settings = {} - for ini_path in ini_paths: - settings = QtCore.QSettings(str(ini_path), QtCore.QSettings.IniFormat) - - # Store / update IPC settings which will be used by subclasses only - settings.beginGroup("ZeroMQ") - sock_ids = [int(_) for _ in settings.childGroups()] - sock_ids.sort() - for sock_id in sock_ids: - settings.beginGroup(str(sock_id)) - ipc_settings[settings.value("topic", type=str)] = settings.value("port", type=int) - settings.endGroup() - settings.endGroup() - - # Buffer - settings.beginGroup("buffer") - for k, t in { - "highpass": bool, - "buffer_duration": float, - "sample_duration": float, - "delay_duration": float, - "validity_threshold": float, - "overwrite_depth": bool, - # electrode_settings ?! - # chk_threshold ?! - }.items(): - if k in settings.allKeys(): - buffer_settings[k] = settings.value(k, type=t) - settings.endGroup() # buffer - - settings.beginGroup("data-source") - for k, t in {"sampling_group": int}.items(): - if k in settings.allKeys(): - buffer_settings[k] = settings.value(k, type=t) - settings.endGroup() - - # Features (used by Features_Process) - settings.beginGroup("features") - feat_inds = [int(_) for _ in settings.childGroups()] - feat_inds.sort() - for f_ind in feat_inds: - settings.beginGroup(str(f_ind)) - feature_settings[f_ind] = (settings.value("name", type=str), settings.value("enable", type=bool)) - settings.endGroup() - settings.endGroup() # features - return ipc_settings, buffer_settings, feature_settings +from open_mer.dbsgui.process import parse_ini, build_ini_paths, NSPBufferWorker def main(): From 873f05670bb4b560bc056d0a0f7a2cc838785e59 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 1 Nov 2023 23:56:00 -0400 Subject: [PATCH 56/65] Procedure SettingsDialog - eliminate widgets from member variable namespace; find on demand. --- open_mer/dbsgui/widgets/SettingsDialog.py | 304 ++++++++++++---------- 1 file changed, 162 insertions(+), 142 deletions(-) diff --git a/open_mer/dbsgui/widgets/SettingsDialog.py b/open_mer/dbsgui/widgets/SettingsDialog.py index 91ad350..0b2ce36 100644 --- a/open_mer/dbsgui/widgets/SettingsDialog.py +++ b/open_mer/dbsgui/widgets/SettingsDialog.py @@ -108,110 +108,108 @@ def __init__(self, procedure_settings): self.update_settings_from_db(-1) self.proc_enums = DBWrapper().return_enums("procedure") + self._setup_ui() + self.update_proc_widgets_from_settings() + + def _setup_ui(self): proc_layout = QGridLayout(self) row = 0 proc_layout.addWidget(QLabel("Previous procedures: "), row, 0, 1, 1) - self.prev_proc = QComboBox() - self.prev_proc.setEnabled(True) + + prev_proc = QComboBox() + prev_proc.setObjectName("procedure_QComboBox") + prev_proc.setEnabled(True) + proc_layout.addWidget(prev_proc, row, 1, 1, 3) self.check_all_procedures(None, False) - self.prev_proc.currentIndexChanged.connect(self.procedure_selection_change) - proc_layout.addWidget(self.prev_proc, row, 1, 1, 3) + prev_proc.currentIndexChanged.connect(self.procedure_selection_change) row += 1 proc_layout.addWidget(QLabel("Target name: "), row, 0, 1, 1) - self.target_name = QLineEdit("") - proc_layout.addWidget(self.target_name, row, 1, 1, 3) + target_name = QLineEdit("") + target_name.setObjectName("targetName_QLineEdit") + proc_layout.addWidget(target_name, row, 1, 1, 3) row += 1 proc_layout.addWidget(QLabel("Type: "), row, 0, 1, 1) - self.type_combo = self.combo_from_enum('type') - proc_layout.addWidget(self.type_combo, row, 1, 1, 3) + type_combo = self.combo_from_enum("type") + type_combo.setObjectName("type_QComboBox") + proc_layout.addWidget(type_combo, row, 1, 1, 3) row += 1 proc_layout.addWidget(QLabel("Recording configuration: "), row, 0, 1, 1) - self.rec_combo = self.combo_from_enum('recording_config') - proc_layout.addWidget(self.rec_combo, row, 1, 1, 3) + rec_combo = self.combo_from_enum('recording_config') + rec_combo.setObjectName("recording_QComboBox") + proc_layout.addWidget(rec_combo, row, 1, 1, 3) row += 1 proc_layout.addWidget(QLabel("Electrode configuration: "), row, 0, 1, 1) - self.electrode_combo = self.combo_from_enum('electrode_config') - proc_layout.addWidget(self.electrode_combo, row, 1, 1, 3) + electrode_combo = self.combo_from_enum("electrode_config") + electrode_combo.setObjectName("electrode_QComboBox") + proc_layout.addWidget(electrode_combo, row, 1, 1, 3) row += 1 proc_layout.addWidget(QLabel("Distance to target: "), row, 0, 1, 1) - self.dist_to_target = self.coord_line_edit() - proc_layout.addWidget(self.dist_to_target, row, 1, 1, 1) + dist_to_target = self.coord_line_edit() + dist_to_target.setObjectName("distance_to_target_QLineEdit") + proc_layout.addWidget(dist_to_target, row, 1, 1, 1) + + for field_name in ["Entry", "Target", "A", "E"]: + row += 1 + proc_layout.addWidget(QLabel(f"{field_name} (x, y, z): "), row, 0, 1, 1) + for ix, d in enumerate(["x", "y", "z"]): + _le = self.coord_line_edit() + _le.setObjectName(f"{field_name.lower()}_{d}_QLineEdit") + proc_layout.addWidget(_le, row, ix + 1, 1, 1) row += 1 - proc_layout.addWidget(QLabel("Entry (x, y, z): "), row, 0, 1, 1) - self.entry_x = self.coord_line_edit() - proc_layout.addWidget(self.entry_x, row, 1, 1, 1) - self.entry_y = self.coord_line_edit() - proc_layout.addWidget(self.entry_y, row, 2, 1, 1) - self.entry_z = self.coord_line_edit() - proc_layout.addWidget(self.entry_z, row, 3, 1, 1) - - row += 1 - proc_layout.addWidget(QLabel("Target (x, y, z): "), row, 0, 1, 1) - self.target_x = self.coord_line_edit() - proc_layout.addWidget(self.target_x, row, 1, 1, 1) - self.target_y = self.coord_line_edit() - proc_layout.addWidget(self.target_y, row, 2, 1, 1) - self.target_z = self.coord_line_edit() - proc_layout.addWidget(self.target_z, row, 3, 1, 1) - - row += 1 - self.comp_dist_to_target = QLabel("Computed distance: 0.000 mm; Difference: 0.000 mm") - proc_layout.addWidget(self.comp_dist_to_target, row, 0, 1, 2) - - row += 1 - proc_layout.addWidget(QLabel("A (x, y, z): "), row, 0, 1, 1) - self.a_x = self.coord_line_edit() - proc_layout.addWidget(self.a_x, row, 1, 1, 1) - self.a_y = self.coord_line_edit() - proc_layout.addWidget(self.a_y, row, 2, 1, 1) - self.a_z = self.coord_line_edit() - proc_layout.addWidget(self.a_z, row, 3, 1, 1) - - row += 1 - proc_layout.addWidget(QLabel("E (x, y, z): "), row, 0, 1, 1) - self.e_x = self.coord_line_edit() - proc_layout.addWidget(self.e_x, row, 1, 1, 1) - self.e_y = self.coord_line_edit() - proc_layout.addWidget(self.e_y, row, 2, 1, 1) - self.e_z = self.coord_line_edit() - proc_layout.addWidget(self.e_z, row, 3, 1, 1) + comp_dist_to_target = QLabel("Computed distance: 0.000 mm; Difference: 0.000 mm") + comp_dist_to_target.setObjectName("computed_QLabel") + proc_layout.addWidget(comp_dist_to_target, row, 0, 1, 2) row += 1 proc_layout.addWidget(QLabel("Offset direction: "), row, 0, 1, 1) - self.offset_direction_combo = self.combo_from_enum('offset_direction') - proc_layout.addWidget(self.offset_direction_combo, row, 1, 1, 3) + offset_direction_combo = self.combo_from_enum('offset_direction') + offset_direction_combo.setObjectName("offset_direction_QComboBox") + proc_layout.addWidget(offset_direction_combo, row, 1, 1, 3) row += 1 proc_layout.addWidget(QLabel("Offset size: "), row, 0, 1, 1) - self.offset_size = self.coord_line_edit() - proc_layout.addWidget(self.offset_size, row, 1, 1, 1) + offset_size = self.coord_line_edit() + offset_size.setObjectName("offset_QLineEdit") + proc_layout.addWidget(offset_size, row, 1, 1, 1) row += 1 proc_layout.addWidget(QLabel("Medication status: "), row, 0, 1, 1) - self.medic_combo = self.combo_from_enum('medication_status') - proc_layout.addWidget(self.medic_combo, row, 1, 1, 3) + medic_combo = self.combo_from_enum("medication_status") + medic_combo.setObjectName("medic_QComboBox") + proc_layout.addWidget(medic_combo, row, 1, 1, 3) row += 1 proc_layout.addWidget(QWidget(), row, 1, 1, 3) - self.update_proc_widgets_from_settings() - def check_all_procedures(self, subject_id, block): - self.prev_proc.blockSignals(block) self.all_procedures = DBWrapper().list_all_procedures(subject_id) - self.prev_proc.clear() - self.prev_proc.addItem('') - self.prev_proc.addItems( - [' '.join([x.target_name, x.recording_config, x.date.strftime('%Y-%m-%d')]) for x in self.all_procedures]) - self.prev_proc.setCurrentIndex(0) - self.prev_proc.blockSignals(False) + + prev_proc: QComboBox = self.findChild(QComboBox, name="procedure_QComboBox") + # Clear combobox and fill with a summary of each procedure + prev_proc.blockSignals(block) + prev_proc.clear() + prev_proc.addItem("") # 0th item is always blank. + prev_proc.addItems([ + " ".join([ + str(x.procedure_id), x.target_name, x.recording_config, x.date.strftime('%Y-%m-%d') + ]) + for x in self.all_procedures + ]) + row_ix = 0 + if "procedure_id" in self.procedure_settings and self.procedure_settings["procedure_id"] != -1: + known_ids = [_.procedure_id for _ in self.all_procedures] + if self.procedure_settings["procedure_id"] in known_ids: + row_ix = known_ids.index(self.procedure_settings["procedure_id"]) + 1 + prev_proc.blockSignals(False) + prev_proc.setCurrentIndex(row_ix) + prev_proc.blockSignals(False) def combo_from_enum(self, enum_name): combo = QComboBox() @@ -232,21 +230,31 @@ def coord_line_edit(self): return line def update_dist_to_target(self, new_string): + if new_string not in ["-", ".", "", "-."]: - ddt = np.sqrt((float(self.target_x.text()) - float(self.entry_x.text()))**2 + - (float(self.target_y.text()) - float(self.entry_y.text()))**2 + - (float(self.target_z.text()) - float(self.entry_z.text()))**2) - diff = float(self.dist_to_target.text()) - ddt - self.comp_dist_to_target.setText( + vals = np.array([ + [ + float(self.findChild(QLineEdit, f"{fn}_{d}_QLineEdit").text()) + for fn in ["target", "entry"] + ] + for d in ["x", "y", "z"] + ]) + ddt = np.sqrt(np.sum((vals[:, 0] - vals[:, 1]) ** 2)) + + dist_to_target: QLineEdit = self.findChild(QLineEdit, "distance_to_target_QLineEdit") + diff = float(dist_to_target.text()) - ddt + + self.findChild(QLabel, "computed_QLabel").setText( "Computed distance: {:.3f} mm; Difference: {:.3f} mm".format(ddt, diff)) def change_subject(self, sub_id, block=False): self.check_all_procedures(sub_id, block) def procedure_selection_change(self): + prev_proc = self.findChild(QComboBox, name="procedure_QComboBox") id = -1 - if self.prev_proc.currentIndex() > 0: - ix = self.prev_proc.currentIndex() - 1 # -1 because first entry is always blank. + if prev_proc.currentIndex() > 0: + ix = prev_proc.currentIndex() - 1 # -1 because first entry is always blank. id = self.all_procedures[ix].procedure_id self.update_settings_from_db(id) self.update_proc_widgets_from_settings() @@ -256,95 +264,105 @@ def update_settings_from_db(self, idx): self.procedure_settings.update(res_dict) def update_proc_widgets_from_settings(self): - self.target_name.setText(self.procedure_settings.get("target_name", "")) - self.type_combo.setCurrentText(self.procedure_settings.get("type", "")) - self.rec_combo.setCurrentText(self.procedure_settings.get("recording_config", "")) - self.electrode_combo.setCurrentText(self.procedure_settings.get("electrode_config", "")) - self.medic_combo.setCurrentText(self.procedure_settings.get("medication_status", "")) - self.offset_size.setText(str(self.procedure_settings.get("offset_size", None))) - self.offset_direction_combo.setCurrentText(self.procedure_settings.get("offset_direction", "")) - entry = self.procedure_settings.get("entry", None) - if entry is None: - entry = [0., 0., 0.] - self.entry_x.setText(str(entry[0])) - self.entry_y.setText(str(entry[1])) - self.entry_z.setText(str(entry[2])) - target = self.procedure_settings.get("target", None) - if target is None: - target = [0., 0., 0.] - self.target_x.setText(str(target[0])) - self.target_y.setText(str(target[1])) - self.target_z.setText(str(target[2])) - ddt = self.procedure_settings.get("distance_to_target", None) - if ddt is None: - ddt = 0.000 - self.dist_to_target.setText(str(ddt)) + target_name: QLineEdit = self.findChild(QLineEdit, name="targetName_QLineEdit") + target_name.setText(self.procedure_settings.get("target_name", "")) + + type_combo: QComboBox = self.findChild(QComboBox, "type_QComboBox") + type_combo.setCurrentText(self.procedure_settings.get("type", "")) + + rec_combo: QComboBox = self.findChild(QComboBox, "recording_QComboBox") + rec_combo.setCurrentText(self.procedure_settings.get("recording_config", "")) + + electrode_combo: QComboBox = self.findChild(QComboBox, "electrode_QComboBox") + electrode_combo.setCurrentText(self.procedure_settings.get("electrode_config", "")) + + medic_combo: QComboBox = self.findChild(QComboBox, name="medic_QComboBox") + medic_combo.setCurrentText(self.procedure_settings.get("medication_status", "")) + + offset_size: QLineEdit = self.findChild(QLineEdit, name="offset_QLineEdit") + offset_size.setText(str(self.procedure_settings.get("offset_size", None))) + + offset_direction: QComboBox = self.findChild(QComboBox, name="offset_direction_QComboBox") + offset_direction.setCurrentText(self.procedure_settings.get("offset_direction", "")) + + for field_name in ["entry", "target", "a", "e"]: + field_value = self.procedure_settings.get(field_name, None) + if field_value is None: + field_value = [0., 0., 0.] + for d, v in zip(["x", "y", "z"], field_value): + _txt: QLineEdit = self.findChild(QLineEdit, f"{field_name}_{d}_QLineEdit") + _txt.setText(str(v)) + + dtt = self.procedure_settings.get("distance_to_target", None) + if dtt is None: + dtt = 0.000 + dtt_edit: QLineEdit = self.findChild(QLineEdit, "distance_to_target_QLineEdit") + dtt_edit.setText(str(dtt)) # self.update_dist_to_target() - a = self.procedure_settings.get("a", None) - if a is None: - a = [0., 0., 0.] - self.a_x.setText(str(a[0])) - self.a_y.setText(str(a[1])) - self.a_z.setText(str(a[2])) - e = self.procedure_settings.get("e", None) - if e is None: - e = [0., 0., 0.] - self.e_x.setText(str(e[0])) - self.e_y.setText(str(e[1])) - self.e_z.setText(str(e[2])) def to_dict(self): - self.procedure_settings["type"] = self.type_combo.currentText() - self.procedure_settings["a"] = np.array([float(self.a_x.text()), - float(self.a_y.text()), - float(self.a_z.text())], dtype=float) - self.procedure_settings["distance_to_target"] = float(self.dist_to_target.text()) - self.procedure_settings["e"] = np.array([float(self.e_x.text()), - float(self.e_y.text()), - float(self.e_z.text())], dtype=float) - self.procedure_settings["electrode_config"] = self.electrode_combo.currentText() - self.procedure_settings["entry"] = np.array([float(self.entry_x.text()), - float(self.entry_y.text()), - float(self.entry_z.text())], dtype=float) - self.procedure_settings["medication_status"] = self.medic_combo.currentText() - self.procedure_settings["target_name"] = self.target_name.text() - self.procedure_settings["recording_config"] = self.rec_combo.currentText() - self.procedure_settings["target"] = np.array([float(self.target_x.text()), - float(self.target_y.text()), - float(self.target_z.text())], dtype=float) - self.procedure_settings["offset_direction"] = self.offset_direction_combo.currentText() - self.procedure_settings["offset_size"] = float(self.offset_size.text()) + type_combo = self.findChild(QComboBox, name="type_QComboBox") + self.procedure_settings["type"] = type_combo.currentText() + + for field in ["entry", "target", "a", "e"]: + vals = np.array([ + float(self.findChild(QLineEdit, name=f"{field}_{dim}_QLineEdit").text()) + for dim in ["x", "y", "z"] + ], dtype=float) + self.procedure_settings[field] = vals + + dist_to_target: QLineEdit = self.findChild(QLineEdit, "distance_to_target_QLineEdit") + self.procedure_settings["distance_to_target"] = float(dist_to_target.text()) + + electrode_combo: QComboBox = self.findChild(QComboBox, name="electrode_QComboBox") + self.procedure_settings["electrode_config"] = electrode_combo.currentText() + + medic_combo: QComboBox = self.findChild(QComboBox, name="medic_QComboBox") + self.procedure_settings["medication_status"] = medic_combo.currentText() + + target_name: QLineEdit = self.findChild(QLineEdit, name="targetName_QLineEdit") + self.procedure_settings["target_name"] = target_name.text() + + rec_combo: QComboBox = self.findChild(QComboBox, name="recording_QComboBox") + self.procedure_settings["recording_config"] = rec_combo.currentText() + + offset_direction_combo: QComboBox = self.findChild(QComboBox, "offset_direction_QComboBox") + self.procedure_settings["offset_direction"] = offset_direction_combo.currentText() + + offset_size: QLineEdit = self.findChild(QLineEdit, name="offset_QLineEdit") + self.procedure_settings["offset_size"] = float(offset_size.text()) class SettingsDialog(QDialog): - def __init__(self, subject_settings: dict, procedure_settings: dict, parent=None): + def __init__( + self, + subject_settings: dict, # Will be mutated by SubjectWidget! + procedure_settings: dict, # Will be mutated by ProcedureWidget! + parent=None + ): super(SettingsDialog, self).__init__(parent) self.setWindowTitle("Enter settings.") - # settings dicts - we will mutate the input dictionaries - self.subject_settings = subject_settings - self.procedure_settings = procedure_settings - # Widgets to show/edit parameters. - self.settings_layout = QVBoxLayout(self) + dialog_layout = QVBoxLayout(self) tab_widget = QTabWidget(self) - self.subject_widget = SubjectWidget(self.subject_settings) + self.subject_widget = SubjectWidget(subject_settings) tab_widget.addTab(self.subject_widget, 'Subject') - self.proc_widget = ProcedureWidget(self.procedure_settings) + self.proc_widget = ProcedureWidget(procedure_settings) tab_widget.addTab(self.proc_widget, 'Procedure') - self.settings_layout.addWidget(tab_widget) + dialog_layout.addWidget(tab_widget) # signals self.subject_widget.subject_change.connect(self.proc_widget.change_subject) # update procedures when re-opening settings window - if "subject_id" not in self.subject_settings.keys(): + if "subject_id" not in subject_settings.keys(): self.subject_widget.check_subject() - elif self.subject_settings["subject_id"] not in [None, ""]: - self.proc_widget.change_subject(self.subject_settings["subject_id"], block=True) + elif subject_settings["subject_id"] not in [None, ""]: + self.proc_widget.change_subject(subject_settings["subject_id"], block=True) # OK and Cancel buttons buttons = QDialogButtonBox( @@ -352,7 +370,9 @@ def __init__(self, subject_settings: dict, procedure_settings: dict, parent=None Qt.Horizontal, self) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) - self.settings_layout.addWidget(buttons, alignment=Qt.AlignHCenter) + dialog_layout.addWidget(buttons, alignment=Qt.AlignHCenter) + + self.setLayout(dialog_layout) def update_settings(self): self.subject_widget.to_dict() From b2a22cfdf3e96d8e060c6af2bc8afedd68e6aad5 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 1 Nov 2023 23:58:09 -0400 Subject: [PATCH 57/65] Debug prints; --- open_mer/dbsgui/features.py | 1 + open_mer/dbsgui/procedure.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/open_mer/dbsgui/features.py b/open_mer/dbsgui/features.py index 3fce90e..873c194 100644 --- a/open_mer/dbsgui/features.py +++ b/open_mer/dbsgui/features.py @@ -361,6 +361,7 @@ def _check_ipc(self): if procedure_id != self._db.current_procedure or len(self._chan_labels) == 0: self._db.select_procedure(procedure_id) self._chan_labels = self._db.list_channel_labels() + print(f"Resetting features for procedure {procedure_id} with channels {self._chan_labels}") self.try_reset_widget() except zmq.ZMQError: pass diff --git a/open_mer/dbsgui/procedure.py b/open_mer/dbsgui/procedure.py index cc23310..2415a03 100644 --- a/open_mer/dbsgui/procedure.py +++ b/open_mer/dbsgui/procedure.py @@ -131,7 +131,9 @@ def _publish_settings(self): "subject": {**self._subject_settings, "birthday": self._subject_settings["birthday"].isoformat()}, "recording": {"state": self._b_recording} } - self._procedure_sock.send_string("procedure_settings " + json.dumps(send_dict)) + pub_string = "procedure_settings " + json.dumps(send_dict) + print(f"Publishing: {pub_string}") + self._procedure_sock.send_string(pub_string) def _do_modal_settings(self): win = SettingsDialog(self._subject_settings, self._procedure_settings) From 759f3caf87b2e9066bf55e32066c1e2257bb6565 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 19 Nov 2023 21:19:07 -0500 Subject: [PATCH 58/65] Update feature-processing logic. --- open_mer/scripts/Features_Process.py | 40 +++++++++++++++------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/open_mer/scripts/Features_Process.py b/open_mer/scripts/Features_Process.py index e0f2798..a6c859d 100644 --- a/open_mer/scripts/Features_Process.py +++ b/open_mer/scripts/Features_Process.py @@ -20,11 +20,9 @@ def __init__(self, ipc_settings, buffer_settings, feature_settings): self._setup_ipc() self.procedure_id = None - self.all_datum_ids = [] - self.gt = 0 # fetch datum ids greater than this value + self.last_datum_id = 0 # fetch datum ids greater than this value - feat_list = [v[0] for k, v in feature_settings.items() if v[1]] - self.reset_features(feat_list) + self._select_latest_procedure() self.is_running = True @@ -36,6 +34,18 @@ def _setup_ipc(self): self._ctrl_sock.connect(f"tcp://localhost:{self._ipc_settings['procedure_settings']}") self._ctrl_sock.setsockopt_string(zmq.SUBSCRIBE, "procedure_settings") + def _select_latest_procedure(self): + # Get the 0th subject and the -1th procedure for that subject + sub_id = list(self.db_wrapper.list_all_subjects())[0] + subj_details = self.db_wrapper.load_subject_details(sub_id) + self.db_wrapper.select_subject(subj_details["subject_id"]) + proc = list(self.db_wrapper.list_all_procedures(subj_details["subject_id"]))[-1] + sett_dict = { + "procedure": {"procedure_id": proc.procedure_id}, + "features": [v[0] for k, v in self._feature_settings.items() if v[1]] + } + self.process_settings(sett_dict) + def process_settings(self, sett_dict): # process inputs if 'procedure' in sett_dict.keys(): @@ -56,8 +66,7 @@ def reset_features(self, feats): def reset_datum(self): # we will list all datum for the subject and all feature types that match the settings - self.all_datum_ids = [] - self.gt = 0 + self.last_datum_id = 0 def run_forever(self): while self.is_running: @@ -71,20 +80,15 @@ def run_forever(self): except zmq.ZMQError: pass - new_datum = self.db_wrapper.list_all_datum_ids(gt=self.gt) - - if len(new_datum) > 0: - self.all_datum_ids += new_datum + new_data = list(self.db_wrapper.list_all_datum_ids(gt=self.last_datum_id)) - if len(self.all_datum_ids) > 0: + while len(new_data) > 0: + print(f"Calculating features for {len(new_data)} remaining segments...") # get oldest data and check if all features have been computed - # in case we're stuck with a datum whose feature can't compute, we - # want to keep the largest datum id. - self.gt = max(self.all_datum_ids + [self.gt]) - d_id = self.all_datum_ids.pop(0) - if not self.db_wrapper.check_and_compute_features(d_id): - # re append at the end of the list? - self.all_datum_ids.append(d_id) + d_id = new_data.pop(0) + if self.db_wrapper.check_and_compute_features(d_id): + # If process was successful then update last_datum_id + self.last_datum_id = max(d_id, self.last_datum_id) time.sleep(0.25) # test to slow down process to decrease HDD load # time.sleep(0.1) From 5ac8f7ee9c3be43d91ceb600c95820f356bd90bb Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 19 Nov 2023 21:19:38 -0500 Subject: [PATCH 59/65] doc update --- docs/for-developers.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/for-developers.md b/docs/for-developers.md index 509e1cb..d690625 100644 --- a/docs/for-developers.md +++ b/docs/for-developers.md @@ -1,8 +1,10 @@ ## Repository Organization -* Library and Application code in the /neuroport_dbs folder -* Unit tests in the /tests folder. Note this uses the pytest framework and conventions. +* Library and Application code in the /open_mer folder + * Entry points in /open_mer/scripts +* TODO: Unit tests in the /tests folder. * Documentation in the /docs folder +* Scratch scripts in /scripts ## Maintaining the Documentation @@ -37,7 +39,7 @@ TODO ## Interprocess Communication -This section is referring to communication among the applications within the OpenMER suite, including mysqld and ~8 Python applications. This section is not referring to communication to/from the data sources. +This section is referring to communication among the applications within the OpenMER suite, including mysqld and ~8 Python applications. Communication to/from the data sources is out-of-scope in this section. The applications all run independently of each other, but most of them work better in combination. To communicate information between applications we use [ZeroMQ](https://zeromq.org/). @@ -51,9 +53,9 @@ The applications all run independently of each other, but most of them work bett We also have one LSL stream coming from the DepthGUI. Old version of the SERF Depth_Process might still be using it but they should be migrated. -| Stream Name | Stream Type | Content | Inlets | -|-----------------|-------------|-------------------------|--------------------------| -| electrode_depth | depth | 1 float32 of elec depth | old Depth_Process (SERF) | +| Stream Name | Stream Type | Content | Inlets | +|-----------------|-------------|-------------------------|----------------------------| +| electrode_depth | depth | 1 float32 of elec depth | *old* Depth_Process (SERF) | ## Development Environment From 6b765c21fdac68db576e7a870c9a495c4e7ec32a Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 30 Nov 2023 16:23:54 -0500 Subject: [PATCH 60/65] SweepGUI visualize snippet status. --- open_mer/dbsgui/sweep.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/open_mer/dbsgui/sweep.py b/open_mer/dbsgui/sweep.py index 2e53378..f4b225b 100644 --- a/open_mer/dbsgui/sweep.py +++ b/open_mer/dbsgui/sweep.py @@ -88,6 +88,11 @@ def create_control_panel(self): range_edit.setMinimumHeight(23) range_edit.setMaximumWidth(80) cntrl_layout.addWidget(range_edit) + # Disabled button for status + snippet_pb = QtWidgets.QPushButton("snippet") + snippet_pb.setObjectName("snippet_PushButton") + snippet_pb.setEnabled(False) + cntrl_layout.addWidget(snippet_pb) # buttons for audio monitoring cntrl_layout.addStretch(1) cntrl_layout.addWidget(QtWidgets.QLabel("Monitor: ")) @@ -409,14 +414,20 @@ def parse_settings(self): settings.endGroup() def _setup_ipc(self): - self._chanselect_context = zmq.Context() - self._chanselect_sock = self._chanselect_context.socket(zmq.PUB) + self._sock_context = zmq.Context() + + self._chanselect_sock = self._sock_context.socket(zmq.PUB) self._chanselect_sock.bind(f"tcp://*:{self._ipc_settings['channel_select']}") + self._snippet_sock = self._sock_context.socket(zmq.SUB) + self._snippet_sock.connect(f"tcp://localhost:{self._ipc_settings['snippet_status']}") + self._snippet_sock.setsockopt_string(zmq.SUBSCRIBE, "snippet_status") + def _cleanup_ipc(self): - self._chanselect_sock.setsockopt(zmq.LINGER, 0) - self._chanselect_sock.close() - self._chanselect_context.term() + for _sock in [self._chanselect_sock, self._snippet_sock]: + _sock.setsockopt(zmq.LINGER, 0) + _sock.close() + self._sock_context.term() def try_reset_widget(self): super().try_reset_widget() @@ -483,6 +494,18 @@ def do_plot_update(self): self._sonify_data(chan_state["name"], proc_data) self._plot_widget.update(chan_state["name"], proc_data, ch_state=chan_state) + try: + received_msg = self._snippet_sock.recv_string(flags=zmq.NOBLOCK)[len("snippet_status") + 1:] + # startup, refresh (ignore), notrecording, recording, accumulating, done + new_colour = {"accumulating": "red", "done": "blue"}.get(received_msg, "yellow") + pb: QtWidgets.QPushButton = self._plot_widget.findChild(QtWidgets.QPushButton, "snippet_PushButton") + pb.setStyleSheet("QPushButton { color: white; " + f"background-color : {new_colour}; " + f"border-color : {new_colour}; " + "border-width: 2px}") + except zmq.ZMQError: + pass + def _postproc_data(self, label, data, ch_state=None): # TODO: Store channel state and filter state in this class, not the plot widget ss_info = self._plot_widget.segmented_series[label] From 25d9c9ce70b19f5496654eb86ed9c3d8a045330d Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 30 Nov 2023 16:24:33 -0500 Subject: [PATCH 61/65] Update FeaturesGUI.ini Change default y_range for FeaturesGUI (abandoned) --- open_mer/resources/settings/FeaturesGUI.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_mer/resources/settings/FeaturesGUI.ini b/open_mer/resources/settings/FeaturesGUI.ini index d0ba1c0..7202b21 100644 --- a/open_mer/resources/settings/FeaturesGUI.ini +++ b/open_mer/resources/settings/FeaturesGUI.ini @@ -37,7 +37,7 @@ overwrite_depth=true [plot] x_start=-4000 x_stop=120000 -y_range=250 +y_range=100 image_plot=false [theme] From 757d022838e7ad61d7a73881276d1e0b7e421396 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 30 Nov 2023 17:11:52 -0500 Subject: [PATCH 62/65] Fix depth-to-cbsdk --- open_mer/dbsgui/depth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_mer/dbsgui/depth.py b/open_mer/dbsgui/depth.py index fda6ed8..732cd16 100644 --- a/open_mer/dbsgui/depth.py +++ b/open_mer/dbsgui/depth.py @@ -203,7 +203,7 @@ def publish(self): if not cbsdk_conn.is_connected: cbsdk_conn.connect() cbsdk_conn.cbsdk_config = {'reset': True, 'get_events': False, 'get_comments': False} - if not cbsdk_conn.is_connected: + if cbsdk_conn.is_connected: cbsdk_conn.set_comments("DTT:" + self.display_string) # Push to LSL From 4c31b5c86cbc8d05532c8fc988a17b2f44e91311 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 7 Dec 2023 19:00:19 -0500 Subject: [PATCH 63/65] tiny readme update. --- docs/usage-instructions.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/usage-instructions.md b/docs/usage-instructions.md index 5e6c288..e43bff2 100644 --- a/docs/usage-instructions.md +++ b/docs/usage-instructions.md @@ -44,12 +44,6 @@ It is very important to modify the [DepthGUI settings](settings.md#depthguiini) ## Procedure -## Features - -Click connect, OK, Add Plot - -Then you're presented with a settings window. - * Under the Subject tab: * Type a patient Id or select from the drop-down list if resuming a previously-stored patient. * Enter the remaining information. @@ -64,6 +58,9 @@ Then you're presented with a settings window. * If you use an offset adapter you can specify the direction (A to H) and the offset size in mm. * Click OK to start the GUI + +## Features + * Click on the "Record" button. * Note: To change the default recording path, edit `WPy64-3850\python-3.8.5.amd64\Lib\site-packages\neuroport_dbs\settings\defaults.py` and change the `BASEPATH` value. (We hope to make this easier via a config file in the future.) * After a new depth is entered and the depth remains constant for 4-8 seconds (depending on signal quality), a segment will be added to the database. From 422808138a982deedadcda8a99ba74ce094dc5ac Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 5 Jan 2024 15:20:47 -0500 Subject: [PATCH 64/65] Modify default features categories in FeaturesGUI.ini --- open_mer/resources/settings/FeaturesGUI.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/open_mer/resources/settings/FeaturesGUI.ini b/open_mer/resources/settings/FeaturesGUI.ini index 7202b21..9dd68d9 100644 --- a/open_mer/resources/settings/FeaturesGUI.ini +++ b/open_mer/resources/settings/FeaturesGUI.ini @@ -23,12 +23,12 @@ overwrite_depth=true # electrode_settings= [features] -0\name=Raw +0\name=DBS 0\enable=true 1\name=Mapping 1\enable=false 2\name=STN -2\enable=true +2\enable=false 3\name=LFP 3\enable=true 4\name=Spikes From 035de0571f07fdf9ca4c5d1c07fedd08b814d19b Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 1 Apr 2024 16:47:34 -0400 Subject: [PATCH 65/65] procedure launches snippets- and features- subprocesses. --- docs/for-developers.md | 26 +++--- docs/getting-started.md | 2 +- docs/introduction.md | 7 +- open_mer/data_source/interface.py | 5 +- open_mer/dbsgui/procedure.py | 99 +++++++++++++++++++---- open_mer/dbsgui/process/__init__.py | 4 +- open_mer/dbsgui/process/trajectory.py | 9 ++- open_mer/dbsgui/widgets/SettingsDialog.py | 22 +++-- open_mer/scripts/Depth_Process.py | 12 --- open_mer/scripts/Features_Process.py | 38 +++++---- open_mer/scripts/Snippets_Process.py | 19 +++++ setup.cfg | 3 +- 12 files changed, 173 insertions(+), 73 deletions(-) delete mode 100644 open_mer/scripts/Depth_Process.py create mode 100644 open_mer/scripts/Snippets_Process.py diff --git a/docs/for-developers.md b/docs/for-developers.md index d690625..874f597 100644 --- a/docs/for-developers.md +++ b/docs/for-developers.md @@ -1,10 +1,10 @@ ## Repository Organization -* Library and Application code in the /open_mer folder - * Entry points in /open_mer/scripts -* TODO: Unit tests in the /tests folder. -* Documentation in the /docs folder -* Scratch scripts in /scripts +* `/open_mer`: Library and Application code + * `/open_mer/scripts`: Entry points +* `/tests` TODO: Unit tests +* `/docs`: Documentation +* `/scripts`: Scratch scripts ## Maintaining the Documentation @@ -43,13 +43,13 @@ This section is referring to communication among the applications within the Ope The applications all run independently of each other, but most of them work better in combination. To communicate information between applications we use [ZeroMQ](https://zeromq.org/). -| Publisher | Port | Topic | Message | Subscribers | -|----------------|-------|--------------------|--------------------------------------------------------|----------------| -| ProcedureGUI | 60001 | procedure_settings | json of settings-dicts "procedure" and ?? | FeaturesGUI | -| Depth_Process | 60002 | snippet_status | (startup, notrecording, recording, accumulating, done) | ProcedureGUI | -| SweepGUI | 60003 | channel_select | json with channel, range, highpass | FeaturesGUI | -| FeaturesGUI | 60004 | features | refresh | ProcedureGUI | -| DepthGUI | 60005 | ddu | float of depth | Depth_Process | +| Publisher | Port | Topic | Message | Subscribers | +|-------------------|-------|--------------------|--------------------------------------------------------|-------------------| +| ProcedureGUI | 60001 | procedure_settings | json of settings. "procedure": {}, "running": bool | Segments_Process | +| Segments_Process | 60002 | snippet_status | (startup, notrecording, recording, accumulating, done) | ProcedureGUI | +| SweepGUI | 60003 | channel_select | json with channel, range, highpass | FeaturesGUI | +| Features_Process | 60004 | features | refresh | ProcedureGUI | +| DepthGUI | 60005 | ddu | float of depth | Segments_Process | We also have one LSL stream coming from the DepthGUI. Old version of the SERF Depth_Process might still be using it but they should be migrated. @@ -88,7 +88,7 @@ Modify the [DepthGUI settings](settings.md#depthguiini) to use "cbsdk playback" ### Playback XDF file -If you have a correctly formatted file, it may be enough to use [XDFStreamer](https://github.com/labstreaminglayer/App-XDFStreamer). +If you have a correctly formatted file, it may be enough to use [pyxdf playback](https://github.com/xdf-modules/pyxdf/blob/main/pyxdf/examples/playback_lsl.py). TODO: More instructions needed. diff --git a/docs/getting-started.md b/docs/getting-started.md index 1fb0954..b3a0fd0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,4 +1,4 @@ -The PC that runs our software is directly connected to the acquisition system but it is never connected to the internet. Thus, we create a portable install on a thumb drive which we then copy to the clinical PC. +The PC that runs our software is directly connected to the acquisition system, but it is never connected to the internet. Thus, we create a portable install on a thumb drive which we then copy to the clinical PC. > For development or testing on an internet-connected computer, or a non-Windows computer, please look at the [For Developers](for-developers.md) documentation. diff --git a/docs/introduction.md b/docs/introduction.md index 95d7dda..9d05e4b 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -28,10 +28,9 @@ OpenMER is a Suite of applications for visualizing signals in real-time: * pushed on a [labstreaminglayer](https://github.com/sccn/labstreaminglayer) outlet; * broadcast over ZeroMQ (see [ZeroMQ Port list](for-developers.md#interprocess-communication)) * *ProcedureGUI* - Sets patient and procedure info, and controls recording state. - * The recording button colour reflects a background database process (monitoring: blue, accumulating: red). -* *FeaturesGUI* - Plots the history of raw segments or features by depth. - * The database interaction occurs via a Django app called [SERF](https://github.com/cboulay/SERF) backed by a MySQL database. - * Requires running MySQL database, and 2 SERF applications: *serf-cbacquire* and *serf-procfeatures*. + * The database interaction occurs via the ORM of a Django app called [SERF](https://github.com/cboulay/SERF) backed by a MySQL database. + * Launching ProcedureGUI also launches two processes: + * Segments_Process to capture raw segments at each depth and Features_Process to calculate features from those segments. * *CommentGUI* (not shown) - Send comments to the Blackrock NSP, with some widgets specialized for kinesthetic mapping. We also use a GUI application we developed called [*CereStimDBS*](https://github.com/CerebusOSS/CereStimDBS) for controlling the Blackrock CereStim96 in a convenient manner for DBS surgeries. diff --git a/open_mer/data_source/interface.py b/open_mer/data_source/interface.py index bdd568c..bc8d155 100644 --- a/open_mer/data_source/interface.py +++ b/open_mer/data_source/interface.py @@ -1,9 +1,12 @@ +import typing +from typing_extensions import Self + from qtpy import QtCore class IDataSource(QtCore.QObject): - def __init__(self, on_connect_cb=None): + def __init__(self, on_connect_cb: typing.Optional[typing.Callable[[Self], None]] = None): super().__init__() # QObject init required for signals to work self._on_connect_cb = on_connect_cb diff --git a/open_mer/dbsgui/procedure.py b/open_mer/dbsgui/procedure.py index 2415a03..c6c7351 100644 --- a/open_mer/dbsgui/procedure.py +++ b/open_mer/dbsgui/procedure.py @@ -1,26 +1,30 @@ -from typing import Optional +import json +import subprocess +import sys +import typing import time -import json import zmq +from zmq.utils.monitor import parse_monitor_message from qtpy import QtCore, QtWidgets from serf.tools.db_wrap import DBWrapper from .widgets.ini_window import IniWindow from .widgets.SettingsDialog import SettingsDialog import open_mer.data_source +import open_mer.data_source.interface class ProcedureGUI(IniWindow): def __init__(self): - self._data_source = None + self._data_source: typing.Optional[open_mer.data_source.interface.IDataSource] = None self._source_settings = {} super().__init__() - # TODO: It doesn't really have icons anymore. This code should move to FeaturesGUI - + self._popen_snips: typing.Optional[subprocess.Popen] = None + self._popen_feats: typing.Optional[subprocess.Popen] = None self._b_recording = False self._init_connection() @@ -32,6 +36,13 @@ def __init__(self): self.show() self._do_modal_settings() + def __del__(self): + if self._data_source is not None: + self._data_source.set_recording_state(False, {"filename": None}) + self._data_source.disconnect_requested() + super().__del__() + self._term_popen() + def parse_settings(self): # Handles MainWindow geometry and collects self._theme_settings and self._ipc_settings super().parse_settings() @@ -57,11 +68,15 @@ def parse_settings(self): def _setup_ipc(self): self._sock_context = zmq.Context() + self._feature_pub_online = False self._features_sock = self._sock_context.socket(zmq.SUB) + self._feature_monitor = self._features_sock.get_monitor_socket() self._features_sock.connect(f"tcp://localhost:{self._ipc_settings['features']}") self._features_sock.setsockopt_string(zmq.SUBSCRIBE, "features") + self._snippet_pub_online = False self._snippet_sock = self._sock_context.socket(zmq.SUB) + self._snippet_monitor = self._snippet_sock.get_monitor_socket() self._snippet_sock.connect(f"tcp://localhost:{self._ipc_settings['snippet_status']}") self._snippet_sock.setsockopt_string(zmq.SUBSCRIBE, "snippet_status") @@ -69,27 +84,36 @@ def _setup_ipc(self): self._procedure_sock.bind(f"tcp://*:{self._ipc_settings['procedure_settings']}") def _cleanup_ipc(self): - for _sock in [self._features_sock, self._procedure_sock]: + for _sock in [ + self._features_sock, + self._feature_monitor, + self._snippet_sock, + self._snippet_monitor, + self._procedure_sock + ]: _sock.setsockopt(zmq.LINGER, 0) _sock.close() self._sock_context.term() @QtCore.Slot(QtCore.QObject) - def _on_source_connected(self, data_source): + def _on_source_connected(self, data_source: open_mer.data_source.interface.IDataSource) -> None: self._data_source = data_source def _init_connection(self): if "class" in self._source_settings and self._source_settings["class"] is not None: + # TODO: self._data_source ?! _data_source = self._source_settings["class"]( settings_path=self._source_settings["settings_path"], on_connect_cb=self._on_source_connected ) - def __del__(self): - if self._data_source is not None: - self._data_source.set_recording_state(False, {"filename": None}) - self._data_source.disconnect_requested() - super().__del__() + def _term_popen(self): + if self._popen_snips is not None: + self._popen_snips.terminate() + self._popen_snips = None + if self._popen_feats is not None: + self._popen_feats.terminate() + self._popen_feats = None def _setup_ui(self): self.setWindowTitle("OpenMER Procedure") @@ -135,7 +159,7 @@ def _publish_settings(self): print(f"Publishing: {pub_string}") self._procedure_sock.send_string(pub_string) - def _do_modal_settings(self): + def _do_modal_settings(self) -> bool: win = SettingsDialog(self._subject_settings, self._procedure_settings) result = win.exec_() if result == QtWidgets.QDialog.Accepted: @@ -157,10 +181,11 @@ def _do_modal_settings(self): self._procedure_settings["procedure_id"] = proc_id self._publish_settings() + return True else: return False - def toggle_recording(self, on_off: Optional[bool] = None): + def toggle_recording(self, on_off: typing.Optional[bool] = None): if self._data_source is None: return @@ -183,7 +208,52 @@ def toggle_recording(self, on_off: Optional[bool] = None): f"border-color : {rec_facecolor}; " "border-width: 2px}") + def _check_monitors(self): + for proc in ["snippet", "feature"]: + _monitor = getattr(self, f"_{proc}_monitor") + monitor_result = _monitor.poll(10, zmq.POLLIN) + if monitor_result: + data = _monitor.recv_multipart() + evt = parse_monitor_message(data) + if evt["event"] in ( + zmq.EVENT_ACCEPTED, + zmq.EVENT_CONNECTED, + zmq.EVENT_HANDSHAKE_SUCCEEDED + ): + setattr(self, f"_{proc}_pub_online", True) + elif evt["event"] in ( + zmq.EVENT_DISCONNECTED, + zmq.EVENT_MONITOR_STOPPED, + zmq.EVENT_CLOSED, + ): + setattr(self, f"_{proc}_pub_online", False) + elif evt["event"] in ( + zmq.EVENT_CONNECT_DELAYED, + zmq.EVENT_CONNECT_RETRIED + ): + pass + else: + print(f"TODO: handle {evt['event']}") + def _run_recording(self, on_off: bool): + # Manage Snippets_Process + self._term_popen() + self._check_monitors() + if self._popen_snips is None and not self._snippet_pub_online and on_off: + self._popen_snips = subprocess.Popen([ + sys.executable, + "-m", + "open_mer.scripts.Snippets_Process", + f"--procedure-id={self._procedure_settings['procedure_id']}" + ]) + if self._popen_feats is None and not self._feature_pub_online and on_off: + self._popen_feats = subprocess.Popen([ + sys.executable, + "-m", + "open_mer.scripts.Features_Process", + f"--procedure-id={self._procedure_settings['procedure_id']}" + ]) + f_name, m_name, l_name = self.parse_patient_name(self._subject_settings['name']) file_info = {'filename': self._get_filename(), 'comment': self._subject_settings['NSP_comment'], @@ -239,6 +309,7 @@ def update(self): except zmq.ZMQError: pass + self._check_monitors() try: received_msg = self._snippet_sock.recv_string(flags=zmq.NOBLOCK)[len("snippet_status") + 1:] b_publish_settings |= received_msg == "refresh" diff --git a/open_mer/dbsgui/process/__init__.py b/open_mer/dbsgui/process/__init__.py index 4ed5e12..c9f6123 100644 --- a/open_mer/dbsgui/process/__init__.py +++ b/open_mer/dbsgui/process/__init__.py @@ -3,10 +3,10 @@ from qtpy import QtCore -from .trajectory import NSPBufferWorker +from .trajectory import SnippetWorker -__all__ = ["build_ini_paths", "parse_ini", "NSPBufferWorker"] +__all__ = ["build_ini_paths", "parse_ini", "SnippetWorker"] def build_ini_paths() -> list[Path]: diff --git a/open_mer/dbsgui/process/trajectory.py b/open_mer/dbsgui/process/trajectory.py index eb4ced4..c048b8a 100644 --- a/open_mer/dbsgui/process/trajectory.py +++ b/open_mer/dbsgui/process/trajectory.py @@ -1,5 +1,6 @@ import time import json +import typing import numpy as np import zmq @@ -10,14 +11,16 @@ from open_mer.data_source.cerebus import SAMPLINGGROUPS -class NSPBufferWorker: +class SnippetWorker: - def __init__(self, ipc_settings, buffer_settings): + def __init__(self, ipc_settings, buffer_settings, procedure_id: typing.Optional = None): self._ipc_settings = ipc_settings self._buffer_settings = buffer_settings self.buffer = None self.db_wrapper = DBWrapper() self.procedure_id = None + if procedure_id is not None: + self.reset_procedure(procedure_id) # cbSDK; connect using default parameters self.cbsdk_conn = CbSdkConnection(simulate_ok=False) @@ -207,7 +210,7 @@ def run_forever(self): # only process the NSP data if Central is recording _status = "recording" if self.cbsdk_conn.get_recording_state() else "notrecording" - if _status == "recording" and data and current_depth: + if _status == "recording" and data and current_depth is not None: if not self.delay_done: self.delay_done = self.wait_for_delay_end(data) diff --git a/open_mer/dbsgui/widgets/SettingsDialog.py b/open_mer/dbsgui/widgets/SettingsDialog.py index 0b2ce36..648b061 100644 --- a/open_mer/dbsgui/widgets/SettingsDialog.py +++ b/open_mer/dbsgui/widgets/SettingsDialog.py @@ -1,3 +1,5 @@ +import typing + import numpy as np # use the same GUI format as the other ones from qtpy.QtWidgets import QComboBox, QLineEdit, QLabel, QDialog, QVBoxLayout, QWidget, \ @@ -105,7 +107,7 @@ def __init__(self, procedure_settings): # populate with defaults if empty if not self.procedure_settings: - self.update_settings_from_db(-1) + self.update_settings_from_db(None) self.proc_enums = DBWrapper().return_enums("procedure") self._setup_ui() @@ -115,7 +117,7 @@ def _setup_ui(self): proc_layout = QGridLayout(self) row = 0 - proc_layout.addWidget(QLabel("Previous procedures: "), row, 0, 1, 1) + proc_layout.addWidget(QLabel("Procedure: "), row, 0, 1, 1) prev_proc = QComboBox() prev_proc.setObjectName("procedure_QComboBox") @@ -195,7 +197,7 @@ def check_all_procedures(self, subject_id, block): # Clear combobox and fill with a summary of each procedure prev_proc.blockSignals(block) prev_proc.clear() - prev_proc.addItem("") # 0th item is always blank. + prev_proc.addItem("(new)") # 0th item indicates a new procedure prev_proc.addItems([ " ".join([ str(x.procedure_id), x.target_name, x.recording_config, x.date.strftime('%Y-%m-%d') @@ -203,7 +205,7 @@ def check_all_procedures(self, subject_id, block): for x in self.all_procedures ]) row_ix = 0 - if "procedure_id" in self.procedure_settings and self.procedure_settings["procedure_id"] != -1: + if "procedure_id" in self.procedure_settings and self.procedure_settings["procedure_id"] is not None: known_ids = [_.procedure_id for _ in self.all_procedures] if self.procedure_settings["procedure_id"] in known_ids: row_ix = known_ids.index(self.procedure_settings["procedure_id"]) + 1 @@ -251,16 +253,20 @@ def change_subject(self, sub_id, block=False): self.check_all_procedures(sub_id, block) def procedure_selection_change(self): - prev_proc = self.findChild(QComboBox, name="procedure_QComboBox") - id = -1 + prev_proc: QComboBox = self.findChild(QComboBox, name="procedure_QComboBox") + id: typing.Optional[int] = None if prev_proc.currentIndex() > 0: + # If the user selected a procedure other than the '(new)' procedure. ix = prev_proc.currentIndex() - 1 # -1 because first entry is always blank. id = self.all_procedures[ix].procedure_id self.update_settings_from_db(id) self.update_proc_widgets_from_settings() - def update_settings_from_db(self, idx): - res_dict = dict({"procedure_id": idx}, **DBWrapper().load_procedure_details(idx, exclude=['subject', 'procedure_id'])) + def update_settings_from_db(self, idx: typing.Optional[int]): + res_dict = DBWrapper().load_procedure_details(idx, exclude=['subject', 'procedure_id']) + if idx is None and bool(self.procedure_settings): + res_dict.update(self.procedure_settings) + res_dict["procedure_id"] = idx self.procedure_settings.update(res_dict) def update_proc_widgets_from_settings(self): diff --git a/open_mer/scripts/Depth_Process.py b/open_mer/scripts/Depth_Process.py deleted file mode 100644 index 1611680..0000000 --- a/open_mer/scripts/Depth_Process.py +++ /dev/null @@ -1,12 +0,0 @@ -from open_mer.dbsgui.process import parse_ini, build_ini_paths, NSPBufferWorker - - -def main(): - ipc_settings, buffer_settings, feature_settings = parse_ini(build_ini_paths()) - - worker = NSPBufferWorker(ipc_settings, buffer_settings) - worker.run_forever() - - -if __name__ == '__main__': - main() diff --git a/open_mer/scripts/Features_Process.py b/open_mer/scripts/Features_Process.py index a6c859d..46f661a 100644 --- a/open_mer/scripts/Features_Process.py +++ b/open_mer/scripts/Features_Process.py @@ -1,15 +1,18 @@ -import time import json +from pathlib import Path +import time +import typing +import typer import zmq from serf.tools.db_wrap import DBWrapper -from open_mer.scripts.Depth_Process import build_ini_paths, parse_ini +from open_mer.scripts.Snippets_Process import build_ini_paths, parse_ini class FeaturesWorker: - def __init__(self, ipc_settings, buffer_settings, feature_settings): + def __init__(self, ipc_settings, buffer_settings, feature_settings, procedure_id: typing.Optional = None): self._ipc_settings = ipc_settings self._buffer_settings = buffer_settings self._feature_settings = feature_settings @@ -22,7 +25,7 @@ def __init__(self, ipc_settings, buffer_settings, feature_settings): self.procedure_id = None self.last_datum_id = 0 # fetch datum ids greater than this value - self._select_latest_procedure() + self._select_procedure(procedure_id) self.is_running = True @@ -34,12 +37,16 @@ def _setup_ipc(self): self._ctrl_sock.connect(f"tcp://localhost:{self._ipc_settings['procedure_settings']}") self._ctrl_sock.setsockopt_string(zmq.SUBSCRIBE, "procedure_settings") - def _select_latest_procedure(self): - # Get the 0th subject and the -1th procedure for that subject - sub_id = list(self.db_wrapper.list_all_subjects())[0] - subj_details = self.db_wrapper.load_subject_details(sub_id) - self.db_wrapper.select_subject(subj_details["subject_id"]) - proc = list(self.db_wrapper.list_all_procedures(subj_details["subject_id"]))[-1] + def _select_procedure(self, procedure_id: typing.Optional[int] = None): + if procedure_id is None: + # Get the 0th subject and the -1th procedure for that subject + sub_id = list(self.db_wrapper.list_all_subjects())[0] + subj_details = self.db_wrapper.load_subject_details(sub_id) + self.db_wrapper.select_subject(subj_details["subject_id"]) + proc = list(self.db_wrapper.list_all_procedures(subj_details["subject_id"]))[-1] + else: + self.db_wrapper.select_procedure(procedure_id) + proc = self.db_wrapper.current_procedure sett_dict = { "procedure": {"procedure_id": proc.procedure_id}, "features": [v[0] for k, v in self._feature_settings.items() if v[1]] @@ -93,9 +100,12 @@ def run_forever(self): # time.sleep(0.1) -def main(): - ipc_settings, buffer_settings, feature_settings = parse_ini(build_ini_paths()) - worker = FeaturesWorker(ipc_settings, buffer_settings, feature_settings) +def main(ini_path: typing.Optional[Path] = None, procedure_id: typing.Optional[int] = None): + ini_paths = build_ini_paths() + if ini_path is not None: + ini_paths.append(ini_path) + ipc_settings, buffer_settings, feature_settings = parse_ini(ini_paths) + worker = FeaturesWorker(ipc_settings, buffer_settings, feature_settings, procedure_id=procedure_id) try: worker.run_forever() except KeyboardInterrupt: @@ -103,4 +113,4 @@ def main(): if __name__ == '__main__': - main() + typer.run(main) diff --git a/open_mer/scripts/Snippets_Process.py b/open_mer/scripts/Snippets_Process.py new file mode 100644 index 0000000..be69ddb --- /dev/null +++ b/open_mer/scripts/Snippets_Process.py @@ -0,0 +1,19 @@ +from pathlib import Path +import typing + +import typer +from open_mer.dbsgui.process import parse_ini, build_ini_paths, SnippetWorker + + +def main(ini_path: typing.Optional[Path] = None, procedure_id: typing.Optional[int] = None): + ini_paths = build_ini_paths() + if ini_path is not None: + ini_paths.append(ini_path) + ipc_settings, buffer_settings, feature_settings = parse_ini(ini_paths) + + worker = SnippetWorker(ipc_settings, buffer_settings, procedure_id=procedure_id) + worker.run_forever() + + +if __name__ == '__main__': + typer.run(main) diff --git a/setup.cfg b/setup.cfg index cafa9e3..53581c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ install_requires = django pyzmq quantities + typer neo @ git+https://github.com/NeuralEnsemble/python-neo.git pylsl pytf @ git+https://github.com/SachsLab/pytf.git @@ -58,5 +59,5 @@ gui_scripts = dbs-mapping = open_mer.scripts.MappingGUI:main dbs-comments = open_mer.scripts.CommentsGUI:main dbs-ddu = open_mer.scripts.DepthGUI:main - dbs-meracquire = open_mer.scripts.Depth_Process:main + dbs-meracquire = open_mer.scripts.Segments_Process:main dbs-procfeatures = open_mer.scripts.Features_Process:main