示例#1
0
    def __init__(self, svg_filepath, name=None, **kwargs):
        self.name = name or path(svg_filepath).namebase

        # Read SVG paths and polygons from `Device` layer into data frame, one
        # row per polygon vertex.
        self.df_shapes = svg_shapes_to_df(svg_filepath, xpath=ELECTRODES_XPATH)

        # Add SVG file path as attribute.
        self.svg_filepath = svg_filepath
        self.shape_i_columns = 'id'

        # Create temporary shapes canvas with same scale as original shapes
        # frame.  This canvas is used for to conduct point queries to detect
        # which shape (if any) overlaps with the endpoint of a connection line.
        svg_canvas = ShapesCanvas(self.df_shapes, self.shape_i_columns)

        # Detect connected shapes based on lines in "Connection" layer of the
        # SVG.
        self.df_shape_connections = extract_connections(self.svg_filepath,
                                                        svg_canvas)

        # Scale coordinates to millimeter units.
        self.df_shapes[['x', 'y']] -= self.df_shapes[['x', 'y']].min().values
        self.df_shapes[['x', 'y']] /= INKSCAPE_PPmm.magnitude

        self.df_shapes = compute_shape_centers(self.df_shapes,
                                               self.shape_i_columns)

        self.df_electrode_channels = self.get_electrode_channels()

        self.graph = nx.Graph()
        for index, row in self.df_shape_connections.iterrows():
            self.graph.add_edge(row['source'], row['target'])

        # Get data frame, one row per electrode, indexed by electrode path id,
        # each row denotes electrode center coordinates.
        self.df_shape_centers = (self.df_shapes.drop_duplicates(subset=['id'])
                                 .set_index('id')[['x_center', 'y_center']])
        (self.adjacency_matrix, self.indexed_shapes,
         self.shape_indexes) = get_adjacency_matrix(self.df_shape_connections)
        self.df_indexed_shape_centers = (self.df_shape_centers
                                         .loc[self.shape_indexes.index]
                                         .reset_index())
        self.df_indexed_shape_centers.rename(columns={'index': 'shape_id'},
                                             inplace=True)

        self.df_shape_connections_indexed = self.df_shape_connections.copy()
        self.df_shape_connections_indexed['source'] = \
            map(str, self.shape_indexes[self.df_shape_connections['source']])
        self.df_shape_connections_indexed['target'] \
            = map(str, self.shape_indexes[self.df_shape_connections
                                          ['target']])

        self.df_shapes_indexed = self.df_shapes.copy()
        self.df_shapes_indexed['id'] = map(str, self.shape_indexes
                                           [self.df_shapes['id']])
        # Modified state (`True` if electrode channels have been updated).
        self._dirty = False
示例#2
0
def test_intersections_from_frame():
    # Load data frame containing vertices from example SVG file.
    df_shapes = svg_model.svg_shapes_to_df(SVG_PATH)
    df_intersections = get_all_intersections(df_shapes)

    # Test device has 92 electrodes (each electrode has a unique id). connected
    # to 90 channels (two pairs of electrodes are each attached to a common
    # electrode).
    nose.tools.eq_(
        92,
        df_intersections.index.get_level_values(
            'id').drop_duplicates().shape[0])
示例#3
0
def get_segments(svg_source, distance_threshold=DEFAULT_DISTANCE_THRESHOLD):
    '''
    Parameters
    ----------
    svg_source : str or file-like or pandas.DataFrame
        File path, URI, or file-like object for SVG device file.

        If specified as ``pandas.DataFrame``, assume argument is in format
        returned by :func:`svg_model.svg_shapes_to_df`.
    distance_threshold : pint.quantity.Quantity
        Maximum gap between electrodes to still be considered neighbours.
    '''
    if not isinstance(svg_source, pd.DataFrame):
        df_shapes = svg_model.svg_shapes_to_df(svg_source)
    else:
        df_shapes = svg_source

    # Calculate distance in pixels assuming 96 pixels per inch (PPI).
    distance_threshold_px = (distance_threshold * 96 *
                             ureg.pixels_per_inch).to('pixels')

    df_segments = (df_shapes.groupby('id').apply(
        lambda x: x.iloc[:-1]).reset_index(drop=True).join(
            df_shapes.groupby('id').apply(lambda x: x.iloc[1:]).reset_index(
                drop=True),
            rsuffix='2'))[[
                'id', 'vertex_i', 'vertex_i2', 'x', 'y', 'x2', 'y2'
            ]]
    v = (df_segments[['x2', 'y2']].values - df_segments[['x', 'y']]).values
    mid = .5 * v + df_segments[['x', 'y']].values
    x_mid = mid[:, 0]
    y_mid = mid[:, 1]
    length = np.sqrt((v**2).sum(axis=1))
    v_scaled = distance_threshold_px.magnitude * v / length[:, None]
    x_normal = -v_scaled[:, 1]
    y_normal = v_scaled[:, 0]

    # Create new data frame from scratch and join it to the `df_segments`
    # frame since it is **much** faster than adding new columns directly
    # the existing `df_segments` frame.
    df_normal = pd.DataFrame(
        np.column_stack([x_mid, y_mid, length, x_normal, y_normal]),
        columns=['x_mid', 'y_mid', 'length', 'x_normal', 'y_normal'])
    return df_segments.join(df_normal).set_index(['id', 'vertex_i'])
