Source code for node_slice

# This file is part of Sympathy for Data.
# Copyright (c) 2013, 2017, Combine Control Systems AB
#
# Sympathy for Data is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# Sympathy for Data is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Sympathy for Data.  If not, see <http://www.gnu.org/licenses/>.
"""
Slice the rows in Tables or elements in lists of Tables or ADAFs.

The slice pattern is expressed with standard Python syntax, [start:stop:step].
See example below to get a clear view how it works for a list.
::

    >>> li = ['elem0', 'elem1', 'elem2', 'elem3', 'elem4']
    >>> li[1:3]
    ['elem1', 'elem2']
    >>> li[1:-1]
    ['elem1', 'elem2', 'elem3']
    >>> li[0:3]
    ['elem0', 'elem1', 'elem2']
    >>> li[:3]
    ['elem0', 'elem1', 'elem2']
    >>> li[3:]
    ['elem3', 'elem4']
    >>> li[:]
    ['elem0', 'elem1', 'elem2', 'elem3', 'elem4']
    >>> li[::2]
    ['elem0', 'elem2', 'elem4']
    >>> li[:4:2]
    ['elem0', 'elem2']
    >>> li[1::2]
    ['elem1', 'elem3']
"""
import numpy
import re
import traceback
import numpy as np

from sympathy.api import qt2 as qt_compat
from sympathy.api import node as synode
from sympathy.api import table
from sympathy.api import ParameterView
from sympathy.api import node_helper
from sympathy.api.nodeconfig import Port, Ports, Tag, Tags
from sympathy.api.exceptions import NoDataError

from sympathy.platform import widget_library as sywidgets
from sympathy.platform.table_viewer import TableModel

QtGui = qt_compat.import_module('QtGui')  # noqa
QtWidgets = qt_compat.import_module('QtWidgets')  # noqa


class SliceError(Exception):
    pass


class GetSlice(object):

    def __getitem__(self, index):
        return index

    @staticmethod
    def from_string(string):
        """
        Construct a slice object from index string.

        >>> GetSlice.from_string('[:]')
        slice(None, None, None)

        >>> GetSlice.from_string('[:1:]')
        slice(None, 1, None)

        >>> GetSlice.from_string('[::1]')
        slice(None, None, 1)

        >>> GetSlice.from_string('[0:-1:-1]')
        slice(0, -1, -1)
        """
        # Compact but insecure method, mitigated by limiting characters.
        if re.match(r'\[[\[\]0-9:, -]*\]', string):
            try:
                return eval('GetSlice(){}'.format(string))
            except SyntaxError:
                pass

    @staticmethod
    def valid_string(string, dims=2, allow_int=True):
        """Validates input string index and returns true if the index was
        valid.
        """
        index = GetSlice.from_string(string)
        if index or index == 0:
            if not allow_int and isinstance(index, int):
                return False
            return len(index) <= dims if isinstance(index, tuple) else True
        else:
            return False


def slice_base_parameters():
    parameters = synode.parameters()
    parameters.set_string(
        'slice', label='Slice', value='[:]',
        description=('Use standard Python syntax to define pattern for slice '
                     'operation, [start:stop:step]'))
    parameters.set_integer(
        'limit', label='Limit preview to', value=100,
        editor=synode.Editors.bounded_spinbox_editor(0, 10000, 1),
        description='Specify the maximum number of rows in the preview table')
    return parameters


