asset.py 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754
  1. """
  2. ASSET is a statistical method [1] for the detection of repeating sequences
  3. of synchronous spiking events in parallel spike trains.
  4. Given a list `sts` of spike trains, the analysis comprises the following
  5. steps:
  6. 1) Build the intersection matrix `imat` (optional) and the associated
  7. probability matrix `pmat` with the desired bin size:
  8. >>> binsize = 5 * pq.ms
  9. >>> dt = 1 * pq.s
  10. >>> imat, xedges, yedges = intersection_matrix(sts, binsize, dt, norm=2)
  11. >>> pmat, xedges, yedges = probability_matrix_analytical(
  12. sts, binsize, dt)
  13. 2) Compute the joint probability matrix jmat, using a suitable filter:
  14. >>> filter_shape = (5,2) # filter shape
  15. >>> nr_neigh = 5 # nr of largest neighbors
  16. >>> jmat = joint_probability_matrix(pmat, filter_shape, nr_neigh)
  17. 3) Create from pmat and jmat a masked version of the intersection matrix:
  18. >>> alpha1 = 0.99
  19. >>> alpha2 = 0.99999
  20. >>> mask = mask_matrices([pmat, jmat], [alpha1, alpha2])
  21. 4) Cluster significant elements of imat into diagonal structures ("DSs"):
  22. >>> epsilon = 10
  23. >>> minsize = 2
  24. >>> stretch = 5
  25. >>> cmat = asset.cluster_matrix_entries(mask, epsilon, minsize, stretch)
  26. 5) Extract sequences of synchronous events associated to each worm
  27. >>> extract_sse(sts, x_edges, y_edges, cmat)
  28. References:
  29. [1] Torre, Canova, Denker, Gerstein, Helias, Gruen (submitted)
  30. """
  31. import numpy as np
  32. import scipy.spatial
  33. import scipy.stats
  34. import quantities as pq
  35. import neo
  36. import itertools
  37. import elephant.conversion as conv
  38. import elephant.spike_train_surrogates as spike_train_surrogates
  39. from sklearn.cluster import dbscan as dbscan
  40. # =============================================================================
  41. # Some Utility Functions to be dealt with in some way or another
  42. # =============================================================================
  43. def _xrange(x, *args):
  44. '''
  45. Auxiliary function to use both in python 3 and python 2 to have a range
  46. function as a generator.
  47. '''
  48. try:
  49. return xrange(x, *args)
  50. except NameError:
  51. return range(x, *args)
  52. def _signals_same_tstart(signals):
  53. '''
  54. Check whether a list of signals (AnalogSignals or SpikeTrains) have same
  55. attribute t_start. If so return that value. Otherwise raise a ValueError.
  56. Parameters
  57. ----------
  58. signals : list
  59. a list of signals (e.g. AnalogSignals or SpikeTrains) having
  60. attribute `t_start`
  61. Returns
  62. -------
  63. t_start : Quantity
  64. The common attribute `t_start` of the list of signals.
  65. Raises a `ValueError` if the signals have a different `t_start`.
  66. '''
  67. t_start = signals[0].t_start
  68. if len(signals) == 1:
  69. return t_start
  70. else:
  71. for sig in signals[1:]:
  72. if sig.t_start != t_start:
  73. raise ValueError('signals have different t_start values')
  74. return t_start
  75. def _signals_same_tstop(signals):
  76. '''
  77. Check whether a list of signals (AnalogSignals or SpikeTrains) have same
  78. attribute t_stop. If so return that value. Otherwise raise a ValueError.
  79. Parameters
  80. ----------
  81. signals : list
  82. a list of signals (e.g. AnalogSignals or SpikeTrains) having
  83. attribute t_stop
  84. Returns
  85. -------
  86. t_stop : Quantity
  87. The common attribute t_stop of the list of signals.
  88. If the signals have a different t_stop, a ValueError is raised.
  89. '''
  90. t_stop = signals[0].t_stop
  91. if len(signals) == 1:
  92. return t_stop
  93. else:
  94. for sig in signals[1:]:
  95. if sig.t_stop != t_stop:
  96. raise ValueError('signals have different t_stop values')
  97. return t_stop
  98. def _quantities_almost_equal(x, y):
  99. '''
  100. Returns True if two quantities are almost equal, i.e. if x-y is
  101. "very close to 0" (not larger than machine precision for floats).
  102. Note: not the same as numpy.testing.assert_allclose (which does not work
  103. with Quantities) and numpy.testing.assert_almost_equal (which works only
  104. with decimals)
  105. Parameters
  106. ----------
  107. x : Quantity
  108. first Quantity to compare
  109. y : Quantity
  110. second Quantity to compare. Must have same unit type as x, but not
  111. necessarily the same shape. Any shapes of x and y for which x-y can
  112. be calculated are permitted
  113. Returns
  114. -------
  115. arr : ndarray of bool
  116. an array of bools, which is True at any position where x-y is almost
  117. zero
  118. '''
  119. eps = np.finfo(float).eps
  120. relative_diff = (x - y).magnitude
  121. return np.all([-eps <= relative_diff, relative_diff <= eps], axis=0)
  122. def _transactions(spiketrains, binsize, t_start=None, t_stop=None, ids=None):
  123. """
  124. Transform parallel spike trains a into list of sublists, called
  125. transactions, each corresponding to a time bin and containing the list
  126. of spikes in spiketrains falling into that bin.
  127. To compute each transaction, the spike trains are binned (with adjacent
  128. exclusive binning) and clipped (i.e. spikes from the same train falling
  129. in the same bin are counted as one event). The list of spike ids within
  130. each bin form the corresponding transaction.
  131. Parameters
  132. ----------
  133. spiketrains: list of neo.SpikeTrains
  134. list of neo.core.SpikeTrain objects, or list of pairs
  135. (Train_ID, SpikeTrain), where Train_ID can be any hashable object
  136. binsize: quantities.Quantity
  137. width of each time bin; time is binned to determine synchrony
  138. t_start: quantity.Quantity, optional
  139. starting time; only spikes occurring at times t >= t_start are
  140. considered; the first transaction contains spikes falling into the
  141. time segment [t_start, t_start+binsize[.
  142. If None, takes the t_start value of the spike trains in spiketrains
  143. if the same for all of them, or returns an error.
  144. Default: None
  145. t_stop: quantities.Quantity, optional
  146. ending time; only spikes occurring at times t < t_stop are
  147. considered.
  148. If None, takes the t_stop value of the spike trains in spiketrains
  149. if the same for all of them, or returns an error.
  150. Default: None
  151. ids : list or None, optional
  152. list of spike train IDs. If None, IDs 0 to N-1 are used, where N
  153. is the number of input spike trains
  154. Default: None
  155. Returns
  156. -------
  157. trans : list of lists
  158. a list of transactions; each transaction corresponds to a time bin
  159. and represents the list of spike trains ids having a spike in that
  160. time bin.
  161. """
  162. # Define the spike trains and their IDs depending on the input arguments
  163. if all([hasattr(elem, '__iter__') and len(elem) == 2 and
  164. type(elem[1]) == neo.SpikeTrain for elem in spiketrains]):
  165. ids = [elem[0] for elem in spiketrains]
  166. trains = [elem[1] for elem in spiketrains]
  167. elif all([type(st) == neo.SpikeTrain for st in spiketrains]):
  168. trains = spiketrains
  169. if ids is None:
  170. ids = range(len(spiketrains))
  171. else:
  172. raise TypeError('spiketrains must be either a list of ' +
  173. 'SpikeTrains or a list of (id, SpikeTrain) pairs')
  174. # Take the minimum and maximum t_start and t_stop of all spike trains
  175. # TODO: the block below should be ageneral routine in elephant
  176. tstarts = [xx.t_start for xx in trains]
  177. tstops = [xx.t_stop for xx in trains]
  178. max_tstart = max(tstarts)
  179. min_tstop = min(tstops)
  180. # Set starting time of binning
  181. if t_start is None:
  182. start = _signals_same_tstart(trains)
  183. elif t_start < max_tstart:
  184. raise ValueError('Some SpikeTrains have a larger t_start ' +
  185. 'than the specified t_start value')
  186. else:
  187. start = t_start
  188. # Set stopping time of binning
  189. if t_stop is None:
  190. stop = _signals_same_tstop(trains)
  191. elif t_stop > min_tstop:
  192. raise ValueError(
  193. 'Some SpikeTrains have a smaller t_stop ' +
  194. 'than the specified t_stop value')
  195. else:
  196. stop = t_stop
  197. # Bin the spike trains and take for each of them the ids of filled bins
  198. binned = conv.BinnedSpikeTrain(
  199. trains, binsize=binsize, t_start=start, t_stop=stop)
  200. Nbins = binned.num_bins
  201. filled_bins = binned.spike_indices
  202. # Compute and return the transaction list
  203. return [[train_id for train_id, b in zip(ids, filled_bins)
  204. if bin_id in b] for bin_id in _xrange(Nbins)]
  205. def _analog_signal_step_interp(signal, times):
  206. '''
  207. Compute the step-wise interpolation of a signal at desired times.
  208. Given a signal (e.g. an AnalogSignal) s taking value s(t0) and s(t1)
  209. at two consecutive time points t0 and t1 (t0 < t1), the value of the
  210. step-wise interpolation at time t: t0 <= t < t1 is given by s(t)=s(t0).
  211. Parameters
  212. ----------
  213. signal : neo.AnalogSignal
  214. The analog signal containing the discretization of the function to
  215. interpolate
  216. times : quantities.Quantity (vector of time points)
  217. The time points at which the step interpolation is computed
  218. Returns
  219. -------
  220. quantities.Quantity object with same shape of `times`, and containing
  221. the values of the interpolated signal at the time points in `times`
  222. '''
  223. dt = signal.sampling_period
  224. # Compute the ids of the signal times to the left of each time in times
  225. time_ids = np.floor(
  226. ((times - signal.t_start) / dt).rescale(
  227. pq.dimensionless).magnitude).astype('i')
  228. return (signal.magnitude[time_ids] * signal.units).rescale(signal.units)
  229. def _sample_quantiles(sample, p):
  230. '''
  231. Given a sample of values extracted from a probability distribution,
  232. estimates the quantile(s) associated to p-value(s) p.
  233. Given a r.v. X with probability distribution P defined over a domain D,
  234. the quantile x associated to the p-value p (0 <= p <= 1) is defined by:
  235. q(p) = min{x \in D: P(X>=x) < p}
  236. Given a sample S = {x1, x2, ..., xn} of n realisations of X, an estimate
  237. of q(p) is given by:
  238. q = min{x \in S: (#{y \in S: y>=x} / #{sample}) < p}
  239. For instance, if p = 0.05, calculates the lowest value q in sample such
  240. that less than 5% other values in sample are higher than q.
  241. Parameters
  242. ----------
  243. sample : ndarray
  244. an array of sample values, which are pooled to estimate the quantile(s)
  245. p : float or list or floats or array, all in the range [0, 1]
  246. p-value(s) for which to compute the quantile(s)
  247. Returns
  248. -------
  249. q : float, or array of floats
  250. quantile(s) associated to the input p-value(s).
  251. '''
  252. # Compute the cumulative probabilities associated to the p-values
  253. if not isinstance(p, np.ndarray):
  254. p = np.array([p])
  255. probs = list((1 - p) * 100.)
  256. quantiles = np.percentile(sample.flatten(), probs)
  257. if hasattr(quantiles, '__len__'):
  258. quantiles = np.array(quantiles)
  259. return quantiles
  260. def _sample_pvalue(sample, x):
  261. '''
  262. Estimates the p-value of each value in x, given a sample of values
  263. extracted from a probability distribution.
  264. Given a r.v. X with probability distribution P, the p-value of X at
  265. the point x is defined as:
  266. ..math::
  267. pv(x) := P(X >= x) = 1 - cdf(x)
  268. The smaller pv(x), the less likely that x was extracted from the same
  269. probability distribution of X.
  270. Given a sample {x1, x2, ..., xn} of n realisations of X, an estimate of
  271. pv(x) is given by:
  272. pv(x) ~ #{i: xi > x} / n
  273. Parameters
  274. ----------
  275. sample : ndarray
  276. a sample of realisations from a probability distribution
  277. x : float or list or floats or array
  278. p-value(s) for which to compute the quantiles
  279. Returns
  280. -------
  281. pv : same type and shape as x
  282. p-values associated to the input values x.
  283. '''
  284. # Convert x to an array
  285. if not isinstance(x, np.ndarray):
  286. x = np.array([x])
  287. # Convert sample to a flattened array
  288. if not isinstance(sample, np.ndarray):
  289. sample = np.array([sample])
  290. sample = sample.flatten()
  291. # Compute and return the p-values associated to the elements of x
  292. return np.array([(sample >= xx).sum() for xx in x]) * 1. / len(sample)
  293. def _time_slice(signal, t_start, t_stop):
  294. '''
  295. Get the time slice of an AnalogSignal between t_start and t_stop.
  296. '''
  297. # Check which elements of the signal are between t_start and t_stop.
  298. # Retain those and the corresponding times
  299. elements_to_keep = np.all(
  300. [signal.times >= t_start, signal.times < t_stop], axis=0)
  301. times = signal.times[elements_to_keep]
  302. # Put the retained values and times into a new AnalogSignal
  303. sliced_signal = neo.AnalogSignal(
  304. signal[elements_to_keep].view(pq.Quantity), t_start=times[0],
  305. sampling_period=signal.sampling_period)
  306. return sliced_signal
  307. # =============================================================================
  308. # HERE ASSET STARTS
  309. # =============================================================================
  310. def intersection_matrix(
  311. spiketrains, binsize, dt, t_start_x=None, t_start_y=None, norm=None):
  312. """
  313. Generates the intersection matrix from a list of spike trains.
  314. Given a list of SpikeTrains, consider two binned versions of them
  315. differing for the starting time of the binning (t_start_x and t_start_y
  316. respectively; the two times can be identical). Then calculate the
  317. intersection matrix M of the two binned data, where M[i,j] is the overlap
  318. of bin i in the first binned data and bin j in the second binned data
  319. (i.e. the number of spike trains spiking both at bin i and at bin j).
  320. The matrix entries can be normalized to values between 0 and 1 via
  321. different normalizations (see below).
  322. Parameters
  323. ----------
  324. spiketrains : list of neo.SpikeTrains
  325. list of SpikeTrains from which to compute the intersection matrix
  326. binsize : quantities.Quantity
  327. size of the time bins used to define synchronous spikes in the given
  328. SpikeTrains.
  329. dt : quantities.Quantity
  330. time span for which to consider the given SpikeTrains
  331. t_start_x : quantities.Quantity, optional
  332. time start of the binning for the first axis of the intersection
  333. matrix, respectively.
  334. If None (default) the attribute t_start of the SpikeTrains is used
  335. (if the same for all spike trains).
  336. Default: None
  337. t_start_y : quantities.Quantity, optional
  338. time start of the binning for the second axis of the intersection
  339. matrix
  340. norm : int, optional
  341. type of normalization to be applied to each entry [i,j] of the
  342. intersection matrix. Given the sets s_i, s_j of neuron ids in the
  343. bins i, j respectively, the normalisation coefficient can be:
  344. * norm = 0 or None: no normalisation (row counts)
  345. * norm = 1: len(intersection(s_i, s_j))
  346. * norm = 2: sqrt(len(s_1) * len(s_2))
  347. * norm = 3: len(union(s_i, s_j))
  348. Default: None
  349. Returns
  350. -------
  351. imat : numpy.ndarray of floats
  352. the intersection matrix of a list of spike trains. Has shape (n,n),
  353. where n is the number of bins time was discretized in.
  354. x_edges : numpy.ndarray
  355. edges of the bins used for the horizontal axis of imat. If imat is
  356. a matrix of shape (n, n), x_edges has length n+1
  357. y_edges : numpy.ndarray
  358. edges of the bins used for the vertical axis of imat. If imat is
  359. a matrix of shape (n, n), y_edges has length n+1
  360. """
  361. # Setting the start and stop time for the x and y axes:
  362. if t_start_x is None:
  363. t_start_x = _signals_same_tstart(spiketrains)
  364. if t_start_y is None:
  365. t_start_y = _signals_same_tstart(spiketrains)
  366. t_stop_x = dt + t_start_x
  367. t_stop_y = dt + t_start_y
  368. # Check that all SpikeTrains are defined until t_stop at least
  369. t_stop_max = max(t_stop_x, t_stop_y)
  370. for i, st in enumerate(spiketrains):
  371. if not (st.t_stop > t_stop_max or
  372. _quantities_almost_equal(st.t_stop, t_stop_max)):
  373. msg = 'SpikeTrain %d is shorter than the required time ' % i + \
  374. 'span: t_stop (%s) < %s' % (st.t_stop, t_stop_max)
  375. raise ValueError(msg)
  376. # For both x and y axis, cut all SpikeTrains between t_start and t_stop
  377. sts_x = [st.time_slice(t_start=t_start_x, t_stop=t_stop_x)
  378. for st in spiketrains]
  379. sts_y = [st.time_slice(t_start=t_start_y, t_stop=t_stop_y)
  380. for st in spiketrains]
  381. # Compute imat either by matrix multiplication (~20x faster) or by
  382. # nested for loops (more memory efficient)
  383. try: # try the fast version
  384. # Compute the binned spike train matrices, along both time axes
  385. bsts_x = conv.BinnedSpikeTrain(
  386. sts_x, binsize=binsize,
  387. t_start=t_start_x, t_stop=t_stop_x).to_bool_array()
  388. bsts_y = conv.BinnedSpikeTrain(
  389. sts_y, binsize=binsize,
  390. t_start=t_start_y, t_stop=t_stop_y).to_bool_array()
  391. # Compute the number of spikes in each bin, for both time axes
  392. spikes_per_bin_x = bsts_x.sum(axis=0)
  393. spikes_per_bin_y = bsts_y.sum(axis=0)
  394. # Compute the intersection matrix imat
  395. N_bins = len(spikes_per_bin_x)
  396. imat = np.zeros((N_bins, N_bins)) * 1.
  397. for ii in _xrange(N_bins):
  398. # Compute the ii-th row of imat
  399. bin_ii = bsts_x[:, ii].reshape(-1, 1)
  400. imat[ii, :] = (bin_ii * bsts_y).sum(axis=0)
  401. # Normalize the row according to the specified normalization
  402. if norm == 0 or norm is None or bin_ii.sum() == 0:
  403. norm_coef = 1.
  404. elif norm == 1:
  405. norm_coef = np.minimum(
  406. spikes_per_bin_x[ii], spikes_per_bin_y)
  407. elif norm == 2:
  408. norm_coef = np.sqrt(
  409. spikes_per_bin_x[ii] * spikes_per_bin_y)
  410. elif norm == 3:
  411. norm_coef = ((bin_ii + bsts_y) > 0).sum(axis=0)
  412. imat[ii, :] = imat[ii, :] / norm_coef
  413. # If normalization required, for each j such that bsts_y[j] is
  414. # identically 0 the code above sets imat[:, j] to identically nan.
  415. # Substitute 0s instead. Then refill the main diagonal with 1s.
  416. if norm and norm >= 0.5:
  417. ybins_equal_0 = np.where(spikes_per_bin_y == 0)[0]
  418. for y_id in ybins_equal_0:
  419. imat[:, y_id] = 0
  420. imat[_xrange(N_bins), _xrange(N_bins)] = 1.
  421. except MemoryError: # use the memory-efficient version
  422. # Compute the list spiking neurons per bin, along both axes
  423. ids_per_bin_x = _transactions(
  424. sts_x, binsize, t_start=t_start_x, t_stop=t_stop_x)
  425. ids_per_bin_y = _transactions(
  426. sts_y, binsize, t_start=t_start_y, t_stop=t_stop_y)
  427. # Generate the intersection matrix
  428. N_bins = len(ids_per_bin_x)
  429. imat = np.zeros((N_bins, N_bins))
  430. for ii in _xrange(N_bins):
  431. for jj in _xrange(N_bins):
  432. if len(ids_per_bin_x[ii]) * len(ids_per_bin_y[jj]) != 0:
  433. imat[ii, jj] = len(set(ids_per_bin_x[ii]).intersection(
  434. set(ids_per_bin_y[jj])))
  435. # Normalise according to the desired normalisation type:
  436. if norm == 1:
  437. imat[ii, jj] /= float(min(len(ids_per_bin_x[ii]),
  438. len(ids_per_bin_y[jj])))
  439. elif norm == 2:
  440. imat[ii, jj] /= np.sqrt(float(
  441. len(ids_per_bin_x[ii]) * len(ids_per_bin_y[jj])))
  442. elif norm == 3:
  443. imat[ii, jj] /= float(len(set(
  444. ids_per_bin_x[ii]).union(set(ids_per_bin_y[jj]))))
  445. # Compute the time edges corresponding to the binning employed
  446. t_start_x_dl = t_start_x.rescale(binsize.units).magnitude
  447. t_start_y_dl = t_start_y.rescale(binsize.units).magnitude
  448. t_stop_x_dl = t_stop_x.rescale(binsize.units).magnitude
  449. t_stop_y_dl = t_stop_y.rescale(binsize.units).magnitude
  450. xx = np.linspace(t_start_x_dl, t_stop_x_dl, N_bins + 1) * binsize.units
  451. yy = np.linspace(t_start_y_dl, t_stop_y_dl, N_bins + 1) * binsize.units
  452. # Return the intersection matrix and the edges of the bins used for the
  453. # x and y axes, respectively.
  454. return imat, xx, yy
  455. def _reference_diagonal(x_edges, y_edges):
  456. '''
  457. Given two arrays of time bin edges :math:`x_edges = (X_1, X_2, ..., X_k)`
  458. and :math:`y_edges = (Y_1, Y_2, ..., Y_k)`, considers the matrix `M`
  459. such that :math:`M_{ij} = (X_i, Y_j)` and finds the reference diagonal of
  460. M, i.e. the diagonal of M whose elements are of the type `(a, a)`.
  461. Returns the index of such diagonal and its elements.
  462. For example, if :math:`x_edges = (0, 1, 2, 3) ms` and :math:`y_edges =
  463. (1, 2, 3, 4) ms`, then the index of the reference diagonal is -1
  464. (first off-diagonal below the main diagonal) and its elements are
  465. (-1, 0), (0, 1), (1, 2), (2, 3).
  466. '''
  467. diag_id = None
  468. error_msg = \
  469. 'the time axes (%s-%s and %s-%s)' % (
  470. x_edges[0], x_edges[-1], y_edges[0], y_edges[-1]) + \
  471. ' overlap but the overlapping bin edges are not aligned' \
  472. '. Bad alignment of the time axes.'
  473. if y_edges[0] > x_edges[0]:
  474. if y_edges[0] < x_edges[-1]:
  475. bin_ids = np.where(
  476. _quantities_almost_equal(x_edges, y_edges[0]))[0]
  477. if len(bin_ids) == 0:
  478. raise ValueError(error_msg)
  479. diag_id = - bin_ids[0]
  480. else:
  481. if y_edges[-1] > x_edges[0]:
  482. bin_ids = np.where(y_edges == x_edges[0])[0]
  483. if len(bin_ids) == 0:
  484. raise ValueError(error_msg)
  485. diag_id = bin_ids[0]
  486. m = len(x_edges) - 1
  487. if diag_id is None:
  488. return diag_id, np.array([])
  489. elif diag_id >= 0:
  490. elements = np.array([_xrange(m - diag_id), _xrange(diag_id, m)]).T
  491. else:
  492. elements = np.array([_xrange(diag_id, m), _xrange(m - diag_id)]).T
  493. return diag_id, elements
  494. def mask_matrices(matrices, thresholds):
  495. '''
  496. Given a list of matrices and a list of thresholds, return a boolean matrix
  497. B ("mask") such that B[i,j] is True if each input matrix in the list
  498. strictly exceeds the corresponding threshold at that position.
  499. Parameters
  500. ----------
  501. matrices : list of numpy.ndarrays
  502. the matrices which are compared to the respective thresholds to
  503. build the mask. All matrices must have the same shape.
  504. thresholds : list of floats
  505. list of thresholds
  506. Returns
  507. -------
  508. mask : numpy.ndarray of bools
  509. mask matrix with same shape of the input matrices.
  510. '''
  511. # Check that input lists have same length
  512. L = len(matrices)
  513. if L != len(thresholds):
  514. raise ValueError('`matrices` and `thresholds` must have same length')
  515. # Compute mask matrix
  516. mask = matrices[0] > thresholds[0]
  517. if L > 1:
  518. for (mat, thresh) in zip(matrices, thresholds):
  519. mask = mask * (mat > thresh)
  520. # Replace nans, coming from False * np.inf, with 0s
  521. # (trick to find nans in masked: a number is nan if it's not >= - np.inf)
  522. mask[np.logical_xor(True, (mask >= -np.inf))] = False
  523. return np.array(mask, dtype=bool)
  524. def _stretched_metric_2d(x, y, stretch, ref_angle):
  525. '''
  526. Given a list of points on the real plane, identified by their absciss x
  527. and ordinate y, compute a stretched transformation of the Euclidean
  528. distance among each of them.
  529. The classical euclidean distance d between points (x1, y1) and (x2, y2),
  530. i.e. \sqrt((x1-x2)^2 + (y1-y2)^2), is multiplied by a factor
  531. .. math::
  532. 1 + (stretch - 1.) * \abs(\sin(ref_angle - \theta)),
  533. where \\theta is the angle between the points and the 45deg direction
  534. (i.e. the line y=x).
  535. The stretching factor thus steadily varies between 1 (if the line
  536. connecting (x1, y1) and (x2, y2) has inclination ref_angle) and stretch
  537. (if that line has inclination 90 + ref_angle).
  538. Parameters
  539. ----------
  540. x : numpy.ndarray
  541. array of abscissa of all points among which to compute the distance
  542. y : numpy.ndarray (same shape as x)
  543. array of ordinates of all points among which to compute the distance
  544. stretch : float
  545. maximum stretching factor, applied if the line connecting the points
  546. has inclination 90 + ref_angle
  547. ref_angle : float
  548. reference angle, i.e. inclination along which the stretching factor
  549. is 1.
  550. Output
  551. ------
  552. D : numpy.ndarray
  553. square matrix of distances between all pairs of points. If x and y
  554. have shape (n, ) then D has shape (n, n).
  555. '''
  556. alpha = np.deg2rad(ref_angle) # reference angle in radians
  557. # Create the array of points (one per row) for which to compute the
  558. # stretched distance
  559. points = np.vstack([x, y]).T
  560. # Compute the matrix D[i, j] of euclidean distances among points i and j
  561. D = scipy.spatial.distance_matrix(points, points)
  562. # Compute the angular coefficients of the line between each pair of points
  563. x_array = 1. * np.vstack([x for i in x])
  564. y_array = 1. * np.vstack([y for i in y])
  565. dX = x_array.T - x_array # dX[i,j]: x difference between points i and j
  566. dY = y_array.T - y_array # dY[i,j]: y difference between points i and j
  567. AngCoeff = dY / dX
  568. # Compute the matrix Theta of angles between each pair of points
  569. Theta = np.arctan(AngCoeff)
  570. n = Theta.shape[0]
  571. Theta[_xrange(n), _xrange(n)] = 0 # set angle to 0 if points identical
  572. # Compute the matrix of stretching factors for each pair of points
  573. stretch_mat = (1 + ((stretch - 1.) * np.abs(np.sin(alpha - Theta))))
  574. # Return the stretched distance matrix
  575. return D * stretch_mat
  576. def cluster_matrix_entries(mat, eps=10, min=2, stretch=5):
  577. '''
  578. Given a matrix mat, replaces its positive elements with integers
  579. representing different cluster ids. Each cluster comprises close-by
  580. elements.
  581. In ASSET analysis, mat is a thresholded ("masked") version of an
  582. intersection matrix imat, whose values are those of imat only if
  583. considered statistically significant, and zero otherwise.
  584. A cluster is built by pooling elements according to their distance,
  585. via the DBSCAN algorithm (see sklearn.cluster.dbscan()). Elements form
  586. a neighbourhood if at least one of them has a distance not larger than
  587. eps from the others, and if they are at least min. Overlapping
  588. neighborhoods form a cluster.
  589. * Clusters are assigned integers from 1 to the total number k of
  590. clusters
  591. * Unclustered ("isolated") positive elements of mat are
  592. assigned value -1
  593. * Non-positive elements are assigned the value 0.
  594. The distance between the positions of two positive elements in mat is
  595. given by an Euclidean metric which is stretched if the two positions are
  596. not aligned along the 45 degree direction (the main diagonal direction),
  597. as more, with maximal stretching along the anti-diagonal. Specifically,
  598. the Euclidean distance between positions (i1, j1) and (i2, j2) is
  599. stretched by a factor
  600. .. math::
  601. 1 + (\mathtt{stretch} - 1.) *
  602. \\left|\\sin((\\pi / 4) - \\theta)\\right|,
  603. where :math:`\\theta` is the angle between the pixels and the 45deg
  604. direction. The stretching factor thus varies between 1 and stretch.
  605. Parameters
  606. ----------
  607. mat : numpy.ndarray
  608. a matrix whose elements with positive values are to be clustered.
  609. eps : float >=0, optional
  610. the maximum distance for two elements in mat to be part of the same
  611. neighbourhood in the DBSCAN algorithm
  612. Default: 10
  613. min : int, optional
  614. the minimum number of elements to form a neighbourhood.
  615. Default: 2
  616. stretch : float > 1, optional
  617. the stretching factor of the euclidean metric for elements aligned
  618. along the 135 degree direction (anti-diagonal). The actual stretching
  619. increases from 1 to stretch as the direction of the two elements
  620. moves from the 45 to the 135 degree direction.
  621. Default: 5
  622. Returns
  623. -------
  624. cmat : numpy.ndarray of integers
  625. a matrix with the same shape of mat, each of whose elements is either
  626. * a positive int (cluster id) if the element is part of a cluster
  627. * 0 if the corresponding element in mat was non-positive
  628. * -1 if the element does not belong to any cluster
  629. '''
  630. # Don't do anything if mat is identically zero
  631. if np.all(mat == 0):
  632. return mat
  633. # List the significant pixels of mat in a 2-columns array
  634. xpos_sgnf, ypos_sgnf = np.where(mat > 0)
  635. # Compute the matrix D[i, j] of euclidean distances among pixels i and j
  636. D = _stretched_metric_2d(
  637. xpos_sgnf, ypos_sgnf, stretch=stretch, ref_angle=45)
  638. # Cluster positions of significant pixels via dbscan
  639. core_samples, config = dbscan(
  640. D, eps=eps, min_samples=min, metric='precomputed')
  641. # Construct the clustered matrix, where each element has value
  642. # * i = 1 to k if it belongs to a cluster i,
  643. # * 0 if it is not significant,
  644. # * -1 if it is significant but does not belong to any cluster
  645. cluster_mat = np.array(np.zeros(mat.shape), dtype=int)
  646. cluster_mat[xpos_sgnf, ypos_sgnf] = \
  647. config * (config == -1) + (config + 1) * (config >= 0)
  648. return cluster_mat
  649. def probability_matrix_montecarlo(
  650. spiketrains, binsize, dt, t_start_x=None, t_start_y=None,
  651. surr_method='dither_spike_train', j=None, n_surr=100, verbose=False):
  652. '''
  653. Given a list of parallel spike trains, estimate the cumulative probability
  654. of each entry in their intersection matrix (see: intersection_matrix())
  655. by a Monte Carlo approach using surrogate data.
  656. Contrarily to the analytical version (see: probability_matrix_analytical())
  657. the Monte Carlo one does not incorporate the assumptions of Poissonianity
  658. in the null hypothesis.
  659. The method produces surrogate spike trains (using one of several methods
  660. at disposal, see below) and calculates their intersection matrix M.
  661. For each entry (i, j), the intersection cdf P[i, j] is then given by:
  662. .. centered:: P[i, j] = #(spike_train_surrogates such that M[i, j] < I[i, j]) /
  663. #(spike_train_surrogates)
  664. If P[i, j] is large (close to 1), I[i, j] is statistically significant:
  665. the probability to observe an overlap equal to or larger then I[i, j]
  666. under the null hypothesis is 1-P[i, j], very small.
  667. Parameters
  668. ----------
  669. sts : list of neo.SpikeTrains
  670. list of spike trains for which to compute the probability matrix
  671. binsize : quantities.Quantity
  672. width of the time bins used to compute the probability matrix
  673. dt : quantities.Quantity
  674. time span for which to consider the given SpikeTrains
  675. t_start_x, t_start_y : quantities.Quantity, optional
  676. time start of the binning for the first and second axes of the
  677. intersection matrix, respectively.
  678. If None (default) the attribute t_start of the SpikeTrains is used
  679. (if the same for all spike trains).
  680. Default: None
  681. surr_method : str, optional
  682. the method to use to generate surrogate spike trains. Can be one of:
  683. * 'dither_spike_train': see spike_train_surrogates.train_shifting() [dt needed]
  684. * 'spike_dithering': see spike_train_surrogates.spike_dithering() [dt needed]
  685. * 'spike_jittering': see spike_train_surrogates.spike_jittering() [dt needed]
  686. * 'spike_time_rand': see spike_train_surrogates.spike_time_rand()
  687. * 'isi_shuffling': see spike_train_surrogates.isi_shuffling()
  688. Default: 'dither_spike_train'
  689. j : quantities.Quantity, optional
  690. For methods shifting spike times randomly around their original time
  691. (spike dithering, train shifting) or replacing them randomly within a
  692. certain window (spike jittering), j represents the size of that
  693. shift / window. For other methods, j is ignored.
  694. Default: None
  695. n_surr : int, optional
  696. number of spike_train_surrogates to generate for the bootstrap
  697. procedure. Default: 100
  698. Returns
  699. -------
  700. pmat : ndarray
  701. the cumulative probability matrix. pmat[i, j] represents the
  702. estimated probability of having an overlap between bins i and j
  703. STRICTLY LOWER than the observed overlap, under the null hypothesis
  704. of independence of the input spike trains.
  705. See also
  706. --------
  707. probability_matrix_analytical() for analytical derivation of the matrix
  708. '''
  709. # Compute the intersection matrix of the original data
  710. imat, x_edges, y_edges = intersection_matrix(
  711. spiketrains, binsize=binsize, dt=dt, t_start_x=t_start_x,
  712. t_start_y=t_start_y)
  713. # Generate surrogate spike trains as a list surrs; for each spike train
  714. # i, surrs[i] is a list of length n_surr, containing the
  715. # spike_train_surrogates of i
  716. surrs = [spike_train_surrogates.surrogates(
  717. st, n=n_surr, surr_method=surr_method, dt=j, decimals=None, edges=True)
  718. for st in spiketrains]
  719. # Compute the p-value matrix pmat; pmat[i, j] counts the fraction of
  720. # surrogate data whose intersection value at (i, j) whose lower than or
  721. # equal to that of the original data
  722. pmat = np.array(np.zeros(imat.shape), dtype=int)
  723. if verbose:
  724. print('pmat_bootstrap(): begin of bootstrap...')
  725. for i in _xrange(n_surr): # For each surrogate id i
  726. if verbose:
  727. print(' surr %d' % i)
  728. surrs_i = [st[i] for st in surrs] # Take each i-th surrogate
  729. imat_surr, xx, yy = intersection_matrix( # compute the related imat
  730. surrs_i, binsize=binsize, dt=dt,
  731. t_start_x=t_start_x, t_start_y=t_start_y)
  732. pmat += (imat_surr <= imat - 1)
  733. pmat = pmat * 1. / n_surr
  734. if verbose:
  735. print('pmat_bootstrap(): done')
  736. return pmat, x_edges, y_edges
  737. def probability_matrix_analytical(
  738. spiketrains, binsize, dt, t_start_x=None, t_start_y=None,
  739. fir_rates='estimate', kernel_width=100 * pq.ms, verbose=False):
  740. '''
  741. Given a list of spike trains, approximates the cumulative probability of
  742. each entry in their intersection matrix (see: intersection_matrix()).
  743. The approximation is analytical and works under the assumptions that the
  744. input spike trains are independent and Poisson. It works as follows:
  745. * Bin each spike train at the specified binsize: this yields a binary
  746. array of 1s (spike in bin) and 0s (no spike in bin) (clipping used)
  747. * If required, estimate the rate profile of each spike train by
  748. convolving the binned array with a boxcar kernel of user-defined
  749. length
  750. * For each neuron k and each pair of bins i and j, compute the
  751. probability p_ijk that neuron k fired in both bins i and j.
  752. * Approximate the probability distribution of the intersection value
  753. at (i, j) by a Poisson distribution with mean parameter
  754. l = \sum_k (p_ijk),
  755. justified by Le Cam's approximation of a sum of independent
  756. Bernouilli random variables with a Poisson distribution.
  757. Parameters
  758. ----------
  759. spiketrains : list of neo.SpikeTrains
  760. list of spike trains for whose intersection matrix to compute the
  761. p-values
  762. binsize : quantities.Quantity
  763. width of the time bins used to compute the probability matrix
  764. dt : quantities.Quantity
  765. time span for which to consider the given SpikeTrains
  766. t_start_x, t_start_y : quantities.Quantity, optional
  767. time start of the binning for the first and second axes of the
  768. intersection matrix, respectively.
  769. If None (default) the attribute t_start of the SpikeTrains is used
  770. (if the same for all spike trains).
  771. Default: None
  772. fir_rates: list of neo.AnalogSignals or 'estimate', optional
  773. if a list, fir_rate[i] is the firing rate of the spike train
  774. spiketrains[i]. If 'estimate', firing rates are estimated by simple
  775. boxcar kernel convolution, with specified kernel width (see below)
  776. Default: 'estimate'
  777. kernel_width : quantities.Quantity, optional
  778. total width of the kernel used to estimate the rate profiles when
  779. fir_rates='estimate'.
  780. Default: 100 * pq.ms
  781. verbose : bool, optional
  782. whether to print messages during the computation.
  783. Default: False
  784. Returns
  785. -------
  786. pmat : numpy.ndarray
  787. the cumulative probability matrix. pmat[i, j] represents the
  788. estimated probability of having an overlap between bins i and j
  789. STRICTLY LOWER THAN the observed overlap, under the null hypothesis
  790. of independence of the input spike trains.
  791. x_edges : numpy.ndarray
  792. edges of the bins used for the horizontal axis of pmat. If pmat is
  793. a matrix of shape (n, n), x_edges has length n+1
  794. y_edges : numpy.ndarray
  795. edges of the bins used for the vertical axis of pmat. If pmat is
  796. a matrix of shape (n, n), y_edges has length n+1
  797. '''
  798. # Bin the spike trains
  799. t_stop_x = None if t_start_x is None else t_start_x + dt
  800. t_stop_y = None if t_start_y is None else t_start_y + dt
  801. bsts_x = conv.BinnedSpikeTrain(
  802. spiketrains, binsize=binsize, t_start=t_start_x, t_stop=t_stop_x)
  803. bsts_y = conv.BinnedSpikeTrain(
  804. spiketrains, binsize=binsize, t_start=t_start_y, t_stop=t_stop_y)
  805. bsts_x_matrix = bsts_x.to_bool_array()
  806. bsts_y_matrix = bsts_y.to_bool_array()
  807. # Check that the duration and nr. neurons is identical between the two axes
  808. if bsts_x_matrix.shape != bsts_y_matrix.shape:
  809. raise ValueError(
  810. 'Different spike train durations along the x and y axis!')
  811. # Define the firing rate profiles
  812. # If rates are to be estimated, create the rate profiles as Quantity
  813. # objects obtained by boxcar-kernel convolution
  814. if fir_rates == 'estimate':
  815. if verbose is True:
  816. print('compute rates by boxcar-kernel convolution...')
  817. # Create the boxcar kernel and convolve it with the binned spike trains
  818. k = int((kernel_width / binsize).rescale(pq.dimensionless))
  819. kernel = np.ones(k) * 1. / k
  820. fir_rate_x = np.vstack([np.convolve(bst, kernel, mode='same')
  821. for bst in bsts_x_matrix])
  822. fir_rate_y = np.vstack([np.convolve(bst, kernel, mode='same')
  823. for bst in bsts_y_matrix])
  824. # The convolution results in an array decreasing at the borders due
  825. # to absence of spikes beyond the borders. Replace the first and last
  826. # (k//2) elements with the (k//2)-th / (n-k//2)-th ones, respectively
  827. k2 = k // 2
  828. for i in _xrange(fir_rate_x.shape[0]):
  829. fir_rate_x[i, :k2] = fir_rate_x[i, k2]
  830. fir_rate_x[i, -k2:] = fir_rate_x[i, -k2 - 1]
  831. fir_rate_y[i, :k2] = fir_rate_y[i, k2]
  832. fir_rate_y[i, -k2:] = fir_rate_y[i, -k2 - 1]
  833. # Multiply the firing rates by the proper unit
  834. fir_rate_x = fir_rate_x * (1. / binsize).rescale('Hz')
  835. fir_rate_y = fir_rate_y * (1. / binsize).rescale('Hz')
  836. # If rates provided as lists of AnalogSignals, create time slices for both
  837. # axes, interpolate in the time bins of interest and convert to Quantity
  838. elif isinstance(fir_rates, list):
  839. # Reshape all rates to one-dimensional array object (e.g. AnalogSignal)
  840. for i, rate in enumerate(fir_rates):
  841. if len(rate.shape) == 2:
  842. fir_rates[i] = rate.reshape((-1,))
  843. elif len(rate.shape) > 2:
  844. raise ValueError(
  845. 'elements in fir_rates have too many dimensions')
  846. if verbose is True:
  847. print('create time slices of the rates...')
  848. # Define the rate by time slices
  849. fir_rate_x = [_time_slice(signal, bsts_x.t_start, bsts_x.t_stop)
  850. for signal in fir_rates]
  851. fir_rate_y = [_time_slice(signal, bsts_y.t_start, bsts_y.t_stop)
  852. for signal in fir_rates]
  853. # Interpolate in the time bins and convert to Quantities
  854. times_x = bsts_x.bin_edges[:-1]
  855. times_y = bsts_y.bin_edges[:-1]
  856. fir_rate_x = pq.Hz * np.vstack([_analog_signal_step_interp(
  857. signal, times_x).rescale('Hz').magnitude for signal in fir_rates])
  858. fir_rate_y = pq.Hz * np.vstack([_analog_signal_step_interp(
  859. signal, times_y).rescale('Hz').magnitude for signal in fir_rates])
  860. else:
  861. raise ValueError('fir_rates must be a list or the string "estimate"')
  862. # For each neuron, compute the prob. that that neuron spikes in any bin
  863. if verbose is True:
  864. print(
  865. 'compute the prob. that each neuron fires in each pair of bins...')
  866. spike_probs_x = [1. - np.exp(-(rate * binsize).rescale(
  867. pq.dimensionless).magnitude) for rate in fir_rate_x]
  868. spike_probs_y = [1. - np.exp(-(rate * binsize).rescale(
  869. pq.dimensionless).magnitude) for rate in fir_rate_y]
  870. # For each neuron k compute the matrix of probabilities p_ijk that neuron
  871. # k spikes in both bins i and j. (For i = j it's just spike_probs[k][i])
  872. spike_prob_mats = [np.outer(probx, proby) for (probx, proby) in
  873. zip(spike_probs_x, spike_probs_y)]
  874. # Compute the matrix Mu[i, j] of parameters for the Poisson distributions
  875. # which describe, at each (i, j), the approximated overlap probability.
  876. # This matrix is just the sum of the probability matrices computed above
  877. if verbose is True:
  878. print("compute the probability matrix by Le Cam's approximation...")
  879. Mu = np.sum(spike_prob_mats, axis=0)
  880. # Compute the probability matrix obtained from imat using the Poisson pdfs
  881. imat, xx, yy = intersection_matrix(
  882. spiketrains, binsize=binsize, dt=dt, t_start_x=t_start_x,
  883. t_start_y=t_start_y)
  884. pmat = np.zeros(imat.shape)
  885. for i in _xrange(imat.shape[0]):
  886. for j in _xrange(imat.shape[1]):
  887. pmat[i, j] = scipy.stats.poisson.cdf(imat[i, j] - 1, Mu[i, j])
  888. # Substitute 0.5 to the elements along the main diagonal
  889. diag_id, elems = _reference_diagonal(xx, yy)
  890. if diag_id is not None:
  891. if verbose is True:
  892. print("substitute 0.5 to elements along the main diagonal...")
  893. for elem in elems:
  894. pmat[elem[0], elem[1]] = 0.5
  895. return pmat, xx, yy
  896. def _jsf_uniform_orderstat_3d(u, alpha, n):
  897. '''
  898. Considered n independent random variables X1, X2, ..., Xn all having
  899. uniform distribution in the interval (alpha, 1):
  900. .. centered:: Xi ~ Uniform(alpha, 1),
  901. with alpha \in [0, 1), and given a 3D matrix U = (u_ijk) where each U_ij
  902. is an array of length d: U_ij = [u0, u1, ..., u_{d-1}] of
  903. quantiles, with u1 <= u2 <= ... <= un, computes the joint survival function
  904. (jsf) of the d highest order statistics (U_{n-d+1}, U_{n-d+2}, ..., U_n),
  905. where U_i := "i-th highest X's" at each u_ij, i.e.:
  906. .. centered:: jsf(u_ij) = Prob(U_{n-k} >= u_ijk, k=0,1,..., d-1).
  907. Arguments
  908. ---------
  909. u : numpy.ndarray of shape (A, B, d)
  910. 3D matrix of floats between 0 and 1.
  911. Each vertical column u_ij is an array of length d, considered a set of
  912. `d` largest order statistics extracted from a sample of `n` random
  913. variables whose cdf is F(x)=x for each x.
  914. The routine computes the joint cumulative probability of the `d`
  915. values in u_ij, for each i and j.
  916. alpha : float in [0, 1)
  917. range where the values of `u` are assumed to vary.
  918. alpha is 0 in the standard ASSET analysis.
  919. n : int
  920. size of the sample where the d largest order statistics u_ij are
  921. assumed to have been sampled from
  922. Returns
  923. -------
  924. S : numpy.ndarray of shape (A, B)
  925. matrix of joint survival probabilities. s_ij is the joint survival
  926. probability of the values {u_ijk, k=0,...,d-1}.
  927. Note: the joint probability matrix computed for the ASSET analysis
  928. is 1-S.
  929. '''
  930. d, A, B = u.shape
  931. # Define ranges [1,...,n], [2,...,n], ..., [d,...,n] for the mute variables
  932. # used to compute the integral as a sum over several possibilities
  933. lists = [_xrange(j, n + 1) for j in _xrange(d, 0, -1)]
  934. # Compute the log of the integral's coefficient
  935. logK = np.sum(np.log(np.arange(1, n + 1))) - n * np.log(1 - alpha)
  936. # Add to the 3D matrix u a bottom layer identically equal to alpha and a
  937. # top layer identically equal to 1. Then compute the difference dU along
  938. # the first dimension.
  939. u_extended = np.ones((d + 2, A, B))
  940. u_extended[0] = u_extended[0] * alpha
  941. for layer_idx, uu in enumerate(u):
  942. u_extended[layer_idx + 1] = u[layer_idx]
  943. dU = np.diff(u_extended, axis=0) # shape (d+1, A, B)
  944. del u_extended
  945. # Compute the probabilities at each (a, b), a=0,...,A-1, b=0,...,B-1
  946. # by matrix algebra, working along the third dimension (axis 0)
  947. Ptot = np.zeros((A, B)) # initialize all A x B probabilities to 0
  948. iter_id = 0
  949. for i in itertools.product(*lists):
  950. iter_id += 1
  951. di = -np.diff(np.hstack([n, list(i), 0]))
  952. if np.all(di >= 0):
  953. dI = di.reshape((-1, 1, 1)) * np.ones((A, B)) # shape (d+1, A, B)
  954. # for each a=0,1,...,A-1 and b=0,1,...,B-1, replace dU_abk with 1
  955. # whenever dI_abk = 0, so that dU_abk ** dI_abk = 1 (this avoids
  956. # nans when both dU_abk and dI_abk are 0, and is mathematically
  957. # correct). dU2 still contains 0s, so that when below exp(log(U2))
  958. # is computed, warnings are arosen; they are no problem though.
  959. dU2 = dU.copy()
  960. dU2[dI == 0] = 1.
  961. # Compute for each i=0,...,A-1 and j=0,...,B-1: log(I_ij !)
  962. # Creates a matrix log_dIfactorial of shape (A, B)
  963. log_di_factorial = np.sum([np.log(np.arange(1, di_k + 1)).sum()
  964. for di_k in di if di_k >= 1])
  965. # Compute for each i,j the contribution to the total
  966. # probability given by this step, and add it to the total prob.
  967. logP = (dI * np.log(dU2)).sum(axis=0) - log_di_factorial
  968. Ptot += np.exp(logP + logK)
  969. return Ptot
  970. def _pmat_neighbors(mat, filter_shape, nr_largest=None, diag=0):
  971. '''
  972. Build the 3D matrix L of largest neighbors of elements in a 2D matrix mat.
  973. For each entry mat[i, j], collects the nr_largest elements with largest
  974. values around mat[i,j], say z_i, i=1,2,...,nr_largest, and assigns them
  975. to L[i, j, :].
  976. The zone around mat[i, j] where largest neighbors are collected from is
  977. a rectangular area (kernel) of shape (l, w) = filter_shape centered around
  978. mat[i, j] and aligned along the diagonal where mat[i, j] lies into
  979. (if diag=0, default) or along the anti-diagonal (is diag = 1)
  980. Arguments
  981. ---------
  982. mat : ndarray
  983. a square matrix of real-valued elements
  984. filter_shape : tuple
  985. a pair (l, w) of integers representing the kernel shape
  986. nr_largest : int, optional
  987. the number of largest neighbors to collect for each entry in mat
  988. If None (default) the filter length l is used
  989. Default: 0
  990. diag : int, optional
  991. which diagonal of mat[i, j] to align the kernel to in order to
  992. find its largest neighbors.
  993. * 0: main diagonal
  994. * 1: anti-diagonal
  995. Default: 0
  996. Returns
  997. -------
  998. L : ndarray
  999. a matrix of shape (nr_largest, l, w) containing along the first
  1000. dimension lmat[:, i, j] the largest neighbors of mat[i, j]
  1001. '''
  1002. l, w = filter_shape
  1003. d = l if nr_largest is None else nr_largest
  1004. # Check consistent arguments
  1005. assert mat.shape[0] == mat.shape[1], 'mat must be a square matrix'
  1006. assert diag == 0 or diag == 1, \
  1007. 'diag must be 0 (45 degree filtering) or 1 (135 degree filtering)'
  1008. assert w < l, 'w must be lower than l'
  1009. # Construct the kernel
  1010. filt = np.ones((l, l), dtype=np.float32)
  1011. filt = np.triu(filt, -w)
  1012. filt = np.tril(filt, w)
  1013. if diag == 1:
  1014. filt = np.fliplr(filt)
  1015. # Convert mat values to floats, and replaces np.infs with specified input
  1016. # values
  1017. mat = 1. * mat.copy()
  1018. # Initialize the matrix of d-largest values as a matrix of zeroes
  1019. lmat = np.zeros((d, mat.shape[0], mat.shape[1]), dtype=np.float32)
  1020. # TODO: make this on a 3D matrix to parallelize...
  1021. N_bin = mat.shape[0]
  1022. bin_range = range(N_bin - l + 1)
  1023. # Compute fmat
  1024. try: # try by stacking the different patches of each row of mat
  1025. flattened_filt = filt.flatten()
  1026. for y in bin_range:
  1027. # creates a 2D matrix of shape (N_bin-l+1, l**2), where each row
  1028. # is a flattened patch (length l**2) from the y-th row of mat
  1029. row_patches = np.zeros((len(bin_range), l ** 2))
  1030. for x in bin_range:
  1031. row_patches[x, :] = (mat[y:y + l, x:x + l]).flatten()
  1032. # take the l largest values in each row (patch) and assign them
  1033. # to the corresponding row in lmat
  1034. largest_vals = np.sort(
  1035. row_patches * flattened_filt, axis=1)[:, -d:]
  1036. lmat[:, y + (l // 2),
  1037. (l // 2): (l // 2) + N_bin - l + 1] = largest_vals.T
  1038. except MemoryError: # if too large, do it serially by for loops
  1039. for y in bin_range: # one step to the right;
  1040. for x in bin_range: # one step down
  1041. patch = mat[y: y + l, x: x + l]
  1042. mskd = np.multiply(filt, patch)
  1043. largest_vals = np.sort(d, mskd.flatten())[-d:]
  1044. lmat[:, y + (l // 2), x + (l // 2)] = largest_vals
  1045. return lmat
  1046. def joint_probability_matrix(
  1047. pmat, filter_shape, nr_largest=None, alpha=0, pvmin=1e-5):
  1048. '''
  1049. Map a probability matrix pmat to a joint probability matrix jmat, where
  1050. jmat[i, j] is the joint p-value of the largest neighbors of pmat[i, j].
  1051. The values of pmat are assumed to be uniformly distributed in the range
  1052. [alpha, 1] (alpha=0 by default). Centered a rectangular kernel of shape
  1053. filter_shape=(l, w) around each entry pmat[i, j], aligned along the
  1054. diagonal where pmat[i, j] lies into, extracts the nr_largest highest values
  1055. falling within the kernel and computes their joint p-value jmat[i, j]
  1056. (see [1]).
  1057. Parameters
  1058. ----------
  1059. pmat : ndarray
  1060. a square matrix of cumulative probability values between alpha and 1.
  1061. The values are assumed to be uniformly distibuted in the said range
  1062. filter_shape : tuple
  1063. a pair (l, w) of integers representing the kernel shape. The
  1064. nr_largest : int, optional
  1065. the number of largest neighbors to collect for each entry in mat
  1066. If None (default) the filter length l is used
  1067. Default: 0
  1068. alpha : float in [0, 1), optional
  1069. the left end of the range [alpha, 1]
  1070. Default: 0
  1071. pvmin : flaot in [0, 1), optional
  1072. minimum p-value for individual entries in pmat. Each pmat[i, j] is
  1073. set to min(pmat[i, j], 1-pvmin) to avoid that a single highly
  1074. significant value in pmat (extreme case: pmat[i, j] = 1) yield
  1075. joint significance of itself and its neighbors.
  1076. Default: 1e-5
  1077. Returns
  1078. -------
  1079. jmat : numpy.ndarray
  1080. joint probability matrix associated to pmat
  1081. References
  1082. ----------
  1083. [1] Torre et al (in prep) ...
  1084. Example
  1085. -------
  1086. # Assuming to have a list sts of parallel spike trains over 1s recording,
  1087. # the following code computes the intersection/probability/joint-prob
  1088. # matrices imat/pmat/jmat using a bin width of 5 ms
  1089. >>> T = 1 * pq.s
  1090. >>> binsize = 5 * pq.ms
  1091. >>> imat, xedges, yedges = intersection_matrix(sts, binsize=binsize, dt=T)
  1092. >>> pmat = probability_matrix_analytical(sts, binsize, dt=T)
  1093. >>> jmat = joint_probability_matrix(pmat, filter_shape=(fl, fw))
  1094. '''
  1095. # Find for each P_ij in the probability matrix its neighbors and maximize
  1096. # them by the maximum value 1-pvmin
  1097. pmat_neighb = _pmat_neighbors(
  1098. pmat, filter_shape=filter_shape, nr_largest=nr_largest, diag=0)
  1099. pmat_neighb = np.minimum(pmat_neighb, 1. - pvmin)
  1100. # Compute the joint p-value matrix jpvmat
  1101. l, w = filter_shape
  1102. n = l * (1 + 2 * w) - w * (w + 1) # number of entries covered by kernel
  1103. jpvmat = _jsf_uniform_orderstat_3d(pmat_neighb, alpha, n)
  1104. return 1. - jpvmat
  1105. def extract_sse(spiketrains, x_edges, y_edges, cmat, ids=None):
  1106. '''
  1107. Given a list of spike trains, two arrays of bin edges and a clustered
  1108. intersection matrix obtained from those spike trains via worms analysis
  1109. using the specified edges, extracts the sequences of synchronous events
  1110. (SSEs) corresponding to clustered elements in the cluster matrix.
  1111. Parameters
  1112. ----------
  1113. spiketrains : list of neo.SpikeTrain
  1114. the spike trains analyzed for repeated sequences of synchronous
  1115. events.
  1116. x_edges : quantities.Quantity
  1117. the first array of time bins used to compute cmat
  1118. y_edges : quantities.Quantity
  1119. the second array of time bins used to compute cmat. Musr have the
  1120. same length as x_array
  1121. cmat: numpy.ndarray
  1122. matrix of shape (n, n), where n is the length of x_edges and
  1123. y_edges, representing the cluster matrix in worms analysis
  1124. (see: cluster_matrix_entries())
  1125. ids : list or None, optional
  1126. a list of spike train IDs. If provided, ids[i] is the identity
  1127. of spiketrains[i]. If None, the IDs 0,1,...,n-1 are used
  1128. Default: None
  1129. Returns
  1130. -------
  1131. sse : dict
  1132. a dictionary D of SSEs, where each SSE is a sub-dictionary Dk,
  1133. k=1,...,K, where K is the max positive integer in cmat (the
  1134. total number of clusters in cmat):
  1135. .. centered:: D = {1: D1, 2: D2, ..., K: DK}
  1136. Each sub-dictionary Dk represents the k-th diagonal structure
  1137. (i.e. the k-th cluster) in cmat, and is of the form
  1138. .. centered:: Dk = {(i1, j1): S1, (i2, j2): S2, ..., (iL, jL): SL}.
  1139. The keys (i, j) represent the positions (time bin ids) of all
  1140. elements in cmat that compose the SSE, i.e. that take value l (and
  1141. therefore belong to the same cluster), and the values Sk are sets of
  1142. neuron ids representing a repeated synchronous event (i.e. spiking
  1143. at time bins i and j).
  1144. '''
  1145. nr_worms = cmat.max() # number of different clusters ("worms") in cmat
  1146. if nr_worms <= 0:
  1147. return {}
  1148. # Compute the transactions associated to the two binnings
  1149. binsize_x = x_edges[1] - x_edges[0]
  1150. t_start_x, t_stop_x = x_edges[0], x_edges[-1]
  1151. tracts_x = _transactions(
  1152. spiketrains, binsize=binsize_x, t_start=t_start_x, t_stop=t_stop_x,
  1153. ids=ids)
  1154. binsize_y = y_edges[1] - y_edges[0]
  1155. t_start_y, t_stop_y = y_edges[0], y_edges[-1]
  1156. tracts_y = _transactions(
  1157. spiketrains, binsize=binsize_y, t_start=t_start_y, t_stop=t_stop_y,
  1158. ids=ids)
  1159. # Find the reference diagonal, whose elements correspond to same time bins
  1160. diag_id, _ = _reference_diagonal(x_edges, y_edges)
  1161. # Reconstruct each worm, link by link
  1162. sse_dict = {}
  1163. for k in _xrange(1, nr_worms + 1): # for each worm
  1164. worm_k = {} # worm k is a list of links (each link will be 1 sublist)
  1165. pos_worm_k = np.array(np.where(cmat == k)).T # position of all links
  1166. # if no link lies on the reference diagonal
  1167. if all([y - x != diag_id for (x, y) in pos_worm_k]):
  1168. for l, (bin_x, bin_y) in enumerate(pos_worm_k): # for each link
  1169. link_l = set(tracts_x[bin_x]).intersection(
  1170. tracts_y[bin_y]) # reconstruct the link
  1171. worm_k[(bin_x, bin_y)] = link_l # and assign it to its pixel
  1172. sse_dict[k] = worm_k
  1173. return sse_dict
  1174. def sse_intersection(sse1, sse2, intersection='linkwise'):
  1175. '''
  1176. Given two sequences of synchronous events (SSEs) `sse1` and `sse2`, each
  1177. consisting of a pool of positions (iK, jK) of matrix entries and
  1178. associated synchronous events SK, finds the intersection among them.
  1179. The intersection can be performed 'pixelwise' or 'linkwise'.
  1180. * if 'pixelwise', it yields a new SSE which retains only events in sse1
  1181. whose pixel position matches a pixel position in sse2. This operation
  1182. is not symmetric: intersection(sse1, sse2) != intersection(sse2, sse1).
  1183. * if 'linkwise', an additional step is performed where each retained
  1184. synchronous event SK in sse1 is intersected with the corresponding
  1185. event in sse2. This yields a symmetric operation:
  1186. intersection(sse1, sse2) = intersection(sse2, sse1).
  1187. Both sse1 and sse2 must be provided as dictionaries of the type
  1188. .. centered:: {(i1, j1): S1, (i2, j2): S2, ..., (iK, jK): SK},
  1189. where each i, j is an integer and each S is a set of neuron ids.
  1190. (See also: extract_sse() that extracts SSEs from given spiketrains).
  1191. Parameters
  1192. ----------
  1193. sse1, sse2 : each a dict
  1194. each is a dictionary of pixel positions (i, j) as keys, and sets S of
  1195. synchronous events as values (see above).
  1196. intersection : str, optional
  1197. the type of intersection to perform among the two SSEs. Either
  1198. 'pixelwise' or 'linkwise' (see above).
  1199. Default: 'linkwise'.
  1200. Returns
  1201. -------
  1202. sse : dict
  1203. a new SSE (same structure as sse1 and sse2) which retains only the
  1204. events of sse1 associated to keys present both in sse1 and sse2.
  1205. If intersection = 'linkwise', such events are additionally
  1206. intersected with the associated events in sse2 (see above).
  1207. '''
  1208. sse_new = sse1.copy()
  1209. for pixel1 in sse1.keys():
  1210. if pixel1 not in sse2.keys():
  1211. del sse_new[pixel1]
  1212. if intersection == 'linkwise':
  1213. for pixel1, link1 in sse_new.items():
  1214. sse_new[pixel1] = link1.intersection(sse2[pixel1])
  1215. if len(sse_new[pixel1]) == 0:
  1216. del sse_new[pixel1]
  1217. elif intersection == 'pixelwise':
  1218. pass
  1219. else:
  1220. raise ValueError(
  1221. "intersection (=%s) can only be" % intersection +
  1222. " 'pixelwise' or 'linkwise'")
  1223. return sse_new
  1224. def sse_difference(sse1, sse2, difference='linkwise'):
  1225. '''
  1226. Given two sequences of synchronous events (SSEs) sse1 and sse2, each
  1227. consisting of a pool of pixel positions and associated synchronous events
  1228. (see below), computes the difference between sse1 and sse2.
  1229. The difference can be performed 'pixelwise' or 'linkwise':
  1230. * if 'pixelwise', it yields a new SSE which contains all (and only) the
  1231. events in sse1 whose pixel position doesn't match any pixel in sse2.
  1232. * if 'linkwise', for each pixel (i, j) in sse1 and corresponding
  1233. synchronous event S1, if (i, j) is a pixel in sse2 corresponding to the
  1234. event S2, it retains the set difference S1 - S2. If (i, j) is not a
  1235. pixel in sse2, it retains the full set S1.
  1236. Note that in either case the difference is a non-symmetric operation:
  1237. intersection(sse1, sse2) != intersection(sse2, sse1).
  1238. Both sse1 and sse2 must be provided as dictionaries of the type
  1239. .. centered:: {(i1, j1): S1, (i2, j2): S2, ..., (iK, jK): SK},
  1240. where each i, j is an integer and each S is a set of neuron ids.
  1241. (See also: extract_sse() that extracts SSEs from given spiketrains).
  1242. Parameters
  1243. ----------
  1244. sse1, sse2 : each a dict
  1245. a dictionary of pixel positions (i, j) as keys, and sets S of
  1246. synchronous events as values (see above).
  1247. difference : str, optional
  1248. the type of difference to perform between sse1 and sse2. Either
  1249. 'pixelwise' or 'linkwise' (see above).
  1250. Default: 'linkwise'.
  1251. Returns
  1252. -------
  1253. sse : dict
  1254. a new SSE (same structure as sse1 and sse2) which retains the
  1255. difference between sse1 and sse2 (see above).
  1256. '''
  1257. sse_new = sse1.copy()
  1258. for pixel1 in sse1.keys():
  1259. if pixel1 in sse2.keys():
  1260. if difference == 'pixelwise':
  1261. del sse_new[pixel1]
  1262. elif difference == 'linkwise':
  1263. sse_new[pixel1] = sse_new[pixel1].difference(sse2[pixel1])
  1264. if len(sse_new[pixel1]) == 0:
  1265. del sse_new[pixel1]
  1266. else:
  1267. raise ValueError(
  1268. "difference (=%s) can only be" % difference +
  1269. " 'pixelwise' or 'linkwise'")
  1270. return sse_new
  1271. def _remove_empty_events(sse):
  1272. '''
  1273. Given a sequence of synchronous events (SSE) sse consisting of a pool of
  1274. pixel positions and associated synchronous events (see below), returns a
  1275. copy of sse where all empty events have been removed.
  1276. sse must be provided as a dictionary of type
  1277. .. centered:: {(i1, j1): S1, (i2, j2): S2, ..., (iK, jK): SK},
  1278. where each i, j is an integer and each S is a set of neuron ids.
  1279. (See also: extract_sse() that extracts SSEs from given spiketrains).
  1280. Parameters
  1281. ----------
  1282. sse : dict
  1283. a dictionary of pixel positions (i, j) as keys, and sets S of
  1284. synchronous events as values (see above).
  1285. Returns
  1286. -------
  1287. sse_new : dict
  1288. a copy of sse where all empty events have been removed.
  1289. '''
  1290. sse_new = sse.copy()
  1291. for pixel, link in sse.items():
  1292. if link == set([]):
  1293. del sse_new[pixel]
  1294. return sse_new
  1295. def sse_isequal(sse1, sse2):
  1296. '''
  1297. Given two sequences of synchronous events (SSEs) sse1 and sse2, each
  1298. consisting of a pool of pixel positions and associated synchronous events
  1299. (see below), determines whether sse1 is strictly contained in sse2.
  1300. sse1 is strictly contained in sse2 if all its pixels are pixels of sse2,
  1301. if its associated events are subsets of the corresponding events
  1302. in sse2, and if sse2 contains events, or neuron ids in some event, which
  1303. do not belong to sse1 (i.e. sse1 and sse2 are not identical)
  1304. Both sse1 and sse2 must be provided as dictionaries of the type
  1305. .. centered:: {(i1, j1): S1, (i2, j2): S2, ..., (iK, jK): SK},
  1306. where each i, j is an integer and each S is a set of neuron ids.
  1307. (See also: extract_sse() that extracts SSEs from given spiketrains).
  1308. Parameters
  1309. ----------
  1310. sse1, sse2 : each a dict
  1311. a dictionary of pixel positions (i, j) as keys, and sets S of
  1312. synchronous events as values (see above).
  1313. Returns
  1314. -------
  1315. is_equal : bool
  1316. returns True if sse1 is identical to sse2
  1317. '''
  1318. # Remove empty links from sse11 and sse22, if any
  1319. sse11 = _remove_empty_events(sse1)
  1320. sse22 = _remove_empty_events(sse2)
  1321. # Return whether sse11 == sse22
  1322. return sse11 == sse22
  1323. def sse_isdisjoint(sse1, sse2):
  1324. '''
  1325. Given two sequences of synchronous events (SSEs) sse1 and sse2, each
  1326. consisting of a pool of pixel positions and associated synchronous events
  1327. (see below), determines whether sse1 and sse2 are disjoint.
  1328. Two SSEs are disjoint if they don't share pixels, or if the events
  1329. associated to common pixels are disjoint.
  1330. Both sse1 and sse2 must be provided as dictionaries of the type
  1331. .. centered:: {(i1, j1): S1, (i2, j2): S2, ..., (iK, jK): SK},
  1332. where each i, j is an integer and each S is a set of neuron ids.
  1333. (See also: extract_sse() that extracts SSEs from given spiketrains).
  1334. Parameters
  1335. ----------
  1336. sse1, sse2 : each a dictionary
  1337. a dictionary of pixel positions (i, j) as keys, and sets S of
  1338. synchronous events as values (see above).
  1339. Returns
  1340. -------
  1341. is_disjoint : bool
  1342. returns True if sse1 is disjoint from sse2.
  1343. '''
  1344. # Remove empty links from sse11 and sse22, if any
  1345. sse11 = _remove_empty_events(sse1)
  1346. sse22 = _remove_empty_events(sse2)
  1347. # If both SSEs are empty, return False (we consider them equal)
  1348. if sse11 == {} and sse22 == {}:
  1349. return False
  1350. common_pixels = set(sse11.keys()).intersection(set(sse22.keys()))
  1351. if common_pixels == set([]):
  1352. return True
  1353. elif all(sse11[p].isdisjoint(sse22[p]) for p in common_pixels):
  1354. return True
  1355. else:
  1356. return False
  1357. def sse_issub(sse1, sse2):
  1358. '''
  1359. Given two sequences of synchronous events (SSEs) sse1 and sse2, each
  1360. consisting of a pool of pixel positions and associated synchronous events
  1361. (see below), determines whether sse1 is strictly contained in sse2.
  1362. sse1 is strictly contained in sse2 if all its pixels are pixels of sse2,
  1363. if its associated events are subsets of the corresponding events
  1364. in sse2, and if sse2 contains non-empty events, or neuron ids in some
  1365. event, which do not belong to sse1 (i.e. sse1 and sse2 are not identical)
  1366. Both sse1 and sse2 must be provided as dictionaries of the type
  1367. {(i1, j1): S1, (i2, j2): S2, ..., (iK, jK): SK},
  1368. where each i, j is an integer and each S is a set of neuron ids.
  1369. (See also: extract_sse() that extracts SSEs from given spiketrains).
  1370. Parameters
  1371. ----------
  1372. sse1, sse2 : each a dict
  1373. a dictionary of pixel positions (i, j) as keys, and sets S of
  1374. synchronous events as values (see above).
  1375. Returns
  1376. -------
  1377. is_sub : bool
  1378. returns True if sse1 is a subset of sse2
  1379. '''
  1380. # Remove empty links from sse11 and sse22, if any
  1381. sse11 = _remove_empty_events(sse1)
  1382. sse22 = _remove_empty_events(sse2)
  1383. # Return False if sse11 and sse22 are disjoint
  1384. if sse_isdisjoint(sse11, sse22):
  1385. return False
  1386. # Return False if any pixel in sse1 is not contained in sse2, or if any
  1387. # link of sse1 is not a subset of the corresponding link in sse2.
  1388. # Otherwise (if sse1 is a subset of sse2) continue
  1389. for pixel1, link1 in sse11.items():
  1390. if pixel1 not in sse22.keys():
  1391. return False
  1392. elif not link1.issubset(sse22[pixel1]):
  1393. return False
  1394. # Check that sse1 is a STRICT subset of sse2, i.e. that sse2 contains at
  1395. # least one pixel or neuron id not present in sse1.
  1396. return not sse_isequal(sse11, sse22)
  1397. def sse_issuper(sse1, sse2):
  1398. '''
  1399. Given two sequences of synchronous events (SSEs) sse1 and sse2, each
  1400. consisting of a pool of pixel positions and associated synchronous events
  1401. (see below), determines whether sse1 strictly contains sse2.
  1402. sse1 strictly contains sse2 if it contains all pixels of sse2, if all
  1403. associated events in sse1 contain those in sse2, and if sse1 additionally
  1404. contains other pixels / events not contained in sse2.
  1405. Both sse1 and sse2 must be provided as dictionaries of the type
  1406. {(i1, j1): S1, (i2, j2): S2, ..., (iK, jK): SK},
  1407. where each i, j is an integer and each S is a set of neuron ids.
  1408. (See also: extract_sse() that extracts SSEs from given spiketrains).
  1409. Note: sse_issuper(sse1, sse2) is identical to sse_issub(sse2, sse1).
  1410. Parameters
  1411. ----------
  1412. sse1, sse2 : each a dict
  1413. a dictionary of pixel positions (i, j) as keys, and sets S of
  1414. synchronous events as values (see above).
  1415. Returns
  1416. -------
  1417. is_super : bool
  1418. returns True if sse1 strictly contains sse2.
  1419. '''
  1420. return sse_issub(sse2, sse1)
  1421. def sse_overlap(sse1, sse2):
  1422. '''
  1423. Given two sequences of synchronous events (SSEs) sse1 and sse2, each
  1424. consisting of a pool of pixel positions and associated synchronous events
  1425. (see below), determines whether sse1 strictly contains sse2.
  1426. sse1 strictly contains sse2 if it contains all pixels of sse2, if all
  1427. associated events in sse1 contain those in sse2, and if sse1 additionally
  1428. contains other pixels / events not contained in sse2.
  1429. Both sse1 and sse2 must be provided as dictionaries of the type
  1430. {(i1, j1): S1, (i2, j2): S2, ..., (iK, jK): SK},
  1431. where each i, j is an integer and each S is a set of neuron ids.
  1432. (See also: extract_sse() that extracts SSEs from given spiketrains).
  1433. Note: sse_issuper(sse1, sse2) is identical to sse_issub(sse2, sse1).
  1434. Parameters
  1435. ----------
  1436. sse1, sse2 : each a dict
  1437. a dictionary of pixel positions (i, j) as keys, and sets S of
  1438. synchronous events as values (see above).
  1439. Returns
  1440. -------
  1441. is_super : bool
  1442. returns True if sse1 strictly contains sse2.
  1443. '''
  1444. return not (sse_issub(sse1, sse2) or sse_issuper(sse1, sse2) or
  1445. sse_isequal(sse1, sse2) or sse_isdisjoint(sse1, sse2))