示例#4
0
def chip_info(svg_source):
    '''
    Parameters
    ----------
    svg_source : `str` or file-like or `pandas.DataFrame`
        File path, URI, or file-like object for SVG device file.

        If specified as ``pandas.DataFrame``, assume argument is in format
        returned by :func:`svg_model.svg_shapes_to_df`.

    Returns
    -------
    `dict`
        Chip info with fields:

        - ``electrode_shapes``: ``x``, ``y``, ``width``, ``height``, and
          ``area`` for each electrode (`pandas.DataFrame`).
        - ``electrode_channels``: mapping from channel number to electrode ID
          (`pandas.Series`).
        - ``channel_electrodes``: mapping from electrode ID to channel number
          (`pandas.Series`).


    .. versionadded:: 1.65
    '''
    if not isinstance(svg_source, pd.DataFrame):
        df_shapes = svg_model.svg_shapes_to_df(svg_source)
    else:
        df_shapes = svg_source

    electrode_shapes = svg_model.data_frame.get_shape_infos(df_shapes, 'id')
    electrode_channels = (df_shapes.drop_duplicates(
        ['id', 'data-channels']).set_index('id')['data-channels'].map(int))
    electrode_channels.name = 'channel'
    channel_electrodes = pd.Series(electrode_channels.index,
                                   index=electrode_channels.values)

    return {
        'electrode_shapes': electrode_shapes,
        'electrode_channels': electrode_channels,
        'channel_electrodes': channel_electrodes
    }
示例#5
0
def get_channel_neighbours(svg_source,
                           distance_threshold=DEFAULT_DISTANCE_THRESHOLD):
    '''
    Parameters
    ----------
    svg_source : str or file-like or pandas.DataFrame
        File path, URI, or file-like object for SVG device file.

        If specified as ``pandas.DataFrame``, assume argument is in format
        returned by :func:`svg_model.svg_shapes_to_df`.
    distance_threshold : pint.quantity.Quantity
        Maximum gap between electrodes to still be considered neighbours.

    Returns
    -------
    pandas.Series

    '''
    if not isinstance(svg_source, pd.DataFrame):
        df_shapes = svg_model.svg_shapes_to_df(svg_source)
    else:
        df_shapes = svg_source
    df_segments = get_segments(df_shapes,
                               distance_threshold=distance_threshold)
    df_intersections = \
        get_all_intersections(df_shapes, distance_threshold=distance_threshold)

    df_neighbours = (df_intersections.reset_index([2, 3]).join(
        df_segments, lsuffix='_neighbour'))
    df_neighbours.reset_index('id', inplace=True)
    df_neighbours.drop_duplicates(['id', 'id_neighbour'], inplace=True)
    df_neighbours.insert(0, 'direction', None)

    # Assign direction labels
    vertical = df_neighbours.x_normal.abs() < df_neighbours.y_normal.abs()
    df_neighbours.loc[vertical & (df_neighbours.y_normal < 0),
                      'direction'] = 'up'
    df_neighbours.loc[vertical & (df_neighbours.y_normal > 0),
                      'direction'] = 'down'
    df_neighbours.loc[~vertical & (df_neighbours.x_normal < 0),
                      'direction'] = 'left'
    df_neighbours.loc[~vertical & (df_neighbours.x_normal > 0),
                      'direction'] = 'right'
    df_neighbours.insert(
        0, 'normal_magnitude', df_neighbours[['x_normal',
                                              'y_normal']].abs().max(axis=1))

    df_neighbours.sort_values(['id', 'direction', 'normal_magnitude'],
                              inplace=True,
                              ascending=False)
    # If multiple neighbours match a direction, only keep the first match.
    df_neighbours.drop_duplicates(['id', 'direction'], inplace=True)

    electrode_channels = (df_shapes.drop_duplicates(
        ['id', 'data-channels']).set_index('id')['data-channels'].map(int))
    electrode_channels.name = 'channel'
    df_neighbours.insert(0, 'channel',
                         electrode_channels.loc[df_neighbours['id']].values)
    df_neighbours.insert(
        0, 'channel_neighbour',
        electrode_channels.loc[df_neighbours['id_neighbour']].values)
    df_neighbours.set_index(['channel', 'direction'], inplace=True)
    df_neighbours.sort_index(inplace=True)

    directions = ['up', 'down', 'left', 'right']
    channel_neighbours = (df_neighbours['channel_neighbour'].loc[[
        i for c in range(120) for i in zip(it.cycle([c]), directions)
    ]])
    return channel_neighbours