[docs]class SliceDataTable(synode.Node): """ Slice rows in Table. The number of columns are conserved during the slice operation. """ author = "Erik der Hagopian" name = 'Slice data Table' nodeid = 'org.sysess.sympathy.slice.slicedatatable' version = '1.0' icon = 'select_table_rows.svg' tags = Tags(Tag.DataProcessing.Select) related = ['org.sysess.sympathy.slice.slicedatatables'] inputs = Ports([Port.Table('Input Table', name='port1')]) outputs = Ports([Port.Table( 'Table consisting of the rows that been sliced out from the incoming ' 'Table according to the defined pattern. The number of columns are ' 'conserved during the slice operation', name='port2')]) parameters = slice_base_parameters() def execute(self, node_context): itable = node_context.input['port1'] otable = node_context.output['port2'] index = node_context.parameters['slice'].value slice_index = GetSlice.from_string(index) otable.update(itable[slice_index]) otable.set_name(itable.get_name()) otable.set_table_attributes(itable.get_table_attributes()) def verify_parameters(self, node_context): return GetSlice.valid_string(node_context.parameters['slice'].value) def exec_parameter_view(self, node_context): itable = node_context.input['port1'] if not itable.is_valid(): itable = None return SliceWidget(node_context, itable)
[docs]@node_helper.list_node_decorator(['port1'], ['port2']) class SliceDataTables(SliceDataTable): name = 'Slice data Tables' nodeid = 'org.sysess.sympathy.slice.slicedatatables'
class SliceWidget(ParameterView): def __init__(self, node_context, itable, dims=2, allow_int=True, parent=None): super(SliceWidget, self).__init__(parent=parent) self._node_context = node_context self._itable = itable self._parameters = node_context.parameters self._dims = dims self._allow_int = allow_int self._init_gui() self._connect_gui() def _init_gui(self): vlayout = QtWidgets.QVBoxLayout() limit_hlayout = QtWidgets.QHBoxLayout() self.slice_label = QtWidgets.QLabel('Slice <I>[start:stop:step]</I>') self.slice_lineedit = sywidgets.ValidatedTextLineEdit() self.limit_label = QtWidgets.QLabel('Limit preview to:') self._limit_gui = self._parameters['limit'].gui() self.limit_spinbox = self._limit_gui.editor() self.preview_button = QtWidgets.QPushButton('Preview') self._preview_table = sywidgets.BasePreviewTable() self._preview_table_model = TableModel() self._preview_table.setModel(self._preview_table_model) limit_hlayout.addWidget(self.limit_label) limit_hlayout.addWidget(self.limit_spinbox) vlayout.addWidget(self.slice_label) vlayout.addWidget(self.slice_lineedit) vlayout.addLayout(limit_hlayout) vlayout.addWidget(self.preview_button) vlayout.addWidget(self._preview_table) self.slice_lineedit.clear() self._init_line_validator() self.slice_lineedit.setText(self._parameters['slice'].value) self._preview_table.setEditTriggers( QtWidgets.QAbstractItemView.NoEditTriggers) self.preview() self.setLayout(vlayout) def _connect_gui(self): self.slice_lineedit.valueChanged[str].connect(self.slice) self.preview_button.clicked.connect(self.preview) @property def valid(self): return GetSlice.valid_string(self._parameters['slice'].value, self._dims, self._allow_int) def _init_line_validator(self): def slice_validator(value): try: valid = GetSlice.valid_string(value, self._dims, self._allow_int) if not valid: raise sywidgets.ValidationError( '"{}" is not a valid slice value.'.format(value)) except Exception as e: raise sywidgets.ValidationError(str(e)) return value self.slice_lineedit.setBuilder(slice_validator) def slice(self, text): self._parameters['slice'].value = text self.status_changed.emit() def limit(self, value): self._parameters['limit'].value = int(value) def clear_preview(self, err_msg=''): empty_table = table.File() if err_msg: empty_table['Error'] = np.array([err_msg]) self._preview_table_model.set_table(empty_table) def preview(self): # Fail immediately if there is no input data if self._itable is None: self.clear_preview('No input data') return try: index = self._parameters['slice'].value limit = self._parameters['limit'].value slice_index = GetSlice.from_string(index) slice_data = self._itable[slice_index] slice_data = slice_data[:limit] self._preview_table_model.set_table(slice_data) if not GetSlice.valid_string(index, self._dims): raise SliceError except SliceError: self.clear_preview('Invalid slice') except: traceback.print_exc() self.clear_preview('Failed to create preview') class SlicesWidget(ParameterView): def __init__(self, node_context, itables, parent=None): super(SlicesWidget, self).__init__(parent=parent) self._node_context = node_context self._parameters = node_context.parameters self._single = None self._itables = itables self._init_gui() self._connect_gui() @property def valid(self): return self._single.valid def _init_gui(self): self.vlayout = QtWidgets.QVBoxLayout() group_hlayout = QtWidgets.QHBoxLayout() self.group_label = QtWidgets.QLabel('Preview group nr:') self.group_spinbox = sywidgets.ValidatedIntSpinBox() group_hlayout.addWidget(self.group_label) group_hlayout.addWidget(self.group_spinbox) self.vlayout.addLayout(group_hlayout) self.group_spinbox.setMinimum(0) if self._itables.is_valid(): self.group_spinbox.setMaximum(max(0, len(self._itables) - 1)) else: self.group_spinbox.setMaximum(0) self.group_spinbox.setValue(self._parameters['group_index'].value) self.group(self._parameters['group_index'].value) self.setLayout(self.vlayout) def _connect_gui(self): self.group_spinbox.valueChanged[int].connect(self.group) self._single.status_changed.connect(self.status_changed) def group(self, value): try: self._single.hide() except: pass self._parameters['group_index'].value = int(value) if self._itables.is_valid() and len(self._itables): itable = self._itables[int(value)] else: itable = None self._single = SliceWidget(self._node_context, itable) self.vlayout.addWidget(self._single)
[docs]class SliceList(synode.Node): """Slice elements in a list.""" author = "Erik der Hagopian" name = 'Slice List' icon = 'slice_list.svg' nodeid = 'org.sysess.sympathy.slice.slicelist' version = '1.0' tags = Tags(Tag.Generic.List, Tag.DataProcessing.List) inputs = Ports([ Port.Custom('[<a>]', 'Input List', name='list')]) outputs = Ports([ Port.Custom('[<a>]', 'Sliced output List', name='list')]) parameters = slice_base_parameters() def execute(self, node_context): slice_index = GetSlice.from_string( node_context.parameters['slice'].value) itables = node_context.input['list'] otables = node_context.output['list'] for itable in list(itables)[slice_index]: otables.append(itable) def verify_parameters(self, node_context): return GetSlice.valid_string( node_context.parameters['slice'].value, 1, allow_int=False) def exec_parameter_view(self, node_context): try: itable = table.File() itable.set_column_from_array( '0', numpy.arange(len(node_context.input['list']))) except NoDataError: itable = None return SliceWidget(node_context, itable, 1, allow_int=False)