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
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])
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'])
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 }
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
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 }