示例#6
0
def draw(svg_source, ax=None, labels=True):
    '''
    Draw the specified device, along with rays casted normal to the electrode
    line segments that intersect with a line segment of a neighbouring
    electrode.


    Parameters
    ----------
    svg_source : str or file-like or pandas.DataFrame
        File path, URI, or file-like object for SVG device file.

        If specified as ``pandas.DataFrame``, assume argument is in format
        returned by :func:`svg_model.svg_shapes_to_df`.
    ax : matplotlib.axes._subplots.AxesSubplot, optional
        Axis to draw on.

        .. versionadded:: 1.69.0
    labels : bool, optional
        Draw channel labels (default: ``True``).

        .. versionadded:: 1.69.0

    Returns
    -------
    `dict`
        Result `dict` includes::
        - ``axis``: axis to which the device was drawn
          (`matplotlib.axes._subplots.AxesSubplot`)
        - ``df_shapes``: table of electrode shape vertices (`pandas.DataFrame`)
        - ``electrode_channels``: mapping from channel number to electrode ID
          (`pandas.Series`).
        - ``channel_patches``: mapping from channel number to corresponding
          `matplotlib` electrode `Patch`.  May be used, e.g., to set color and
          alpha (`pandas.Series`).
    '''
    if not isinstance(svg_source, pd.DataFrame):
        df_shapes = svg_model.svg_shapes_to_df(svg_source)
    else:
        df_shapes = svg_source
    electrode_channels = (df_shapes.drop_duplicates(
        ['id', 'data-channels']).set_index('id')['data-channels'].map(int))
    electrode_channels.name = 'channel'

    # Compute center `(x, y)` for each electrode.
    electrode_centers = df_shapes.groupby('id')[['x', 'y']].mean()
    # Index by **channel number** instead of **electrode id**.
    electrode_centers.index = electrode_channels.reindex(
        electrode_centers.index)

    patches = OrderedDict(
        sorted([(id_,
                 mpl.patches.Polygon(df_shape_i[['x', 'y']].values,
                                     closed=False,
                                     label=id_))
                for id_, df_shape_i in df_shapes.groupby('id')]))
    channel_patches = pd.Series(patches.values(),
                                index=electrode_channels.loc[patches.keys()])

    if ax is None:
        fig, ax = plt.subplots(1, 1, figsize=(10, 10))
    ax.set_aspect(True)

    # For colors, see: https://gist.github.com/cfobel/fd939073cf13a309d7a9
    for patch_i in patches.values():
        # Light blue
        patch_i.set_facecolor('#88bde6')
        # Medium grey
        patch_i.set_edgecolor('#4d4d4d')
        ax.add_patch(patch_i)

    ax.set_xlim(df_shapes.x.min(), df_shapes.x.max())
    ax.set_ylim(df_shapes.y.max(), df_shapes.y.min())

    if labels:
        for channel_i, center_i in electrode_centers.iterrows():
            ax.text(center_i.x,
                    center_i.y,
                    str(channel_i),
                    horizontalalignment='center',
                    verticalalignment='center',
                    color='white',
                    fontsize=10,
                    bbox={
                        'facecolor': 'black',
                        'alpha': 0.2,
                        'pad': 5
                    })

    return {
        'axis': ax,
        'electrode_channels': electrode_channels,
        'df_shapes': df_shapes,
        'channel_patches': channel_patches
    }