phy
phy copied to clipboard
Events
Would allow for PSTH, rasters, etc. But we first need to decide a file format within the existing phy file format (with .npy
and .tsv
files).
Further suggestions by @marius10p :
- [ ] Time-dependent plots (mostly amplitude view and trace view) could show events/stimulation periods in the background.
- [ ] Behavioral traces could be added to the amplitude view
Hey Cyrille,
I've created this feature as a matplotlib plugin. This custom view shows the raster plot around event times defined in the file events.csv. It also includes a plot of the firing rate aligned with this raster plot. I will include here the plugin called 'EventPlugin' which enables 'EventView'. My code requires a .csv file with event times to be in the directory phy is currently operating in, and will report that it is missing in the terminal otherwise.
Bottom plot: Each row of the event plot corresponds to one event time from events.csv. In this case, all activity +/- 5 seconds around the event time is plotted with the time of the event as 0. In my code this data is stored in the 'rasters' variable on line 129.
Top plot: Firing rate plot. The easiest way to create this is as a histogram of all the data used to make the bottom plot. In my code this data is stored in the 'activity' variable on line 131. Note that the unit on the Y-axis of this histogram is firing rate in spikes per second (Hz). In my histogram there are 1000 10ms bins from -5 seconds to 5 seconds. Thus counts from all trials are weighted by: (100)*(1/(# of events in events.csv)). This results in the actual firing rate in Hz being displayed on the histogram. The first term accounts for the bin size ((1/0.01sec.)=100). The second term accounts for the fact that many events are being considered 'at once'. This weighting is done on line 89.
It would be great to make some of the parameters (xmin, xmax, nbins) editable from the view->eventView menu, but I haven't done that yet.
Currently, the graphics are a bit laggy, particularly for big clusters, using matplotlib. I'm sure you could speed it up with an openGL implementation.
Plots appear for all selected clusters and are vertically stacked.
File version here: https://drive.google.com/open?id=1SjvlLI1HsTzygNFd6kcRYvofa3sbDBr7
or you can get it directly here:
matplotlib_view.py:
# import from plugins/matplotlib_view.py
"""Show how to create a custom matplotlib view in the GUI."""
from phy import IPlugin
from phy.cluster.views import ManualClusteringView # Base class for phy views
from phy.plot.plot import PlotCanvasMpl # matplotlib canvas
from numpy import genfromtxt
import matplotlib
import matplotlib.pyplot as plt
import colorcet as cc
import numpy as np
import warnings
import time
import sys
import os
warnings.filterwarnings("ignore")
axis_list = []
plot_handles = []
nodata=False
try:
events = genfromtxt('events.csv', delimiter=',')
except OSError:
sys.stderr.write("EventView: events.csv not found in: "+str(os.getcwd())+"\n")
nodata=True
def _make_default_colormap():
"""Return the default colormap, with custom first colors."""
colormap = np.array(cc.glasbey_bw_minc_20_minl_30)
# Reorder first colors.
colormap[[0, 1, 2, 3, 4, 5]] = colormap[[3, 0, 4, 5, 2, 1]]
# Replace first two colors.
colormap[0] = [0.03137, 0.5725, 0.9882]
colormap[1] = [1.0000, 0.0078, 0.0078]
return colormap
class EventView(ManualClusteringView):
plot_canvas_class = PlotCanvasMpl # use matplotlib instead of OpenGL (the default)
def __init__(self, c=None):
"""features is a function (cluster_id => Bunch(data, ...)) where data is a 3D array."""
super(EventView, self).__init__()
self.controller = c
self.model = c.model
self.supervisor = c.supervisor
self.cmap=_make_default_colormap()
def on_request_similar_clusters(self,cid=None):
self.on_select()
def on_select(self, cluster_ids=(), **kwargs):
if nodata:
return
global axis_list,plot_handles
cluster_ids=self.supervisor.selected
self.cluster_ids=cluster_ids
self.nclusts = len(cluster_ids)
if axis_list:
axis_diff = (self.nclusts-len(axis_list)//2)*2
if axis_diff<0:
axis_list = axis_list[0:(len(axis_list))+axis_diff]
plot_handles = plot_handles[0:len(plot_handles)+axis_diff//2]
# We don't display anything if no clusters are selected.
if not cluster_ids:
return
for i,d in enumerate(np.arange(start=1,stop=self.nclusts*2+1)):
if d%2 == 0:
setattr(self,'canvas.ax'+str(d), plt.subplot(2*self.nclusts,1,d,sharex=axis_list[i-1]))
else:
setattr(self,'canvas.ax'+str(d), plt.subplot(2*self.nclusts,1,d))
if (len(axis_list)-1)<i:
axis_list.append(getattr(self,'canvas.ax'+str(d)))
else:
axis_list[i]=(getattr(self,'canvas.ax'+str(d)))
axis_list[i].cla()
for i,d in enumerate(cluster_ids):
rasters,activity,yrast,ntrials,nevents=self.get_spikes(d)
axis_list[i*2+1].scatter(rasters,yrast,c=[self.cmap[i]],
marker='|',s=np.ones(len(rasters))*.4,alpha=0.8)
axis_list[i*2+1].axvline(x=0,color='white',alpha=.5)
axis_list[i*2].hist(activity,bins=np.linspace(-5,5,500),color=self.cmap[i],
weights=np.ones(nevents)*(50/ntrials))
axis_list[i*2].axvline(x=0,color='white',alpha=.5)
axis_list[i*2].set_xticks(np.linspace(-5,5,9))
axis_list[i*2].set_xlim(left=-5,right=5)
axis_list[i*2+1].set_ylim(bottom=0,top=ntrials)
axis_list[i*2].set_ylim(bottom=0,top=None)
self.fix_axis(axis_list[i*2],10)
self.fix_axis(axis_list[i*2+1],10)
# Use this to update the matplotlib figure.
self.canvas.update()
return
def fix_axis(self,ax,textsize):
ax.tick_params(axis='x', labelsize=textsize)
ax.tick_params(axis='y', labelsize=textsize)
ax.xaxis.label.set_color('white')
ax.tick_params(axis='x', colors='white')
ax.tick_params(axis='y', colors='white')
ax.grid(False)
def get_spikes(self,clust):
spikes = self.model.get_cluster_spikes(clust)
spike_times =np.array(self.model.spike_times[spikes])
rasters = np.array([])
yrast = np.array([])
activity = []
last_ind = 0
for i,d in enumerate(events):
if d<np.amax(spike_times) and d>np.amin(spike_times):
st = spike_times[last_ind:]
temp1 = st-(d+5)
ind1 = np.abs(temp1).argmin()
if temp1[ind1]>0:
ind1-=1
temp2 = st-(d-5)
ind2 = np.abs(temp2).argmin()
if temp2[ind2]<0:
ind2+=1
temp=st[ind2:ind1]-d
last_ind=ind1
rasters=np.append(rasters,temp)
yrast=np.append(yrast,np.ones(len(temp))*i)
activity.extend(temp)
return rasters,activity,yrast,np.amax(yrast),len(activity)
class EventPlugin(IPlugin):
def attach_to_controller(self, controller):
def create_event_view():
"""A function that creates and returns a view."""
return EventView(c=controller)
controller.view_creator['EventView'] = create_event_view
Hey Cyrille,
I recently rewrote this code a bit so that it updates at about the same speed other views in phy (AmplitudeView, etc.). The version I posted previously took a good deal longer than the other views to update on cluster change, so this one should be much more usable.
File version here: https://drive.google.com/file/d/1jYQ9hkdbXR8WYVOlP2Ft0zm_a6MTDaAU
or you can get it directly here:
event_view_v2.py
# import from plugins/matplotlib_view.py
"""Show how to create a custom matplotlib view in the GUI."""
from phy import IPlugin
from phy.cluster.views import ManualClusteringView # Base class for phy views
from phy.plot.plot import PlotCanvasMpl # matplotlib canvas
from numpy import genfromtxt
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import colorcet as cc
import numpy as np
import warnings
import time
import sys
import os
warnings.filterwarnings("ignore")
axis_list = []
plot_handles = []
nodata=False
try:
events = genfromtxt('events.csv', delimiter=',')
except OSError:
sys.stderr.write("EventView: events.csv not found in: "+str(os.getcwd())+"\n")
nodata=True
def _make_default_colormap():
"""Return the default colormap, with custom first colors."""
colormap = np.array(cc.glasbey_bw_minc_20_minl_30)
# Reorder first colors.
colormap[[0, 1, 2, 3, 4, 5]] = colormap[[3, 0, 4, 5, 2, 1]]
# Replace first two colors.
colormap[0] = [0.03137, 0.5725, 0.9882]
colormap[1] = [1.0000, 0.0078, 0.0078]
return colormap
class EventView(ManualClusteringView):
plot_canvas_class = PlotCanvasMpl # use matplotlib instead of OpenGL (the default)
def __init__(self, c=None):
"""features is a function (cluster_id => Bunch(data, ...)) where data is a 3D array."""
super(EventView, self).__init__()
self.controller = c
self.model = c.model
self.supervisor = c.supervisor
self.cmap=_make_default_colormap()
def on_request_similar_clusters(self,cid=None):
self.on_select()
def on_select(self, cluster_ids=(), **kwargs):
if nodata:
return
global axis_list,plot_handles
cluster_ids=self.supervisor.selected
self.cluster_ids=cluster_ids
self.nclusts = len(cluster_ids)
if axis_list:
axis_diff = (self.nclusts-len(axis_list)//2)*2
if axis_diff<0:
axis_list = axis_list[0:(len(axis_list))+axis_diff]
plot_handles = plot_handles[0:len(plot_handles)+axis_diff//2]
# We don't display anything if no clusters are selected.
if not cluster_ids:
return
for i,d in enumerate(np.arange(start=1,stop=self.nclusts*2+1)):
if d%2 == 0:
setattr(self,'canvas.ax'+str(d), plt.subplot(2*self.nclusts,1,d,sharex=axis_list[i-1]))
else:
setattr(self,'canvas.ax'+str(d), plt.subplot(2*self.nclusts,1,d))
if (len(axis_list)-1)<i:
axis_list.append(getattr(self,'canvas.ax'+str(d)))
else:
axis_list[i]=(getattr(self,'canvas.ax'+str(d)))
axis_list[i].cla()
t1=time.time()
ttime=0
for i,d in enumerate(cluster_ids):
rasters,activity,yrast,ntrials,nevents=self.get_spikes(d)
t=(time.time())
axis_list[i*2+1].scatter(rasters,yrast,c=[self.cmap[i]],
marker='|',s=np.ones(len(rasters))*.4,alpha=0.8)
axis_list[i*2+1].axvline(x=0,color='white',alpha=.5)
hist, bins = np.histogram(activity,weights=np.ones(nevents)*(50/ntrials),range=(-5,5),bins=250)
axis_list[i*2].plot(bins[:-1],hist,color=self.cmap[i])
axis_list[i*2].axvline(x=0,color='white',alpha=.5)
axis_list[i*2].set_xticks(np.linspace(-2,3,9))
axis_list[i*2].set_xlim(left=-2,right=3)
axis_list[i*2+1].set_ylim(bottom=0,top=ntrials)
axis_list[i*2].set_ylim(bottom=0,top=None)
self.fix_axis(axis_list[i*2],10)
self.fix_axis(axis_list[i*2+1],10)
print(ttime,'plotting')
t=time.time()
# Use this to update the matplotlib figure.
self.canvas.show()
print(time.time()-t,'update')
print(time.time()-t1,'total')
return
def fix_axis(self,ax,textsize):
ax.tick_params(axis='x', labelsize=textsize)
ax.tick_params(axis='y', labelsize=textsize)
ax.xaxis.label.set_color('white')
ax.tick_params(axis='x', colors='white')
ax.tick_params(axis='y', colors='white')
ax.grid(False)
def get_spikes(self,clust):
spikes = self.model.get_cluster_spikes(clust)
spike_times =np.array(self.model.spike_times[spikes])
rasters = np.array([])
yrast = np.array([])
activity = []
last_ind = 0
for i,d in enumerate(events):
if d<np.amax(spike_times) and d>np.amin(spike_times):
st = spike_times[last_ind:]
temp1 = st-(d+5)
ind1 = np.abs(temp1).argmin()
if temp1[ind1]>0:
ind1-=1
temp2 = st-(d-5)
ind2 = np.abs(temp2).argmin()
if temp2[ind2]<0:
ind2+=1
temp=st[ind2:ind1]-d
last_ind=ind1
rasters=np.append(rasters,temp)
yrast=np.append(yrast,np.ones(len(temp))*i)
activity.extend(temp)
return rasters,np.array(activity),yrast,np.amax(yrast),len(activity)
class EventPlugin(IPlugin):
def attach_to_controller(self, controller):
def create_event_view():
"""A function that creates and returns a view."""
return EventView(c=controller)
controller.view_creator['EventView'] = create_event_view
thanks, that's very helpful! I have other features to implement in the short term, but I'll look at this closely when I'm ready to implement support for events.
Just managed to add the plugin, it´s great, thanks a lot @mswallac. To make it work I add to to comment out matplotlib.use('TkAgg').
Hi @mswallac , thanks a lot for creating this plugin. It's really helpful! The only problem is that my phy crashes a lot if I enable the plugin, typically when I switch around different clusters, or use the shortcuts to annotate clusters. One frequently seen error is attached. I'm using your v2 version with the TkAgg line commented out as suggested by @RobertoDF.
phy TemplateGUI v2.0b1 phylib v2.2