OSCR

Low-dimensional population dynamics in the brainstem gate REM sleep.

Code ↔ Paper

17 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 17 matches
  1. [1] § Methods › Identification of laser-modulated units ↔ PySleep/sleepy.py, lines 2946–3001 · score 0.71 · interval preceding laser, laser pulse, laser stimulation, artifacts, train, onset
  2. [2] § Methods › Identification of laser-modulated units ↔ PySpike/spyke.py, lines 722–771 · score 0.67 · spontaneous spikes, laser pulse, spike waveforms, correlation, mice
  3. [3] § Methods › Dimensionality reduction using PCA ↔ neuropyx.py, lines 1610–1681 · score 0.66 · excluded long wake, wake episodes, longer, PCA, Firing rates, matrix
  4. [4] § Results › Stereotypic NREM→REM transitions in state space ↔ neuropyx.py, lines 5096–5160 · score 0.64 · REM offset, state space, REM onset, ellipse, subspaces, trajectory
  5. [5] § Methods › Definition of subspaces ↔ neuropyx.py, lines 5546–5677 · score 0.63 · ellipses capturing, state space, eigenvalue, subspaces, eigenvector, angles
  6. [6] § Methods › Cross-correlation between single units, σ power and PC2 ↔ Photometry/pyphi.py, lines 1332–1481 · score 0.63 · temporal resolution, cross correlation, EEG spectrogram, lags, consecutive, overlap
  7. [7] § Methods › Cross-correlation between single units, σ power and PC2 ↔ PySpike/spyke.py, lines 5273–5328 · score 0.61 · temporal resolution, cross correlation, EEG spectrogram, consecutive, overlap, spike
  8. [8] § Methods › Neuron activity during infraslow cycles and inter-REM ↔ Photometry/pyphi.py, lines 3580–3688 · score 0.58 · fast Fourier transforms, band, microarousals, windows, signal, box
  9. [9] § Methods › Spike sorting ↔ neuropyx.py, lines 750–825 · score 0.57 · Gaussian kernel, standard deviation, encoded, firing rates, smoothed, binned
  10. [10] § Results › Low-dimensional population activity in midbrain and pons during sleep ↔ Photometry/pyphi.py, lines 1587–1645 · score 0.56 · confidence intervals, Bonferroni correction, EMG amplitude, EEG spectrogram, denote, error
  11. [11] § Methods › Cross-correlation between single units, σ power and PC2 ↔ neuropyx.py, lines 750–825 · score 0.55 · Gaussian kernel, standard deviation, firing rates, episode, smoothed, bins
  12. [12] § Methods › Habituation to head fixation and polysomnographic recordings ↔ Photometry/pyphi.py, lines 3580–3688 · score 0.55 · fast Fourier transforms, Brain states, REM sleep, window, signals, scored
  13. [13] § Methods › Functional connectivity analysis ↔ PySpike/spyke.py, lines 5273–5328 · score 0.53 · negative peak, cross correlated, resolution, consecutive, Spike, windows
  14. [14] § Methods › Analysis of coupling frequency ↔ Photometry/pyphi.py, lines 1587–1645 · score 0.53 · mouse identity, Bonferroni corrected, CI
  15. [15] § Methods › Electrode tract reconstruction ↔ basic_analysis_howto.ipynb, lines 69–106 · score 0.53 · MB, PRNc, PRNr, MRN, CS, RPO
  16. [16] § Methods › Spike sorting ↔ PySpike/spyke.py, lines 1657–1770 · score 0.51 · spike trains binned, Clusters, encoded, waveform, downsampled
  17. [17] § Methods › Electrophysiological recordings ↔ PySleep/sleepy.py, lines 2946–3001 · score 0.51 · pulse trains, laser stimulation, REM sleep, interval, wake, mice

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 5,681 lines · 195 KB · MIT · 5 matches

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. neuropyx.py
  5. """
  6. import sys
  7. sys.path.append('/Users/tortugar/My Drive/Penn/Programming/PySleep')
  8. import sleepy
  9. import numpy as np
  10. import scipy
  11. import pandas as pd
  12. import matplotlib.pyplot as plt
  13. import matplotlib.patches as patches
  14. import os
  15. import csv
  16. import pingouin as pg
  17. import seaborn as sns
  18. import scipy.io as so
  19. import re
  20. import scipy.stats as stats
  21. from scipy import linalg
  22. import matplotlib as mpl
  23. from sklearn.decomposition import PCA
  24. import math
  25. import h5py
  26. import shutil
  27. # debugger
  28. import pdb
  29. def brstate_class(np_path, sleep_path, sleep_rec, mouse, tend=-1, tstart=0, pzscore=True,
  30. class_mode='', pearson=True, pnorm_spec=True, single_mice=True,
  31. ma_thr=10, ma_rem_exception=False, box_filt=[],
  32. pplot=True, config_file='mouse_config.txt'):
  33. """
  34. calculate average firing rate during each brain state and then
  35. perform statistics for units to classify them into REM-max, Wake-max, or NREM-max.
  36. For each ROI anova is performed, followed by Tukey-test
  37. :param np_path: folder where firing rates are located
  38. :param sleep_path: folder where EEG data and sleep annotation is located
  39. :param sleep_rec: name of sleep recording
  40. Note: easiest way to get np_path, sleep_path, and sleep_rec:
  41. paths = neuropyx.load_config(config_file)[mouse]
  42. ppath, name = os.path.split(paths['SL_PATH'])
  43. np_path = paths['NP_PATH']
  44. :param mouse: mouse name
  45. :param pzscore: if True, z-score DF/F traces
  46. :param class_mode: class_mode == 'basic': classify ROIs into
  47. REM-max, Wake-max and NREM-max ROIs
  48. class_mode == 'rem': further separate REM-max ROIs
  49. into REM > Wake > NREM (R>W>N) and REM > NREM > Wake (R>N>W) ROIs
  50. REM-max neurons where Wake and NREM is not significantly different
  51. are classified as 'R>N=W'
  52. Neurons that are signficantly modulated by the brain state
  53. but not part of any of these different classes are labeled Z
  54. Neurons that are not significantly modulated by the brain state
  55. are labeled X
  56. NOTE: A given unit can only be part of one subclass.
  57. R-Off comes before W-max.
  58. :param single_mice: boolean, if True use separate colors for single mice in
  59. summary plots
  60. :return df_class: pd.DataFrame
  61. with columns
  62. ['ID', 'R', 'W', 'N', 'F-anova', 'P-anova', 'P-tukey', 'Type',
  63. 'Depth', 'Quality', 'mouse', 'brain_region']
  64. :rem df_pearson: pd.DataFrame
  65. with columns
  66. ['unit', 'r', 'p', 'sig', 'state','depth','Type','Quality']
  67. """
  68. units, cell_info, M, kcut = load_mouse(mouse, config_file)
  69. # import file that has information on depth
  70. with open(os.path.join(np_path,'cluster_info.TSV')) as inf:
  71. reader = csv.reader(inf, delimiter="\t")
  72. cell_info=list(reader)
  73. cell_list=[]
  74. for x in cell_info[1:]:
  75. cell_list.append([x[0],x[3],x[6]])
  76. # import sleep annotation file and also do MA functions if chosen
  77. sr = sleepy.get_snr(sleep_path,sleep_rec)
  78. nbin = int(np.round(sr)*2.5)
  79. sdt = nbin * (1.0/sr)
  80. # flatten out MAs #########################################################
  81. if ma_thr>0:
  82. seq = sleepy.get_sequences(np.where(M==2)[0])
  83. for s in seq:
  84. if np.round(len(s)*sdt) <= ma_thr:
  85. if ma_rem_exception:
  86. if (s[0]>1) and (M[s[0] - 1] != 1):
  87. M[s] = 3
  88. else:
  89. M[s] = 3
  90. ###########################################################################
  91. # cut out kcuts: ###############
  92. #print('Applying kcut')
  93. tidx = kcut_idx(M, units, kcut)
  94. if tidx[-1] >= units.shape[0]:
  95. tidx = tidx[0:-1]
  96. M = M[tidx]
  97. units = units.iloc[tidx,:]
  98. ################################
  99. istart = int(np.round(tstart/sdt))
  100. if tend == -1:
  101. iend = M.shape[0]
  102. else:
  103. iend = int(np.round(tend/sdt))
  104. if iend >= len(units):
  105. M = M[istart:len(units)]
  106. else:
  107. M = M[istart:iend]
  108. # calculate sigma power
  109. if pearson:
  110. band=[10,15]
  111. state_map = {1:'REM', 2:'Wake', 3:'NREM'}
  112. ddir = os.path.join(sleep_path, sleep_rec)
  113. P = so.loadmat(os.path.join(ddir, 'sp_%s.mat' % sleep_rec), squeeze_me=True)
  114. SP = P['SP']
  115. freq = P['freq']
  116. ifreq = np.where((freq >= band[0]) & (freq <= band[1]))[0]
  117. df = freq[1] - freq[0]
  118. if len(box_filt) > 0:
  119. filt = np.ones(box_filt)
  120. filt = np.divide(filt, filt.sum())
  121. SP = scipy.signal.convolve2d(SP, filt, boundary='symm', mode='same')
  122. if pnorm_spec:
  123. sp_mean = SP.mean(axis=1)
  124. SP = np.divide(SP, np.tile(sp_mean, (SP.shape[1], 1)).T)
  125. pow_band = SP[ifreq, :].mean(axis=0)
  126. else:
  127. pow_band = SP[ifreq,:].sum(axis=0)*df
  128. pow_band = pow_band[istart:iend]
  129. # make a nested dict that has each of the units and inside the fr values for all the different sleep bins
  130. units_stateval = {}
  131. for unit in units:
  132. units_stateval[unit] = {1:[], 2:[], 3:[],'depth':[]}
  133. for unit in units:
  134. depth_idx=units.columns.get_loc(unit)
  135. if pzscore:
  136. values = np.array((units[unit] - units[unit].mean())/units[unit].std(ddof=0))
  137. else:
  138. values=np.array(units[unit])
  139. for state in [1,2,3]:
  140. seq = np.where(M==state)[0]
  141. units_stateval[unit][state]=values[seq].tolist()
  142. units_stateval[unit]['depth']=cell_list[depth_idx][2]
  143. columns = ['ID', 'R', 'W', 'N', 'F-anova', 'P-anova', 'P-tukey', 'Type','Depth','Quality']
  144. data = []
  145. data_p = []
  146. for unit in units_stateval:
  147. stateval = units_stateval[unit]
  148. val = np.concatenate([stateval[1], stateval[2], stateval[3]],axis=0)
  149. state = ['R']*len(stateval[1]) + ['W']*len(stateval[2]) + ['N']*len(stateval[3])
  150. depth=float(units_stateval[unit]['depth'])
  151. if 'good' in unit:
  152. unit_quality='good'
  153. elif 'noise' in unit:
  154. continue
  155. else:
  156. unit_quality='mua'
  157. continue
  158. d = {'state':state, 'val':val}
  159. df = pd.DataFrame(d)
  160. res = pg.anova(data=df, dv='val', between='state')
  161. try:
  162. res2 = pg.pairwise_tukey(data=df, dv='val', between='state')
  163. except:
  164. print("Unit %s did not have enough data points for tukey test" % unit)
  165. print(df)
  166. def _get_mean(s):
  167. return df[df['state']==s]['val'].mean()
  168. rmean = _get_mean('R')
  169. wmean = _get_mean('W')
  170. nmean = _get_mean('N')
  171. if class_mode == 'basic':
  172. roi_type = 'X'
  173. # REM-max
  174. if (rmean > wmean) and (rmean > nmean):
  175. cond1 = res2[(res2['A'] == 'N') & (res2['B'] == 'R')]
  176. cond2 = res2[(res2['A'] == 'R') & (res2['B'] == 'W')]
  177. if cond1['p-tukey'].iloc[0] < 0.05 and cond2['p-tukey'].iloc[0] < 0.05 and res['p-unc'].iloc[0] < 0.05:
  178. roi_type = 'R-max'
  179. #REM-Off (R-Off)
  180. elif (rmean < wmean) and (rmean < nmean):
  181. cond1 = res2[(res2['A'] == 'R') & (res2['B'] == 'W')]
  182. cond2 = res2[(res2['A'] == 'R') & (res2['B'] == 'N')]
  183. if cond1['p-tukey'].iloc[0] < 0.05 and cond2['p-tukey'].iloc[0] < 0.05 and res['p-unc'].iloc[0] < 0.05:
  184. roi_type = 'R-Off'
  185. # W-max
  186. elif (wmean > nmean) and (wmean > rmean):
  187. cond1 = res2[(res2['A'] == 'N') & (res2['B'] == 'W')]
  188. cond2 = res2[(res2['A'] == 'R') & (res2['B'] == 'W')]
  189. if cond1['p-tukey'].iloc[0] < 0.05 and cond2['p-tukey'].iloc[0] < 0.05 and res['p-unc'].iloc[0] < 0.05:
  190. roi_type = 'W-max'
  191. # N-max
  192. elif (nmean > wmean) and (nmean > rmean):
  193. cond1 = res2[(res2['A'] == 'N') & (res2['B'] == 'W')]
  194. cond2 = res2[(res2['A'] == 'N') & (res2['B'] == 'R')]
  195. if cond1['p-tukey'].iloc[0] < 0.05 and cond2['p-tukey'].iloc[0] < 0.05 and res['p-unc'].iloc[0] < 0.05:
  196. roi_type = 'N-max'
  197. else:
  198. roi_type = 'X'
  199. tmp = [unit, rmean, wmean, nmean, res.F.iloc[0], res['p-unc'].iloc[0], res2['p-tukey'].iloc[0], roi_type,depth,unit_quality]
  200. data.append(tmp)
  201. # REM mode:
  202. else:
  203. roi_type = 'X'
  204. if res['p-unc'].iloc[0] < 0.05:
  205. p_nr = res2[(res2['A'] == 'N') & (res2['B'] == 'R')]['p-tukey'].iloc[0]
  206. p_rw = res2[(res2['A'] == 'R') & (res2['B'] == 'W')]['p-tukey'].iloc[0]
  207. p_nw = res2[(res2['A'] == 'N') & (res2['B'] == 'W')]['p-tukey'].iloc[0]
  208. # R>W>N
  209. if (rmean > wmean) and (rmean > nmean) and (wmean > nmean) and p_nr < 0.05 and p_rw<0.05 and p_nw < 0.05:
  210. roi_type = 'R>W>N'
  211. # R>N>W
  212. elif (rmean > wmean) and (rmean > nmean) and (nmean > wmean) and p_nr < 0.05 and p_rw<0.05 and p_nw < 0.05:
  213. roi_type = 'R>N>W'
  214. # NEW:R>N=W #####################################################
  215. # The remaining REM-max units: R>N and R>W, but N and W are not significantly different
  216. # I'm calling these units R>N=W
  217. elif (rmean > wmean) and (rmean > nmean) and p_nr < 0.05 and p_rw<0.05 and p_nw >= 0.05:
  218. roi_type = 'R>N=W'
  219. # END[NEW:R>N=W] ###############################################
  220. # Rem-off
  221. elif (rmean < wmean) and (rmean < nmean) and p_nr < 0.05 and p_rw<0.05:
  222. roi_type = 'R-Off'
  223. # W-max
  224. elif (wmean > nmean) and (wmean > rmean) and p_nw < 0.05 and p_rw < 0.05:
  225. roi_type = 'W-max'
  226. # N-max
  227. elif (nmean > wmean) and (nmean > rmean) and p_nw < 0.05 and p_nr < 0.05:
  228. roi_type = 'N-max'
  229. # NEW:Z ######################################################
  230. # significantly modulated by brainstate (according to ANOVA),
  231. # but not part of any of these subclasses
  232. else:
  233. roi_type = 'Z'
  234. else:
  235. roi_type = 'X'
  236. tmp = [unit, rmean, wmean, nmean, res.F.iloc[0], res['p-unc'].iloc[0], res2['p-tukey'].iloc[0], roi_type,depth,unit_quality]
  237. data.append(tmp)
  238. if pearson:
  239. for s in [1,2,3]:
  240. idx = np.where(M==s)[0]
  241. r,p = scipy.stats.pearsonr(np.array(units[unit])[idx], pow_band[idx])
  242. if p < 0.05:
  243. sig = 'yes'
  244. else:
  245. sig = 'no'
  246. pearson_temp=[unit, r, p, sig, state_map[s],depth,roi_type,unit_quality]
  247. data_p.append(pearson_temp)
  248. df_class = pd.DataFrame(data, columns=columns)
  249. # NEW 3/11/26: replaced 'unit' with 'ID'
  250. df_pearson = pd.DataFrame(data_p,columns=['ID', 'r', 'p', 'sig', 'state','depth','Type','Quality'])
  251. if pplot:
  252. # plotting for unit type
  253. mice = [mouse]
  254. j = 0
  255. mdict = {}
  256. for m in mice:
  257. mdict[m] = j
  258. j+=1
  259. clrs = sns.color_palette("husl", len(mice))
  260. types = df_class['Type'].unique()
  261. types = [i for i in types if not (i=='X')]
  262. types.sort()
  263. j = 1
  264. plt.figure()
  265. for typ in types:
  266. mouse_shown = {m:0 for m in mice}
  267. plt.subplot(int('1%d%d' % (len(types), j)))
  268. df = df_class[df_class['Type']==typ][['R', 'N', 'W']]
  269. sns.barplot(data=df[['R', 'N', 'W']], color='gray')
  270. for index, row in df.iterrows():
  271. if single_mice:
  272. if mouse_shown[m] > 0:
  273. plt.plot(['R', 'N', 'W'], row[['R', 'N', 'W']], color=clrs[mdict[m]])
  274. else:
  275. plt.plot(['R', 'N', 'W'], row[['R', 'N', 'W']], color=clrs[mdict[m]], label=m)
  276. mouse_shown[m] += 1
  277. else:
  278. plt.plot(['R', 'N', 'W'], row[['R', 'N', 'W']], color='black')
  279. sns.despine()
  280. plt.title(typ)
  281. plt.legend()
  282. if j == 1:
  283. if not pzscore:
  284. plt.ylabel('DF/F (%)')
  285. else:
  286. plt.ylabel('Firing Rate (z-scored)')
  287. j += 1
  288. # plot swarm plot with depth vs type
  289. df_class_good=df_class.loc[df_class['Quality']=='good']
  290. plt.figure()
  291. plt.title('Unit type sorted by depth ')
  292. sns.swarmplot(data=df_class_good, x='Type', y='Depth',palette="husl")
  293. if pearson:
  294. #plotting pearson plot
  295. test_df=df_pearson.loc[df_pearson['sig']=='yes']
  296. test_df=test_df.loc[test_df['Quality']=='good']
  297. plt.figure()
  298. sns.swarmplot(data=test_df, x='Type', y='r', hue='state',palette="husl")
  299. plt.title('correllation of firing rate to'+ ' '+ 'eeg band:'+ str(band))
  300. plt.figure()
  301. sns.swarmplot(data=test_df, x='Type', y='depth', hue='state',palette="husl")
  302. plt.title('correllation of firing rate to'+ ' '+ 'eeg band:'+ str(band))
  303. return df_class, df_pearson
  304. def load_config(config):
  305. """
  306. Load a config file specifying the file locations for each mouse of
  307. the sleep recording and the neuropixel recording;
  308. Syntax:
  309. MOUSE: Mouse_name
  310. SL_PATH: Sleep_recording_folder
  311. NP_PATH: Neuropixels_data_folder
  312. MOUSE: Mouse_name
  313. SL_PATH: Sleep_recording_folder
  314. NP_PATH: Neuropixels_data_folder
  315. KCUT: t0-t1;t2-t3
  316. NO_REGION: A-B-C
  317. EXCLUDE: unit_ID1,unit_ID2,...,unit_ID2
  318. NOTE:
  319. Different mice are separated by new lines
  320. After a newline the first entry must be MOUSE.
  321. KCUT: is optional and allows to specify a time frame to be discarded in the
  322. recording.
  323. NO_REGION: brain_regions to exclude
  324. Parameters
  325. ----------
  326. config : str
  327. text file (including path)
  328. Returns
  329. -------
  330. recordings : dict
  331. dictionary: mouse_ID --> SL_PATH, NP_PATH
  332. """
  333. fid = open(config, 'r')
  334. lines = fid.readlines()
  335. mouse = 'X'
  336. recordings = dict()
  337. newline = True
  338. for l in lines:
  339. if re.match(r'^\s*#', l):
  340. continue
  341. if len(l)==0 or re.match(r'^\s*$', l):
  342. newline = True
  343. continue
  344. a = re.split(r'\s+', l)
  345. # cut away the ':' or ' :'
  346. field = re.split(r'\s*:', a[0])[0]
  347. value = a[1]
  348. if not newline:
  349. recordings[mouse][field] = value
  350. else: # newline == True
  351. mouse = a[1]
  352. if not mouse in recordings:
  353. recordings[mouse] = {}
  354. newline = False
  355. for m in recordings:
  356. if 'KCUT' in recordings[m]:
  357. a = recordings[m]['KCUT']
  358. a = re.split(';', a)
  359. kcuts = []
  360. for b in a:
  361. c = re.split('-', b)
  362. c = [s.strip() for s in c]
  363. k1 = float(c[0])
  364. if re.match(r'[\d\.]+', c[1]):
  365. k2 = float(c[1])
  366. else:
  367. k2 = c[1]
  368. kcuts.append( [k1, k2] )
  369. recordings[m]['KCUT'] = kcuts
  370. for m in recordings:
  371. if 'EXCLUDE' in recordings[m]:
  372. a = recordings[m]['EXCLUDE']
  373. k = re.split(r',\s*', a)
  374. recordings[m]['EXCLUDE'] = k
  375. return recordings
  376. def load_mouse(mouse_id, config_file):
  377. """
  378. Load neuropixels recordings as described in mouse_config.txt
  379. Parameters
  380. ----------
  381. mouse_id : TYPE
  382. DESCRIPTION.
  383. config_file : TYPE
  384. DESCRIPTION.
  385. Returns
  386. -------
  387. units : pd.DataFrame
  388. unit DataFrame: The columns correspond to the firing rates;
  389. the column name is the ID of the unit, the rows correspond to single time points.
  390. cell_info : pd.DataFrame
  391. DESCRIPTION.
  392. """
  393. dt = 2.5
  394. recs = load_config(config_file)
  395. sl_path = recs[mouse_id]['SL_PATH']
  396. (sl_path, sl_name) = os.path.split(sl_path)
  397. np_path = recs[mouse_id]['NP_PATH']
  398. # Get sleep annotation:
  399. M = sleepy.load_stateidx(sl_path, sl_name)[0]
  400. # load kcuts, i.e. regions at beginning or end to discard from the recording --
  401. if 'KCUT' in recs[mouse_id]:
  402. kcut = recs[mouse_id]['KCUT']
  403. for k in kcut:
  404. if k[1] == '$':
  405. k[1] = len(M)*dt
  406. else:
  407. kcut = ()
  408. traind_file = ''
  409. if os.path.isfile(os.path.join(np_path, 'traind.csv')):
  410. traind_file = 'traind.csv'
  411. elif os.path.isfile(os.path.join(np_path, 'spike_train.csv')):
  412. traind_file = 'spike_train.csv'
  413. else:
  414. traind_file = 'traind_lfp.csv'
  415. units = pd.read_csv(os.path.join(np_path, traind_file))
  416. if os.path.isfile(os.path.join(np_path,'channel_locations.json')):
  417. regions=pd.read_json(os.path.join(np_path,'channel_locations.json')).T
  418. regions=regions.iloc[0:-1]
  419. regions['ch']=regions.index.str.split('_').str[-1].astype('int64')
  420. cell_info = pd.read_csv(os.path.join(np_path,'cluster_info.TSV'),delimiter="\t")
  421. cell_info['group']=cell_info['group'].fillna(cell_info['KSLabel'])
  422. cl_id = ''
  423. if cell_info.columns.isin(['cluster_id']).sum():
  424. cl_id = 'cluster_id'
  425. else:
  426. cl_id = 'id'
  427. cell_info['ID'] = cell_info[cl_id].astype(str) +'_'+cell_info['group'].astype(str)
  428. if os.path.isfile(os.path.join(np_path,'channel_locations.json')):
  429. cell_info=cell_info.merge(regions,on='ch')
  430. # NEW 07/15/23
  431. if len(M) > units.shape[0]:
  432. M = M[0:units.shape[0]]
  433. return units, cell_info, M, kcut
  434. def exclude_units(units, mouse, config_file):
  435. """
  436. Exclude units listed in mouse_config.txt under 'EXCLUDE:'
  437. Parameters
  438. ----------
  439. units : pd.pandas
  440. each colums hold the firing rate of a unit.
  441. mouse : string
  442. the mouse name.
  443. config_file : string
  444. string of the name of the config file as loaded by &load_config().
  445. Returns
  446. -------
  447. None.
  448. """
  449. paths = load_config(config_file)[mouse]
  450. if 'EXCLUDE' in paths:
  451. ex_units = paths['EXCLUDE']
  452. units.drop(columns=ex_units, inplace=True)
  453. def fr_corr_state(units, M, idx1=[], idx2=[], win=60, state=3, ma_thr=10, mode='cross', pzscore=True, pplot=True, dt=2.5):
  454. """
  455. Perform cross-correlation between firing rates for a given brain state.
  456. Calculate the correlation for each pair of the provided neurons unitIDs (in idx1 and idx2)
  457. Parameters
  458. ----------
  459. units : pd.DataFrame
  460. Each column corresponds to one unit. The column name is the unitID.
  461. So to get all unit ID, get all column names
  462. M : np.array
  463. hypnogram.
  464. idx1 : list, optional
  465. List of unitIDs for neuron1. If empty, use all neurons (unitIDs)
  466. The default is [].
  467. idx2 : list, optional
  468. List of unitIDs for neuron1. If empty, use all neurons (unitIDs)
  469. The default is [].
  470. win : float, optional
  471. DESCRIPTION. The default is 120.
  472. state : TYPE, optional
  473. DESCRIPTION. The default is 3.
  474. ma_thr : TYPE, optional
  475. DESCRIPTION. The default is 10.
  476. mode : TYPE, optional
  477. DESCRIPTION. The default is 'cross'.
  478. pzscore : TYPE, optional
  479. DESCRIPTION. The default is True.
  480. pplot : TYPE, optional
  481. DESCRIPTION. The default is True.
  482. dt : TYPE, optional
  483. DESCRIPTION. The default is 2.5.
  484. Returns
  485. -------
  486. df : TYPE
  487. DESCRIPTION.
  488. """
  489. # If no IDs are provided for unit1, use all units as "unit1"
  490. if len(idx1) == 0:
  491. idx1 = units.columns.unique()
  492. # If no IDs are provided for unit2, use all units as "unit2"
  493. if len(idx2) == 0:
  494. idx2 = units.columns.unique()
  495. # get all firing rates and cast them to np.array
  496. arr1 = units[idx1]
  497. arr2 = units[idx2]
  498. fr1 = np.array(arr1)
  499. fr2 = np.array(arr2)
  500. n1 = fr1.shape[1]
  501. n2 = fr2.shape[1]
  502. # z-scoring
  503. if pzscore:
  504. for i in range(n1):
  505. fr1[:,i] = (fr1[:,i]-fr1[:,i].mean()) / fr1[:,i].std()
  506. for i in range(n2):
  507. fr2[:,i] = (fr2[:,i]-fr2[:,i].mean()) / fr2[:,i].std()
  508. iwin = int(win/dt)
  509. t = np.arange(-iwin, iwin+1) * dt
  510. seq = sleepy.get_sequences(np.where(M==2)[0])
  511. if ma_thr > 0:
  512. seq = sleepy.get_sequences(np.where(M == 2)[0])
  513. for s in seq:
  514. if len(s) * dt < ma_thr:
  515. M[s] = 3
  516. seq = sleepy.get_sequences(np.where(M==state)[0])
  517. seq = [s for s in seq if len(s)*dt >= 2*win]
  518. data = []
  519. for i in range(n1):
  520. for j in range(n2):
  521. CC = []
  522. for s in seq:
  523. m = len(s)
  524. fr1_cut = fr1[s,i]
  525. fr2_cut = fr2[s,j]
  526. fr1_cut = fr1_cut - fr1_cut.mean()
  527. fr2_cut = fr2_cut - fr2_cut.mean()
  528. norm = np.nanstd(fr1_cut) * np.nanstd(fr2_cut)
  529. # for used normalization, see: https://en.wikipedia.org/wiki/Cross-correlation
  530. if norm > 0:
  531. xx = (1/m) * scipy.signal.correlate(fr1_cut, fr2_cut) / norm
  532. ii = np.arange(len(xx) / 2 - iwin, len(xx) / 2 + iwin + 1)
  533. ii = [int(i) for i in ii]
  534. ii = np.concatenate((np.arange(m-iwin-1, m), np.arange(m, m+iwin, dtype='int')))
  535. # note: point ii[iwin] is the "0", so xx[ii[iwin]] corresponds to the 0-lag correlation point
  536. CC.append(xx[ii])
  537. if len(CC) > 0:
  538. CC = np.array(CC).mean(axis=0)
  539. #pdb.set_trace()
  540. m = CC.shape[0]
  541. un1 = list(idx1)[i]
  542. un2 = list(idx2)[j]
  543. label = r'%s~%s' % (un1, un2)
  544. data += zip(t, CC, [un1]*m, [un2]*m, [label]*m)
  545. df = pd.DataFrame(data=data, columns=['time', 'cc', 'unit1', 'unit2', 'label'])
  546. return df
  547. def sort_xcorr_byregion(type1, type2, corr_frame, unit_info):
  548. """
  549. Go through all brain regions contained in DataFrame unit_info['brain_region'],
  550. For each pair of brain regions take all neurons of type1 in area A and all neurons
  551. of type2 in area B and determine the average cross-correlation for this type/area pair.
  552. Parameters
  553. ----------
  554. type1 : TYPE
  555. type2 : TYPE
  556. DESCRIPTION.
  557. corr_frame : pd.DataFrame with columns ['time', 'cc', 'unit1', 'unit2', 'label']
  558. DataFrame as returned by the function neuropyx.fr_corr_state()
  559. The DataFrame contains for pairs of neurons (indicated by the IDs in columns 'unit1' and 'unit2')
  560. the cross-correlation
  561. unit_info : pd.DataFrame
  562. Necessary columns: 'ID' (IDs of units), 'brain_region' (brain region of unit 'ID'),
  563. 'Type' (neuron subclass as computed by neuropyx.brstate_fr())
  564. Returns
  565. -------
  566. None.
  567. """
  568. regions = unit_info.brain_region.unique()
  569. nregions = len(regions)
  570. data = []
  571. for i in range(nregions):
  572. for j in range(0, nregions):
  573. region1 = regions[i]
  574. region2 = regions[j]
  575. ids1 = unit_info[(unit_info['brain_region']==region1) & (unit_info['Type']==type1)]['ID']
  576. ids2 = unit_info[(unit_info['brain_region']==region2) & (unit_info['Type']==type2)]['ID']
  577. print(ids2)
  578. if len(ids1) > 0 and len(ids2) > 0:
  579. df = corr_frame[(corr_frame.unit1.isin(ids1)) & (corr_frame.unit2.isin(ids2))]
  580. dfm = df.groupby(['time']).mean()
  581. time = np.array(dfm.index)
  582. cc = dfm['cc']
  583. m = len(cc)
  584. label = '%s~%s' % (region1, region2)
  585. data += zip(time, cc, [region1]*m, [region2]*m, [label]*m)
  586. df = pd.DataFrame(data=data, columns=['time', 'cc', 'region1', 'region2', 'label'])
  587. return df
  588. def fr_transitions(units, M, unit_info, transitions, pre, post, si_threshold, sj_threshold,
  589. ma_thr=10, ma_rem_exception=False, sdt=2.5, pzscore=True, sf=0, ma_mode=False,
  590. attributes=[], kcuts=[],
  591. pspec=False, fmax=20, spe_filt=[], mouse='', config_file='mouse_config.txt'):
  592. """
  593. Note: If you like to also calculate the average EEG specotrogram for each transition and unit,
  594. set $psec=True, set the $mouse name and select the right $config_file
  595. Parameters
  596. ----------
  597. units : pd.DataFrame
  598. Each columns, one unit.
  599. M : np.array
  600. hypnogram; 1 - REM, 2 - Wake, 3 - NREM.
  601. unit_info : pd.DataFrame
  602. Additional unit information, such as brain_region. Units are referenced
  603. using the same IDs as in @units
  604. transitions : list of tuples (2 element lists)
  605. Specific the transitions to be calculated.
  606. REM - 1, Wake - 2, NREM - 3, MA - 4;
  607. So a NREM to REM transition is specified as [3,1]
  608. pre : float
  609. time before transition.
  610. post : float
  611. time after transition.
  612. si_threshold : list with 3 floats
  613. For example, if we're looking at a NREM to REM transition, for how long
  614. should the mouse be in NREM before the actual transition. Specify for
  615. NREM, Wake, REM the minimum preceding bout duration
  616. sj_threshold : list with 3 floats
  617. For example, if we're looking at a NREM to REM transition, for how long
  618. should the mouse be in NREM before the actual transition. Specify for
  619. NREM, Wake, REM the minimum following bout duration.
  620. ma_thr : TYPE, float
  621. Wake sequences <= $ma_thr s are interpreted as NREM (3). The default is 10.
  622. ma_rem_exception : bool, optional
  623. If True, then the MA rule does not apply for wake episodes directly following REM.
  624. The default is False.
  625. sdt : float, optional
  626. Time binning for firing rates and hypnogram. The default is 2.5.
  627. pzscore : bool, optional
  628. If True, z-score firing rates. The default is True.
  629. sf : float, optional
  630. Standard deviation for Gaussian kernel to smooth firing rates. The default is 0.
  631. ma_mode : bool, optional
  632. If True, treat MAs as their own brain state.
  633. attributes : list of strings, optional
  634. Allows you to transfer columns in DataFrame @unit_info to the returned DataFrame @df. The default is [].
  635. kcuts : list of tuples or lists with two elements.
  636. Discard the time interval ranging from kcuts[i][0] to kcuts[i][1] seconds
  637. pspect : bool
  638. if True, also claculate EEG spectrogram
  639. mouse : str,
  640. If $pspec == True, needs to be set to an existing
  641. mouse name
  642. config_file : str
  643. Name of mouse recording configuration file
  644. fmax : float
  645. Maximum frequency for EEG spectrogram
  646. Returns
  647. -------
  648. df : pd.DataFrame
  649. with columns ['ID', 'time', 'fr', 'trans'], i.e.
  650. unit IDs, time point relative to transition, firing rate value, transition type
  651. encoded as NR, NW etc.
  652. mx_transspe : dict: Transition type --> np.array
  653. The dict holds for each calculated transition (encoded as NR, NW, etc.)
  654. and each unit (axis=2) the average spectrogram during that transition.
  655. """
  656. dt = 2.5
  657. states = {1:'R', 2:'W', 3:'N', 4:'M'}
  658. # cut out kcuts: ###############
  659. tidx = kcut_idx(M, units, kcuts)
  660. M = M[tidx]
  661. units = units.iloc[tidx,:]
  662. ################################
  663. ipre = int(np.round(pre/sdt))
  664. ipost = int(np.round(post/sdt))
  665. m = ipre + ipost
  666. t = np.arange(-ipre, ipost) * sdt
  667. unitIDs = units.columns.unique()
  668. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  669. # flatten out MAs
  670. if ma_thr>0:
  671. seq = sleepy.get_sequences(np.where(M==2)[0])
  672. for s in seq:
  673. if np.round(len(s)*sdt) <= ma_thr:
  674. if ma_rem_exception:
  675. if (s[0]>=1) and (M[s[0] - 1] != 1):
  676. if ma_mode:
  677. M[s] = 4
  678. else:
  679. M[s] = 3
  680. else:
  681. if ma_mode:
  682. M[s] = 4
  683. else:
  684. M[s] = 3
  685. # NEW: load spectrogram
  686. # load spectrogram and normalize
  687. if pspec:
  688. path = load_config(config_file)[mouse]['SL_PATH']
  689. ppath, name = os.path.split(path)
  690. P = so.loadmat(os.path.join(ppath, name, 'sp_%s.mat' % name), squeeze_me=True)
  691. SP = P['SP']
  692. freq = P['freq']
  693. ifreq = np.where(freq <= fmax)[0]
  694. if len(spe_filt) > 0:
  695. filt = np.ones(spe_filt)
  696. filt = np.divide(filt, filt.sum())
  697. SP = scipy.signal.convolve2d(SP, filt, boundary='symm', mode='same')
  698. sp_mean = SP.mean(axis=1)
  699. SP = np.divide(SP, np.tile(sp_mean, (SP.shape[1], 1)).T)
  700. SP[:,tidx]
  701. unit_transspe = {}
  702. mx_transspe = {}
  703. for (si,sj) in transitions:
  704. # string label for type of transition:
  705. sid = states[si] + states[sj]
  706. unit_transspe[sid] = {r:[] for r in unitIDs}
  707. mx_transspe[sid] = np.zeros((len(ifreq), len(t), len(unitIDs)))
  708. ##########################################################################
  709. data = []
  710. for unit in unitIDs:
  711. unit_annotation = unit_info[unit_info.ID == unit]
  712. if unit_annotation.shape[0] > 0:
  713. attr = unit_annotation[attributes].values.tolist()[0]
  714. else:
  715. attr = ['X', 'X']
  716. fr = np.array(units[unit])
  717. if sf > 0:
  718. fr = sleepy.smooth_data(fr, sf)
  719. if pzscore:
  720. fr = (fr-fr.mean()) / fr.std()
  721. for (si,sj) in transitions:
  722. # string label for type of transition:
  723. sid = states[si] + states[sj]
  724. seq = sleepy.get_sequences(np.where(M==si)[0])
  725. for s in seq:
  726. ti = s[-1]
  727. # check if next state is sj; only then continue
  728. if ti < len(M)-1 and M[ti+1] == sj:
  729. # go into future
  730. p = ti+1
  731. while p<len(M)-1 and M[p] == sj:
  732. p += 1
  733. p -= 1
  734. sj_idx = list(range(ti+1, p+1))
  735. # so the indices of state si are seq
  736. # the indices of state sj are sj_idx
  737. if ipre <= ti < len(M)-ipost and len(s)*sdt >= si_threshold[si-1] and len(sj_idx)*sdt >= sj_threshold[sj-1]:
  738. rem_ID = '%d' % (s[0])
  739. act = fr[ti-ipre+1:ti+ipost+1]
  740. # Note: ti+1 is the first time point of the "post" state
  741. # i = 10, ipre = 2, ipost = 2
  742. # 8,9,10
  743. # np.arange(8,12) = 8,9,10,11,12
  744. if pspec:
  745. spe = SP[ifreq, ti-ipre+1:ti+ipost+1]
  746. unit_transspe[sid][unit].append(spe)
  747. dur_post = len(sj_idx)*dt
  748. fr_post = np.mean(fr[sj_idx])
  749. new_data = zip([unit]*m, t, act, [sid]*m, [rem_ID]*m, [dur_post]*m, [fr_post]*m)
  750. new_data = [list(x) + attr for x in list(new_data)]
  751. data += new_data
  752. df = pd.DataFrame(data=data, columns=['ID', 'time', 'fr', 'trans', 'remID', 'dur_post', 'fr_post'] + attributes)
  753. if pspec:
  754. for (si,sj) in transitions:
  755. for i,unit in enumerate(unitIDs):
  756. # string label for type of transition:
  757. sid = states[si] + states[sj]
  758. tmp = np.array(unit_transspe[sid][unit])
  759. mx_transspe[sid][:,:,i] = np.nanmean(tmp, axis=0)
  760. if not pspec:
  761. return df, []
  762. else:
  763. return df, mx_transspe
  764. def fr_transitions_stats(df_trans, base_int, unit_avg=True, dt=2.5, time_mode='midpoint'):
  765. """
  766. Parameters
  767. ----------
  768. df_trans : TYPE
  769. DESCRIPTION.
  770. base_int : TYPE
  771. DESCRIPTION.
  772. unit_avg : TYPE, optional
  773. DESCRIPTION. The default is True.
  774. dt : TYPE, optional
  775. DESCRIPTION. The default is 2.5.
  776. time_mode : str, optional
  777. options: 'midpoint' or 'endpoint'. The default is 'midpoint'.
  778. Returns
  779. -------
  780. df_stats : TYPE
  781. DESCRIPTION.
  782. """
  783. # test if df has column ms_id
  784. if not 'ms_id' in df_trans.columns:
  785. mice = list(df_trans['mouse'])
  786. ids = list(df_trans['ID'])
  787. ms_ids = [m + '-' + i for m,i in zip(mice, ids)]
  788. df_trans['ms_id'] = ms_ids
  789. ids = list(df_trans.ms_id.unique())
  790. dfm_trans = df_trans[['ms_id', 'time', 'fr', 'trans']].groupby(['ms_id', 'time', 'trans',]).mean().reset_index()
  791. t = dfm_trans.time.unique()
  792. # number of bins per time bin
  793. ibin = int(base_int / dt)
  794. pre = t[0]
  795. post = t[-1]
  796. nbin = int(np.floor((abs(pre)+post)/base_int))
  797. trans_dict = {}
  798. for tr in df_trans.trans.unique():
  799. trans_mx = np.zeros((len(ids), len(t)))
  800. for i,ID in enumerate(ids):
  801. fr = dfm_trans.loc[(dfm_trans.ms_id == ID) & (dfm_trans.trans==tr), 'fr']
  802. trans_mx[i,:] = fr
  803. trans_dict[tr] = trans_mx
  804. tinit = t[0]
  805. # Statistics: When does activity becomes significantly different from baseline?
  806. ibin = int(np.round(base_int / dt))
  807. nbin = int(np.floor((abs(pre)+post)/base_int))
  808. data = []
  809. for tr in trans_dict:
  810. trans = trans_dict[tr]
  811. base = trans[:,0:ibin].mean(axis=1)
  812. for i in range(1,nbin):
  813. test_vals = trans[:, i * ibin:(i + 1) * ibin].mean(axis=1)
  814. ttest_res = stats.ttest_rel(base, trans[:,i*ibin:(i+1)*ibin].mean(axis=1))
  815. tval = ttest_res.statistic
  816. pval = ttest_res.pvalue
  817. dof = len(base) - 1
  818. # Compute Cohen's d
  819. diff = base - test_vals
  820. cohend = diff.mean() / diff.std(ddof=1)
  821. sig = 'no'
  822. if pval < (0.05 / (nbin-1)):
  823. sig = 'yes'
  824. if time_mode == 'midpoint':
  825. tpoint = i*(ibin*dt)+tinit + ibin*dt/2
  826. else:
  827. tpoint = i*(ibin*dt)+tinit + ibin*dt
  828. tpoint = float('%.2f'%tpoint)
  829. pval = pval * (nbin-1)
  830. if pval > 1:
  831. pval = 1
  832. data.append([tpoint, pval, sig, tr, tval, dof, cohend])
  833. df_stats = pd.DataFrame(data = data, columns = ['time', 'p-value', 'sig', 'trans', 'tval', 'dof', 'cohend'])
  834. return df_stats
  835. def pc_transitions(PC, M, transitions, pre, post, si_threshold, sj_threshold,
  836. ma_thr=10, ma_rem_exception=False, sdt=2.5, ma_mode=False,
  837. kcuts=[], allowed_idx=[], pzscore_pc=False):
  838. """
  839. Calculate timecourse of PCs in population activity relative to brain state
  840. transitions.
  841. Parameters
  842. ----------
  843. PC : np.array
  844. Number of PCx x number of time bins
  845. PCs; each row corresponds to one PC.
  846. M : np.array
  847. hynpogram.
  848. transitions : TYPE
  849. DESCRIPTION.
  850. pre : TYPE
  851. DESCRIPTION.
  852. post : TYPE
  853. DESCRIPTION.
  854. si_threshold : TYPE
  855. DESCRIPTION.
  856. sj_threshold : TYPE
  857. DESCRIPTION.
  858. ma_thr : TYPE, optional
  859. DESCRIPTION. The default is 10.
  860. ma_rem_exception : bool, optional
  861. If True, don't touch wake following REM sleep. The default is False.
  862. sdt : float, optional
  863. time bin duration in seconds of one brain state. The default is 2.5.
  864. ma_mode : bool, optional
  865. If True, then specifically analyze transitions from and to MAs.
  866. Note that when considering transitions from and to NREM, MAs are
  867. considered as NREM sleep.
  868. kcuts : TYPE, optional
  869. DESCRIPTION. The default is [].
  870. allowed_idx : TYPE, optional
  871. DESCRIPTION. The default is [].
  872. pzscore_pc : bool
  873. If True, z-score PCs across entire recording
  874. Returns
  875. -------
  876. df : pd.DataFrame
  877. with columns ['event', 'pc', 'time', 'fr', 'trans'].
  878. """
  879. states = {1:'R', 2:'W', 3:'N', 4:'M'}
  880. # cut out kcuts: ###############
  881. tidx = kcut_idx(M, PC, kcuts)
  882. M = M[tidx]
  883. ################################
  884. ipre = int(np.round(pre/sdt))
  885. ipost = int(np.round(post/sdt))
  886. m = ipre + ipost
  887. t = np.arange(-ipre, ipost) * sdt
  888. # flatten out MAs
  889. if ma_thr>0:
  890. seq = sleepy.get_sequences(np.where(M==2)[0])
  891. for s in seq:
  892. if np.round(len(s)*sdt) <= ma_thr:
  893. if ma_rem_exception:
  894. if (s[0]>=1) and (M[s[0] - 1] != 1):
  895. if ma_mode:
  896. M[s] = 4
  897. else:
  898. M[s] = 3
  899. else:
  900. if ma_mode:
  901. M[s] = 4
  902. else:
  903. M[s] = 3
  904. Mrepr = M.copy()
  905. M[M==4] = 3
  906. if not ma_mode:
  907. #just forget about MAs:
  908. Mrepr = M
  909. if len(allowed_idx) == 0:
  910. allowed_idx = range(0, len(M))
  911. # zscore pcs: #############################################################
  912. if pzscore_pc:
  913. for i in range(PC.shape[0]):
  914. PC[i,:] = (PC[i,:]- PC[i,:].mean()) / PC[i,:].std()
  915. ###########################################################################
  916. data = []
  917. ev = 0
  918. for count,fr in enumerate(PC):
  919. label = 'pc%d' % int(count+1)
  920. for (si,sj) in transitions:
  921. # string label for type of transition:
  922. sid = states[si] + states[sj]
  923. if si == 3 and not ma_mode:
  924. seq = sleepy.get_sequences(np.where(M==si)[0])
  925. elif si==3 and ma_mode:
  926. seq = sleepy.get_sequences(np.where(Mrepr==si)[0])
  927. else:
  928. seq = sleepy.get_sequences(np.where(Mrepr==si)[0])
  929. for s in seq:
  930. # ti is the last bin in the current sequence
  931. ti = s[-1]
  932. if si == 3 and ma_mode:
  933. p = s[0]
  934. p = p-1
  935. while p >0 and M[p] == 3:
  936. p = p-1
  937. p = p+1
  938. s = np.arange(p, ti+1)
  939. # check if next state is sj; only then continue
  940. if ti < len(M)-1 and Mrepr[ti+1] == sj:
  941. # go into future
  942. p = ti+1
  943. if sj == 3:
  944. # if sj == 3, we're treating MAs as NREM
  945. while p<len(M)-1 and M[p] == sj:
  946. p += 1
  947. else:
  948. while p<len(M)-1 and Mrepr[p] == sj:
  949. p += 1
  950. p -= 1
  951. sj_idx = list(range(ti+1, p+1))
  952. # so the indices of state si are seq
  953. # the indices of state sj are sj_idx
  954. if ti in allowed_idx and ipre <= ti < len(M)-ipost and len(s)*sdt >= si_threshold[si-1] and len(sj_idx)*sdt >= sj_threshold[sj-1]:
  955. act = fr[ti-ipre+1:ti+ipost+1]
  956. # Note: ti+1 is the first time point of the "post" state
  957. # i = 10, ipre = 2, ipost = 2
  958. # 8,9,10
  959. # np.arange(8,12) = 8,9,10,11,12
  960. data += zip([s[0]]*m, [label]*m, t, act, [sid]*m)
  961. ev += 1
  962. df = pd.DataFrame(data=data, columns=['event', 'pc', 'time', 'fr', 'trans'])
  963. return df
  964. def pc_transitions_laser(mouse, PC, M, transitions, pre, post, si_threshold, sj_threshold,
  965. ma_thr=10, ma_rem_exception=False, sdt=2.5, ma_mode=False, rnd_laser=False,
  966. kcuts=[], allowed_idx=[], pzscore_pc=True, config_file='', laser_dur=-1):
  967. """
  968. Compare spontaneous and laser-induced brain state transitions
  969. Parameters
  970. ----------
  971. PC : np.array
  972. Number of PCx x number of time bins
  973. PCs; each row corresponds to one PC.
  974. M : np.array
  975. hynpogram.
  976. transitions : TYPE
  977. DESCRIPTION.
  978. pre : TYPE
  979. DESCRIPTION.
  980. post : TYPE
  981. DESCRIPTION.
  982. si_threshold : TYPE
  983. DESCRIPTION.
  984. sj_threshold : TYPE
  985. DESCRIPTION.
  986. ma_thr : TYPE, optional
  987. DESCRIPTION. The default is 10.
  988. ma_rem_exception : bool, optional
  989. If True, don't touch wake following REM sleep. The default is False.
  990. sdt : float, optional
  991. time bin duration in seconds of one brain state. The default is 2.5.
  992. ma_mode : bool, optional
  993. If True, then specifically analysis transitions from and to MAs.
  994. Note that when considering transitions from and to NREM, MAs are
  995. considered as NREM sleep.
  996. kcuts : TYPE, optional
  997. DESCRIPTION. The default is [].
  998. allowed_idx : TYPE, optional
  999. DESCRIPTION. The default is [].
  1000. Returns
  1001. -------
  1002. df : TYPE
  1003. DESCRIPTION.
  1004. """
  1005. dt = 2.5
  1006. states = {1:'R', 2:'W', 3:'N', 4:'M'}
  1007. nhypno = M.shape[0]
  1008. ndim = PC.shape[0]
  1009. tidx = np.arange(0, nhypno)
  1010. # NEW 07/01/22:
  1011. # get the indices (in brainstate time) that we're going to completely discard:
  1012. if len(kcuts) > 0:
  1013. kidx = []
  1014. for kcut in kcuts:
  1015. a = int(kcut[0]/dt)
  1016. b = int(kcut[-1]/dt)
  1017. if b > len(M):
  1018. b = len(M)
  1019. kidx += list(np.arange(a, b))
  1020. tidx = np.setdiff1d(tidx, kidx)
  1021. M = M[tidx]
  1022. nhypno = len(tidx)
  1023. ###########################################################################
  1024. ipre = int(np.round(pre/sdt))
  1025. ipost = int(np.round(post/sdt))
  1026. # m = ipre + ipost
  1027. t = np.arange(-ipre, ipost) * sdt
  1028. m = len(t)
  1029. # flatten out MAs
  1030. if ma_thr>0:
  1031. seq = sleepy.get_sequences(np.where(M==2)[0])
  1032. for s in seq:
  1033. if np.round(len(s)*sdt) <= ma_thr:
  1034. if ma_rem_exception:
  1035. if (s[0]>=1) and (M[s[0] - 1] != 1):
  1036. if ma_mode:
  1037. M[s] = 4
  1038. else:
  1039. M[s] = 3
  1040. else:
  1041. if ma_mode:
  1042. M[s] = 4
  1043. else:
  1044. M[s] = 3
  1045. Mrepr = M.copy()
  1046. M[M==4] = 3
  1047. if not ma_mode:
  1048. #just forget about MAs:
  1049. Mrepr = M
  1050. if len(allowed_idx) == 0:
  1051. allowed_idx = range(0, len(M))
  1052. #######################################################################
  1053. # get laser start and end index after excluding kcuts: ################
  1054. ddir = load_config(config_file)[mouse]['SL_PATH']
  1055. ppath, name = os.path.split(ddir)
  1056. sr = sleepy.get_snr(ppath, name)
  1057. nbin = int(np.round(sr)*dt)
  1058. lsr = so.loadmat(os.path.join(ddir, 'laser_%s.mat' % name), squeeze_me=True)['laser']
  1059. idxs, idxe = sleepy.laser_start_end(lsr)
  1060. idxs = [int(i/nbin) for i in idxs]
  1061. if laser_dur == -1:
  1062. idxe = [int(i/nbin) for i in idxe]
  1063. b = [i for i in idxe if i in tidx]
  1064. a = [i for i in idxs if i in tidx]
  1065. b = np.array(b)
  1066. a = np.array(a)
  1067. laser_dur = (np.mean(b-a) + 1) * dt
  1068. print('Laser duration: %f' % laser_dur)
  1069. else:
  1070. idxe = [(int(i + int(laser_dur/dt))) for i in idxs]
  1071. # randomize laser #########################################################
  1072. dur = int(laser_dur/dt)
  1073. if rnd_laser:
  1074. idxs_rnd = []
  1075. idxe_rnd = []
  1076. tmp = np.random.randint(dur, idxs[0]-dur)
  1077. idxs_rnd.append(tmp)
  1078. idxe_rnd.append(tmp+dur)
  1079. for (a,b) in zip(idxe[0:-1], idxs[1:]):
  1080. if a+2*dur < b-dur:
  1081. tmp = np.random.randint(a+2*dur,b-dur)
  1082. idxs_rnd.append(tmp)
  1083. idxe_rnd.append(tmp+dur)
  1084. idxs = idxs_rnd
  1085. idxe = idxe_rnd
  1086. ###########################################################################
  1087. laser_idx = []
  1088. for (si,sj) in zip(idxs, idxe):
  1089. laser_idx += list(range(si,sj+1))
  1090. laser_idx = np.array(laser_idx)
  1091. nlsr = int(np.floor(lsr.shape[0]/nbin))
  1092. laser = np.zeros((nlsr,))
  1093. laser_idx = laser_idx[laser_idx < nlsr]
  1094. laser[laser_idx] = 1
  1095. laser = laser[tidx]
  1096. # get again indices after kcut
  1097. laser_idx = np.where(laser == 1)[0]
  1098. idxs = [s[0] for s in sleepy.get_sequences(np.where(laser == 1)[0])]
  1099. idxe = [s[-1] for s in sleepy.get_sequences(np.where(laser == 1)[0])]
  1100. #######################################################################
  1101. # zscore pcs:
  1102. if pzscore_pc:
  1103. for i in range(PC.shape[0]):
  1104. PC[i,:] = (PC[i,:]- PC[i,:].mean()) / PC[i,:].std()
  1105. data = []
  1106. ev = 0
  1107. for count,fr in enumerate(PC):
  1108. label = 'pc%d' % int(count+1)
  1109. for (si,sj) in transitions:
  1110. # string label for type of transition:
  1111. sid = states[si] + states[sj]
  1112. if si == 3 and not ma_mode:
  1113. seq = sleepy.get_sequences(np.where(M==si)[0])
  1114. elif si==3 and ma_mode:
  1115. seq = sleepy.get_sequences(np.where(Mrepr==si)[0])
  1116. else:
  1117. seq = sleepy.get_sequences(np.where(Mrepr==si)[0])
  1118. for s in seq:
  1119. # ti is the last bin in the current sequence
  1120. ti = s[-1]
  1121. if si == 3 and ma_mode:
  1122. p = s[0]
  1123. p = p-1
  1124. while p > 0 and M[p] == 3:
  1125. p = p-1
  1126. p = p+1
  1127. s = np.arange(p, ti+1)
  1128. # check if next state is sj; only then continue
  1129. if ti < len(M)-1 and Mrepr[ti+1] == sj:
  1130. # go into future
  1131. p = ti+1
  1132. if sj == 3:
  1133. # if sj == 3, we're treating MAs as NREM
  1134. while p<len(M)-1 and M[p] == sj:
  1135. p += 1
  1136. else:
  1137. while p<len(M)-1 and Mrepr[p] == sj:
  1138. p += 1
  1139. p -= 1
  1140. sj_idx = list(range(ti+1, p+1))
  1141. # so the indices of state si are seq
  1142. # the indices of state sj are sj_idx
  1143. if ti in allowed_idx and ipre <= ti < len(M)-ipost and len(s)*sdt >= si_threshold[si-1] and len(sj_idx)*sdt >= sj_threshold[sj-1]:
  1144. act = fr[ti-ipre+1:ti+ipost+1]
  1145. # Note: ti+1 is the first time point of the "post" state
  1146. # i = 10, ipre = 2, ipost = 2
  1147. # 8,9,10
  1148. # np.arange(8,12) = 8,9,10,11,12
  1149. delay = -1
  1150. laser_on = 'no'
  1151. if ti+1 in laser_idx:
  1152. laser_on = 'yes'
  1153. a = ti
  1154. while laser[a] == 1:
  1155. a = a-1
  1156. a = a+1
  1157. delay = (ti - a + 1) * dt
  1158. laser_cut = laser[ti-ipre+1:ti+ipost+1]
  1159. data += zip([s[0]]*m, [label]*m, t, act, [sid]*m, [laser_on]*m, laser_cut, [delay]*m)
  1160. ev += 1
  1161. df = pd.DataFrame(data=data, columns=['event', 'pc', 'time', 'fr', 'trans', 'laser_on', 'laser', 'delay'])
  1162. return df
  1163. def fr_svd(units, nsmooth=0, pzscore=False):
  1164. """
  1165. perform SVD on firing rates of a population of neurons
  1166. Parameters
  1167. ----------
  1168. units : pd.DataFrame
  1169. each column is a unit; the column names are the unitIDs.
  1170. ndim : int, optional
  1171. DESCRIPTION. The default is 3.
  1172. nsmooth : float, optional
  1173. If > 0, som. The default is 0.
  1174. Returns
  1175. -------
  1176. PC : np.array
  1177. Each row vector corresponds to one principal component.
  1178. dimensions: ndim x timepoints
  1179. V : np.array
  1180. Variance captured by the principal components
  1181. """
  1182. # first transform pd.DataFrame units to np.array:
  1183. # Matrix arrangement:
  1184. # rows - neurons; columns - time points
  1185. # NOTE: the rows (neurons) are the variables (dimensions),
  1186. # the time points are the samples (trials)
  1187. # through PCA we want to keep the number of samples, but we want to reduce
  1188. # the dimensions. Again, for our matrix R, we have the arrangement:
  1189. # samples (=time)
  1190. # variables x
  1191. #
  1192. # In our case, that's
  1193. # timepoints
  1194. # units x
  1195. unitIDs = [unit for unit in units.columns if re.split('_', unit)[1] == 'good']
  1196. nsample = units.shape[0] # number of time points
  1197. nvar = len(unitIDs) # number of units
  1198. R = np.zeros((nvar, nsample))
  1199. i = 0
  1200. for unit in unitIDs:
  1201. R[i,:] = sleepy.smooth_data(np.array(units[unit]),nsmooth)
  1202. if pzscore:
  1203. R[i,:] = (R[i,:] - R[i,:].mean()) / R[i,:].std()
  1204. i += 1
  1205. # first, for each varible (dimension), we first need to remove the mean:
  1206. # mean-zero rows:
  1207. for i in range(nvar):
  1208. R[i,:] = R[i,:] - R[i,:].mean()
  1209. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  1210. Y = R.T / np.sqrt(nsample-1)
  1211. U,S,Vh = scipy.linalg.svd(Y)
  1212. return U, S, Vh
  1213. def pr_components(units, ndim=3, nsmooth=0, pzscore=False):
  1214. """
  1215. Calculate principal components of simultaneously recorded firing rates.
  1216. Parameters
  1217. ----------
  1218. units : pd.DataFrame
  1219. each column is a unit; the column names are the unitIDs.
  1220. ndim : int, optional
  1221. DESCRIPTION. The default is 3.
  1222. nsmooth : float, optional
  1223. If > 0, som. The default is 0.
  1224. Returns
  1225. -------
  1226. PC : np.array
  1227. Each row vector corresponds to one principal component.
  1228. dimensions: ndim x timepoints
  1229. V : np.array
  1230. Variance captured by the principal components
  1231. """
  1232. # first transform pd.DataFrame units to np.array:
  1233. # Matrix arrangement:
  1234. # rows - neurons; columns - time points
  1235. # NOTE: the rows (neurons) are the variables (dimensions),
  1236. # the time points are the samples (trials)
  1237. # through PCA we want to keep the number of samples, but we want to reduce
  1238. # the dimensions. Again, for our matrix R, we have the arrangement:
  1239. # samples (=time)
  1240. # variables x
  1241. #
  1242. # In our case, that's
  1243. # timepoints
  1244. # units x
  1245. unitIDs = [unit for unit in units.columns if re.split('_', unit)[1] == 'good']
  1246. nsample = units.shape[0] # number of time points
  1247. nvar = len(unitIDs) # number of units
  1248. R = np.zeros((nvar, nsample))
  1249. i = 0
  1250. for unit in unitIDs:
  1251. R[i,:] = sleepy.smooth_data(np.array(units[unit]),nsmooth)
  1252. if pzscore:
  1253. R[i,:] = (R[i,:] - R[i,:].mean()) / R[i,:].std()
  1254. i += 1
  1255. # first, for each varible (dimension), we first need to remove the mean:
  1256. # mean-zero rows:
  1257. for i in range(nvar):
  1258. R[i,:] = R[i,:] - R[i,:].mean()
  1259. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  1260. Y = R.T / np.sqrt(nsample-1)
  1261. U,S,Vh = scipy.linalg.svd(Y)
  1262. # project data @R onto principal direction @Vh
  1263. # Vh: nvar x nvar; each row is an eigenvector of the covariance matrix.
  1264. # So using Vh * R, we project the data into the covariance space.
  1265. PC = np.dot(Vh, R)[0:ndim,:]
  1266. V = S**2
  1267. # Side note; the same result you can get also with:
  1268. # U * S ... in more detail:
  1269. # SM = np.zeros((nsample, nsample))
  1270. # for i in range(nvar):
  1271. # SM[i,i] = S[i]
  1272. # PC = np.dot(U, SM) * np.sqrt(nsample-1)
  1273. ###########################################################################
  1274. # plots
  1275. plt.figure()
  1276. plt.subplot(211)
  1277. plt.plot(V, '.')
  1278. plt.subplot(212)
  1279. plt.plot(PC.T)
  1280. var = S**2
  1281. var_total = np.sum(S**2)
  1282. # Calculate the cumulative variance explained, i.e. how much
  1283. # of the variance is captured by the first i principal components.
  1284. p = []
  1285. for i in range(1,len(var)+1):
  1286. s = np.sum(var[0:i])
  1287. p.append(s/var_total)
  1288. plt.figure()
  1289. plt.plot(p, '.')
  1290. return PC, V
  1291. def sleep_components(units, M, wake_dur=600, wake_break=60, ndim=3, nsmooth=0,
  1292. pzscore=False, ddof=0, detrend=False, pplot=True, pc_sign=[], scale_pc=False,
  1293. kcuts=[], mode='w', mouse='', collapse=False,
  1294. ylim=[], config_file='mouse_config.txt'):
  1295. """
  1296. Calculate principal components, excluding long wake periods in the recording.
  1297. Long wake periods are defined by the parameters $wake_dur (duration of wake episodes)
  1298. and $wake_break (wake episodes separated by less than $wake_break seconds are fused).
  1299. Parameters
  1300. ----------
  1301. units : pd.DataFrame
  1302. each column corresponds to a unit; each row is a time bin
  1303. M : np.array
  1304. hypnogram.
  1305. wake_dur : float, optional
  1306. Exclude wake periods that are longer than $wake_dur seconds. The default is 600.
  1307. wake_break : float, optional
  1308. Two wake periods that are separated by less than $wake_break seconds
  1309. are merged to one period. The default is 60.
  1310. ndim : int, optional
  1311. reduce data (matrix of firing rate vector) to $ndim dimensions using PCA. The default is 3.
  1312. nsmooth : float, optional
  1313. Smooth firing rate vector. The default is 0.
  1314. pzscore : boolean, optional
  1315. If True, zscore firing rates. The default is False.
  1316. detrend : boolean, optional
  1317. If True, detrend each firing rate vector.
  1318. pc_sign : list with $ndim elements, either 1 or -1.
  1319. The sign of PCs is ambiguous, so if preferred multiply, PC i with pc_sign[i]
  1320. scale_pc: if True, scale PCs by 1 / np.sqrt(nsample - 1); nsample is the number of time bins used
  1321. for PC calculation; Vh * S /sqrt(nsample-1) is conventionally used as PCs
  1322. ddof: degrees of freedom for z-score calculation;
  1323. kcuts : list of tuples or lists with two elements.
  1324. Discard the time interval ranging from kcuts[i][0] to kcuts[i][1] seconds
  1325. mode : string with characters 'r' and/or 'w'
  1326. if 'r' in mode, remove all REM indices for PC computation
  1327. if 'w' in mode, remove all Wake indices for PC computation
  1328. collapse: boolean
  1329. if True, plot each PC in its own axis
  1330. ylim: empty list, or tuple
  1331. if empty list, don't fix ylims, otherwise set plt.ylim(ylim) for each PC
  1332. config_file: str
  1333. mouse configuration file as loaded by &load_config()
  1334. mouse: str
  1335. To plot laser, set $mouse to mouse name
  1336. Returns
  1337. -------
  1338. PC : np.array
  1339. The $ndim principal components. Note although the PCs have been calculated
  1340. without long wake periods, the returned PCs do include all wake periods.
  1341. V : np.array
  1342. Eigenvalues of the covariance matrix = Variance associated with each PC
  1343. Vh : np.array
  1344. each row in Vh is an eigenvector of the covariance matrix
  1345. idx : np.array
  1346. Indices of time bins used for PC calculation.
  1347. NOTE that @idx are the indices obtained
  1348. AFTER cutting out the KCUT intervals!
  1349. """
  1350. dt = 2.5
  1351. nhypno = np.min((len(M), units.shape[0]))
  1352. Morig = M.copy()
  1353. M = M[0:nhypno]
  1354. tidx = np.arange(0, nhypno)
  1355. # KCUT ####################################################################
  1356. # get the indices (in brainstate time) that we're going to completely discard:
  1357. if len(kcuts) > 0:
  1358. kidx = []
  1359. for kcut in kcuts:
  1360. a = int(kcut[0]/dt)
  1361. b = int(kcut[-1]/dt)
  1362. if b > len(M):
  1363. b = len(M)
  1364. kidx += list(np.arange(a, b))
  1365. tidx = np.setdiff1d(tidx, kidx)
  1366. M = M[tidx]
  1367. nhypno = len(tidx)
  1368. print(len(tidx))
  1369. ###########################################################################
  1370. unitIDs = [unit for unit in units.columns if '_' in unit]
  1371. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  1372. nsample = nhypno # number of time points
  1373. nvar = len(unitIDs) # number of units
  1374. # Note the number of samples stays the same; while the number is variables
  1375. # (or dimensions) is reduced! We want to keep the same number of time points,
  1376. # but have only a few 'modes'.
  1377. R = np.zeros((nvar, nsample))
  1378. #@tidx are the indices we're further considering.
  1379. for i,unit in enumerate(unitIDs):
  1380. tmp = sleepy.smooth_data(np.array(units[unit]),nsmooth)
  1381. tmp = tmp[tidx]
  1382. if detrend:
  1383. tmp = scipy.signal.detrend(tmp)
  1384. if pzscore:
  1385. R[i,:] = (tmp - tmp.mean()) / tmp.std(ddof=ddof)
  1386. else:
  1387. R[i,:] = tmp
  1388. # first, for each varible (dimension), we first need to remove the mean:
  1389. # mean-zero rows:
  1390. #for i in range(nvar):
  1391. # R[i,:] = R[i,:] - R[i,:].mean()
  1392. # find long wake blocks:
  1393. widx = sleepy.get_sequences(np.where(M==2)[0], ibreak=int(wake_break/dt))
  1394. # all REM sequences
  1395. ridx = sleepy.get_sequences(np.where(M==1)[0])
  1396. nidx = sleepy.get_sequences(np.where(M==3)[0])
  1397. tmp = []
  1398. for w in widx:
  1399. if len(w) * dt > wake_dur:
  1400. tmp += list(w)
  1401. widx = tmp
  1402. tmp = []
  1403. for r in ridx:
  1404. tmp += list(r)
  1405. ridx = tmp
  1406. tmp = []
  1407. for r in nidx:
  1408. tmp += list(r)
  1409. nidx = tmp
  1410. nhypno = np.min((len(M), units.shape[0]))
  1411. idx = np.arange(0, np.min((len(M), units.shape[0])))
  1412. if 'w' in mode:
  1413. idx = np.setdiff1d(idx, widx)
  1414. else:
  1415. widx = []
  1416. if 'r' in mode:
  1417. idx = np.setdiff1d(idx, ridx)
  1418. if 'n' in mode:
  1419. idx = np.setdiff1d(idx, nidx)
  1420. else:
  1421. ridx = []
  1422. ### NEW: 5/14/25:
  1423. nsample = len(idx)
  1424. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  1425. Y = R[:,idx].T / np.sqrt(nsample-1)
  1426. # make sure the the columns of Y are mean-zero
  1427. for i in range(nvar):
  1428. Y[:,i] = Y[:,i] - Y[:,i].mean()
  1429. # redundant (see few lines below $mu); removed the line on 5/14/25
  1430. # R[i,:] = R[i,:] - R[i,idx].mean()
  1431. # SVD
  1432. # Note that each column is a neuron,
  1433. # and each row is a time point
  1434. U,S,Vh = scipy.linalg.svd(Y)
  1435. # each row in Vh is an eigenvector of the COV matrix;
  1436. # to get the PCs, we project R onto the eigenvectors:
  1437. # NEW 05/14/25!!!
  1438. #PC = np.dot(Vh, R)[0:ndim,:]
  1439. mu = R[:, idx].mean(axis=1, keepdims=True)
  1440. sigma = R[:, idx].std(axis=1, ddof=1, keepdims=True) if pzscore else 1.0
  1441. R0 = (R - mu) / sigma
  1442. if scale_pc:
  1443. PC = (Vh[:ndim] @ R0) / np.sqrt(nsample - 1)
  1444. else:
  1445. PC = np.dot(Vh, R0)[0:ndim,:]
  1446. V = S**2
  1447. if len(pc_sign) > 0:
  1448. i = 0
  1449. for s in pc_sign:
  1450. PC[i,:] = PC[i,:] * s
  1451. i += 1
  1452. if pplot:
  1453. add_laser = False
  1454. t = np.arange(0, nhypno) * dt
  1455. plt.figure()
  1456. tmp = widx+ridx
  1457. tmp.sort()
  1458. widx = sleepy.get_sequences(np.array(tmp))
  1459. axes_exc = plt.axes([0.2, 0.9, 0.7, 0.05])
  1460. for w in widx:
  1461. if len(w) > 1:
  1462. if w[-1] < len(M):
  1463. plt.plot([t[w[0]], t[w[-1]]], [1, 1], 'k', lw=2)
  1464. plt.ylim([0, 2])
  1465. plt.xlim((t[0], t[-1]))
  1466. # if laser exists also add laser here --
  1467. if mouse:
  1468. ddir = load_config(config_file)[mouse]['SL_PATH']
  1469. ppath, name = os.path.split(ddir)
  1470. if os.path.isfile(os.path.join(ddir, 'laser_%s.mat' % name)):
  1471. lsr = so.loadmat(os.path.join(ddir, 'laser_%s.mat' % name), squeeze_me=True)['laser']
  1472. sr = sleepy.get_snr(ppath, name)
  1473. nbin = int(np.round(sr)*2.5)
  1474. idxs, idxe = sleepy.laser_start_end(lsr)
  1475. idxs = [int(i/nbin) for i in idxs]
  1476. idxe = [int(i/nbin) for i in idxe]
  1477. laser_idx = []
  1478. for (si,sj) in zip(idxs, idxe):
  1479. laser_idx += list(range(si,sj+1))
  1480. nlsr = int(np.floor(lsr.shape[0]/nbin))
  1481. laser = np.zeros((nlsr,))
  1482. laser[laser_idx] = 1
  1483. laser = laser[tidx]
  1484. lsr_seq = sleepy.get_sequences(np.where(laser==1)[0])
  1485. for w in lsr_seq:
  1486. if w[-1] < len(M):
  1487. plt.plot([t[w[0]], t[w[-1]]], [1.5, 1.5], 'b', lw=2)
  1488. plt.xlim((t[0], t[-1]))
  1489. add_laser = True
  1490. sleepy._despine_axes(axes_exc)
  1491. axes_brs = plt.axes([0.2, 0.85, 0.7, 0.05], sharex=axes_exc)
  1492. cmap = plt.cm.jet
  1493. my_map = cmap.from_list('brs', [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8]], 4)
  1494. tmp = axes_brs.pcolorfast(t, [0, 1], np.array([M]), vmin=0, vmax=3)
  1495. tmp.set_cmap(my_map)
  1496. axes_brs.axis('tight')
  1497. sleepy._despine_axes(axes_brs)
  1498. if collapse:
  1499. plt.axes([0.2, 0.2, 0.7, 0.6], sharex=axes_brs)
  1500. plt.plot(t, PC[:,:].T)
  1501. plt.xlim((t[0], t[-1]))
  1502. sns.despine()
  1503. plt.xlabel('Time (s)')
  1504. plt.ylabel('PC')
  1505. else:
  1506. clrs = sns.color_palette("husl", ndim)
  1507. d = (0.6 / ndim) * 0.3
  1508. ny = (0.6 / ndim)-d
  1509. for i in range(ndim):
  1510. ax = plt.axes([0.2, 0.2+i*(ny+d), 0.7, ny], sharex=axes_exc)
  1511. ax.plot(t, PC[ndim-1-i,:], color=clrs[i])
  1512. plt.xlim([t[0], t[-1]])
  1513. sleepy.box_off(ax)
  1514. plt.ylabel('PC%d' % (ndim-i))
  1515. if add_laser:
  1516. ylims = ax.get_ylim()
  1517. for w in lsr_seq:
  1518. if w[-1] < len(M):
  1519. if 1 in M[w]:
  1520. color = 'b'
  1521. else:
  1522. color = 'r'
  1523. #if M[w[0]] == 3:
  1524. #plt.plot(t[w[0]], PC[ndim-1-i,w[0]-1:w[0]+1].mean(), '.', color=color, lw=2)
  1525. plt.plot(t[w[0]], PC[ndim-1-i,w[0]], '.', color=color, lw=2)
  1526. laser_tend = (w[-1] - w[0]) * dt
  1527. dy = ylims[1] - ylims[0]
  1528. ax.add_patch(patches.Rectangle(
  1529. (t[w[0]], ylims[0]), laser_tend, dy,
  1530. facecolor=[0.6, 0.6, 1], edgecolor=[0.6, 0.6, 1]))
  1531. if i > 0:
  1532. ax.spines["bottom"].set_visible(False)
  1533. ax.axes.get_xaxis().set_visible(False)
  1534. else:
  1535. plt.xlabel('Time (s)')
  1536. if len(ylim) > 0:
  1537. plt.ylim(ylim)
  1538. var = S**2
  1539. var_total = np.sum(S**2)
  1540. # Calculate the cumulative variance explained, i.e. how much
  1541. # of the variance is captured by the first i principal components.
  1542. p = []
  1543. for i in range(1,len(var)+1):
  1544. s = np.sum(var[0:i])
  1545. p.append(s/var_total)
  1546. plt.figure(figsize=(4,4))
  1547. plt.plot(p, '.', color='gray')
  1548. plt.xlabel(r'$\mathrm{PC_i}$')
  1549. plt.ylabel('Cum. variance')
  1550. plt.subplots_adjust(bottom=0.2, left=0.2)
  1551. plt.ylim([0, 1.1])
  1552. sns.despine()
  1553. if len(kcuts) > 0:
  1554. # find long wake blocks:
  1555. widx = sleepy.get_sequences(np.where(Morig==2)[0], ibreak=int(wake_break/dt))
  1556. tmp = []
  1557. for w in widx:
  1558. if len(w) * dt > wake_dur:
  1559. tmp += list(w)
  1560. widx = tmp
  1561. nhypno = np.min((len(Morig), units.shape[0]))
  1562. idx_total = np.arange(0, nhypno)
  1563. idx_total = np.setdiff1d(idx_total, kidx)
  1564. idx_total = np.setdiff1d(idx_total, widx)
  1565. return PC, V, Vh, idx
  1566. def sleep_components_laser(units, M, wake_dur=600, wake_break=60, ndim=3, nsmooth=0,
  1567. pzscore=False, detrend=False, pplot=True, pc_sign=[],
  1568. kcuts=[], mode='w', mouse='', collapse=False,
  1569. ylim=[], config_file='mouse_config.txt'):
  1570. """
  1571. Similar to &sleep_components(), but allows for discarding laser intervals
  1572. for PC estimation.
  1573. Calculate principal components, excluding long wake periods in the recording.
  1574. Long wake periods are defined by the parameters $wake_dur (duration of wake episodes)
  1575. and $wake_break (wake episodes separated by less than $wake_break seconds are fused).
  1576. Note: To make sure that the laser is read, set $mouse to the mouse name
  1577. Parameters
  1578. ----------
  1579. units : pd.DataFrame
  1580. each column corresponds to a unit; each row is a time bin
  1581. M : np.array
  1582. hypnogram.
  1583. wake_dur : float, optional
  1584. Exclude wake periods that are longer than $wake_dur seconds. The default is 600.
  1585. wake_break : float, optional
  1586. Two wake periods that are separated by less than $wake_break seconds
  1587. are merged to one period. The default is 60.
  1588. ndim : int, optional
  1589. reduce data (matrix of firing rate vector) to $ndim dimensions using PCA. The default is 3.
  1590. nsmooth : float, optional
  1591. Smooth firing rate vector. The default is 0.
  1592. pzscore : boolean, optional
  1593. If True, zscore firing rates. The default is False.
  1594. detrend : boolean, optional
  1595. If True, detrend each firing rate vector.
  1596. pc_sign : list with $ndim elements, either 1 or -1.
  1597. The sign of PCs is ambiguous, so if preferred multiply, PC i with pc_sign[i]
  1598. kcuts : list of tuples or lists with two elements.
  1599. Discard the time interval ranging from kcuts[i][0] to kcuts[i][1] seconds
  1600. mode : string with characters 'r' and/or 'w'
  1601. if 'r' in mode, remove all REM indices for PC computation
  1602. if 'w' in mode, remove all Wake indices for PC computation
  1603. collapse: boolean
  1604. if True, plot each PC in its own axis
  1605. ylim: empty list, or tuple
  1606. if empty list, don't fix ylims, otherwise set plt.ylim(ylim) for each PC
  1607. config_file: str
  1608. mouse configuration file as loaded by &load_config()
  1609. Returns
  1610. -------
  1611. PC : np.array
  1612. The $ndim principal components. Note although the PCs have been calculated
  1613. without long wake periods, the returned PCs do include all wake periods.
  1614. V : np.array
  1615. Eigenvalues of the covariance matrix = Variance associated with each PC
  1616. Vh : np.array
  1617. each row in Vh is an eigenvector of the covariance matrix
  1618. idx : np.array
  1619. Indices of time bins used for PC calculation.
  1620. NOTE that @idx are the indices obtained
  1621. AFTER cutting out the KCUT intervals!
  1622. """
  1623. dt = 2.5
  1624. nhypno = np.min((len(M), units.shape[0]))
  1625. Morig = M.copy()
  1626. M = M[0:nhypno]
  1627. tidx = np.arange(0, nhypno)
  1628. # KCUT ####################################################################
  1629. # get the indices (in brainstate time) that we're going to completely discard:
  1630. if len(kcuts) > 0:
  1631. kidx = []
  1632. for kcut in kcuts:
  1633. a = int(kcut[0]/dt)
  1634. b = int(kcut[-1]/dt)
  1635. if b > len(M):
  1636. b = len(M)
  1637. kidx += list(np.arange(a, b))
  1638. tidx = np.setdiff1d(tidx, kidx)
  1639. M = M[tidx]
  1640. nhypno = len(tidx)
  1641. print(len(tidx))
  1642. ###########################################################################
  1643. unitIDs = [unit for unit in units.columns if '_' in unit]
  1644. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  1645. nsample = nhypno # number of time points
  1646. nvar = len(unitIDs) # number of units
  1647. # Note the number of samples stays the same; while the number is variables
  1648. # (or dimensions) is reduced! We want to keep the same number of time points,
  1649. # but have only a few 'modes'.
  1650. R = np.zeros((nvar, nsample))
  1651. #@tidx are the indices we're further considering.
  1652. for i,unit in enumerate(unitIDs):
  1653. tmp = sleepy.smooth_data(np.array(units[unit]),nsmooth)
  1654. tmp = tmp[tidx]
  1655. if detrend:
  1656. tmp = scipy.signal.detrend(tmp)
  1657. if pzscore:
  1658. R[i,:] = (tmp - tmp.mean()) / tmp.std()
  1659. else:
  1660. R[i,:] = tmp
  1661. # find long wake blocks:
  1662. widx = sleepy.get_sequences(np.where(M==2)[0], ibreak=int(wake_break/dt))
  1663. # all REM sequences
  1664. ridx = sleepy.get_sequences(np.where(M==1)[0])
  1665. nidx = sleepy.get_sequences(np.where(M==3)[0])
  1666. tmp = []
  1667. for w in widx:
  1668. if len(w) * dt > wake_dur:
  1669. tmp += list(w)
  1670. widx = tmp
  1671. tmp = []
  1672. for r in ridx:
  1673. tmp += list(r)
  1674. ridx = tmp
  1675. tmp = []
  1676. for r in nidx:
  1677. tmp += list(r)
  1678. nidx = tmp
  1679. nhypno = np.min((len(M), units.shape[0]))
  1680. idx = np.arange(0, np.min((len(M), units.shape[0])))
  1681. if 'w' in mode:
  1682. idx = np.setdiff1d(idx, widx)
  1683. else:
  1684. widx = []
  1685. if 'r' in mode:
  1686. idx = np.setdiff1d(idx, ridx)
  1687. if 'n' in mode:
  1688. idx = np.setdiff1d(idx, nidx)
  1689. else:
  1690. ridx = []
  1691. # collect laser information ###############################################
  1692. if mouse:
  1693. ddir = load_config(config_file)[mouse]['SL_PATH']
  1694. ppath, name = os.path.split(ddir)
  1695. if os.path.isfile(os.path.join(ddir, 'laser_%s.mat' % name)):
  1696. lsr = so.loadmat(os.path.join(ddir, 'laser_%s.mat' % name), squeeze_me=True)['laser']
  1697. sr = sleepy.get_snr(ppath, name)
  1698. nbin = int(np.round(sr)*2.5)
  1699. idxs, idxe = sleepy.laser_start_end(lsr)
  1700. idxs = [int(i/nbin) for i in idxs]
  1701. idxe = [int(i/nbin) for i in idxe]
  1702. laser_idx = []
  1703. for (si,sj) in zip(idxs, idxe):
  1704. laser_idx += list(range(si,sj+1))
  1705. nlsr = int(np.floor(lsr.shape[0]/nbin))
  1706. laser = np.zeros((nlsr,))
  1707. laser[laser_idx] = 1
  1708. laser = laser[tidx]
  1709. laser_idx = np.where(laser == 1)[0]
  1710. print('Removing laser indices from mouse %s' % mouse)
  1711. idx = np.setdiff1d(idx, laser_idx)
  1712. ###########################################################################
  1713. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  1714. Y = R[:,idx].T / np.sqrt(nsample-1)
  1715. # make sure the the columns of Y are mean-zero
  1716. for i in range(nvar):
  1717. Y[:,i] = Y[:,i] - Y[:,i].mean()
  1718. R[i,:] = R[i,:] - R[i,idx].mean()
  1719. # SVD
  1720. # Note that each column is a neuron,
  1721. # and each row is a time point
  1722. U,S,Vh = scipy.linalg.svd(Y)
  1723. # each row in Vh is an eigenvector of the COV matrix;
  1724. # to get the PCs, we project R onto the eigenvectors:
  1725. PC = np.dot(Vh, R)[0:ndim,:]
  1726. V = S**2
  1727. if len(pc_sign) > 0:
  1728. i = 0
  1729. for s in pc_sign:
  1730. PC[i,:] = PC[i,:] * s
  1731. i += 1
  1732. if pplot:
  1733. add_laser = False
  1734. t = np.arange(0, nhypno) * dt
  1735. plt.figure()
  1736. tmp = widx+ridx
  1737. tmp.sort()
  1738. widx = sleepy.get_sequences(np.array(tmp))
  1739. axes_exc = plt.axes([0.2, 0.9, 0.7, 0.05])
  1740. for w in widx:
  1741. if len(w) > 1:
  1742. if w[-1] < len(M):
  1743. plt.plot([t[w[0]], t[w[-1]]], [1, 1], 'k', lw=2)
  1744. plt.ylim([0, 2])
  1745. plt.xlim((t[0], t[-1]))
  1746. # if laser exists also add laser here --
  1747. if mouse:
  1748. ddir = load_config(config_file)[mouse]['SL_PATH']
  1749. ppath, name = os.path.split(ddir)
  1750. if os.path.isfile(os.path.join(ddir, 'laser_%s.mat' % name)):
  1751. lsr = so.loadmat(os.path.join(ddir, 'laser_%s.mat' % name), squeeze_me=True)['laser']
  1752. sr = sleepy.get_snr(ppath, name)
  1753. nbin = int(np.round(sr)*2.5)
  1754. idxs, idxe = sleepy.laser_start_end(lsr)
  1755. idxs = [int(i/nbin) for i in idxs]
  1756. idxe = [int(i/nbin) for i in idxe]
  1757. laser_idx = []
  1758. for (si,sj) in zip(idxs, idxe):
  1759. laser_idx += list(range(si,sj+1))
  1760. nlsr = int(np.floor(lsr.shape[0]/nbin))
  1761. laser = np.zeros((nlsr,))
  1762. laser[laser_idx] = 1
  1763. laser = laser[tidx]
  1764. lsr_seq = sleepy.get_sequences(np.where(laser==1)[0])
  1765. for w in lsr_seq:
  1766. if w[-1] < len(M):
  1767. plt.plot([t[w[0]], t[w[-1]]], [1.5, 1.5], 'b', lw=2)
  1768. plt.xlim((t[0], t[-1]))
  1769. add_laser = True
  1770. sleepy._despine_axes(axes_exc)
  1771. axes_brs = plt.axes([0.2, 0.85, 0.7, 0.05], sharex=axes_exc)
  1772. cmap = plt.cm.jet
  1773. my_map = cmap.from_list('brs', [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8]], 4)
  1774. tmp = axes_brs.pcolorfast(t, [0, 1], np.array([M]), vmin=0, vmax=3)
  1775. tmp.set_cmap(my_map)
  1776. axes_brs.axis('tight')
  1777. sleepy._despine_axes(axes_brs)
  1778. if collapse:
  1779. plt.axes([0.2, 0.2, 0.7, 0.6], sharex=axes_brs)
  1780. plt.plot(t, PC[:,:].T)
  1781. plt.xlim((t[0], t[-1]))
  1782. sns.despine()
  1783. plt.xlabel('Time (s)')
  1784. plt.ylabel('PC')
  1785. else:
  1786. clrs = sns.color_palette("husl", ndim)
  1787. d = (0.6 / ndim) * 0.3
  1788. ny = (0.6 / ndim)-d
  1789. for i in range(ndim):
  1790. ax = plt.axes([0.2, 0.2+i*(ny+d), 0.7, ny], sharex=axes_exc)
  1791. ax.plot(t, PC[ndim-1-i,:], color=clrs[i])
  1792. plt.xlim([t[0], t[-1]])
  1793. sleepy.box_off(ax)
  1794. plt.ylabel('PC%d' % (ndim-i))
  1795. if add_laser:
  1796. ylims = ax.get_ylim()
  1797. for w in lsr_seq:
  1798. if w[-1] < len(M):
  1799. if 1 in M[w]:
  1800. color = 'b'
  1801. else:
  1802. color = 'r'
  1803. #if M[w[0]] == 3:
  1804. #plt.plot(t[w[0]], PC[ndim-1-i,w[0]-1:w[0]+1].mean(), '.', color=color, lw=2)
  1805. plt.plot(t[w[0]], PC[ndim-1-i,w[0]], '.', color=color, lw=2)
  1806. laser_tend = (w[-1] - w[0]) * dt
  1807. dy = ylims[1] - ylims[0]
  1808. ax.add_patch(patches.Rectangle(
  1809. (t[w[0]], ylims[0]), laser_tend, dy,
  1810. facecolor=[0.6, 0.6, 1], edgecolor=[0.6, 0.6, 1]))
  1811. if i > 0:
  1812. ax.spines["bottom"].set_visible(False)
  1813. ax.axes.get_xaxis().set_visible(False)
  1814. else:
  1815. plt.xlabel('Time (s)')
  1816. if len(ylim) > 0:
  1817. plt.ylim(ylim)
  1818. var = S**2
  1819. var_total = np.sum(S**2)
  1820. # Calculate the cumulative variance explained, i.e. how much
  1821. # of the variance is captured by the first i principal components.
  1822. p = []
  1823. for i in range(1,len(var)+1):
  1824. s = np.sum(var[0:i])
  1825. p.append(s/var_total)
  1826. plt.figure(figsize=(4,4))
  1827. plt.plot(p, '.', color='gray')
  1828. plt.xlabel(r'$\mathrm{PC_i}$')
  1829. plt.ylabel('Cum. variance')
  1830. plt.subplots_adjust(bottom=0.2, left=0.2)
  1831. plt.ylim([0, 1.1])
  1832. sns.despine()
  1833. if len(kcuts) > 0:
  1834. # find long wake blocks:
  1835. widx = sleepy.get_sequences(np.where(Morig==2)[0], ibreak=int(wake_break/dt))
  1836. tmp = []
  1837. for w in widx:
  1838. if len(w) * dt > wake_dur:
  1839. tmp += list(w)
  1840. widx = tmp
  1841. nhypno = np.min((len(Morig), units.shape[0]))
  1842. idx_total = np.arange(0, nhypno)
  1843. idx_total = np.setdiff1d(idx_total, kidx)
  1844. idx_total = np.setdiff1d(idx_total, widx)
  1845. return PC, V, Vh, idx
  1846. def sleep_components_fine(ids, wake_dur=600, wake_break=60, ndim=3, nsmooth=0,
  1847. pzscore=False, detrend=False, pplot=True, pc_sign=[], ndown=250,
  1848. kcuts=[], mode='w', ppath='', mouse='', collapse=False,
  1849. ylim=[], config_file='mouse_config.txt'):
  1850. dt = 2.5
  1851. NDOWN = ndown
  1852. NUP = int(dt / (0.001 * NDOWN))
  1853. fine_scale = True
  1854. if len(config_file) == 0:
  1855. config_file = 'mouse_config.txt'
  1856. path = load_config(config_file)[mouse]['SL_PATH']
  1857. ppath, file = os.path.split(path)
  1858. M = sleepy.load_stateidx(ppath, file)[0]
  1859. ###########################################################################
  1860. recs = load_config(config_file)[mouse]
  1861. if 'TR_PATH' in recs:
  1862. tr_path = load_config(config_file)[mouse]['TR_PATH']
  1863. else:
  1864. tr_path = load_config(config_file)[mouse]['NP_PATH']
  1865. units = np.load(os.path.join(tr_path,'1k_train.npz'))
  1866. unitIDs = [unit for unit in list(units.keys()) if '_' in unit]
  1867. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  1868. # re-scale dt
  1869. dt = dt / NUP
  1870. M = upsample_mx(M, NUP)
  1871. nhypno = int(np.min((len(M), units[unitIDs[0]].shape[0]/NDOWN)))
  1872. M = M[0:nhypno]
  1873. tidx = np.arange(0, nhypno)
  1874. # NEW 07/01/22:
  1875. # get the indices (in brainstate time) that we're going to completely discard:
  1876. if len(kcuts) > 0:
  1877. kidx = []
  1878. for kcut in kcuts:
  1879. a = int(kcut[0]/dt)
  1880. b = int(kcut[-1]/dt)
  1881. if b > len(M):
  1882. b = len(M)
  1883. kidx += list(np.arange(a, b))
  1884. tidx = np.setdiff1d(tidx, kidx)
  1885. M = M[tidx]
  1886. nhypno = len(tidx)
  1887. ###########################################################################
  1888. nsample = nhypno # number of time points
  1889. nvar = len(unitIDs) # number of units
  1890. R = np.zeros((nvar, nsample))
  1891. #@tidx are the indices we're further considereing.
  1892. print('Starting downsampling, smoothing and z-scoring')
  1893. fr_file = os.path.join(tr_path, 'fr_fine_ndown%d.mat' % NDOWN)
  1894. if not os.path.isfile(fr_file):
  1895. for i,unit in enumerate(unitIDs):
  1896. tmp = sleepy.downsample_vec(np.array(units[unit]), NDOWN)
  1897. R[i,:] = tmp[tidx]
  1898. so.savemat(fr_file, {'R':R, 'ndown':NDOWN})
  1899. else:
  1900. # NOTE: the save matrix are the responses AFTER KCUT!
  1901. R = so.loadmat(fr_file, squeeze_me=True)['R']
  1902. for i,unit in enumerate(unitIDs):
  1903. tmp = R[i,:]
  1904. tmp = sleepy.smooth_data(tmp, nsmooth)
  1905. if pzscore:
  1906. R[i,:] = (tmp[:] - tmp[:].mean()) / tmp[:].std()
  1907. else:
  1908. R[i,:] = tmp[:]
  1909. # find long wake blocks:
  1910. widx = sleepy.get_sequences(np.where(M==2)[0], ibreak=int(wake_break/dt))
  1911. # all REM sequences
  1912. ridx = sleepy.get_sequences(np.where(M==1)[0])
  1913. nidx = sleepy.get_sequences(np.where(M==3)[0])
  1914. tmp = []
  1915. for w in widx:
  1916. if len(w) * dt > wake_dur:
  1917. tmp += list(w)
  1918. widx = tmp
  1919. tmp = []
  1920. for r in ridx:
  1921. tmp += list(r)
  1922. ridx = tmp
  1923. tmp = []
  1924. for r in nidx:
  1925. tmp += list(r)
  1926. nidx = tmp
  1927. nhypno = np.min((len(M), R.shape[1]))
  1928. idx = np.arange(0, nhypno)
  1929. if 'w' in mode:
  1930. idx = np.setdiff1d(idx, widx)
  1931. else:
  1932. widx = []
  1933. if 'r' in mode:
  1934. idx = np.setdiff1d(idx, ridx)
  1935. if 'n' in mode:
  1936. idx = np.setdiff1d(idx, nidx)
  1937. else:
  1938. ridx = []
  1939. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  1940. Y = R[:,idx].T / np.sqrt(nsample-1)
  1941. # make sure the the columns of Y are mean-zero
  1942. for i in range(nvar):
  1943. Y[:,i] = Y[:,i] - Y[:,i].mean()
  1944. R[i,:] = R[i,:] - R[i,idx].mean()
  1945. # SVD
  1946. # Note that each column is a neuron,
  1947. # and each row is a time point
  1948. U,S,Vh = scipy.linalg.svd(Y)
  1949. # each row in Vh is an eigenvector of the COV matrix;
  1950. # to get the PCs, we project R onto the eigenvectors:
  1951. PC = np.dot(Vh, R)[0:ndim,:]
  1952. V = S**2
  1953. if len(pc_sign) > 0:
  1954. i = 0
  1955. for s in pc_sign:
  1956. PC[i,:] = PC[i,:] * s
  1957. i += 1
  1958. ## add figure
  1959. if pplot:
  1960. t = np.arange(0, nhypno) * dt
  1961. plt.figure()
  1962. axes_brs = plt.axes([0.2, 0.85, 0.7, 0.05])
  1963. cmap = plt.cm.jet
  1964. my_map = cmap.from_list('brs', [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8]], 4)
  1965. tmp = axes_brs.pcolorfast(t, [0, 1], np.array([M]), vmin=0, vmax=3)
  1966. tmp.set_cmap(my_map)
  1967. axes_brs.axis('tight')
  1968. sleepy._despine_axes(axes_brs)
  1969. plt.axes([0.2, 0.2, 0.7, 0.6], sharex=axes_brs)
  1970. plt.plot(t, PC[:,:].T)
  1971. plt.xlim((t[0], t[-1]))
  1972. sns.despine()
  1973. plt.xlabel('Time (s)')
  1974. plt.ylabel('PC')
  1975. return PC, V
  1976. def kcut_idx(M, X, kcuts, dt=2.5):
  1977. """
  1978. Using the values defined in the mouse_config.txt file (field KCUT:), determine
  1979. the indices used for further calculations.
  1980. @param: np.array, brainstate sequence
  1981. @param X: np.pandas or np.array, array or DataFrame with time axis using same binning as @M
  1982. @param kcuts: list of tuples, areas at the beginning or end that should be discarded
  1983. @return tidx: np.array, list of indices in @M used for further calculation, i.e. indices
  1984. that are NOT within the ranges defined in @kcuts.
  1985. """
  1986. #n = np.max(X.shape)
  1987. #nhypno = np.min((len(M), n))
  1988. nhypno = len(M)
  1989. #M = M[0:nhypno]
  1990. tidx = np.arange(0, nhypno)
  1991. # get the indices (in brainstate time) that we're going to completely discard:
  1992. if len(kcuts) > 0:
  1993. kidx = []
  1994. for kcut in kcuts:
  1995. a = int(kcut[0]/dt)
  1996. b = int(kcut[-1]/dt)
  1997. if b > len(M):
  1998. b = len(M)
  1999. kidx += list(np.arange(a, b))
  2000. tidx = np.setdiff1d(tidx, kidx)
  2001. M = M[tidx]
  2002. nhypno = len(tidx)
  2003. return tidx
  2004. def kcut_idx2(M, kcuts, X = [], dt=2.5):
  2005. """
  2006. Using the values defined in the mouse_config.txt file (field KCUT:), determine
  2007. the indices used for further calculations.
  2008. @param: np.array, brainstate sequence
  2009. @param X: np.pandas or np.array, array or DataFrame with time axis using same binning as @M
  2010. @param kcuts: list of tuples, areas at the beginning or end that should be discarded
  2011. @return tidx: np.array, list of indices in @M used for further calculation, i.e. indices
  2012. that are NOT within the ranges defined in @kcuts.
  2013. """
  2014. nhypno = len(M)
  2015. if len(X) > 0:
  2016. n = np.max(X.shape)
  2017. nhypno = np.min((len(M), n))
  2018. tidx = np.arange(0, nhypno)
  2019. # get the indices (in brainstate time) that we're going to completely discard:
  2020. if len(kcuts) > 0:
  2021. kidx = []
  2022. for kcut in kcuts:
  2023. a = int(kcut[0]/dt)
  2024. b = int(kcut[-1]/dt)
  2025. if b > len(M):
  2026. b = len(M)
  2027. kidx += list(np.arange(a, b))
  2028. tidx = np.setdiff1d(tidx, kidx)
  2029. M = M[tidx]
  2030. nhypno = len(tidx)
  2031. return tidx
  2032. def align_pcsign(PC, mouse, rem_pca=0, kcuts=[], align_pc3=True, config_file='', pnorm_spec=True):
  2033. """
  2034. Automatically determine the sign of the given PCs.
  2035. Fix PC1 (normally increased during REM) though activity during REM sleep.
  2036. Fix PC2 (normally positively correlated with sigma power) through its
  2037. correlation with the sigma power.
  2038. Fix PC3...PCn the same way as PC1.
  2039. Parameters
  2040. ----------
  2041. PC : np.array
  2042. Matrix with PCs; each row corresponds to one PC.
  2043. mouse : str
  2044. mouse name.
  2045. rem_pca : int, optional
  2046. 0 or 1. If 0, assume that PC1 (PC[0,:]) is the "REM-PC".
  2047. kcuts : list of tuples
  2048. Areas at beginning or end to remove from recording. The default is [].
  2049. align_pc3 : bool, optional
  2050. If True, adjust sign of PC3, ... PCn using the same strategy as for PC1
  2051. config_file : str, optional
  2052. File name of mouse config file. The default is ''.
  2053. pnorm_spec : bool, optional
  2054. If True, normalize EEG spectrogram. The default is True.
  2055. Returns
  2056. -------
  2057. pc_sign : list of length PC.shape[0]
  2058. 1 or -1 depending on whether the orientation of the PC should be changed or not.
  2059. """
  2060. ndim = PC.shape[0]
  2061. pc_sign = [1 for i in range(ndim)]
  2062. if len(config_file) == 0:
  2063. config_file = 'mouse_config.txt'
  2064. path = load_config(config_file)[mouse]['SL_PATH']
  2065. ppath, name = os.path.split(path)
  2066. M = sleepy.load_stateidx(ppath, name)[0]
  2067. sigma = [10,15]
  2068. P = so.loadmat(os.path.join(ppath, name, 'sp_%s.mat'%name), squeeze_me=True)
  2069. SP = P['SP']
  2070. freq = P['freq']
  2071. dfreq = freq[1]-freq[0]
  2072. isigma = np.where((freq>=sigma[0])&(freq<=sigma[1]))[0]
  2073. # cut out kcuts: ###############
  2074. tidx = kcut_idx(M, PC, kcuts)
  2075. if tidx[-1] > PC.shape[1]-1:
  2076. tidx = tidx[0:-1]
  2077. M = M[tidx]
  2078. #PC = PC[:,tidx]
  2079. SP = SP[:,tidx]
  2080. ################################
  2081. if SP.shape[1] != PC.shape[1]:
  2082. print('Check kcut!!!')
  2083. print('Shape of SP = %d; shape of PC = %d' % (SP.shape[1], PC.shape[1]))
  2084. if rem_pca == 0:
  2085. sigma_pca = 1
  2086. else:
  2087. sigma_pca = 0
  2088. # fix PC1
  2089. mmin = np.min((len(M), PC.shape[1]))
  2090. M = M[0:mmin]
  2091. PC = PC[:,0:mmin]
  2092. pc1 = PC[rem_pca,:]
  2093. rem_idx = np.where(M==1)[0]
  2094. a = pc1[rem_idx].mean()
  2095. if a < 0:
  2096. pc_sign[rem_pca] = -1
  2097. # fix PC3...PCn the same way
  2098. if align_pc3:
  2099. if PC.shape[0] >= 3:
  2100. for j in range(2, PC.shape[0]):
  2101. pc3 = PC[j,:]
  2102. rem_idx = np.where(M==1)[0]
  2103. a = pc3[rem_idx].mean()
  2104. if a < 0:
  2105. pc_sign[j] = -1
  2106. # fix PC2 through correlation with sigma power
  2107. if pnorm_spec:
  2108. sp_mean = SP.mean(axis=1)
  2109. SP = np.divide(SP, np.tile(sp_mean, (SP.shape[1], 1)).T)
  2110. sigma_pow = SP[isigma,:].mean(axis=0)
  2111. else:
  2112. sigma_pow = SP[isigma, :].sum(axis=0)*dfreq
  2113. CC, t = state_correlation(PC[sigma_pca,:], sigma_pow, M, win=60, pplot=False)
  2114. CC = np.nanmean(CC, axis=0)
  2115. i = np.argmax(np.abs(CC))
  2116. if CC[i] < 0:
  2117. pc_sign[sigma_pca] = -1
  2118. return pc_sign
  2119. def plot_pcs_withsigma(PC, M, mouse, ndim=2, pc_sign=[], kcuts=[],
  2120. ma_thr=10, ma_rem_exception=False,
  2121. tstart=0, tend=-1,
  2122. dt=2.5, sigma=[10,15], fmax=20, vm=[],
  2123. box_filt=[], pnorm_spec=True,
  2124. reverse_pcs=False, tlegend=120, zoomin=[], r_mu=[10,200],
  2125. pplot=True, config_file=''):
  2126. """
  2127. Plot principal components along with sigma power.
  2128. Parameters
  2129. ----------
  2130. PC : TYPE
  2131. DESCRIPTION.
  2132. M : TYPE
  2133. DESCRIPTION.
  2134. mouse : TYPE
  2135. DESCRIPTION.
  2136. ndim : TYPE, optional
  2137. DESCRIPTION. The default is 2.
  2138. pc_sign : TYPE, optional
  2139. DESCRIPTION. The default is [].
  2140. kcuts : TYPE, optional
  2141. DESCRIPTION. The default is [].
  2142. ma_thr : TYPE, optional
  2143. DESCRIPTION. The default is 10.
  2144. ma_rem_exception : TYPE, optional
  2145. DESCRIPTION. The default is False.
  2146. tstart : TYPE, optional
  2147. DESCRIPTION. The default is 0.
  2148. tend : TYPE, optional
  2149. DESCRIPTION. The default is -1.
  2150. dt : TYPE, optional
  2151. DESCRIPTION. The default is 2.5.
  2152. sigma : TYPE, optional
  2153. DESCRIPTION. The default is [10,15].
  2154. fmax : TYPE, optional
  2155. DESCRIPTION. The default is 20.
  2156. vm : list, optional
  2157. List with two elements defining vmin and vmax for the EEG spectrogram colormap.
  2158. If [], matplotlib will automatically set the color range.
  2159. box_filt : list, optional
  2160. Dimensions of box filter to smooth EEG spectrogram. If [], no filteringThe default is [].
  2161. pnorm_spec : bool, optional
  2162. If True, normalize spectrogram.
  2163. reverse_pcs : TYPE, optional
  2164. DESCRIPTION. The default is False.
  2165. tlegend : TYPE, optional
  2166. DESCRIPTION. The default is 120.
  2167. zoomin : TYPE, optional
  2168. DESCRIPTION. The default is [].
  2169. pplot : TYPE, optional
  2170. DESCRIPTION. The default is True.
  2171. config_file : TYPE, optional
  2172. DESCRIPTION. The default is ''.
  2173. Returns
  2174. -------
  2175. PC_orig : TYPE
  2176. DESCRIPTION.
  2177. M : TYPE
  2178. DESCRIPTION.
  2179. sigma_pow : TYPE
  2180. DESCRIPTION.
  2181. """
  2182. if len(r_mu) == 0:
  2183. no_emg = True
  2184. else:
  2185. no_emg = False
  2186. if len(config_file) == 0:
  2187. config_file = 'mouse_config.txt'
  2188. path = load_config(config_file)[mouse]['SL_PATH']
  2189. ppath, name = os.path.split(path)
  2190. M = sleepy.load_stateidx(ppath, name)[0]
  2191. # load EEG spectrogram
  2192. P = so.loadmat(os.path.join(ppath, name, 'sp_%s.mat'%name), squeeze_me=True)
  2193. SP = P['SP']
  2194. # load EMG spectrogram
  2195. P = so.loadmat(os.path.join(ppath, name, 'msp_%s.mat'%name), squeeze_me=True)
  2196. SPM = P['mSP']
  2197. # cut out kcuts: ###############
  2198. tidx = kcut_idx(M, PC, kcuts)
  2199. tidx = tidx[0:PC.shape[1]]
  2200. M = M[tidx]
  2201. #PC = PC[:,tidx]
  2202. SP = SP[:,tidx]
  2203. if not(len(M) == SP.shape[1] == PC.shape[1]):
  2204. print('Something went wrong with KCUT')
  2205. print('returning')
  2206. return
  2207. ################################
  2208. # flatten out MAs #########################################################
  2209. if ma_thr>0:
  2210. seq = sleepy.get_sequences(np.where(M==2)[0])
  2211. for s in seq:
  2212. if np.round(len(s)*dt) <= ma_thr:
  2213. if ma_rem_exception:
  2214. if (s[0]>1) and (M[s[0] - 1] != 1):
  2215. M[s] = 3
  2216. else:
  2217. M[s] = 3
  2218. ###########################################################################
  2219. freq = P['freq']
  2220. dfreq = freq[1]-freq[0]
  2221. isigma = np.where((freq>=sigma[0])&(freq<=sigma[1]))[0]
  2222. if len(box_filt) > 0:
  2223. filt = np.ones(box_filt)
  2224. filt = np.divide(filt, filt.sum())
  2225. SP = scipy.signal.convolve2d(SP, filt, boundary='symm', mode='same')
  2226. if pnorm_spec:
  2227. sp_mean = SP.mean(axis=1)
  2228. SP = np.divide(SP, np.tile(sp_mean, (SP.shape[1], 1)).T)
  2229. sigma_pow = SP[isigma,:].mean(axis=0)
  2230. else:
  2231. sigma_pow = SP[isigma, :].sum(axis=0)*dfreq
  2232. if not no_emg:
  2233. #EMG amplitude
  2234. i_mu = np.where((freq >= r_mu[0]) & (freq <= r_mu[1]))[0]
  2235. p_mu = np.sqrt(SPM[i_mu, :].sum(axis=0) * dfreq)
  2236. istart = int(tstart/dt)
  2237. if tend == -1:
  2238. iend = len(M)
  2239. else:
  2240. iend = int(tend/dt)
  2241. # cut out time interval istart - iend:
  2242. M = M[istart:iend]
  2243. PC = PC[:,istart:iend]
  2244. PC_orig = PC.copy()
  2245. PC_orig = PC_orig[istart:iend]
  2246. sigma_pow = sigma_pow[istart:iend]
  2247. p_mu = p_mu[istart:iend]
  2248. t = np.arange(0, len(M))*dt
  2249. if pplot:
  2250. plt.figure()
  2251. if no_emg:
  2252. ### define all axes:
  2253. axes_brs = plt.axes([0.1, 0.9, 0.8, 0.05])
  2254. axes_spec = plt.axes([0.1, 0.72, 0.8, 0.15], sharex=axes_brs)
  2255. # axes for colorbar
  2256. axes_cbar = plt.axes([0.9, 0.72, 0.05, 0.15])
  2257. axes_sig = plt.axes([0.1, 0.57, 0.8, 0.1], sharex=axes_brs)
  2258. axes_pcs = plt.axes([0.1,0.05,0.8,0.5], sharex=axes_brs)
  2259. axes_legend = plt.axes([0.1,0.04,0.8,0.04], sharex=axes_brs)
  2260. else:
  2261. # -----------------------------------------------------------------
  2262. # Define a vertical stack: (rel. heights add up to 1.00)
  2263. # -----------------------------------------------------------------
  2264. lay = {
  2265. "axes_brs" : 0.05, # hypnogram / brs bar
  2266. "axes_spec" : 0.15, # spectrogram
  2267. "axes_emg" : 0.10, # NEW: EMG, same height as sig
  2268. "axes_sig" : 0.10, # significance trace
  2269. "axes_pcs" : 0.52, # PC/trace panel
  2270. "axes_legend": 0.04 # legend bar
  2271. }
  2272. pad = 0.01 # small vertical padding between rows
  2273. # cumulative bottom edges
  2274. y_bottom = 1.0
  2275. axes_dict = {}
  2276. for name, h in lay.items():
  2277. y_bottom -= h
  2278. ax = plt.axes([0.10, y_bottom, 0.80, h], sharex=axes_dict.get("axes_brs"))
  2279. axes_dict[name] = ax
  2280. y_bottom -= pad
  2281. # separate colour bar next to spectrogram
  2282. axes_dict["axes_cbar"] = plt.axes([0.91, # x‑pos
  2283. 1.0 - lay["axes_brs"] - pad - lay["axes_spec"],
  2284. 0.04, # width
  2285. lay["axes_spec"]])
  2286. # unpack for backward compatibility
  2287. axes_brs = axes_dict["axes_brs"]
  2288. axes_spec = axes_dict["axes_spec"]
  2289. axes_emg = axes_dict["axes_emg"]
  2290. axes_sig = axes_dict["axes_sig"]
  2291. axes_pcs = axes_dict["axes_pcs"]
  2292. axes_legend = axes_dict["axes_legend"]
  2293. axes_cbar = axes_dict["axes_cbar"]
  2294. # show brainstate
  2295. cmap = plt.cm.jet
  2296. my_map = cmap.from_list('brs', [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8]], 4)
  2297. tmp = axes_brs.pcolorfast(t, [0, 1], np.array([M]), vmin=0, vmax=3)
  2298. tmp.set_cmap(my_map)
  2299. axes_brs.axis('tight')
  2300. sleepy._despine_axes(axes_brs)
  2301. # show EEG spectrogram
  2302. # calculate median for choosing right saturation for heatmap
  2303. med = np.median(SP.max(axis=0))
  2304. if len(vm) == 0:
  2305. vm = [0, med*2.0]
  2306. ifreq = np.where(freq <= fmax)[0]
  2307. im = axes_spec.pcolorfast(t, freq[ifreq], SP[ifreq, istart:iend], cmap='jet', vmin=vm[0], vmax=vm[1])
  2308. axes_spec.axis('tight')
  2309. axes_spec.set_xticklabels([])
  2310. axes_spec.set_xticks([])
  2311. axes_spec.spines["bottom"].set_visible(False)
  2312. axes_spec.set_ylabel('Freq (Hz)')
  2313. sleepy.box_off(axes_spec)
  2314. axes_spec.set_xlim([t[0], t[-1]])
  2315. # colorbar for EEG spectrogram
  2316. cb = plt.colorbar(im, ax=axes_cbar, pad=0.0, aspect=10.0)
  2317. if pnorm_spec:
  2318. cb.set_label('Norm. power')
  2319. else:
  2320. cb.set_label('Power ($\mathrm{\mu}$V$^2$s)')
  2321. #if len(cb_ticks) > 0:
  2322. # cb.set_ticks(cb_ticks)
  2323. axes_cbar.set_alpha(0.0)
  2324. sleepy._despine_axes(axes_cbar)
  2325. #im = axes_spec.pcolorfast(t, freq[ifreq], SP[ifreq, istart:iend], cmap='jet', vmin=vm[0], vmax=vm[1])
  2326. if not(no_emg):
  2327. axes_emg.plot(t, p_mu, color='k')
  2328. axes_emg.set_xlim([t[0], t[-1]])
  2329. axes_emg.set_ylabel('EMG ampl. $\mathrm{\mu}$V')
  2330. axes_emg.spines["top"].set_visible(False)
  2331. axes_emg.spines["right"].set_visible(False)
  2332. axes_emg.spines["bottom"].set_visible(False)
  2333. axes_emg.axes.get_xaxis().set_visible(False)
  2334. # show sigmapower
  2335. axes_sig.plot(t, sigma_pow, color='gray')
  2336. axes_sig.set_xlim([t[0], t[-1]])
  2337. axes_sig.set_ylabel('$\mathrm{\sigma}$ power')
  2338. axes_sig.spines["top"].set_visible(False)
  2339. axes_sig.spines["right"].set_visible(False)
  2340. axes_sig.spines["bottom"].set_visible(False)
  2341. axes_sig.axes.get_xaxis().set_visible(False)
  2342. a = np.percentile(sigma_pow, 99.5)
  2343. plt.ylim([0, a+a*0.1])
  2344. # show PCs
  2345. for i in range(ndim):
  2346. PC[i,:] = PC[i,:] - np.min(PC[i,:])
  2347. pc_max = []
  2348. for i in range(ndim):
  2349. p = np.max(PC[i,:])
  2350. pc_max.append(p)
  2351. mmax = np.max(np.array(pc_max))
  2352. pos = [0]
  2353. for i in range(1,ndim):
  2354. if reverse_pcs:
  2355. PC[i,:] = PC[i,:] + i*mmax
  2356. pos.append(i*mmax)
  2357. else:
  2358. PC[i,:] = PC[i,:] - i*mmax
  2359. pos.append(-i*mmax)
  2360. # axes for PCs
  2361. # colors for PCs
  2362. cmap = sns.color_palette("husl", ndim)
  2363. for i in range(ndim):
  2364. axes_pcs.plot(t, PC[i,:], c=cmap[i])
  2365. axes_pcs.text(t[-1], pos[i], 'PC%d' % (i+1), fontsize=14, color=cmap[i])
  2366. plt.xlim((t[0], t[-1]))
  2367. axes_pcs.spines["left"].set_visible(False)
  2368. axes_pcs.spines["right"].set_visible(False)
  2369. axes_pcs.axes.get_yaxis().set_visible(False)
  2370. sleepy._despine_axes(axes_pcs)
  2371. if len(zoomin) > 0:
  2372. zoomin = [int(z/dt) for z in zoomin]
  2373. for z in zoomin:
  2374. pos = np.array(pos)
  2375. plt.plot([t[z], t[z]], [mmax, -(ndim-1)*mmax], 'k--')
  2376. # axes for time legend
  2377. axes_legend.plot([0, tlegend], [1, 1], lw=2, color='k')
  2378. axes_legend.set_ylim([-1, 1])
  2379. axes_legend.text(0, -2, '%d s' % tlegend, verticalalignment='bottom', horizontalalignment='left')
  2380. plt.xlim((t[0], t[-1]))
  2381. sleepy._despine_axes(axes_legend)
  2382. return PC_orig, M, sigma_pow
  2383. def sleep_subspaces(units, mouse, ndim=2, nsmooth=0, ma_thr=10, ma_rem_exception=False,
  2384. pzscore=False, detrend=False, pplot=True, coords=[], traj_mode='full', trig_state=1,
  2385. local_rotation=True, pspec=True,
  2386. kcuts=[], proj_3d=False, config_file=''):
  2387. """
  2388. Calculate (and plot) PCA separately for each state (REM, Wake, NREM).
  2389. Parameters
  2390. ----------
  2391. units : np.DataFrame or []
  2392. If [], use 1 ms spike trains to calculate firing rates.
  2393. mouse : TYPE
  2394. DESCRIPTION.
  2395. ndim : int, optional
  2396. Number of PCs to keep for dimensionality reduction.
  2397. The default is 2.
  2398. nsmooth : TYPE, optional
  2399. DESCRIPTION. The default is 0.
  2400. ma_thr : float, optional
  2401. Wake sequences <= $ma_thr s are interpreted as NREM (3). The default is 10.
  2402. ma_rem_exception : bool, optional
  2403. If True, then the MA rule does not apply for wake episodes directly following REM.
  2404. The default is False.
  2405. pzscore : TYPE, optional
  2406. DESCRIPTION. The default is False.
  2407. pplot : TYPE, optional
  2408. DESCRIPTION. The default is True.
  2409. coords : list, optional
  2410. Specific the two PCs that should be shown on the x and y-axis.
  2411. PC1 corresponds to "0".
  2412. The default is [].
  2413. traj_mode : string, optional
  2414. If 'full', show complete trajectory.
  2415. If 'trig', only show trajectories for the specific state transitions (-> trig_state)
  2416. The default is 'full'.
  2417. trig_state : int, optional
  2418. 1,2, or 3. If traj_mode == 'trig', only show the the transitions to state $trig_state
  2419. instead of the full trajectories across the whole recording session
  2420. The default is 1.
  2421. local_rotation : bool, optional
  2422. If True, rotate the ndim dimensional space (by performing another PCA) and
  2423. use the resulting first two dimensions to get the PCs.
  2424. If True, the parameter @coords has no effect.
  2425. The default is True.
  2426. pspec : TYPE, optional
  2427. DESCRIPTION. The default is True.
  2428. kcuts : TYPE, optional
  2429. DESCRIPTION. The default is [].
  2430. proj_3d : bool, optional
  2431. If True, plot 3D subspaces
  2432. config_file : str
  2433. file name of mouse configuration file, as loaded by &load_config()
  2434. Returns
  2435. -------
  2436. pc_dict : TYPE
  2437. DESCRIPTION.
  2438. vh_dict : TYPE
  2439. DESCRIPTION.
  2440. idx_dict : TYPE
  2441. DESCRIPTION.
  2442. """
  2443. dt = 2.5
  2444. NDOWN = 500
  2445. NUP = int(dt / (0.001 * NDOWN))
  2446. fine_scale = False
  2447. if len(config_file) == 0:
  2448. config_file = 'mouse_config.txt'
  2449. path = load_config(config_file)[mouse]['SL_PATH']
  2450. ppath, file = os.path.split(path)
  2451. M = sleepy.load_stateidx(ppath, file)[0]
  2452. ###########################################################################
  2453. if len(units) == 0:
  2454. fine_scale = True
  2455. tr_path = load_config(config_file)[mouse]['TR_PATH']
  2456. units = np.load(os.path.join(tr_path,'1k_train.npz'))
  2457. unitIDs = [unit for unit in list(units.keys()) if '_' in unit]
  2458. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  2459. dt = dt / NUP
  2460. M = upsample_mx(M, NUP)
  2461. nhypno = int(np.min((len(M), units[unitIDs[0]].shape[0]/NDOWN)))
  2462. else:
  2463. unitIDs = [unit for unit in units.columns if '_' in unit]
  2464. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  2465. nhypno = np.min((len(M), units.shape[0]))
  2466. M = M[0:nhypno]
  2467. tidx = np.arange(0, nhypno)
  2468. # OLD:
  2469. # if len(kcut) > 0:
  2470. # kidx = np.arange(int(kcut[0]/dt), int(kcut[-1]/dt))
  2471. # tidx = np.setdiff1d(tidx, kidx)
  2472. # M = M[tidx]
  2473. # nhypno = len(tidx)
  2474. # NEW 07/01/22:
  2475. # get the indices (in brainstate time) that we're going to completely discard:
  2476. if len(kcuts) > 0:
  2477. kidx = []
  2478. for kcut in kcuts:
  2479. a = int(kcut[0]/dt)
  2480. b = int(kcut[-1]/dt)
  2481. if b > len(M):
  2482. b = len(M)
  2483. kidx += list(np.arange(a, b))
  2484. tidx = np.setdiff1d(tidx, kidx)
  2485. M = M[tidx]
  2486. nhypno = len(tidx)
  2487. ###########################################################################
  2488. #print(len(tidx))
  2489. nsample = nhypno # number of time points
  2490. nvar = len(unitIDs) # number of units
  2491. R = np.zeros((nvar, nsample))
  2492. #@tidx are the indices we're further considereing.
  2493. print('Starting downsampling, smoothing and z-scoring')
  2494. if fine_scale:
  2495. fr_file = os.path.join(tr_path, 'fr_fine_ndown%d.mat' % NDOWN)
  2496. if not os.path.isfile(fr_file):
  2497. for i,unit in enumerate(unitIDs):
  2498. tmp = sleepy.downsample_vec(np.array(units[unit]), NDOWN)
  2499. R[i,:] = tmp[tidx]
  2500. so.savemat(fr_file, {'R':R, 'ndown':NDOWN})
  2501. else:
  2502. R = so.loadmat(fr_file, squeeze_me=True)['R']
  2503. for i,unit in enumerate(unitIDs):
  2504. tmp = R[i,:]
  2505. tmp = sleepy.smooth_data(tmp, nsmooth)
  2506. if pzscore:
  2507. R[i,:] = (tmp[tidx] - tmp[tidx].mean()) / tmp[tidx].std()
  2508. else:
  2509. R[i,:] = tmp[tidx]
  2510. if not fine_scale:
  2511. for i,unit in enumerate(unitIDs):
  2512. tmp = sleepy.smooth_data(np.array(units[unit]), nsmooth)
  2513. tmp = tmp[tidx]
  2514. if detrend:
  2515. tmp = scipy.signal.detrend(tmp)
  2516. if pzscore:
  2517. #R[i,:] = (tmp[tidx] - tmp[tidx].mean()) / tmp[tidx].std()
  2518. R[i,:] = (tmp - tmp.mean()) / tmp.std()
  2519. else:
  2520. #R[i,:] = tmp[tidx]
  2521. R[i,:] = tmp
  2522. # flatten out MAs
  2523. if ma_thr>0:
  2524. seq = sleepy.get_sequences(np.where(M==2)[0])
  2525. for s in seq:
  2526. if np.round(len(s)*dt) <= ma_thr:
  2527. if ma_rem_exception:
  2528. if (s[0]>1) and (M[s[0] - 1] != 1):
  2529. M[s] = 3
  2530. else:
  2531. M[s] = 4
  2532. # all REM sequences
  2533. ridx = sleepy.get_sequences(np.where(M==1)[0])
  2534. # find long wake blocks:
  2535. widx = sleepy.get_sequences(np.where(M==2)[0])
  2536. # all NREM sequences
  2537. nidx = sleepy.get_sequences(np.where(M>=3)[0])
  2538. midx = sleepy.get_sequences(np.where(M==4)[0])
  2539. ridx = [list(a) for a in ridx]
  2540. widx = [list(a) for a in widx]
  2541. nidx = [list(a) for a in nidx]
  2542. midx = [list(a) for a in midx]
  2543. ridx = sum(ridx, [])
  2544. widx = sum(widx, [])
  2545. nidx = sum(nidx, [])
  2546. midx = sum(midx, [])
  2547. idx_dict = {'REM':ridx, 'Wake':widx, 'NREM':nidx, 'MA':midx}
  2548. pc_dict = {}
  2549. vh_dict = {}
  2550. labels = ['REM', 'Wake', 'NREM', 'MA']
  2551. for idx,label in zip([ridx, widx, nidx], labels):
  2552. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  2553. Y = R[:,idx].T / np.sqrt(nsample-1)
  2554. # make sure the the columns of Y are mean-zero
  2555. for i in range(nvar):
  2556. Y[:,i] = Y[:,i] - Y[:,i].mean()
  2557. # Note the PCs are orthogonal to each other, but
  2558. # only for the segments for which we calculate PCA
  2559. # and only if we center the firing rates across these
  2560. # segments!
  2561. R[i,:] = R[i,:] - R[i,idx].mean()
  2562. #R[i,:] = R[i,:] - R[i,:].mean()
  2563. # SVD
  2564. U,S,Vh = scipy.linalg.svd(Y)
  2565. # each row in Vh is an eigenvector of the COV matrix:
  2566. PC = np.dot(Vh, R)[0:ndim,:]
  2567. if local_rotation:
  2568. if not proj_3d:
  2569. PC = pca(PC.copy().T, dims=2)[0].T
  2570. coords = [0,1]
  2571. else:
  2572. PC = pca(PC.copy().T, dims=3)[0].T
  2573. coords = [0,1,2]
  2574. pc_dict[label] = PC
  2575. vh_dict[label] = Vh[0:ndim,:]
  2576. if coords == []:
  2577. coords = list(range(ndim))
  2578. if pplot:
  2579. sleepy.set_fontsize(12)
  2580. plt.figure(figsize=(12,10))
  2581. t = np.arange(0, len(M))*dt
  2582. if pspec:
  2583. # load spectrogram
  2584. tmp = so.loadmat(os.path.join(ppath, file, 'sp_%s.mat'%file), squeeze_me=True)
  2585. SP = tmp['SP']
  2586. freq = tmp['freq']
  2587. ifreq = np.where(freq < 30)[0]
  2588. axes_spec = plt.axes([0.4, 0.85, 0.55, 0.08])
  2589. axes_spec.pcolorfast(t, freq[ifreq], SP[ifreq,:], vmin=0, vmax=2000, cmap='jet')
  2590. sleepy._despine_axes(axes_spec)
  2591. axes_brs = plt.axes([0.4, 0.8, 0.55, 0.025], sharex=axes_spec)
  2592. else:
  2593. axes_brs = plt.axes([0.4, 0.8, 0.55, 0.025])
  2594. cmap = plt.cm.jet
  2595. my_map = cmap.from_list('brs', [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8]], 4)
  2596. tmp = axes_brs.pcolorfast(t, [0, 1], np.array([M]), vmin=0, vmax=3)
  2597. tmp.set_cmap(my_map)
  2598. axes_brs.axis('tight')
  2599. axes_brs.axes.get_xaxis().set_visible(False)
  2600. axes_brs.axes.get_yaxis().set_visible(False)
  2601. axes_brs.spines["top"].set_visible(False)
  2602. axes_brs.spines["right"].set_visible(False)
  2603. axes_brs.spines["bottom"].set_visible(False)
  2604. axes_brs.spines["left"].set_visible(False)
  2605. ax1 = plt.axes([0.4, 0.6, 0.55, 0.2], sharex=axes_brs)
  2606. sns.despine()
  2607. ax2 = plt.axes([0.4, 0.35, 0.55, 0.2], sharex=ax1)
  2608. sns.despine()
  2609. ax3 = plt.axes([0.4, 0.1, 0.55, 0.2], sharex=ax1)
  2610. sns.despine()
  2611. axes = [ax1, ax2, ax3]
  2612. i = 0
  2613. for ax, label in zip(axes, pc_dict):
  2614. ax.plot(t, pc_dict[label][coords].T)
  2615. ax.set_xlim([t[0], t[-1]])
  2616. if i < 2:
  2617. #ax.set_xticklabels([])
  2618. pass
  2619. else:
  2620. ax.set_xlabel('Time (s)')
  2621. i += 1
  2622. if not proj_3d:
  2623. ax1 = plt.axes([0.1, 0.6, 0.2, 0.2])
  2624. sns.despine()
  2625. ax2 = plt.axes([0.1, 0.35, 0.2, 0.2])
  2626. sns.despine()
  2627. ax3 = plt.axes([0.1, 0.1, 0.2, 0.2])
  2628. sns.despine()
  2629. else:
  2630. ax1 = plt.axes([0.1, 0.6, 0.2, 0.2], projection='3d')
  2631. sns.despine()
  2632. ax2 = plt.axes([0.1, 0.35, 0.2, 0.2], projection='3d')
  2633. sns.despine()
  2634. ax3 = plt.axes([0.1, 0.1, 0.2, 0.2], projection='3d')
  2635. sns.despine()
  2636. axes = [ax1, ax2, ax3]
  2637. clrs = [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8], [1, 0.2, 0.2]]
  2638. if traj_mode == 'full':
  2639. for ax, label in zip(axes, pc_dict):
  2640. PC = pc_dict[label]
  2641. p = M[0]
  2642. k = 0
  2643. kold = k
  2644. while k < len(M)-1:
  2645. while M[k] == p and k < len(M)-1:
  2646. k+=1
  2647. if not proj_3d:
  2648. ax.plot(PC[coords[0],kold:k], PC[coords[1], kold:k], color=clrs[int(p)], lw=0.5)
  2649. else:
  2650. ax.plot3D(PC[coords[0],kold:k], PC[coords[1], kold:k], PC[coords[2], kold:k], color=clrs[int(p)], lw=0.5)
  2651. p = M[k]
  2652. kold = k-1
  2653. if local_rotation:
  2654. ax.set_xlabel("PC1'")
  2655. ax.set_ylabel("PC2'")
  2656. else:
  2657. ax.set_xlabel("PC1")
  2658. ax.set_ylabel("PC2")
  2659. else:
  2660. for ax, label in zip(axes, pc_dict):
  2661. PC = pc_dict[label]
  2662. plot_trajectories(PC, M, 300, 0, dt=dt, min_dur=20, istate=trig_state, pre_state=2,
  2663. state_num=[], ma_thr=20, kcuts=(), coords=coords, ax=ax, lw=0.5)
  2664. if local_rotation:
  2665. ax.set_xlabel("PC1'")
  2666. ax.set_ylabel("PC2'")
  2667. else:
  2668. ax.set_xlabel("PC1")
  2669. ax.set_ylabel("PC2")
  2670. return pc_dict, vh_dict, idx_dict
  2671. def subspace_mixing_mx(units, mouse, ndim=2, nsmooth=0, ma_thr=10, ma_rem_exception=False,
  2672. pzscore=False, detrend=False, pplot=True, coords=[], traj_mode='full', trig_state=1,
  2673. pspec=True, kcuts=[], proj_3d=False, config_file=''):
  2674. """
  2675. Determine coordindates systems for REM, NREM, and Wake subspace.
  2676. Then, project the firing rate vectors for REM, NREM, and Wake into each subspace
  2677. and determine the variance of the projected data.
  2678. See also function &sleep_subspaces()
  2679. Calculate (and plot) PCA separately for each state (REM, Wake, NREM).
  2680. Parameters
  2681. ----------
  2682. units : TYPE
  2683. DESCRIPTION.
  2684. mouse : TYPE
  2685. DESCRIPTION.
  2686. ndim : int, optional
  2687. Number of PCs to keep for dimensionality reduction.
  2688. The default is 2.
  2689. nsmooth : float, optional
  2690. Smooth firing rates using sleepy.smooth_data(data, nsmooth). The default is 0.
  2691. ma_thr : float, optional
  2692. Wake sequences <= $ma_thr s are interpreted as NREM (3). The default is 10.
  2693. ma_rem_exception : bool, optional
  2694. If True, then the MA rule does not apply for wake episodes directly following REM.
  2695. The default is False.
  2696. pzscore : TYPE, optional
  2697. DESCRIPTION. The default is False.
  2698. pplot : TYPE, optional
  2699. DESCRIPTION. The default is True.
  2700. coords : list, optional
  2701. Specific the two PCs that should be shown on the x and y-axis.
  2702. PC1 corresponds to "0".
  2703. The default is [].
  2704. traj_mode : string, optional
  2705. If 'full', show complete trajectory.
  2706. If 'trig', only show trajectories for the specific state transitions (-> trig_state)
  2707. The default is 'full'.
  2708. trig_state : int, optional
  2709. 1,2, or 3. If traj_mode == 'trig', only show the the transitions to state $trig_state
  2710. instead of the full trajectories across the whole recording session
  2711. The default is 1.
  2712. pspec : TYPE, optional
  2713. DESCRIPTION. The default is True.
  2714. kcuts : TYPE, optional
  2715. DESCRIPTION. The default is [].
  2716. proj_3d : bool, optional
  2717. If True, plot 3D subspaces
  2718. config_file : str
  2719. file name of mouse configuration file, as loaded by &load_config()
  2720. Returns
  2721. -------
  2722. pc_dict : dict
  2723. dict: state --> PCs.
  2724. vh_dict : TYPE
  2725. DESCRIPTION.
  2726. idx_dict : TYPE
  2727. DESCRIPTION.
  2728. """
  2729. dt = 2.5
  2730. if len(config_file) == 0:
  2731. config_file = 'mouse_config.txt'
  2732. path = load_config(config_file)[mouse]['SL_PATH']
  2733. ppath, file = os.path.split(path)
  2734. M = sleepy.load_stateidx(ppath, file)[0]
  2735. unitIDs = [unit for unit in units.columns if '_' in unit]
  2736. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  2737. nhypno = np.min((len(M), units.shape[0]))
  2738. M = M[0:nhypno]
  2739. tidx = np.arange(0, nhypno)
  2740. # NEW 07/01/22:
  2741. # get the indices (in brainstate time) that we're going to completely discard:
  2742. if len(kcuts) > 0:
  2743. kidx = []
  2744. for kcut in kcuts:
  2745. a = int(kcut[0]/dt)
  2746. b = int(kcut[-1]/dt)
  2747. if b > len(M):
  2748. b = len(M)
  2749. kidx += list(np.arange(a, b))
  2750. tidx = np.setdiff1d(tidx, kidx)
  2751. M = M[tidx]
  2752. nhypno = len(tidx)
  2753. #@tidx are the indices we're further considereing.
  2754. ###########################################################################
  2755. nsample = nhypno # number of time points
  2756. nvar = len(unitIDs) # number of units
  2757. R = np.zeros((nvar, nsample))
  2758. # R: each row is one neuron with firing rates for len(tidx) time points
  2759. for i,unit in enumerate(unitIDs):
  2760. tmp = sleepy.smooth_data(np.array(units[unit]), nsmooth)
  2761. tmp = tmp[tidx]
  2762. if detrend:
  2763. tmp = scipy.signal.detrend(tmp)
  2764. if pzscore:
  2765. R[i,:] = (tmp - tmp.mean()) / tmp.std()
  2766. else:
  2767. R[i,:] = tmp
  2768. # flatten out MAs
  2769. if ma_thr>0:
  2770. seq = sleepy.get_sequences(np.where(M==2)[0])
  2771. for s in seq:
  2772. if np.round(len(s)*dt) <= ma_thr:
  2773. if ma_rem_exception:
  2774. if (s[0]>1) and (M[s[0] - 1] != 1):
  2775. M[s] = 3
  2776. else:
  2777. M[s] = 3
  2778. # all REM sequences
  2779. ridx = sleepy.get_sequences(np.where(M==1)[0])
  2780. # find long wake blocks:
  2781. widx = sleepy.get_sequences(np.where(M==2)[0])
  2782. # all NREM sequences
  2783. nidx = sleepy.get_sequences(np.where(M>=3)[0])
  2784. midx = sleepy.get_sequences(np.where(M==4)[0])
  2785. ridx = [list(a) for a in ridx]
  2786. widx = [list(a) for a in widx]
  2787. nidx = [list(a) for a in nidx]
  2788. midx = [list(a) for a in midx]
  2789. ridx = sum(ridx, [])
  2790. widx = sum(widx, [])
  2791. nidx = sum(nidx, [])
  2792. midx = sum(midx, [])
  2793. idx_dict = {'REM':ridx, 'Wake':widx, 'NREM':nidx, 'MA':midx}
  2794. pc_dict = {}
  2795. vh_dict = {}
  2796. cc = np.sqrt(nsample-1)
  2797. cc = 1
  2798. labels = ['REM', 'Wake', 'NREM']
  2799. for idx,label in zip([ridx, widx, nidx], labels):
  2800. Rsub = R.copy()
  2801. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  2802. Y = R[:,idx].T / cc
  2803. # make sure the the columns of Y are mean-zero
  2804. for i in range(nvar):
  2805. Y[:,i] = Y[:,i] - Y[:,i].mean()
  2806. # Note the PCs are orthogonal to each other, but
  2807. # only for the segments for which we calculate PCA
  2808. # and only if we center the firing rates across these
  2809. # segments!
  2810. Rsub[i,:] = Rsub[i,:] - Rsub[i,idx].mean()
  2811. # SVD
  2812. U,S,Vh = scipy.linalg.svd(Y)
  2813. # each row in Vh is an eigenvector of the COV matrix:
  2814. PC = np.dot(Vh, Rsub)[0:ndim,:]
  2815. pc_dict[label] = PC
  2816. vh_dict[label] = Vh[0:ndim,:] * cc
  2817. MX = np.zeros((len(labels), len(labels)))
  2818. # Build mixing matrix
  2819. for i in range(len(labels)):
  2820. PCsub = pc_dict[labels[i]]
  2821. # For example take NREM subspace and project
  2822. # NREM, Wake and REM firing rates into this space, and
  2823. # then calculate the total variance of NREM, Wake, and REM
  2824. # neurons within this space
  2825. for j in range(0, len(labels)):
  2826. state_idx = idx_dict[labels[j]]
  2827. A = PCsub[:,state_idx]
  2828. MX[i,j] = np.trace(np.cov(A))
  2829. if coords == []:
  2830. coords = list(range(ndim))
  2831. if pplot:
  2832. sleepy.set_fontsize(12)
  2833. plt.figure(figsize=(12,10))
  2834. t = np.arange(0, len(M))*dt
  2835. if pspec:
  2836. # load spectrogram
  2837. tmp = so.loadmat(os.path.join(ppath, file, 'sp_%s.mat'%file), squeeze_me=True)
  2838. SP = tmp['SP']
  2839. freq = tmp['freq']
  2840. ifreq = np.where(freq < 30)[0]
  2841. axes_spec = plt.axes([0.4, 0.85, 0.55, 0.08])
  2842. axes_spec.pcolorfast(t, freq[ifreq], SP[ifreq,:], vmin=0, vmax=2000, cmap='jet')
  2843. sleepy._despine_axes(axes_spec)
  2844. axes_brs = plt.axes([0.4, 0.8, 0.55, 0.025], sharex=axes_spec)
  2845. else:
  2846. axes_brs = plt.axes([0.4, 0.8, 0.55, 0.025])
  2847. cmap = plt.cm.jet
  2848. my_map = cmap.from_list('brs', [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8]], 4)
  2849. tmp = axes_brs.pcolorfast(t, [0, 1], np.array([M]), vmin=0, vmax=3)
  2850. tmp.set_cmap(my_map)
  2851. axes_brs.axis('tight')
  2852. axes_brs.axes.get_xaxis().set_visible(False)
  2853. axes_brs.axes.get_yaxis().set_visible(False)
  2854. axes_brs.spines["top"].set_visible(False)
  2855. axes_brs.spines["right"].set_visible(False)
  2856. axes_brs.spines["bottom"].set_visible(False)
  2857. axes_brs.spines["left"].set_visible(False)
  2858. ax1 = plt.axes([0.4, 0.6, 0.55, 0.2], sharex=axes_brs)
  2859. sns.despine()
  2860. ax2 = plt.axes([0.4, 0.35, 0.55, 0.2], sharex=ax1)
  2861. sns.despine()
  2862. ax3 = plt.axes([0.4, 0.1, 0.55, 0.2], sharex=ax1)
  2863. sns.despine()
  2864. axes = [ax1, ax2, ax3]
  2865. i = 0
  2866. for ax, label in zip(axes, pc_dict):
  2867. ax.plot(t, pc_dict[label][coords,:].T)
  2868. ax.set_xlim([t[0], t[-1]])
  2869. if i < 2:
  2870. #ax.set_xticklabels([])
  2871. pass
  2872. else:
  2873. ax.set_xlabel('Time (s)')
  2874. i += 1
  2875. if not proj_3d:
  2876. ax1 = plt.axes([0.1, 0.6, 0.2, 0.2])
  2877. sns.despine()
  2878. ax2 = plt.axes([0.1, 0.35, 0.2, 0.2])
  2879. sns.despine()
  2880. ax3 = plt.axes([0.1, 0.1, 0.2, 0.2])
  2881. sns.despine()
  2882. else:
  2883. ax1 = plt.axes([0.1, 0.6, 0.2, 0.2], projection='3d')
  2884. sns.despine()
  2885. ax2 = plt.axes([0.1, 0.35, 0.2, 0.2], projection='3d')
  2886. sns.despine()
  2887. ax3 = plt.axes([0.1, 0.1, 0.2, 0.2], projection='3d')
  2888. sns.despine()
  2889. axes = [ax1, ax2, ax3]
  2890. clrs = [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8], [1, 0.2, 0.2]]
  2891. if traj_mode == 'full':
  2892. for ax, label in zip(axes, pc_dict):
  2893. PC = pc_dict[label]
  2894. p = M[0]
  2895. k = 0
  2896. kold = k
  2897. while k < len(M)-1:
  2898. while M[k] == p and k < len(M)-1:
  2899. k+=1
  2900. if not proj_3d:
  2901. ax.plot(PC[coords[0],kold:k], PC[coords[1], kold:k], color=clrs[int(p)], lw=0.5)
  2902. else:
  2903. ax.plot3D(PC[coords[0],kold:k], PC[coords[1], kold:k], PC[coords[2], kold:k], color=clrs[int(p)], lw=0.5)
  2904. p = M[k]
  2905. kold = k-1
  2906. else:
  2907. for ax, label in zip(axes, pc_dict):
  2908. PC = pc_dict[label]
  2909. plot_trajectories(PC, M, 300, 0, dt=dt, min_dur=20, istate=trig_state, pre_state=2,
  2910. state_num=[], ma_thr=20, kcuts=(), coords=coords, ax=ax, lw=0.5)
  2911. return pc_dict, vh_dict, idx_dict, MX
  2912. def pc_reconstruction(units, cell_info, ndim=3, nsmooth=0, pnorm=True, pzscore=False,
  2913. pc_sign=[], dt=2.5):
  2914. unitIDs = [unit for unit in units.columns if '_' in unit]
  2915. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  2916. nsample = units.shape[0] # number of time points
  2917. nvar = len(unitIDs) # number of units
  2918. R = np.zeros((nvar, nsample))
  2919. # OLD:
  2920. i = 0
  2921. for unit in unitIDs:
  2922. R[i,:] = sleepy.smooth_data(np.array(units[unit]),nsmooth)
  2923. if pzscore:
  2924. R[i,:] = (R[i,:] - R[i,:].mean()) / R[i,:].std()
  2925. i += 1
  2926. # first, for each varible (dimension), we first need to remove the mean:
  2927. # mean-zero rows:
  2928. for i in range(nvar):
  2929. R[i,:] = R[i,:] - R[i,:].mean()
  2930. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  2931. Y = R.T / np.sqrt(nsample-1)
  2932. U,S,Vh = scipy.linalg.svd(Y)
  2933. PC = np.dot(Vh, R)[0:ndim,:]
  2934. plt.figure()
  2935. plt.plot(PC.T)
  2936. # reconstruct neural responses with the first $ndim eigenvectors
  2937. SM = np.zeros((nsample, ndim))
  2938. for i in range(ndim):
  2939. SM[i,i] = S[i]
  2940. # That's the reconstruction:
  2941. Yhat = (np.dot(U[:,0:ndim], np.dot(SM[0:ndim,:], Vh[0:ndim,:]))) * np.sqrt(nsample-1)
  2942. # Alternative:
  2943. # neurons x dim * [dim x neurons * neurons x time]
  2944. # Yhat2 = np.dot(Vh[0:ndim,:].T, np.dot(Vh[0:ndim,:], R)).T
  2945. C = Vh[0:ndim,:].T
  2946. if pnorm:
  2947. for i in range(C.shape[0]):
  2948. C[i,:] = C[i,:] / np.sqrt(np.sum(C[i,:]**2))
  2949. if len(pc_sign) > 0:
  2950. i = 0
  2951. for s in pc_sign:
  2952. C[:,i] = C[:,i] * s
  2953. i += 1
  2954. labels = ['c'+str(i) for i in range(1, ndim+1)]
  2955. df = pd.DataFrame(data=C, columns=labels)
  2956. df['ID'] = unitIDs
  2957. labels = ['c'+str(i) for i in range(1, ndim+1)]
  2958. df = pd.DataFrame(data=C, columns=labels)
  2959. df['ID'] = unitIDs
  2960. #df['brain_region'] = list(cell_info[cell_info.ID.isin(unitIDs)]['brain_region'])
  2961. df['brain_region'] = [cell_info[cell_info.ID == i].brain_region.iloc[0] for i in unitIDs]
  2962. plt.figure()
  2963. plt.subplot(211)
  2964. sns.histplot(data=df, x='brain_region', y='c1')
  2965. plt.subplot(212)
  2966. sns.histplot(data=df, x='brain_region', y='c2')
  2967. return C, unitIDs, df, Yhat
  2968. def pc_reconstruction2(units, cell_info, time_idx=[], ndim=3, nsmooth=0, detrend=False, pnorm=True, pzscore=False,
  2969. pc_sign=[], scale_pc=False, pearson_scaling=False,
  2970. dim_reconstr=[], dt=2.5, kcuts=[], pearson=False, pplot=True, sign_plot=True):
  2971. """
  2972. Use whole time axis for smoothing and z-scoring.
  2973. Calculate SVD only using time points in @time_idx and reconstruct firing rates
  2974. only for timepoints in @time_idx.
  2975. When calculating the coefficients for each PC this function
  2976. takes only time points in time_idx into account!
  2977. Note on SVD:
  2978. Assume Y is a matrix with each column corresponding to the firing rate vector
  2979. of one recorded unit (Y ~ time bins x units).
  2980. Assume
  2981. U,S,Vh = scipy.linalg.svd(Y)
  2982. is the SVD of matrix Y
  2983. Then, PC = U * S are the PCs;
  2984. PC[i,:] is the i-th PC
  2985. Vh[i,:] are the coefficients of each neurons for PCi
  2986. The firing rates of unit fr_i can be reconstructed using,
  2987. fr_i = PC1 * c[0,i] + PC2 * c2[1,i] + ...
  2988. which we rewrite as
  2989. fr_i = PC1 * c1_i + PC2 * c2_i + ...
  2990. Parameters
  2991. ----------
  2992. units : pd.DataFrame
  2993. Firing rates (time_bins x num_units). Each column is a unit with unit ID as column name.
  2994. cell_info : pd.DataFrame
  2995. Cell metadata. Must contain columns:
  2996. - 'ID': Unit identifiers (matching units.columns)
  2997. - 'brain_region': Brain region assignment for each unit
  2998. time_idx : list or np.ndarray, optional
  2999. Time indices to use for SVD computation. If [], uses all available timepoints.
  3000. Default is [].
  3001. ndim : int, optional
  3002. Number of principal components to extract and use for reconstruction. Default is 3.
  3003. nsmooth : float, optional
  3004. Gaussian kernel smoothing factor (σ) applied to firing rates before analysis. Default is 0.
  3005. detrend : bool, optional
  3006. If True, detrend firing rates before analysis. Default is False.
  3007. pnorm : bool, optional
  3008. If True, normalize PC coefficients (L2 norm). Default is True.
  3009. pzscore : bool, optional
  3010. If True, z-score firing rates after smoothing. Default is False.
  3011. pc_sign : list or np.ndarray, optional
  3012. Sign correction vector: PC_i is multiplied by pc_sign[i-1]. Must have length ndim.
  3013. Use to ensure consistent PC orientation across analyses. Default is [].
  3014. scale_pc : bool, optional
  3015. If True, scale PC coefficients by PC variance. Default is False.
  3016. pearson_scaling : bool, optional
  3017. If True, apply Pearson correlation-based scaling. Default is False.
  3018. dim_reconstr : list, optional
  3019. Specific dimensions to reconstruct. If [], reconstructs all ndim dimensions. Default is [].
  3020. dt : float, optional
  3021. Sampling period (in seconds) for converting between time and sample indices. Default is 2.5.
  3022. kcuts : list of tuples, optional
  3023. Timepoints to exclude. Each tuple (start, end) specifies interval in seconds to discard.
  3024. Default is [].
  3025. pearson : bool, optional
  3026. If True, calculate Pearson correlation between each PC and firing rates, returning
  3027. r and p values in the output DataFrame. Default is False.
  3028. pplot : bool, optional
  3029. If True, display plots of principal components. Default is True.
  3030. sign_plot : bool, optional
  3031. If True, plots show PCs after multiplying by pc_sign. Default is True.
  3032. Returns
  3033. -------
  3034. C : np.ndarray with shape (num_units, ndim)
  3035. PC coefficients for each unit. C[i, j] is the coefficient of unit i for PC j.
  3036. df : pd.DataFrame
  3037. Coefficient summary table with columns:
  3038. - 'c1', 'c2', ..., 'cn': PC coefficients for each unit
  3039. - 'ID': Unit identifier
  3040. - 'brain_region': Brain region assignment (from cell_info)
  3041. - 'r1', 'r2', ..., 'rn': Pearson r values (if pearson=True)
  3042. - 'p1', 'p2', ..., 'pn': Pearson p-values (if pearson=True)
  3043. units : pd.DataFrame
  3044. Original firing rates (after smoothing/detrending if applicable).
  3045. units_hat : pd.DataFrame
  3046. Reconstructed firing rates: all_vars @ C.T. Shape (num_timepoints, num_units).
  3047. """
  3048. nhypno = units.shape[0]
  3049. tidx = np.arange(0, nhypno)
  3050. # get the indices (in brainstate time) that we're going to completely discard:
  3051. if len(kcuts) > 0:
  3052. kidx = []
  3053. for kcut in kcuts:
  3054. a = int(kcut[0]/dt)
  3055. b = int(kcut[-1]/dt)
  3056. kidx += list(np.arange(a, b))
  3057. tidx = np.setdiff1d(tidx, kidx)
  3058. nhypno = len(tidx)
  3059. ###########################################################################
  3060. unitIDs = [unit for unit in units.columns if '_' in unit]
  3061. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  3062. nsample = units.shape[0] # number of time points
  3063. nvar = len(unitIDs) # number of units
  3064. R = np.zeros((nvar, nhypno)) # dimensions: number of units x time bins
  3065. for i,unit in enumerate(unitIDs):
  3066. tmp = sleepy.smooth_data(np.array(units[unit]),nsmooth)
  3067. tmp = tmp[tidx]
  3068. if detrend:
  3069. tmp = scipy.signal.detrend(tmp)
  3070. if pzscore:
  3071. R[i,:] = (tmp - tmp.mean()) / tmp.std()
  3072. else:
  3073. R[i,:] = tmp
  3074. # first, for each varible (dimension), we first need to remove the mean:
  3075. # mean-zero rows:
  3076. R = R[:,time_idx]
  3077. for i in range(nvar):
  3078. R[i,:] = R[i,:] - R[i,:].mean()
  3079. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  3080. # NEW: 05/14/25!!!
  3081. nsample = R.shape[1]
  3082. Y = R.T / np.sqrt(nsample-1)
  3083. U,S,Vh = scipy.linalg.svd(Y)
  3084. PC = np.dot(Vh, R)[0:ndim,:]
  3085. if scale_pc:
  3086. PC = PC / np.sqrt(nsample - 1)
  3087. ## double check code using chat-gpt:
  3088. # n_units = len(unitIDs)
  3089. # R = np.empty((n_units, len(tidx)))
  3090. # for i, unit in enumerate(unitIDs):
  3091. # x = sleepy.smooth_data(np.asarray(units[unit]), nsmooth)[tidx]
  3092. # if detrend:
  3093. # x = scipy.signal.detrend(x)
  3094. # R[i] = (x - x.mean()) / x.std(ddof=1) if pzscore else x
  3095. # # --- restrict to analysis window ------------------------------------------
  3096. # R = R[:, time_idx] # if time_idx == tidx you can drop one of them
  3097. # nsample = R.shape[1]
  3098. # # --- mean-centre (skipped automatically if pzscore) ------------------------
  3099. # R -= R.mean(axis=1, keepdims=True)
  3100. # # --- PCA via SVD -----------------------------------------------------------
  3101. # Y = R.T / np.sqrt(nsample - 1)
  3102. # U, S, Vh = scipy.linalg.svd(Y, full_matrices=False)
  3103. # scores = (S[:ndim, None] * U[:, :ndim].T) # PC time series
  3104. # loadings = Vh[:ndim] # neuron weights
  3105. # eigvals = S[:ndim]**2 # variance of each PC
  3106. # expl_var = eigvals / eigvals.sum()
  3107. if len(pc_sign) == []:
  3108. pc_sign = np.ones((ndim,))
  3109. if sign_plot:
  3110. plt.figure()
  3111. for i in range(ndim):
  3112. if i < len(pc_sign):
  3113. plt.plot(PC[i,:]*pc_sign[i])
  3114. else:
  3115. plt.plot(PC[i,:])
  3116. # reconstruct neural responses with the first $ndim eigenvectors
  3117. SM = np.zeros((nsample, ndim))
  3118. for i in range(ndim):
  3119. SM[i,i] = S[i]
  3120. if len(dim_reconstr) == 0:
  3121. dim_reconstr = range(0, ndim)
  3122. # That's the reconstruction:
  3123. Yhat = (np.dot(U[:,dim_reconstr], np.dot(SM[dim_reconstr,:], Vh[dim_reconstr,:]))) * np.sqrt(nsample-1)
  3124. # Alternative:
  3125. # neurons x dim * [dim x neurons * neurons x time]
  3126. # Yhat2 = np.dot(Vh[0:ndim,:].T, np.dot(Vh[0:ndim,:], R)).T
  3127. # 11/08/23 added the factor '* np.sqrt(nsample-1)'
  3128. C = Vh[0:ndim,:].T #* np.sqrt(nsample-1)
  3129. if scale_pc:
  3130. C = C * np.sqrt(nsample-1)
  3131. if pnorm:
  3132. for i in range(C.shape[0]):
  3133. C[i,:] = C[i,:] / np.sqrt(np.sum(C[i,:]**2))
  3134. if pearson_scaling:
  3135. C = C * S[:ndim]
  3136. if len(pc_sign) > 0:
  3137. i = 0
  3138. for s in pc_sign:
  3139. C[:,i] = C[:,i] * s
  3140. i += 1
  3141. labels = ['c'+str(i) for i in range(1, ndim+1)]
  3142. df = pd.DataFrame(data=C, columns=labels)
  3143. df['ID'] = unitIDs
  3144. labels = ['c'+str(i) for i in range(1, ndim+1)]
  3145. df = pd.DataFrame(data=C, columns=labels)
  3146. df['ID'] = unitIDs
  3147. if pplot:
  3148. df['brain_region'] = [cell_info[cell_info.ID == i].brain_region.iloc[0] for i in unitIDs]
  3149. plt.figure()
  3150. plt.subplot(211)
  3151. sns.histplot(data=df, x='brain_region', y='c1')
  3152. plt.subplot(212)
  3153. sns.histplot(data=df, x='brain_region', y='c2')
  3154. #convert Yhat to pd.DataFrame with rows of Yhat as columns
  3155. units_hat = pd.DataFrame(data=Yhat, columns=unitIDs)
  3156. units = pd.DataFrame(data=Y*np.sqrt(nsample-1), columns=unitIDs)
  3157. if pearson:
  3158. for j in range(ndim):
  3159. p = []
  3160. cc = []
  3161. for i,ID in enumerate(unitIDs):
  3162. r = R[i,:]
  3163. res = scipy.stats.pearsonr(r, PC[j,:])
  3164. p.append(res.pvalue)
  3165. cc.append(res.statistic)
  3166. df['r' + str(j+1)] = cc
  3167. df['p' + str(j+1)] = p
  3168. return C, df, units, units_hat
  3169. def partialpc_reconstruction(units, cell_info, time_idx, neuron_ids=[], ndim=3, nsmooth=0,
  3170. pnorm=True, pzscore=False, pc_sign=[], fit_timeidx=True, pplot=True):
  3171. """
  3172. NOTE: In its current implementation, the function fits the PC coefficients
  3173. to the complete time axis, although the principal axes have been fit using
  3174. a (potentially smaller) time range.
  3175. Parameters
  3176. ----------
  3177. units : TYPE
  3178. DESCRIPTION.
  3179. cell_info : TYPE
  3180. DESCRIPTION.
  3181. time_idx : TYPE
  3182. DESCRIPTION.
  3183. neuron_ids : TYPE, optional
  3184. DESCRIPTION. The default is [].
  3185. ndim : TYPE, optional
  3186. DESCRIPTION. The default is 3.
  3187. nsmooth : TYPE, optional
  3188. DESCRIPTION. The default is 0.
  3189. pnorm : TYPE, optional
  3190. DESCRIPTION. The default is True.
  3191. pzscore : TYPE, optional
  3192. DESCRIPTION. The default is False.
  3193. pc_sign : TYPE, optional
  3194. DESCRIPTION. The default is [].
  3195. fit_timeidx: boolean, optional
  3196. If True, take for fit of firing rates using the PCs only time points in time_idx into account
  3197. Returns
  3198. -------
  3199. C : TYPE
  3200. DESCRIPTION.
  3201. PC : TYPE
  3202. DESCRIPTION.
  3203. df : TYPE
  3204. DESCRIPTION.
  3205. units: pd.DataFrame
  3206. smoothed and centered firing rates
  3207. units_hat: pd.DataFrame
  3208. fitted firing rates using $ndim PCs
  3209. """
  3210. unitIDs = [unit for unit in units.columns if '_' in unit]
  3211. unitIDs = [unit for unit in unitIDs if re.split('_', unit)[1] == 'good']
  3212. nsample = units.shape[0] # number of time points
  3213. nvar = len(unitIDs) # number of units
  3214. R = np.zeros((nvar, nsample))
  3215. #all_units = False
  3216. if neuron_ids == []:
  3217. neuron_ids = unitIDs
  3218. #all_units = True
  3219. i = 0
  3220. neuron_idx = []
  3221. for unit in unitIDs:
  3222. R[i,:] = sleepy.smooth_data(np.array(units[unit]),nsmooth)
  3223. if pzscore:
  3224. R[i,:] = (R[i,:] - R[i,:].mean()) / R[i,:].std()
  3225. if unit in neuron_ids:
  3226. neuron_idx.append(i)
  3227. i += 1
  3228. # first, for each varible (dimension), we first need to remove the mean:
  3229. # mean-zero rows:
  3230. for i in range(nvar):
  3231. R[i,:] = R[i,:] - R[i,:].mean()
  3232. # divide by sqrt(nsample - 1) to make SVD equivalent to PCA
  3233. R2 = R[neuron_idx,:]
  3234. Y = R2[:,time_idx].T / np.sqrt(len(time_idx)-1)
  3235. # make sure the the columns of Y are mean-zero
  3236. for i in range(Y.shape[1]):
  3237. Y[:,i] = Y[:,i] - Y[:,i].mean()
  3238. U,S,Vh = scipy.linalg.svd(Y)
  3239. # that's the projections of all neuron_idx neurons (rows) into the PCA space,
  3240. # giving us the principal components
  3241. PC = np.dot(Vh, R2)[0:ndim,:]
  3242. # Now, for ALL neurons and time points the to optimal coefficients to
  3243. # reconstruct the original firing rates using the $ndim PCs
  3244. A = np.ones((PC.shape[1], ndim+1))
  3245. A[:,0:ndim] = PC.T
  3246. Rhat = np.zeros(R.shape)
  3247. C = np.zeros((nvar, ndim+1))
  3248. for i in range(nvar):
  3249. r = R[i,:]
  3250. if fit_timeidx:
  3251. w = np.linalg.lstsq(A[time_idx], r[time_idx])[0]
  3252. else:
  3253. w = np.linalg.lstsq(A, r)[0]
  3254. C[i,:] = w
  3255. # reconstruction of units using ndeim PCs
  3256. rhat = np.dot(A, w)
  3257. Rhat[i,:] = rhat
  3258. C = C[:,0:ndim]
  3259. #C = Vh[0:ndim,:].T
  3260. if pnorm:
  3261. for i in range(C.shape[0]):
  3262. C[i,:] = C[i,:] / np.sqrt(np.sum(C[i,:]**2))
  3263. if len(pc_sign) > 0:
  3264. for i,s in enumerate(pc_sign):
  3265. C[:,i] = C[:,i] * s
  3266. labels = ['c'+str(i) for i in range(1, ndim+1)]
  3267. df = pd.DataFrame(data=C, columns=labels)
  3268. df['ID'] = unitIDs
  3269. labels = ['c'+str(i) for i in range(1, ndim+1)]
  3270. df = pd.DataFrame(data=C, columns=labels)
  3271. df['ID'] = unitIDs
  3272. #df['brain_region'] = list(cell_info[cell_info.ID.isin(unitIDs)]['brain_region'])
  3273. df['brain_region'] = [cell_info[cell_info.ID == i].brain_region.iloc[0] for i in unitIDs]
  3274. ### FIGURE ################################################################
  3275. if pplot:
  3276. plt.figure()
  3277. plt.subplot(211)
  3278. sns.histplot(data=df, x='brain_region', y='c1')
  3279. plt.subplot(212)
  3280. sns.histplot(data=df, x='brain_region', y='c2')
  3281. # reconstruct neural responses with the first $ndim eigenvectors
  3282. #SM = np.zeros((nsample, ndim))
  3283. #for i in range(ndim):
  3284. # SM[i,i] = S[i]
  3285. # That's the reconstruction:
  3286. #Yhat = (np.dot(U[:,0:ndim], np.dot(SM[0:ndim,:], Vh[0:ndim,:]))) * np.sqrt(nsample-1)
  3287. # UNDER CONSTRUCTION
  3288. units_hat = pd.DataFrame(data=Rhat.T, columns=unitIDs)
  3289. units = pd.DataFrame(data=R.T, columns=unitIDs)
  3290. return C, PC, df, units, units_hat
  3291. def optimal_direction(dfc, pref_direction, thr=0, pplot=True, ax='', brain_region=False):
  3292. """
  3293. Parameters
  3294. ----------
  3295. dfc : pd.DataFrame
  3296. DataFrame with coefficients, c1, ... c_n (loadings) for PCs.
  3297. Columns: 'c1', ... 'c_n', 'ID', 'brain_region'
  3298. pref_direction : TYPE
  3299. DESCRIPTION.
  3300. thr : TYPE
  3301. DESCRIPTION.
  3302. Returns
  3303. -------
  3304. df2 : TYPE
  3305. DESCRIPTION.
  3306. """
  3307. pref_direction = pref_direction / scipy.linalg.norm(pref_direction)
  3308. cols = ['c'+str(i+1) for i in range(pref_direction.shape[0])]
  3309. data = []
  3310. for index, row in dfc.iterrows():
  3311. vec = np.array(row[cols])
  3312. # normalize each vector
  3313. vec = vec / scipy.linalg.norm(vec)
  3314. d = np.dot(vec, pref_direction)
  3315. if brain_region:
  3316. data += [[d, row['ID'], row['brain_region']]]
  3317. else:
  3318. data += [[d, row['ID']]]
  3319. if brain_region:
  3320. df2 = pd.DataFrame(data=data, columns=['proj', 'ID', 'brain_region'])
  3321. else:
  3322. df2 = pd.DataFrame(data=data, columns=['proj', 'ID'])
  3323. if thr > 0:
  3324. dfs = dfc[(df2.proj > thr)]
  3325. else:
  3326. dfs = dfc[(df2.proj < thr)]
  3327. if pplot:
  3328. if ax == '':
  3329. plt.figure()
  3330. ax = plt.axes([0.2, 0.2, 0.7, 0.7])
  3331. #ax.axis('equal')
  3332. ax.axhline(0, linestyle='--', color='k') # horizontal lines
  3333. ax.axvline(0, linestyle='--', color='k') # vertical lines
  3334. if thr != 0:
  3335. sns.scatterplot(data=dfc, x='c1', y='c2', color='gray')
  3336. sns.scatterplot(data=dfs, x='c1', y='c2', color='red')
  3337. else:
  3338. sns.scatterplot(data=dfc, x='c1', y='c2', hue='brain_region')
  3339. plt.grid(False)
  3340. sns.despine()
  3341. return df2
  3342. def laser_triggered_pcs(PC, pre, post, M, mouse, kcuts=[], min_laser=20, pzscore_pc=False, local_pzscore=True,
  3343. pplot=True, ci=None, refractory_rule=False, ma_thr=10, ma_rem_exception=False, rnd_laser=False, seed=1,
  3344. config_file='mouse_config.txt', laser_dur=-1, start_mode='floor'):
  3345. """
  3346. Calculated the time course of the provided PCs relative to the laser onset.
  3347. Parameters
  3348. ----------
  3349. PC : np.array
  3350. Each row corresponds to one PC.
  3351. pre : float
  3352. Time before laser onset.
  3353. post : float
  3354. Time after laser onset.
  3355. M : np.array
  3356. Hypnogram.
  3357. mouse : str
  3358. Mouse name.
  3359. kcuts : list of tuples, optional
  3360. DESCRIPTION. The default is [].
  3361. min_laser : float, optional
  3362. Minimum duration of laser train. If laser duration < $min_laser,
  3363. disregard the laser trial.
  3364. pzscore_pc : bool, optional
  3365. If true, z-score the PCs (across entire recording)
  3366. local_pzscore : bool, optional
  3367. DESCRIPTION. The default is True.
  3368. pplot : bool, optional
  3369. If True, plot figures summarizing results.
  3370. ci : float or None, optional
  3371. Confidence interval for plots.
  3372. refractory_rule : TYPE, optional
  3373. DESCRIPTION. The default is False.
  3374. config_file : str, optional
  3375. Mouse recordings configuration file.
  3376. laser_dur : float
  3377. Duration of laser; if -1, calculate laser duration using laser_*.mat file
  3378. start_mode : str
  3379. 'floor': The first bins that touches the laser, is the laser onset;
  3380. 'before': Time point 0 is the bin that just does not touch the laser.
  3381. Returns
  3382. -------
  3383. df : pandas.DataFrame
  3384. with columns ['mouse', 'time', 'val', 'valz',
  3385. 'pc', 'lsr_start', 'start_state', 'start_state_int',
  3386. 'state', 'success', 'refractory']
  3387. 'state': brain state sequence
  3388. 'start_state_int': How long does it take till the brain state at laser onset
  3389. switches to a different state
  3390. 'valz': PC values during laser trial, for each trial the PC vector
  3391. is z-scored.
  3392. 'rem_delay': Delay from laser to REM onset; if there's no REM the value is set to -1
  3393. """
  3394. if rnd_laser:
  3395. np.random.seed(seed)
  3396. dt = 2.5
  3397. ddir = load_config(config_file)[mouse]['SL_PATH']
  3398. ppath, name = os.path.split(ddir)
  3399. nhypno = M.shape[0]
  3400. ndim = PC.shape[0]
  3401. tidx = np.arange(0, nhypno)
  3402. # NEW 07/01/22:
  3403. # get the indices (in brainstate time) that we're going to completely discard:
  3404. if len(kcuts) > 0:
  3405. kidx = []
  3406. for kcut in kcuts:
  3407. a = int(kcut[0]/dt)
  3408. b = int(kcut[-1]/dt)
  3409. if b > len(M):
  3410. b = len(M)
  3411. kidx += list(np.arange(a, b))
  3412. tidx = np.setdiff1d(tidx, kidx)
  3413. M = M[tidx]
  3414. nhypno = len(tidx)
  3415. ###########################################################################
  3416. # flatten out MAs #########################################################
  3417. if ma_thr>0:
  3418. seq = sleepy.get_sequences(np.where(M==2)[0])
  3419. for s in seq:
  3420. if np.round(len(s)*dt) <= ma_thr:
  3421. if ma_rem_exception:
  3422. if (s[0]>0) and (M[s[0] - 1] != 1):
  3423. M[s] = 3
  3424. else:
  3425. M[s] = 3
  3426. ###########################################################################
  3427. if os.path.isfile(os.path.join(ddir, 'laser_%s.mat' % name)):
  3428. sr = sleepy.get_snr(ppath, name)
  3429. nbin = int(np.round(sr)*dt)
  3430. dt = nbin * (1.0/sr)
  3431. ipre = int(pre/dt)
  3432. ipost = int(post/dt)
  3433. t = np.arange(-ipre, ipost)*dt
  3434. nt = len(t)
  3435. #######################################################################
  3436. # get laser start and end index after excluding kcuts: ################
  3437. lsr = so.loadmat(os.path.join(ddir, 'laser_%s.mat' % name), squeeze_me=True)['laser']
  3438. idxs, idxe = sleepy.laser_start_end(lsr)
  3439. if start_mode == 'floor':
  3440. idxs = [int(i/nbin) for i in idxs]
  3441. elif start_mode == 'before':
  3442. [int(i/nbin)-1 for i in idxs]
  3443. else:
  3444. idxs = [int(i/nbin)+1 for i in idxs]
  3445. if laser_dur == -1:
  3446. idxe = [int(i/nbin) for i in idxe]
  3447. b = [i for i in idxe if i in tidx]
  3448. a = [i for i in idxs if i in tidx]
  3449. b = np.array(b)
  3450. a = np.array(a)
  3451. laser_dur = (np.mean(b-a) + 1) * dt
  3452. print('Laser duration: %f' % laser_dur)
  3453. else:
  3454. idxe = [(int(i + int(laser_dur/dt))) for i in idxs]
  3455. # randomize laser #####################################################
  3456. dur = int(laser_dur/dt)
  3457. if rnd_laser:
  3458. idxs_rnd = []
  3459. idxe_rnd = []
  3460. tmp = np.random.randint(dur, idxs[0]-dur)
  3461. idxs_rnd.append(tmp)
  3462. idxe_rnd.append(tmp+dur)
  3463. for (a,b) in zip(idxe[0:-1], idxs[1:]):
  3464. if a+2*dur < b-dur:
  3465. tmp = np.random.randint(a+2*dur,b-dur)
  3466. idxs_rnd.append(tmp)
  3467. idxe_rnd.append(tmp+dur)
  3468. idxs = idxs_rnd
  3469. idxe = idxe_rnd
  3470. #######################################################################
  3471. laser_idx = []
  3472. for (si,sj) in zip(idxs, idxe):
  3473. laser_idx += list(range(si,sj+1))
  3474. laser_idx = np.array(laser_idx)
  3475. nlsr = int(np.floor(lsr.shape[0]/nbin))
  3476. laser = np.zeros((nlsr,))
  3477. laser_idx = laser_idx[laser_idx < nlsr]
  3478. laser[laser_idx] = 1
  3479. laser = laser[tidx]
  3480. idxs = [s[0] for s in sleepy.get_sequences(np.where(laser == 1)[0])]
  3481. idxe = [s[-1] for s in sleepy.get_sequences(np.where(laser == 1)[0])]
  3482. #######################################################################
  3483. label = []
  3484. for p in range(1, ndim+1):
  3485. l = 'pc' + str(p)
  3486. label.extend([l]*nt)
  3487. # zscore pcs:
  3488. if pzscore_pc:
  3489. for i in range(PC.shape[0]):
  3490. PC[i,:] = (PC[i,:]- PC[i,:].mean()) / PC[i,:].std()
  3491. data = []
  3492. ev = 0
  3493. for i,j in zip(idxs, idxe):
  3494. if i >= ipre and i+ipost < nhypno:
  3495. if (j-i)*dt < min_laser:
  3496. continue
  3497. idx = np.arange(i-ipre, i+ipost).astype('int')
  3498. pc_cut = PC[:,idx]
  3499. pc_cut_z = pc_cut.copy()
  3500. for k in range(ndim):
  3501. pc_cut_z[k,:] = (pc_cut[k,:] - pc_cut[k,:].mean()) / pc_cut[k,:].std()
  3502. m_lsr = M[i:j+1]
  3503. # repeat m_cut ndim-times
  3504. m_cut = np.tile(M[idx], (ndim,))
  3505. vec = np.reshape(pc_cut, (ndim*nt,))
  3506. vecz = np.reshape(pc_cut_z, (ndim*nt,))
  3507. tvec = np.tile(t, (ndim,))
  3508. lsr_rem = 'no'
  3509. if 1 in m_lsr:
  3510. lsr_rem='yes'
  3511. start_state = M[i]
  3512. rem_delay = -1
  3513. if lsr_rem == 'yes':
  3514. rem_delay = np.where(m_lsr == 1)[0][0] * dt
  3515. # duration of interval of brainstate start_state till the
  3516. # brain_state switches:
  3517. start_state_int = len(m_lsr)*dt
  3518. a = np.where(m_lsr != start_state)[0]
  3519. if len(a) > 0:
  3520. start_state_int = len(a) * dt
  3521. l = i-1
  3522. while M[l] != 1 and l>0:
  3523. l = l-1
  3524. refr = 'no'
  3525. if M[l] == 1 and l != i-1:
  3526. v = l
  3527. while M[v] == 1 and v > 0:
  3528. v = v-1
  3529. v = v+1
  3530. dur_rem_pre = (l-v+1)*dt
  3531. inrem = len(np.where(M[l+1:i] == 3)[0]) * dt
  3532. if inrem <= dur_rem_pre * 2:
  3533. refr = 'yes'
  3534. data += zip([mouse]*nt*ndim, [ev]*nt*ndim, tvec, vec, vecz,
  3535. label, [i]*nt*ndim, [start_state]*nt*ndim, [start_state_int]*nt*ndim,
  3536. m_cut, [lsr_rem]*nt*ndim, [rem_delay]*nt*ndim, [refr]*nt*ndim)
  3537. ev += 1
  3538. df = pd.DataFrame(data=data, columns=['mouse', 'ev', 'time', 'val', 'valz',
  3539. 'pc', 'lsr_start', 'start_state', 'start_state_int',
  3540. 'state', 'success', 'rem_delay', 'refractory'])
  3541. if pplot:
  3542. plt.figure()
  3543. if local_pzscore:
  3544. sns.lineplot(data=df, x='time', y='valz', hue='pc', palette='husl')
  3545. else:
  3546. sns.lineplot(data=df, x='time', y='val', hue='pc', palette='husl')
  3547. plt.xlim([t[0], t[-1]])
  3548. sns.despine()
  3549. pcs = df.pc.unique()
  3550. data = {p:[] for p in pcs}
  3551. data_start = []
  3552. for p in pcs:
  3553. for si in df.lsr_start.unique():
  3554. a = np.array(df[(df.lsr_start==si) & (df.pc == p)]['val'])
  3555. m_cut = np.array(df[(df.lsr_start==si) & (df.pc == p)]['state'])
  3556. t = np.array(df[(df.lsr_start==si) & (df.pc == p)]['time'])
  3557. l = np.array(df[(df.lsr_start==si) & (df.pc == p)]['success'])[0]
  3558. if p == 'pc1':
  3559. if l == 'yes':
  3560. rem_start = np.where((t >= 0) & (m_cut == 1) )[0][0]
  3561. data_start.append(t[rem_start])
  3562. else:
  3563. data_start.append(-1)
  3564. data[p].append(a)
  3565. for p in pcs:
  3566. data[p] = np.array(data[p])
  3567. f, axes = plt.subplots(nrows=ndim, ncols=1, sharex='all')
  3568. for ax,p in zip(axes, pcs):
  3569. mx = data[p].copy()
  3570. if local_pzscore:
  3571. for i in range(mx.shape[0]):
  3572. mx[i,:] = (mx[i,:] - mx[i,:].mean()) / mx[i,:].std()
  3573. ax.pcolorfast(t, range(0, mx.shape[0]+1), mx, cmap='jet')
  3574. for ii in range(mx.shape[0]):
  3575. tt = data_start[ii]
  3576. if tt >= 0:
  3577. ax.plot([tt,tt], [ii,ii+1], color='black', lw=3)
  3578. ax.set_ylabel('')
  3579. return df
  3580. def laser_triggered_pcs_fine(PC, pre, post, M, mouse, kcuts=[], min_laser=20, pzscore_pc=False, local_pzscore=True, ndown=250,
  3581. pplot=True, ci=None, refractory_rule=False, ma_thr=10, ma_rem_exception=False, rnd_laser=False, seed=1,
  3582. config_file='mouse_config.txt', laser_dur=-1):
  3583. """
  3584. Calculated the time course of the provided PCs relative to the laser onset using finer timescale.
  3585. Parameters
  3586. ----------
  3587. PC : np.array
  3588. Each row corresponds to one PC.
  3589. pre : float
  3590. Time before laser onset.
  3591. post : float
  3592. Time after laser onset.
  3593. M : np.array
  3594. Hypnogram.
  3595. mouse : str
  3596. Mouse name.
  3597. kcuts : list of tuples, optional
  3598. DESCRIPTION. The default is [].
  3599. min_laser : float, optional
  3600. Minimum duration of laser train. If laser duration < $min_laser,
  3601. disregard the laser trial.
  3602. pzscore_pc : bool, optional
  3603. If true, z-score the PCs (across entire recording)
  3604. local_pzscore : bool, optional
  3605. DESCRIPTION. The default is True.
  3606. pplot : bool, optional
  3607. If True, plot figures summarizing results.
  3608. ci : float or None, optional
  3609. Confidence interval for plots.
  3610. refractory_rule : TYPE, optional
  3611. DESCRIPTION. The default is False.
  3612. config_file : str, optional
  3613. Mouse recordings configuration file.
  3614. laser_dur : float
  3615. Duration of laser; if -1, calculate laser duration using laser_*.mat file
  3616. Returns
  3617. -------
  3618. df : pandas.DataFrame
  3619. with columns ['mouse', 'time', 'val', 'valz',
  3620. 'pc', 'lsr_start', 'start_state', 'start_state_int',
  3621. 'state', 'success', 'refractory']
  3622. 'state': brain state sequence
  3623. 'start_state_int': How long does it take till the brain state at laser onset
  3624. switches to a different state
  3625. 'valz': PC values during laser trial, for each trial the PC vector
  3626. is z-scored.
  3627. 'rem_delay': Delay from laser to REM onset; if there's no REM the value is set to -1
  3628. """
  3629. dt = 2.5
  3630. NDOWN = ndown
  3631. NUP = int(dt / (0.001 * NDOWN))
  3632. ddir = load_config(config_file)[mouse]['SL_PATH']
  3633. ppath, name = os.path.split(ddir)
  3634. M = sleepy.load_stateidx(ppath, name)[0]
  3635. # flatten out MAs #########################################################
  3636. if ma_thr>0:
  3637. seq = sleepy.get_sequences(np.where(M==2)[0])
  3638. for s in seq:
  3639. if np.round(len(s)*dt) <= ma_thr:
  3640. if ma_rem_exception:
  3641. if (s[0]>1) and (M[s[0] - 1] != 1):
  3642. M[s] = 3
  3643. else:
  3644. M[s] = 3
  3645. ###########################################################################
  3646. dt = dt / NUP
  3647. M = upsample_mx(M, NUP)
  3648. nhypno = len(M)
  3649. M = M[0:nhypno]
  3650. tidx = np.arange(0, nhypno)
  3651. # NEW 07/01/22:
  3652. # get the indices (in brainstate time) that we're going to completely discard:
  3653. if len(kcuts) > 0:
  3654. kidx = []
  3655. for kcut in kcuts:
  3656. a = int(kcut[0]/dt)
  3657. b = int(kcut[-1]/dt)
  3658. if b > len(M):
  3659. b = len(M)
  3660. kidx += list(np.arange(a, b))
  3661. tidx = np.setdiff1d(tidx, kidx)
  3662. M = M[tidx]
  3663. nhypno = len(tidx)
  3664. ###########################################################################
  3665. if os.path.isfile(os.path.join(ddir, 'laser_%s.mat' % name)):
  3666. sr = sleepy.get_snr(ppath, name)
  3667. nbin = NDOWN
  3668. ipre = int(pre/dt)
  3669. ipost = int(post/dt)
  3670. t = np.arange(-ipre, ipost)*dt
  3671. nt = len(t)
  3672. #######################################################################
  3673. # get laser start and end index after excluding kcuts: ################
  3674. lsr = so.loadmat(os.path.join(ddir, 'laser_%s.mat' % name), squeeze_me=True)['laser']
  3675. idxs, idxe = sleepy.laser_start_end(lsr)
  3676. idxs = [int(i/nbin) for i in idxs]
  3677. if laser_dur == -1:
  3678. idxe = [int(i/nbin) for i in idxe]
  3679. b = [i for i in idxe if i in tidx]
  3680. a = [i for i in idxs if i in tidx]
  3681. b = np.array(b)
  3682. a = np.array(a)
  3683. laser_dur = (np.mean(b-a) + 1) * dt
  3684. print('Laser duration: %f' % laser_dur)
  3685. else:
  3686. idxe = [(int(i + int(laser_dur/dt))) for i in idxs]
  3687. laser_idx = []
  3688. for (si,sj) in zip(idxs, idxe):
  3689. laser_idx += list(range(si,sj+1))
  3690. laser_idx = np.array(laser_idx)
  3691. nlsr = int(np.floor(lsr.shape[0]/nbin))
  3692. laser = np.zeros((nlsr,))
  3693. laser_idx = laser_idx[laser_idx < nlsr]
  3694. laser[laser_idx] = 1
  3695. laser = laser[tidx]
  3696. idxs = [s[0] for s in sleepy.get_sequences(np.where(laser == 1)[0])]
  3697. idxe = [s[-1] for s in sleepy.get_sequences(np.where(laser == 1)[0])]
  3698. #######################################################################
  3699. ndim = np.min(PC.shape)
  3700. label = []
  3701. for p in range(1, ndim+1):
  3702. l = 'pc' + str(p)
  3703. label.extend([l]*nt)
  3704. # zscore pcs:
  3705. if pzscore_pc:
  3706. for i in range(PC.shape[0]):
  3707. PC[i,:] = (PC[i,:]- PC[i,:].mean()) / PC[i,:].std()
  3708. data = []
  3709. ev = 0
  3710. for i,j in zip(idxs, idxe):
  3711. if i > ipre and i+ipost < nhypno:
  3712. if (j-i)*dt < min_laser:
  3713. continue
  3714. idx = np.arange(i-ipre, i+ipost).astype('int')
  3715. pc_cut = PC[:,idx]
  3716. pc_cut_z = pc_cut.copy()
  3717. for k in range(ndim):
  3718. pc_cut_z[k,:] = (pc_cut[k,:] - pc_cut[k,:].mean()) / pc_cut[k,:].std()
  3719. m_lsr = M[i:j+1]
  3720. # repeat m_cut ndim-times
  3721. m_cut = np.tile(M[idx], (ndim,))
  3722. vec = np.reshape(pc_cut, (ndim*nt,))
  3723. vecz = np.reshape(pc_cut_z, (ndim*nt,))
  3724. tvec = np.tile(t, (ndim,))
  3725. lsr_rem = 'no'
  3726. if 1 in m_lsr:
  3727. lsr_rem='yes'
  3728. start_state = M[i]
  3729. rem_delay = -1
  3730. if lsr_rem == 'yes':
  3731. rem_delay = np.where(m_lsr == 1)[0][0] * dt
  3732. # duration of interval of brainstate start_state till the
  3733. # brain_state switches:
  3734. start_state_int = len(m_lsr)*dt
  3735. a = np.where(m_lsr != start_state)[0]
  3736. if len(a) > 0:
  3737. start_state_int = len(a) * dt
  3738. l = i-1
  3739. while M[l] != 1 and l>0:
  3740. l = l-1
  3741. refr = 'no'
  3742. if M[l] == 1 and l != i-1:
  3743. v = l
  3744. while M[v] == 1 and v > 0:
  3745. v = v-1
  3746. v = v+1
  3747. dur_rem_pre = (l-v+1)*dt
  3748. inrem = len(np.where(M[l+1:i] == 3)[0]) * dt
  3749. if inrem <= dur_rem_pre * 2:
  3750. refr = 'yes'
  3751. data += zip([mouse]*nt*ndim, [ev]*nt*ndim, tvec, vec, vecz,
  3752. label, [i]*nt*ndim, [start_state]*nt*ndim, [start_state_int]*nt*ndim,
  3753. m_cut, [lsr_rem]*nt*ndim, [rem_delay]*nt*ndim, [refr]*nt*ndim)
  3754. ev += 1
  3755. df = pd.DataFrame(data=data, columns=['mouse', 'ev', 'time', 'val', 'valz',
  3756. 'pc', 'lsr_start', 'start_state', 'start_state_int',
  3757. 'state', 'success', 'rem_delay', 'refractory'])
  3758. if pplot:
  3759. plt.figure()
  3760. if local_pzscore:
  3761. sns.lineplot(data=df, x='time', y='valz', hue='pc', palette='husl')
  3762. else:
  3763. sns.lineplot(data=df, x='time', y='val', hue='pc', palette='husl')
  3764. plt.xlim([t[0], t[-1]])
  3765. sns.despine()
  3766. pcs = df.pc.unique()
  3767. data = {p:[] for p in pcs}
  3768. data_start = []
  3769. for p in pcs:
  3770. for si in df.lsr_start.unique():
  3771. a = np.array(df[(df.lsr_start==si) & (df.pc == p)]['val'])
  3772. m_cut = np.array(df[(df.lsr_start==si) & (df.pc == p)]['state'])
  3773. t = np.array(df[(df.lsr_start==si) & (df.pc == p)]['time'])
  3774. l = np.array(df[(df.lsr_start==si) & (df.pc == p)]['success'])[0]
  3775. if p == 'pc1':
  3776. if l == 'yes':
  3777. rem_start = np.where((t >= 0) & (m_cut == 1) )[0][0]
  3778. data_start.append(t[rem_start])
  3779. else:
  3780. data_start.append(-1)
  3781. data[p].append(a)
  3782. for p in pcs:
  3783. data[p] = np.array(data[p])
  3784. f, axes = plt.subplots(nrows=ndim, ncols=1, sharex='all')
  3785. for ax,p in zip(axes, pcs):
  3786. mx = data[p].copy()
  3787. if local_pzscore:
  3788. for i in range(mx.shape[0]):
  3789. mx[i,:] = (mx[i,:] - mx[i,:].mean()) / mx[i,:].std()
  3790. ax.pcolorfast(t, range(0, mx.shape[0]+1), mx, cmap='jet')
  3791. for ii in range(mx.shape[0]):
  3792. tt = data_start[ii]
  3793. if tt >= 0:
  3794. ax.plot([tt,tt], [ii,ii+1], color='black', lw=3)
  3795. ax.set_ylabel('')
  3796. return df
  3797. # --------------------------------------------------------------
  3798. # replacement for laser_triggered_pcs()
  3799. # randomises *where* a laser trial is assumed to occur
  3800. # but only keeps trials whose PC2 value (row index 1) exceeds pc_thr
  3801. # --------------------------------------------------------------
  3802. def random_pc2_triggered_pcs(
  3803. PC, # (n_dim × T) principal components
  3804. pre, post, # flank lengths in seconds
  3805. M, # hypnogram (1=REM,2=Wake,3=NREM ...)
  3806. mouse,
  3807. n_trials = 40, # how many pseudo-laser trials
  3808. pc_thr = 2.0, # threshold for PC2
  3809. dt = 2.5, # bin size of M and PC (s)
  3810. kcuts = None, # list of (t0,t1) tuples to exclude
  3811. pzscore_pc = False,
  3812. local_pzscore= True,
  3813. pplot = True,
  3814. config_file = 'mouse_config.txt'):
  3815. """
  3816. Pick `n_trials` random time points where PC2 > pc_thr and
  3817. build the same trial-aligned PC dataframe as laser_triggered_pcs().
  3818. """
  3819. # <- your helpers
  3820. # ---------- basic sizes ----------
  3821. ndim, T = PC.shape
  3822. ipre = int(round(pre / dt))
  3823. ipost = int(round(post / dt))
  3824. nt = ipre + ipost
  3825. tvec = np.arange(-ipre, ipost) * dt
  3826. # ---------- mask out k-cuts (if any) ------------------------
  3827. kmask = np.ones(T, dtype=bool)
  3828. if kcuts:
  3829. for t0, t1 in kcuts:
  3830. kmask[int(t0 / dt) : int(t1 / dt)] = False
  3831. # ---------- admissible centres --------------------------------
  3832. ok = np.ones(T, dtype=bool)
  3833. ok[:ipre] = False # need full window on the left
  3834. ok[-ipost:] = False # ... and on the right
  3835. ok &= kmask
  3836. # PC2 threshold
  3837. ok &= PC[1, :] > pc_thr
  3838. centres = np.where(ok)[0]
  3839. if len(centres) < n_trials:
  3840. raise RuntimeError(f"Only {len(centres)} time points fulfil PC2>{pc_thr}")
  3841. rng = np.random.default_rng(1)
  3842. sel = rng.choice(centres, size=n_trials, replace=False)
  3843. # ---------- assemble dataframe (same columns as before) -------
  3844. rows = []
  3845. for ev, c in enumerate(sel):
  3846. idx = np.arange(c-ipre, c+ipost)
  3847. start_state = M[c]
  3848. m_window = M[idx]
  3849. # per-trial z-score
  3850. win = PC[:, idx].copy()
  3851. win_z = (win - win.mean(1, keepdims=True)) / win.std(1, keepdims=True)
  3852. for k, lab in enumerate([f'pc{i+1}' for i in range(ndim)]):
  3853. rows.append(pd.DataFrame({
  3854. 'mouse' : mouse,
  3855. 'ev' : ev,
  3856. 'time' : tvec,
  3857. 'val' : win[k],
  3858. 'valz' : win_z[k] if local_pzscore else win[k],
  3859. 'pc' : lab,
  3860. 'lsr_start' : c,
  3861. 'start_state' : start_state,
  3862. 'start_state_int': (np.where(m_window != start_state)[0][0] * dt
  3863. if np.any(m_window != start_state) else len(m_window)*dt),
  3864. 'state' : m_window,
  3865. 'success' : 'rnd', # no real “success”
  3866. 'rem_delay' : -1,
  3867. 'refractory' : 'na'
  3868. }))
  3869. df = pd.concat(rows, ignore_index=True)
  3870. # ---------- optional global z-score ---------------------------
  3871. if pzscore_pc:
  3872. df['val'] = (df['val'] - df['val'].mean()) / df['val'].std()
  3873. df['valz'] = (df['valz'] - df['valz'].mean()) / df['valz'].std()
  3874. # ---------- optional quick QC plot ---------------------------
  3875. if pplot:
  3876. sns.lineplot(data=df, x='time', y='valz' if local_pzscore else 'val',
  3877. hue='pc', palette='husl', errorbar='se')
  3878. plt.axvline(0, ls='--', c='k', lw=.8)
  3879. plt.xlabel('Time (s)'); plt.ylabel('PC value (z-score)')
  3880. sns.despine(); plt.tight_layout()
  3881. return df
  3882. def laser_triggered_frs(units, pre, post, mouse, kcuts=[], ma_thr=10, ma_rem_exception=True,
  3883. pzscore=True, nsmooth=0, detrend=True, min_laser=20,
  3884. config_file='mouse_config.txt'):
  3885. """
  3886. Parameters
  3887. ----------
  3888. units : TYPE
  3889. DESCRIPTION.
  3890. pre : TYPE
  3891. DESCRIPTION.
  3892. post : TYPE
  3893. DESCRIPTION.
  3894. mouse : TYPE
  3895. DESCRIPTION.
  3896. kcuts : TYPE, optional
  3897. DESCRIPTION. The default is [].
  3898. ma_thr : TYPE, optional
  3899. DESCRIPTION. The default is 10.
  3900. ma_rem_exception : TYPE, optional
  3901. DESCRIPTION. The default is True.
  3902. pzscore : TYPE, optional
  3903. DESCRIPTION. The default is True.
  3904. nsmooth : TYPE, optional
  3905. DESCRIPTION. The default is 0.
  3906. detrend : TYPE, optional
  3907. DESCRIPTION. The default is True.
  3908. min_laser : TYPE, optional
  3909. DESCRIPTION. The default is 20.
  3910. config_file : TYPE, optional
  3911. DESCRIPTION. The default is 'mouse_config.txt'.
  3912. Returns
  3913. -------
  3914. df : TYPE
  3915. DESCRIPTION.
  3916. """
  3917. dt = 2.5
  3918. ddir = load_config(config_file)[mouse]['SL_PATH']
  3919. ppath, name = os.path.split(ddir)
  3920. M = sleepy.load_stateidx(ppath, name)[0]
  3921. nhypno = M.shape[0]-1
  3922. ndim = units.shape[1]
  3923. tidx = np.arange(0, nhypno)
  3924. # NEW 07/01/22:
  3925. # get the indices (in brainstate time) that we're going to completely discard:
  3926. if len(kcuts) > 0:
  3927. kidx = []
  3928. for kcut in kcuts:
  3929. a = int(kcut[0]/dt)
  3930. b = int(kcut[-1]/dt)
  3931. if b > len(M):
  3932. b = len(M)
  3933. kidx += list(np.arange(a, b))
  3934. tidx = np.setdiff1d(tidx, kidx)
  3935. M = M[tidx]
  3936. nhypno = len(tidx)
  3937. ###########################################################################
  3938. # flatten out MAs #########################################################
  3939. if ma_thr>0:
  3940. seq = sleepy.get_sequences(np.where(M==2)[0])
  3941. for s in seq:
  3942. if np.round(len(s)*dt) <= ma_thr:
  3943. if ma_rem_exception:
  3944. if (s[0]>0) and (M[s[0] - 1] != 1):
  3945. M[s] = 3
  3946. else:
  3947. M[s] = 3
  3948. ###########################################################################
  3949. if os.path.isfile(os.path.join(ddir, 'laser_%s.mat' % name)):
  3950. sr = sleepy.get_snr(ppath, name)
  3951. nbin = int(np.round(sr)*dt)
  3952. dt = nbin * (1.0/sr)
  3953. ipre = int(pre/dt)
  3954. ipost = int(post/dt)
  3955. t = np.arange(-ipre, ipost)*dt
  3956. nt = len(t)
  3957. #######################################################################
  3958. # get laser start and end index after excluding kcuts: ################
  3959. lsr = so.loadmat(os.path.join(ddir, 'laser_%s.mat' % name), squeeze_me=True)['laser']
  3960. idxs, idxe = sleepy.laser_start_end(lsr)
  3961. idxs = [int(i/nbin) for i in idxs]
  3962. idxe = [int(i/nbin) for i in idxe]
  3963. laser_idx = []
  3964. for (si,sj) in zip(idxs, idxe):
  3965. laser_idx += list(range(si,sj+1))
  3966. nlsr = int(np.floor(lsr.shape[0]/nbin))
  3967. laser = np.zeros((nlsr,))
  3968. laser[laser_idx] = 1
  3969. laser = laser[tidx]
  3970. idxs = [s[0] for s in sleepy.get_sequences(np.where(laser == 1)[0])]
  3971. idxe = [s[-1] for s in sleepy.get_sequences(np.where(laser == 1)[0])]
  3972. #######################################################################
  3973. unitIDs = [unit for unit in units.columns if '_' in unit]
  3974. nvar = len(unitIDs)
  3975. R = np.zeros((nvar, nhypno)) # dimensions: number of units x time bins
  3976. for i,unit in enumerate(unitIDs):
  3977. tmp = sleepy.smooth_data(np.array(units[unit]),nsmooth)
  3978. tmp = tmp[tidx]
  3979. if detrend:
  3980. tmp = scipy.signal.detrend(tmp)
  3981. if pzscore:
  3982. R[i,:] = (tmp - tmp.mean()) / tmp.std()
  3983. else:
  3984. R[i,:] = tmp
  3985. label = []
  3986. for p in range(0, nvar):
  3987. l = unitIDs[p]
  3988. label.extend([l]*nt)
  3989. ms_id = [mouse + '_' + i for i in label]
  3990. data = []
  3991. for i,j in zip(idxs, idxe):
  3992. if i > ipre and i+ipost < nhypno:
  3993. if (j-i)*dt < min_laser:
  3994. continue
  3995. idx = np.arange(i-ipre, i+ipost).astype('int')
  3996. r_cut = R[:,idx]
  3997. m_lsr = M[i:j+1]
  3998. # repeat m_cut ndim-times
  3999. m_cut = np.tile(M[idx], (nvar,))
  4000. vec = np.reshape(r_cut, (nvar*nt,))
  4001. tvec = np.tile(t, (ndim,))
  4002. lsr_rem = 'no'
  4003. if 1 in m_lsr:
  4004. lsr_rem='yes'
  4005. start_state = M[i]
  4006. # duration of interval of brainstate start_state till the
  4007. # brain state switches:
  4008. start_state_int = len(m_lsr)*dt
  4009. a = np.where(m_lsr != start_state)[0]
  4010. if len(a) > 0:
  4011. start_state_int = len(a) * dt
  4012. data += zip([mouse]*nt*nvar, tvec, vec,
  4013. label, ms_id, [i]*nt*nvar, [start_state]*nt*nvar, [start_state_int]*nt*nvar,
  4014. m_cut, [lsr_rem]*nt*nvar)
  4015. df = pd.DataFrame(data=data, columns=['mouse', 'time', 'fr', 'ID',
  4016. 'ms_id', 'lsr_start', 'start_state', 'start_state_int',
  4017. 'state', 'success'])
  4018. return df
  4019. def plot_trajectories(PC, M, pre, post, istate=1, dt=2.5, ma_thr=10, ma_rem_exception=False,
  4020. kcuts=[],min_dur=0, pre_state=0, state_num=[], ax='', lw=1, coords=[0,1], mouse=''):
  4021. tidx = np.arange(0, len(M))
  4022. # NEW 07/01/22:
  4023. # get the indices (in brainstate time) that we're going to completely discard:
  4024. if len(kcuts) > 0:
  4025. kidx = []
  4026. for kcut in kcuts:
  4027. a = int(kcut[0]/dt)
  4028. b = int(kcut[-1]/dt)
  4029. if b > len(M):
  4030. b = len(M)
  4031. kidx += list(np.arange(a, b))
  4032. tidx = np.setdiff1d(tidx, kidx)
  4033. M = M[tidx]
  4034. nhypno = len(tidx)
  4035. ###########################################################################
  4036. nhypno = np.min((len(M), PC.shape[1]))
  4037. M = M[0:nhypno]
  4038. # flatten out MAs
  4039. if ma_thr>0:
  4040. seq = sleepy.get_sequences(np.where(M==2)[0])
  4041. for s in seq:
  4042. if np.round(len(s)*dt) <= ma_thr:
  4043. if ma_rem_exception:
  4044. if (s[0]>1) and (M[s[0] - 1] != 1):
  4045. M[s] = 3
  4046. else:
  4047. M[s] = 3
  4048. if ax == '':
  4049. plt.figure()
  4050. ax = plt.axes([0.15, 0.15, 0.7, 0.7])
  4051. ax_3d = False
  4052. if ax == '3D':
  4053. ax_3d = True
  4054. fig = plt.figure()
  4055. ax = fig.add_subplot(111, projection='3d')
  4056. clrs = [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8], [1, 0.2, 0.2]]
  4057. ipre = int(pre/dt)
  4058. ipost = int(post/dt)
  4059. if istate in [1,2,3]:
  4060. if istate > 0:
  4061. seq = sleepy.get_sequences(np.where(M==istate)[0])
  4062. else:
  4063. seq = [np.arange(0, len(M))]
  4064. if len(state_num) > 0:
  4065. seq = [seq[i] for i in state_num]
  4066. for s in seq:
  4067. si = s[0]
  4068. sj = s[-1]
  4069. if si-ipre > 0 and sj+ipost < nhypno and len(s)*dt > min_dur and M[si-1]==pre_state:
  4070. #print(len(s)*dt)
  4071. mcut = M[si-ipre:sj+ipost]
  4072. k = si-ipre
  4073. p = mcut[0]
  4074. kold = k
  4075. while k < sj+ipost:
  4076. while M[k] == p and k < sj+ipost:
  4077. k+=1
  4078. if ax_3d:
  4079. ax.plot(PC[coords[0],kold:k+1], PC[coords[1], kold:k+1], PC[coords[2], kold:k+1], color=clrs[int(p)], lw=lw)
  4080. else:
  4081. ax.plot(PC[coords[0],kold:k+1], PC[coords[1], kold:k+1], color=clrs[int(p)], lw=lw)
  4082. p = M[k]
  4083. kold = k
  4084. plt.xlabel('PC1')
  4085. plt.ylabel('PC2')
  4086. if istate == 'laser':
  4087. ddir = load_config('mouse_config.txt')[mouse]['SL_PATH']
  4088. ppath, name = os.path.split(ddir)
  4089. sr = sleepy.get_snr(ppath, name)
  4090. nbin = int(np.round(sr)*2.5)
  4091. dt = nbin * (1.0/sr)
  4092. #######################################################################
  4093. # get laser start and end index after excluding kcuts: ################
  4094. lsr = so.loadmat(os.path.join(ddir, 'laser_%s.mat' % name), squeeze_me=True)['laser']
  4095. idxs, idxe = sleepy.laser_start_end(lsr)
  4096. idxs = [int(i/nbin) for i in idxs]
  4097. idxe = [int(i/nbin) for i in idxe]
  4098. laser_idx = []
  4099. for (si,sj) in zip(idxs, idxe):
  4100. laser_idx += list(range(si,sj+1))
  4101. nlsr = int(np.floor(lsr.shape[0]/nbin))
  4102. laser = np.zeros((nlsr,))
  4103. laser[laser_idx] = 1
  4104. laser = laser[tidx]
  4105. idxs = np.array([s[0] for s in sleepy.get_sequences(np.where(laser == 1)[0])])
  4106. idxe = np.array([s[-1] for s in sleepy.get_sequences(np.where(laser == 1)[0])])
  4107. #######################################################################
  4108. for (si,sj) in zip(idxs[state_num], idxe[state_num]):
  4109. if (sj-si) * dt < 20:
  4110. continue
  4111. mcut = M[si:sj+1]
  4112. if 1 in mcut:
  4113. ax.plot(PC[coords[0],si-ipre:si], PC[coords[1], si-ipre:si], color='cornflowerblue', lw=lw)
  4114. ax.plot(PC[coords[0],si-1:sj], PC[coords[1], si-1:sj], color='blue', lw=lw)
  4115. ax.plot(PC[coords[0],si-1], PC[coords[1], si-1], color='orange', marker='o', lw=lw)
  4116. else:
  4117. ax.plot(PC[coords[0],si-ipre:si], PC[coords[1], si-ipre:si], color='pink', lw=lw)
  4118. ax.plot(PC[coords[0],si-1:sj], PC[coords[1], si-1:sj], color='red', lw=lw)
  4119. ax.plot(PC[coords[0],si-1], PC[coords[1], si-1], color='green', marker='o', lw=lw)
  4120. def pc_state_space(PC, M, ma_thr=10, ma_rem_exception=False, kcuts=[], dt=2.5, ax='', nrem2wake=False, nrem2wake_step=4,
  4121. pscatter=True, local_coord=False, outline_std=True, rem_onset=False, rem_offset=False, rem_offset_only_nrem=False, show_avgtraj=False,
  4122. pre_win=30, post_win=0, rem_min_dur=0, break_out=True, break_in=False, prefr=False, scale=1.645, pzscore_pc=False):
  4123. """
  4124. Plot for each time point the population activity within the 2D state space spanned
  4125. by PC[0,:] and PC[1,:]
  4126. Parameters
  4127. ----------
  4128. PC : np.array
  4129. each row are the PC coefficients or scores
  4130. M : np.array
  4131. brain state annotation.
  4132. 1 - REM, 2 - Wake, 3 - NREM
  4133. ma_thr : float, optional
  4134. Microarousal threshold. The default is 10.
  4135. ma_rem_exception : bool, optional
  4136. If True, don't set a wake episodes after REM that is shorter then $ma_thr to NREM.
  4137. The default is False.
  4138. kcuts : list of tuples, optional
  4139. Each tuple describes the start and end of an interval to be discarded. The default is [].
  4140. dt : float, optional
  4141. Time bin of firing rates and hypnogram. The default is 2.5.
  4142. ax : figure axis handle, optional
  4143. If $as is provided, use it to draw all plots on it.
  4144. Otherwise generate new figure.
  4145. The default is ''.
  4146. nrem2wake : Bool, optional
  4147. if True, show NREM->Wake transitions
  4148. nrem2wake_step: int, optional
  4149. Show every nrem2wake_step-th NREM->Wake transition; otherwise
  4150. it gets too clusttered
  4151. pscatter : bool, optional
  4152. If True, draw each (dimensionally reduced) population vector in a scatter plot.
  4153. The default is True.
  4154. local_coord : bool, optional
  4155. If True, draw into the NREM ellipse a local coordinate system. The default is False.
  4156. outline_std : bool
  4157. If True, draw outline of one std using an ellipse;
  4158. if False, use sns.kdeplot to draw outline of data spread
  4159. rem_onset : bool
  4160. If True, color-code the REM onset and the preceding $pre_win seconds
  4161. rem_offset : bool
  4162. If True, plot REM->Wake->NREM transitions
  4163. rem_offset_only_nrem: bool
  4164. If True, plot only REM->Wake->NREM transitions where NREM occurs within
  4165. the $post_win interval following the REM offset.
  4166. show_avgtraj : bool
  4167. If True, show average across all trajectories instead of each individual
  4168. trajectory
  4169. rem_mindur: float
  4170. Only REM episodes >= rem_mindur are considered
  4171. break_out: bool
  4172. if True, draw a dot where a NREM to REM trajectory
  4173. leaves the NREM subspace, defined by $scale
  4174. prefr: bool,
  4175. If True, also draw outline of refractory period within state space
  4176. scale: float
  4177. $scale == 1 means that the drawn ellipse outlines one standard deviation
  4178. for each subspace.
  4179. $scale == 1.645 outlines the area (of the fitted Gaussian)
  4180. that comprises 90% of the data distribution.
  4181. $scale == 1.96 outlines 95% of the distribution
  4182. pzscore_pc: bool
  4183. If True, zscore PCs.
  4184. Returns
  4185. -------
  4186. ax : plt.axes
  4187. return axes of current figure.
  4188. df_breakout : pd.DataFrame
  4189. with columns ['angle':first_angles, 'pc1':c1, 'pc2':c2, 'pc1_org':porg1, 'pc2_org':porg2]
  4190. Describes along each NREM->REM trajectory the first point and angle leaving the
  4191. NREM subspace
  4192. df_breakin : pd.DataFrame
  4193. Describes for each REM->Wake->NREM transition trajectory the first point within NREM.
  4194. df_ampl: pd.DataFrame
  4195. Describes for each NREM->REM ($nrem2wake=False) or NREM->Wake ($nrem2wake=True) the maximum
  4196. PC1 and PC2 values of the preceding trajectory of during $pre_win.
  4197. """
  4198. bs_map = {'rem':[0, 1, 1], 'wake':[0.6, 0, 1], 'nrem':[0.8, 0.8, 0.8]}
  4199. tidx = np.arange(0, len(M))
  4200. # get the indices (in brainstate time) that we're going to completely discard:
  4201. if len(kcuts) > 0:
  4202. kidx = []
  4203. for kcut in kcuts:
  4204. a = int(kcut[0]/dt)
  4205. b = int(kcut[-1]/dt)
  4206. if b > len(M):
  4207. b = len(M)
  4208. kidx += list(np.arange(a, b))
  4209. tidx = np.setdiff1d(tidx, kidx)
  4210. M = M[tidx]
  4211. nhypno = len(tidx)
  4212. ###########################################################################
  4213. nhypno = np.min((len(M), PC.shape[1]))
  4214. M = M[0:nhypno]
  4215. # zscore PCs:
  4216. if pzscore_pc:
  4217. for i in range(PC.shape[0]):
  4218. PC[i,:] = (PC[i,:] - PC[i,:].mean()) / PC[i,:].std()
  4219. # flatten out MAs
  4220. if ma_thr>0:
  4221. seq = sleepy.get_sequences(np.where(M==2)[0])
  4222. for s in seq:
  4223. if np.round(len(s)*dt) <= ma_thr:
  4224. if ma_rem_exception:
  4225. if (s[0]>1) and (M[s[0] - 1] != 1):
  4226. M[s] = 3
  4227. else:
  4228. M[s] = 3
  4229. if ax == '':
  4230. plt.figure()
  4231. ax = plt.axes([0.2, 0.15, 0.7, 0.7])
  4232. clrs = [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.6, 0.6, 0.6], [1, 0.2, 0.2]]
  4233. state_idx = {}
  4234. for s in [1,2,3]:
  4235. idx = np.where(M==s)[0]
  4236. state_idx[s] = idx
  4237. # get all indices for REM, Wake, and NREM
  4238. for s in [1,2,3]:
  4239. idx = state_idx[s]
  4240. C = PC[0:2,idx].T
  4241. if outline_std:
  4242. mean = C.mean(axis=0)
  4243. covar = np.cov(C.T)
  4244. v, w = linalg.eigh(covar)
  4245. # columns of w are the eigenvectors
  4246. # v are the eigenvalues in ascending order
  4247. # 2 * scale * std (the 2 is to transform the radius to diameter)
  4248. v = 2.0 * scale * np.sqrt(v)
  4249. u = w[0] / linalg.norm(w[0])
  4250. # Plot an ellipse to show the Gaussian component
  4251. angle = np.arctan(u[1] / u[0])
  4252. angle = 180.0 * angle / np.pi # convert to degrees
  4253. ell = mpl.patches.Ellipse(mean, v[0], v[1], angle=180.0 + angle, color=clrs[s], lw=2)
  4254. ell.set_clip_box(ax.bbox)
  4255. ell.set_alpha(0.3)
  4256. ax.add_artist(ell)
  4257. else:
  4258. sns.kdeplot(x=C[:, 0], y=C[:, 1], ax=ax, color=clrs[s], fill=True, alpha=0.8, levels=[0.25, 0.5, 0.75, 1])
  4259. # Add refractory period to state space
  4260. if prefr:
  4261. refr_color = 'maroon'
  4262. df_refr, refr_vec, _ = add_refr(M)
  4263. refr_idx = np.where(refr_vec == 1)[0]
  4264. nr_idx = np.where(M==3)[0]
  4265. idx = np.intersect1d(refr_idx, nr_idx)
  4266. C = PC[0:2,idx].T
  4267. if outline_std:
  4268. mean = C.mean(axis=0)
  4269. covar = np.cov(C.T)
  4270. v, w = linalg.eigh(covar)
  4271. # columns of w are the eigenvectors
  4272. # v are the eigenvalues in ascending order
  4273. # 2 * scale * std (the 2 is to transform the radius to diameter)
  4274. v = 2.0 * scale * np.sqrt(v)
  4275. u = w[0] / linalg.norm(w[0])
  4276. # Plot an ellipse to show the Gaussian component
  4277. angle = np.arctan(u[1] / u[0])
  4278. angle = 180.0 * angle / np.pi # convert to degrees
  4279. ell = mpl.patches.Ellipse(mean, v[0], v[1], angle=180.0 + angle, color=refr_color, lw=2)
  4280. ell.set_clip_box(ax.bbox)
  4281. ell.set_alpha(0.3)
  4282. ax.add_artist(ell)
  4283. else:
  4284. sns.kdeplot(x=C[:, 0], y=C[:,1], ax=ax, color=refr_color, fill=True, alpha=0.8, levels=[0.25, 0.5, 0.75, 1])
  4285. if local_coord:
  4286. idx = state_idx[3]
  4287. C = PC[0:2,idx].T
  4288. pca = PCA(n_components=2)
  4289. pca.fit(C)
  4290. mean_x, mean_y = np.mean(C, axis=0)
  4291. pc1, pc2 = pca.components_
  4292. if pc1[1] < 0:
  4293. pc1 = -pc1
  4294. # Calculate the standard deviations along the principal components
  4295. std_x, std_y = np.sqrt(pca.explained_variance_)
  4296. # Scale the principal components by the standard deviations
  4297. pc1_scaled = pc1 * std_x * 1 * scale
  4298. pc2_scaled = pc2 * std_y * 1 * scale
  4299. origin = [mean_x], [mean_y]
  4300. ax.quiver(*origin, pc1_scaled[0], pc1_scaled[1], angles='xy', scale_units='xy', scale=1, color='black', label='PC1 (Scaled)')
  4301. ax.quiver(*origin, pc2_scaled[0], pc2_scaled[1], angles='xy', scale_units='xy', scale=1, color='black', label='PC2 (Scaled)')
  4302. pc1_min = PC[0,:].min()
  4303. pc1_max = PC[0,:].max()
  4304. pc2_min = PC[1,:].min()
  4305. pc2_max = PC[1,:].max()
  4306. d1 = pc1_max - pc1_min
  4307. d2 = pc2_max - pc2_min
  4308. ax.set_xlim(pc1_min - 0.1*d1, pc1_max + 0.1*d1)
  4309. ax.set_ylim(pc2_min - 0.1*d2, pc2_max + 0.1*d2)
  4310. if pscatter:
  4311. for s in [1,2,3]:
  4312. idx = state_idx[s]
  4313. ax.scatter(PC[0,idx], PC[1,idx], color=clrs[s], s=1, alpha=1)
  4314. ax.set_xlabel('PC1')
  4315. ax.set_ylabel('PC2')
  4316. sns.despine()
  4317. # just the onset of REM as single dot:
  4318. ipre_win = int(pre_win/dt)
  4319. ipost_win = int(post_win/dt)
  4320. rem_start = [s[0] for s in sleepy.get_sequences(np.where(M==1)[0]) if len(s)*dt >= rem_min_dur and s[0]*dt >= pre_win and s[0]+ipost_win < len(M)]
  4321. if nrem2wake:
  4322. rem_start = [s[0] for s in sleepy.get_sequences(np.where(M==2)[0]) if len(s)*dt >= rem_min_dur and s[0]*dt >= pre_win and M[s[0]-1]==3]
  4323. rem_start = rem_start[1::nrem2wake_step]
  4324. # show trajectories for REM onset
  4325. if rem_onset:
  4326. if not show_avgtraj:
  4327. for r in rem_start[:]:
  4328. if not nrem2wake:
  4329. plt.plot(PC[0,r], PC[1,r], '*', color=bs_map['rem'], markersize=10, zorder=3)
  4330. else:
  4331. plt.plot(PC[0,r], PC[1,r], '*', color=bs_map['wake'], markersize=10, zorder=3)
  4332. for r in rem_start:
  4333. # NEW - 04/17/24:
  4334. pc1, pc2 = PC[0,r-ipre_win:r+ipost_win+1], PC[1,r-ipre_win:r+ipost_win+1]
  4335. sm = _jet_plot(pc1, pc2, ax, lw=2, cmap='magma')
  4336. else:
  4337. tmp1, tmp2 = [], []
  4338. for r in rem_start:
  4339. pc1, pc2 = PC[0,r-ipre_win:r+ipost_win+1], PC[1,r-ipre_win:r+ipost_win+1]
  4340. tmp1.append(pc1)
  4341. tmp2.append(pc2)
  4342. pc1 = np.array(tmp1).mean(axis=0)
  4343. pc2 = np.array(tmp2).mean(axis=0)
  4344. sm = _jet_plot(pc1, pc2, ax, lw=2, cmap='magma')
  4345. cbar = plt.colorbar(sm, ax=ax, orientation='vertical', pad=0.05, shrink=0.6) # pad adjusts the distance between the plot and colorbar
  4346. sm.set_clim(-pre_win, post_win)
  4347. cbar.set_ticks([-pre_win, post_win])
  4348. cbar.set_label("Time (s)")
  4349. if rem_offset:
  4350. ipre_win = int(pre_win/dt)
  4351. ipost_win = int(post_win/dt)
  4352. rem_end_all = [s[-1]+1 for s in sleepy.get_sequences(np.where(M==1)[0]) if len(s)*dt >= rem_min_dur and s[-1]+ipost_win < len(M) and s[-1]>ipre_win]
  4353. tmp = []
  4354. # for each REM offset search for the end of the following wake episode
  4355. for r in rem_end_all:
  4356. i = r
  4357. while i < len(M) and M[i] != 3:
  4358. i += 1
  4359. dur = (i - r) * dt
  4360. if dur <= post_win:
  4361. tmp.append(r)
  4362. rem_end_nrem = tmp
  4363. if rem_offset_only_nrem:
  4364. rem_end = rem_end_nrem
  4365. else:
  4366. rem_end = rem_end_all
  4367. for r in rem_end:
  4368. pc1, pc2 = PC[0,r-ipre_win:r+ipost_win+1], PC[1,r-ipre_win:r+ipost_win+1]
  4369. sm = _jet_plot(pc1, pc2, ax, lw=2, cmap='magma')
  4370. plt.plot(PC[0,r+1], PC[1,r+1], '*', color=bs_map['wake'], markersize=10, zorder=3)
  4371. cbar = plt.colorbar(sm, ax=ax, orientation='vertical', pad=0.05, shrink=0.6) # pad adjusts the distance between the plot and colorbar
  4372. sm.set_clim(-pre_win, post_win)
  4373. cbar.set_ticks([-pre_win, post_win])
  4374. cbar.set_label("Time (s)")
  4375. df_breakout = []
  4376. data_ampl = []
  4377. df_ampl = []
  4378. if break_out:
  4379. seq = sleepy.get_sequences(np.where(M==1)[0])
  4380. idx = state_idx[3]
  4381. C = PC[0:2,idx].T
  4382. meanc = C.mean(axis=0)
  4383. covar = np.cov(C.T)
  4384. pca = PCA(n_components=2)
  4385. pca.fit(C)
  4386. # get the eigenvectors of the covariance matrix:
  4387. pc1, pc2 = pca.components_
  4388. if pc1[1] < 0:
  4389. pc1 = -pc1
  4390. w = np.zeros((2,2))
  4391. w[:,0] = pc1
  4392. w[:,1] = pc2
  4393. first = []
  4394. if not nrem2wake:
  4395. # NREM -> REM
  4396. for r in rem_start:
  4397. ifirst = last_subspace_point(r, PC[0:2,:], meanc, covar, scale=scale)
  4398. first.append(ifirst)
  4399. plt.plot(PC[0,ifirst], PC[1,ifirst], 'ro')
  4400. else:
  4401. # NREM -> Wake
  4402. for r in rem_start:
  4403. if not is_in_ellipse(r, PC[0:2,:], meanc, covar, scale=scale):
  4404. ifirst = last_subspace_point(r, PC[0:2,:], meanc, covar, scale=scale)
  4405. first.append(ifirst)
  4406. #plt.plot(PC[0,ifirst], PC[1,ifirst], 'ro')
  4407. for r in rem_start:
  4408. tmp1 = PC[0,r-ipre_win:r+1]
  4409. tmp2 = PC[1,r-ipre_win:r+1]
  4410. # calculation current REM duration
  4411. state = M[r]
  4412. ii = r
  4413. while ii < len(M) and M[ii] == state:
  4414. ii = ii+1
  4415. dur = (ii-r) * dt
  4416. # END #################################
  4417. #data_ampl += [[tmp1.max(), tmp2.max(), dur]]
  4418. data_ampl += [[tmp1.max(), tmp2.max(), dur]]
  4419. df_ampl = pd.DataFrame(data=data_ampl, columns=['pc1', 'pc2', 'dur_post'])
  4420. # Alternative way of calculating the eigenvectors of the
  4421. # covariance matrix:
  4422. # v, w = linalg.eigh(covar)
  4423. # ii = np.argsort(v)[::-1]
  4424. # v = v[ii]
  4425. # w = w[:,ii]
  4426. first_angles = []
  4427. c1, c2 = [], []
  4428. porg1, porg2 = [], []
  4429. porg1_rel, porg2_rel = [], []
  4430. for ifirst in first:
  4431. # take the first point outside the NREM subspace
  4432. p = PC[0:2,ifirst]
  4433. # center the point
  4434. pctr = p - meanc
  4435. # project it onto the eigenvectors
  4436. a = np.dot(pctr, w)
  4437. x, y = a[0], a[1]
  4438. # calculate the angle in radians
  4439. theta = math.atan2(x, y)
  4440. # Convert the angle to degrees;
  4441. # NOTE: 0 deg. corresponds to 3; 90 deg. corresponds to 12
  4442. angle_degrees = math.degrees(theta)
  4443. first_angles.append(angle_degrees)
  4444. c1.append(a[0])
  4445. c2.append(a[1])
  4446. porg1_rel.append(pctr[0])
  4447. porg2_rel.append(pctr[1])
  4448. porg1.append(p[0])
  4449. porg2.append(p[1])
  4450. df_breakout = pd.DataFrame({'angle':first_angles, 'pc1':c1, 'pc2':c2,
  4451. 'pc1_org':porg1, 'pc2_org':porg2,
  4452. 'pc1_rel':porg1_rel, 'pc2_rel':porg2_rel})
  4453. df_breakin = []
  4454. if break_in:
  4455. rem_end = rem_end_nrem
  4456. seq = sleepy.get_sequences(np.where(M==1)[0])
  4457. idx = state_idx[3]
  4458. C = PC[0:2,idx].T
  4459. meanc = C.mean(axis=0)
  4460. covar = np.cov(C.T)
  4461. pca = PCA(n_components=2)
  4462. pca.fit(C)
  4463. # get the eigenvectors of the covariance matrix:
  4464. pc1, pc2 = pca.components_
  4465. if pc1[1] < 0:
  4466. pc1 = -pc1
  4467. w = np.zeros((2,2))
  4468. w[:,0] = pc1
  4469. w[:,1] = pc2
  4470. first = []
  4471. for r in rem_end:
  4472. ifirst = first_subspace_point(r, PC[0:2,:], meanc, covar, scale=scale)
  4473. first.append(ifirst)
  4474. plt.plot(PC[0,ifirst], PC[1,ifirst], 'bo')
  4475. first_angles = []
  4476. c1, c2 = [], []
  4477. porg1, porg2 = [], []
  4478. porg1_rel, porg2_rel = [], []
  4479. for ifirst in first:
  4480. # take the first point outside the NREM subspace
  4481. p = PC[0:2,ifirst]
  4482. # center the point
  4483. pctr = p - meanc
  4484. # project it onto the eigenvectors
  4485. a = np.dot(pctr, w)
  4486. x, y = a[0], a[1]
  4487. # calculate the angle in radians
  4488. theta = math.atan2(x, y)
  4489. # Convert the angle to degrees;
  4490. # NOTE: 0 deg. corresponds to 3; 90 deg. corresponds to 12
  4491. angle_degrees = math.degrees(theta)
  4492. first_angles.append(angle_degrees)
  4493. c1.append(a[0])
  4494. c2.append(a[1])
  4495. porg1_rel.append(pctr[0])
  4496. porg2_rel.append(pctr[1])
  4497. porg1.append(p[0])
  4498. porg2.append(p[1])
  4499. df_breakin = pd.DataFrame({'angle':first_angles, 'pc1':c1, 'pc2':c2,
  4500. 'pc1_org':porg1, 'pc2_org':porg2,
  4501. 'pc1_rel':porg1_rel, 'pc2_rel':porg2_rel})
  4502. return ax, df_breakout, df_breakin, df_ampl
  4503. def state_space_geometry(PC, M, ma_thr=10, ma_rem_exception=False, kcuts=[], dt=2.5, ax='',
  4504. outline_std=True, prefr=True, show_nrem=True, scale=1.645):
  4505. """
  4506. (1) Distance between different subspaces
  4507. (2) Refractory and permissive state space; draw ellipses capturing the distribution
  4508. of the refractory and permissive period. Note that these periods only include
  4509. NREM sleep
  4510. Returns
  4511. -------
  4512. df_geom: pd.DataFrame
  4513. with columns ['pc1', 'pc2', 'area', 'state'] that
  4514. describe for each $state the coordindates of the mean of its subspace
  4515. spanned by 'pc1' and 'pc2' and the 'area' of this subspace.
  4516. df_distr: pd.DataFrame
  4517. with columns ['pc1', 'pc2', 'state']
  4518. All PC1 and PC2 values within refractory and permissive state space
  4519. """
  4520. state_map = {1:'REM', 2:'Wake', 3:'NREM'}
  4521. # KCUTS
  4522. tidx = np.arange(0, len(M))
  4523. # get the indices (in brainstate time) that we're going to completely discard:
  4524. if len(kcuts) > 0:
  4525. kidx = []
  4526. for kcut in kcuts:
  4527. a = int(kcut[0]/dt)
  4528. b = int(kcut[-1]/dt)
  4529. if b > len(M):
  4530. b = len(M)
  4531. kidx += list(np.arange(a, b))
  4532. tidx = np.setdiff1d(tidx, kidx)
  4533. M = M[tidx]
  4534. nhypno = len(tidx)
  4535. ###########################################################################
  4536. nhypno = np.min((len(M), PC.shape[1]))
  4537. M = M[0:nhypno]
  4538. # flatten out MAs
  4539. if ma_thr>0:
  4540. seq = sleepy.get_sequences(np.where(M==2)[0])
  4541. for s in seq:
  4542. if np.round(len(s)*dt) <= ma_thr:
  4543. if ma_rem_exception:
  4544. if (s[0]>1) and (M[s[0] - 1] != 1):
  4545. M[s] = 3
  4546. else:
  4547. M[s] = 3
  4548. if ax == '':
  4549. plt.figure()
  4550. ax = plt.axes([0.2, 0.15, 0.7, 0.7])
  4551. clrs = [[0, 0, 0], [0, 1, 1], [0.6, 0, 1], [0.8, 0.8, 0.8], [1, 0.2, 0.2]]
  4552. state_idx = {}
  4553. for s in [1,2,3]:
  4554. idx = np.where(M==s)[0]
  4555. state_idx[s] = idx
  4556. # get all indices for REM, Wake, and NREM
  4557. data_geom = []
  4558. if not show_nrem:
  4559. shown_states = [1,2]
  4560. else:
  4561. shown_states = [1,2,3]
  4562. for s in shown_states:
  4563. idx = state_idx[s]
  4564. C = PC[0:2,idx].T
  4565. if outline_std:
  4566. mean = C.mean(axis=0)
  4567. covar = np.cov(C.T)
  4568. v, w = linalg.eigh(covar)
  4569. # columns of w are the eigenvectors
  4570. # v are the eigenvalues in ascending order
  4571. # 2 * scale * std (the 2 is to transform the radius to diameter)
  4572. v = 2.0 * scale * np.sqrt(v)
  4573. u = w[0] / linalg.norm(w[0])
  4574. # Plot an ellipse to show the Gaussian component
  4575. angle = np.arctan(u[1] / u[0])
  4576. angle = 180.0 * angle / np.pi # convert to degrees
  4577. ell = mpl.patches.Ellipse(mean, v[0], v[1], angle=180.0 + angle, color=clrs[s], lw=2, fill=True)
  4578. #else:
  4579. # ell = mpl.patches.Ellipse(mean, v[0], v[1], angle=180.0 + angle, color=clrs[s], lw=2)
  4580. ell.set_clip_box(ax.bbox)
  4581. ell.set_alpha(0.3)
  4582. ax.add_artist(ell)
  4583. area = np.pi * v[0] * v[1]
  4584. data_geom += [list(C.mean(axis=0)) + [area] + [state_map[s]]]
  4585. else:
  4586. sns.kdeplot(x=C[:, 0], y=C[:, 1], ax=ax, color=clrs[s], fill=True, alpha=0.8, levels=[0.25, 0.5, 0.75, 1])
  4587. # Add refractory period to state space
  4588. if prefr:
  4589. refr_color = 'maroon'
  4590. perm_color = 'dodgerblue'
  4591. rp_clrs = [perm_color, refr_color]
  4592. df_refr, refr_vec, _ = add_refr(M)
  4593. refr_idx = np.where(refr_vec == 1)[0]
  4594. perm_idx = np.where(refr_vec == 2)[0]
  4595. nr_idx = np.where(M==3)[0]
  4596. nr_idx_refr = np.intersect1d(refr_idx, nr_idx)
  4597. nr_idx_perm = np.intersect1d(perm_idx, nr_idx)
  4598. Crefr = PC[0:2,nr_idx_refr].T
  4599. Cperm = PC[0:2,nr_idx_perm].T
  4600. data_distr = []
  4601. data_distr += zip(Crefr[:,0], Crefr[:,1], ['refr']*Crefr.shape[0])
  4602. data_distr += zip(Cperm[:,0], Cperm[:,1], ['perm']*Cperm.shape[0])
  4603. df_distr = pd.DataFrame(data=data_distr, columns=['pc1', 'pc2', 'state'])
  4604. if outline_std:
  4605. for C,clr,label in zip([Cperm, Crefr], rp_clrs, ['perm', 'refr']):
  4606. mean = C.mean(axis=0)
  4607. covar = np.cov(C.T)
  4608. v, w = linalg.eigh(covar)
  4609. # columns of w are the eigenvectors
  4610. # v are the eigenvalues in ascending order

neuropyx.py at commit ded0273, under MIT · at the source

Overview

Authors: David E. Lozano1, Jiso Hong1, Xi Jin1, Joseph A. Stucynski1, Christian K. Machens2, Shinjae Chung1, Franz Weber1
  1. Department of Neuroscience, Chronobiology and Sleep Institute, Perelman School of Medicine, University of Pennsylvania,Philadelphia, PA USA
  2. Champalimaud Neuroscience Programme, Champalimaud Foundation,Lisbon, Portugal
Institutions: University of Pennsylvania (United States); Champalimaud Foundation (Portugal)
Journal: Nature neuroscience, volume 29, issue 7, pages 1625-1637
Dates: received 8 October 2024; accepted 22 April 2026; published online 25 May 2026; in print 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41593-026-02314-z · PMID 42185574 · PMCID PMC13270127 · OpenAlex W7162341888
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), mouse (organism), systems (subfield)
Methods: Spectral & time-frequency, Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, Evoked potentials, Single-unit activity, calcium imaging, Physiology & signal measures
Keywords: Circadian rhythms and sleep, Neural circuits
MeSH: Brain Stem*, Neurons*, Sleep, REM*, Animals, Electroencephalography, Male, Mice, Mice, Inbred C57BL (* major topic)
Topic: Sleep and Wakefulness Research (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: HHS | NIH | National Heart, Lung, and Blood Institute (NHLBI) (R01HL149133, R01HL172283); NCCIH (R01AT013188); NINDS (T32NS105607)
Citations: cited by 1 paper (Europe PMC); 75 references in the paper
Research resources: RRID:Addgene_20298, RRID:Addgene_27056, we injected AAVrg-Ef1a-mCherry-IRES-Cre RRID:Addgene_55632

Abstract

Rapid-eye-movement (REM) sleep is generated in the brainstem, but the brainstem population dynamics that drive transitions to REM sleep remain largely unknown. Here, combining mouse Neuropixels recordings and dimensionality reduction, we found that population activity in the midbrain and pons is dominated by two components, one of which captures strong infraslow fluctuations in neural activity. During transitions from non-REM (NREM) to REM sleep, the population activity followed a stereotypic trajectory that was preceded by an increase in the infraslow component. Our analysis revealed—across all brainstem areas—subpopulations of REM sleep-activated and REM sleep-inhibited neurons with opposing infraslow dynamics and diverging ramping activity between REM sleep episodes, reinforced through antagonistic functional connections. Activation of REM sleep-promoting medullary neurons rapidly enhanced the infraslow component, whose strength gated the ability of upstream circuits to induce REM sleep. Collectively, our results identify a population-level mechanism for gating REM sleep, suggesting that NREM-to-REM sleep transitions are coordinated by low-dimensional, antagonistic brainstem dynamics.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 17 matches between paragraphs and lines of code.

tortugar/Lab

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: bcb8dae1594e64a511545e34f6050e2a417c1f45, 19 March 2025
Languages: Python (33)
Size: 35 files, 33 scripts
Software Heritage: not archived
Found in: “Code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (29 files), SciPy (25 files), Matplotlib (19 files), h5py (11 files), pandas (10 files), seaborn (9 files), OpenCV (5 files), Pingouin (2 files), scikit-learn (2 files), statsmodels (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
33 files

tortugar/Npx

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: ded027368c37f19c837baf02b81802007886ba46, 16 April 2026
Languages: Jupyter (1), Python (1)
Size: 6 files, 2 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, license file, 1 notebook
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (2 files), NumPy (2 files), seaborn (2 files), h5py (1 file), pandas (1 file), Pingouin (1 file), scikit-learn (1 file), SciPy (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
4 files

Code availability

Code for sleep annotation, analysis of sleep data and EEG/EMG recordings is available on GitHub via https://github.com/tortugar/Lab/. Code for analysis of Neuropixels data is available at https://github.com/tortugar/Npx/.

Reproduced under the paper's license (CC BY), from the paper cited above.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 35 scripts, each with its path and the digest of its content;
  • 17 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data availability

Neuropixels datasets generated as part of this study are available on Zenodo via 10.5281/zenodo.19462601 (ref. 75). Source data are provided with this paper.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Publisher: n/a → Nature Portfolio

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 7 authors, 2 keywords, 8 MeSH terms, 3 funders, 75 references, 3 RRIDs.

Cite

This paper

Lozano, D. E., Hong, J., Jin, X., Stucynski, J. A., Machens, C. K., Chung, S., & Weber, F. (2026). Low-dimensional population dynamics in the brainstem gate REM sleep. Nature neuroscience, 29(7), 1625-1637. https://doi.org/10.1038/s41593-026-02314-z

BibTeX

@article{lozano2026low,
author = {Lozano, David E. and Hong, Jiso and Jin, Xi and Stucynski, Joseph A. and Machens, Christian K. and Chung, Shinjae and Weber, Franz},
title = {{Low-dimensional population dynamics in the brainstem gate REM sleep}},
journal = {Nature neuroscience},
year = {2026},
month = may,
volume = {29},
number = {7},
pages = {1625--1637},
publisher = {Nature Portfolio},
issn = {1097-6256},
doi = {10.1038/s41593-026-02314-z},
url = {https://doi.org/10.1038/s41593-026-02314-z},
pmid = {42185574},
pmcid = {PMC13270127}
}

RIS

TY - JOUR
AU - Lozano, David E.
AU - Hong, Jiso
AU - Jin, Xi
AU - Stucynski, Joseph A.
AU - Machens, Christian K.
AU - Chung, Shinjae
AU - Weber, Franz
TI - Low-dimensional population dynamics in the brainstem gate REM sleep
T2 - Nature neuroscience
J2 - Nat Neurosci
PY - 2026
DA - 2026/05/25
VL - 29
IS - 7
SP - 1625
EP - 1637
SN - 1097-6256
PB - Nature Portfolio
DO - 10.1038/s41593-026-02314-z
UR - https://doi.org/10.1038/s41593-026-02314-z
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41593-026-02314-z",
"type": "article-journal",
"title": "Low-dimensional population dynamics in the brainstem gate REM sleep",
"container-title": "Nature neuroscience",
"author": [
{
"family": "Lozano",
"given": "David E."
},
{
"family": "Hong",
"given": "Jiso"
},
{
"family": "Jin",
"given": "Xi"
},
{
"family": "Stucynski",
"given": "Joseph A."
},
{
"family": "Machens",
"given": "Christian K."
},
{
"family": "Chung",
"given": "Shinjae"
},
{
"family": "Weber",
"given": "Franz"
}
],
"container-title-short": "Nat Neurosci",
"volume": "29",
"issue": "7",
"page": "1625-1637",
"DOI": "10.1038/s41593-026-02314-z",
"PMID": "42185574",
"PMCID": "PMC13270127",
"ISSN": "1097-6256",
"publisher": "Nature Portfolio",
"URL": "https://doi.org/10.1038/s41593-026-02314-z",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
25
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41467-026-74768-5 [code]
Parallel cholinergic circuit in oculomotor nucleus to control eye movements and REM sleep.
Journal: Nature communications
In common: seaborn, scikit-learn, pandas, 3 other tools, systems, mouse, 11 references
[2] doi:10.1038/s41467-026-73106-z [code]
Respiratory pauses highlight sleep architecture in mice.
Journal: Nature communications
In common: h5py, SciPy, Matplotlib, 1 other tool, EEG, mouse, 8 references
[3] doi:10.1038/s41593-026-02232-0 [code]
Entorhinal cortex represents task-relevant remote locations independently of CA1.
Journal: Nature neuroscience
In common: Pingouin, OpenCV, h5py, 7 other tools, systems, mouse, 1 reference
[4] doi:10.1038/s41467-026-74823-1 [code]
Cerebellar activity is triggered by reach endpoint during learning of a complex locomotor task.
Journal: Nature communications
In common: Pingouin, OpenCV, h5py, 7 other tools, mouse, 1 reference
[5] doi:10.1016/j.celrep.2026.117420 [code]
Neural population dynamics of direct electrical stimulation of neocortex.
Journal: Cell reports
In common: OpenCV, statsmodels, seaborn, 5 other tools, systems, mouse, 3 references
[6] doi: [code]
Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement control
Journal: eLife
In common: Pingouin, OpenCV, h5py, 7 other tools, mouse, 1 reference
[7] doi:10.1038/s41467-026-76581-6 [code]
Thalamocortical bursts encode reward contingencies and drive associative learning.
Journal: Nature communications
In common: OpenCV, h5py, scikit-learn, 4 other tools, systems, mouse, 3 references
[8] doi:10.1016/j.celrep.2026.117590 [code]
Impaired behavioral inhibition in Fmr1 KO mice is linked to disrupted visual cortex theta oscillations.
Journal: Cell reports
In common: Pingouin, statsmodels, seaborn, 5 other tools, systems, EEG, mouse, 2 references
[9] doi:10.1016/j.isci.2026.116825 [code]
Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.
Journal: iScience
In common: Pingouin, OpenCV, h5py, 7 other tools, systems, mouse
[10] doi:10.1371/journal.pcbi.1013138 [code]
Hierarchical recurrent temporal prediction as a model of the mammalian dorsal visual pathway.
Journal: PLoS computational biology
In common: Pingouin, OpenCV, h5py, 6 other tools, systems, 1 reference

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.