OSCR

A modular multi-color fluorescence microscope for simultaneous tracking of cellular activity and behavior.

Code ↔ Paper

6 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 6 matches
  1. [1] § Methods › Statistical analysis of calcium imaging ↔ TRN_MixedLinearModel.ipynb, lines 78–91 · score 0.64 · mixed linear model, post, calcium, worms, reversals, signal
  2. [2] § Methods › Gait identification of tardigrades ↔ tardigrade_cluster.ipynb, lines 299–323 · score 0.62 · pairwise distance, UMAP, embedding, clustering, tardigrades
  3. [3] § Methods › Statistical analysis of prey signal ↔ GFP_preycontact.ipynb, lines 243–299 · score 0.59 · biting events, bite onset, baseline, ratio, GFP, signal
  4. [4] § Methods › Pose estimation of tardigrades ↔ tardigrade_cluster.ipynb, lines 128–152 · score 0.59 · stance duration, duty factor, swing, tardigrades
  5. [5] § Results › Brightfield tracking of tardigrades ↔ tardigrade_cluster.ipynb, lines 336–365 · score 0.50 · silhouette score, embedding, cycles, clustering, leg, tardigrades
  6. [6] § Results › Ratiometric imaging of single neurons in moving animals ↔ Dros_fig2.ipynb, lines 14–137 · score 0.50 · motion correction, ratiometric imaging, R0, tracked

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

Jupyter notebook · 602 lines · 24 KB · no license · 3 matches

  1. # %%
  2. import pandas as pd
  3. import numpy as np
  4. from scipy.signal import savgol_filter
  5. import os
  6. import yaml
  7. import matplotlib.pyplot as plt
  8. from tslearn import metrics, barycenters
  9. import matplotlib.pyplot as plt
  10. from tslearn.preprocessing import TimeSeriesScalerMeanVariance, \
  11. TimeSeriesResampler
  12. from scipy.signal import find_peaks
  13. import tardigrade_functions as tg
  14. from sklearn.cluster import AgglomerativeClustering
  15. from sklearn.metrics import silhouette_score
  16. from matplotlib.gridspec import GridSpec
  17. from scipy import stats
  18. from umap import UMAP
  19. # %%
  20. # settings for dataset
  21. date = 20251209
  22. home = os.path.expanduser("~")
  23. expID = 'LB016'
  24. inpath = os.path.join(home, f'data/{expID}/out_{date}')
  25. pose_path = os.path.join(home, f'data/{expID}/poseestimation')
  26. pose_file = 'DLC_Resnet50_TardiLocoOct23shuffle1_snapshot_200.csv'
  27. # %%
  28. # load config
  29. config_path = "config.yaml"
  30. config = yaml.safe_load(open(config_path, "r"))
  31. fps = config['video']['fps']
  32. bodyparts = config['analysis']['bodyparts']
  33. limbs = config['analysis']['limbs']
  34. limbs_ord = config['analysis']['limbs_ord']
  35. pairs = config['analysis']['pairs']
  36. bp_pairs = config['analysis']['bp_pairs']
  37. bp_color_dict = config['color']['bp_color']
  38. roi_defined = config['analysis']['roi_defined']
  39. idx = pd.IndexSlice
  40. # %%
  41. Trkangle_all = pd.DataFrame([])
  42. CMSvelo_all = pd.DataFrame([])
  43. CLbp_all = pd.DataFrame([])
  44. swing_all = pd.DataFrame([])
  45. turns_all = pd.DataFrame([])
  46. straight_all = pd.DataFrame([])
  47. lastmaxidx = 0
  48. for f in os.listdir(inpath):
  49. if expID in f and not '.' in f:
  50. # load angle of tardigrade track
  51. fangle = pd.read_csv(os.path.join(inpath,f,f'{f}_trackangle.csv'), index_col=0).fillna(0)
  52. _ = savgol_filter(fangle, fps, 2, axis=0)
  53. fangle_smooth = pd.DataFrame(_, columns=fangle.columns, index=fangle.index)
  54. fangle_smooth.index = pd.MultiIndex.from_product([[f],fangle_smooth.index])
  55. Trkangle_all = pd.concat([Trkangle_all,fangle_smooth])
  56. # load cms coordinates
  57. cms = pd.read_csv(os.path.join(inpath,f,f'{f}_stage.csv'),index_col=0).loc[:,['Xcms','Ycms']]
  58. cmsvelo = np.sqrt(cms.iloc[:,0].diff(5)**2 + cms.iloc[:,1].diff(5)**2).interpolate(limit_direction='both')/5*fps
  59. cmsvelo.index = pd.MultiIndex.from_product([[f],cmsvelo.index])
  60. CMSvelo_all = pd.concat([CMSvelo_all,cmsvelo.to_frame()])
  61. # load leg position on centerline
  62. f_clbp = pd.read_csv(os.path.join(inpath,f,f'{f}_CLbp.csv'), index_col=0, header=[0,1,2])
  63. f_clbp_norm = (f_clbp - f_clbp.mean(axis=0)) / f_clbp.std(axis=0)
  64. f_clbp_x = f_clbp_norm.loc[:,idx[:,:,'x']].droplevel(level=[0,2],axis=1).fillna(0)
  65. CLbp_all = pd.concat([CLbp_all,f_clbp_x])
  66. # load bool for swings
  67. fswing = pd.read_csv(os.path.join(inpath,f,f'{f}_swing.csv'), index_col=0)
  68. fswing.index = pd.MultiIndex.from_product([[f],fswing.index])
  69. swing_all = pd.concat([swing_all,fswing])
  70. # load bool for turns
  71. fturns = pd.read_csv(os.path.join(inpath,f,f'{f}_turns.csv'), index_col=0)+lastmaxidx
  72. fturns.index = pd.MultiIndex.from_product([[f],fturns.index])
  73. turns_all = pd.concat([turns_all,fturns])
  74. # load bool for straight walks
  75. fstraight = pd.read_csv(os.path.join(inpath,f,f'{f}_straight.csv'), index_col=0)
  76. fstraight.index = pd.MultiIndex.from_product([[f],fstraight.index])
  77. st_offset = pd.DataFrame([False]*(len(f_clbp)-len(fstraight)), columns=fstraight.columns)
  78. st_offset.index = pd.MultiIndex.from_product([[f],st_offset.index])
  79. straight_all = pd.concat([straight_all, fstraight, st_offset])
  80. lastmaxidx += len(f_clbp)
  81. # %%
  82. lastmaxidx = 0
  83. rec_cycinfo = {}
  84. cyc_bouts = pd.Series([])
  85. cyc_rec = pd.Series([])
  86. # split recordings into bins based on swings
  87. for i,rec in enumerate(swing_all.index.get_level_values(0).unique()):
  88. # get swing onsets
  89. swing_dict = tg.get_bouts(swing_all.loc[rec][limbs])
  90. swing_on = {k:[s[0] for s in t] for k,t in swing_dict.items()}
  91. swing_dur = {k:[s[1] for s in t] for k,t in swing_dict.items()}
  92. on_df = pd.DataFrame().from_dict(swing_on, orient='index').T
  93. # use the first new swing onset of leg to determine bin end
  94. cyc = on_df.min(axis=1).astype(int)
  95. cyc = pd.concat([pd.Series([0]), cyc, pd.Series(len(swing_all.loc[rec]))])
  96. cyc_ = cyc + lastmaxidx
  97. # keep bin duration and bin index for each rec
  98. cyc_info = cyc_.diff().rename('dur').dropna().reset_index(drop=True).to_frame()
  99. cyc_info['idx'] = cyc_info.index + len(cyc_bouts) #use +len(cyc_bouts) to have one index for all recs
  100. rec_cycinfo[rec] = cyc_info
  101. # keep rec index for bins
  102. cyc_bouts = pd.concat([cyc_bouts,cyc_[:-1]])
  103. cyc_rec = pd.concat([cyc_rec,pd.Series(np.full_like(cyc_[:-1], i))])
  104. lastmaxidx += len(swing_all.loc[rec])
  105. cyc_bouts = cyc_bouts.reset_index(drop=True)
  106. len(cyc_bouts), cyc_bouts.diff().mean(), cyc_bouts.diff().std()
  107. # %%
  108. # duty factor
  109. stancedur_all = pd.DataFrame([])
  110. phasedur_all = pd.DataFrame([])
  111. phasevelo_all = pd.DataFrame([])
  112. for i,rec in enumerate(swing_all.index.get_level_values(0).unique()):
  113. stance_dict = tg.get_bouts(~swing_all.loc[rec][limbs])
  114. stance_on = {k:[s[0] for s in t] for k,t in stance_dict.items()}
  115. stance_dur = {k:[s[1] for s in t] for k,t in stance_dict.items()}
  116. on_df = pd.DataFrame().from_dict(stance_on, orient='index').T
  117. stancedur = pd.DataFrame().from_dict(stance_dur, orient='index').T
  118. stancedur.index = pd.MultiIndex.from_product([[rec],stancedur.index])
  119. stancedur_all = pd.concat([stancedur_all,stancedur])
  120. phasedur = pd.DataFrame([np.diff(on_df[bp], append=len(swing_all.loc[rec]), axis=0) for bp in limbs], index=limbs).T
  121. phasedur.index = pd.MultiIndex.from_product([[rec],phasedur.index])
  122. phasedur_all = pd.concat([phasedur_all,phasedur])
  123. bp_grouper = pd.DataFrame([on_df[bp].dropna().set_axis(on_df[bp].dropna()).reindex(np.arange(len(swing_all.loc[rec])), method='ffill') for bp in limbs], index=limbs).T
  124. phasevelo = pd.concat([CMSvelo_all.loc[rec].groupby(bp_grouper[bp].values).mean().reset_index(drop=True) for bp in limbs], axis=1)
  125. phasevelo.columns = limbs
  126. phasevelo.index = pd.MultiIndex.from_product([[rec],phasevelo.index])
  127. phasevelo_all = pd.concat([phasevelo_all,phasevelo])
  128. duty_factor = stancedur_all / phasedur_all
  129. # %%
  130. # set base outpath
  131. outpath = os.path.join(inpath, 'cluster', 'out_cluster_050226')
  132. if not os.path.exists(outpath):
  133. os.makedirs(outpath)
  134. # %%
  135. np.sum(duty_factor.count(axis=0).values.reshape(4,2),axis=1), duty_factor.count(axis=0)
  136. # %%
  137. duty_factor.mean(axis=0)
  138. # %%
  139. #pair_color = ['r', 'g', 'b', 'y']
  140. fig, axs = plt.subplots(1,5, figsize=(10,3), width_ratios=[1,1,1,1,.05])
  141. for i,p in enumerate(['leg_1', 'leg_2', 'leg_3', 'hindleg']):
  142. pair = [bp for bp in duty_factor.columns if p in bp]
  143. or_nan = (phasevelo_all[pair].isna().any(axis=1) | duty_factor[pair].isna().any(axis=1))
  144. phasevelo_pair = phasevelo_all[pair][~or_nan]#.values.flatten()
  145. phasevelo_pair = pd.concat([phasevelo_pair[c] for c in pair])
  146. duty_factor_pair = duty_factor[pair][~or_nan]#.values.flatten()
  147. duty_factor_pair = pd.concat([duty_factor_pair[c] for c in pair])
  148. # test individually
  149. rs, pvs = [],[]
  150. for rec in phasevelo_pair.index.get_level_values(0).unique():
  151. r,pv = stats.spearmanr(phasevelo_pair.loc[rec], duty_factor_pair.loc[rec])
  152. rs.append(r)
  153. pvs.append(pv)
  154. # combine after fisher, using fisher-z transformation for rho and fisher method for p values
  155. fisher_z = np.arctanh(rs)
  156. fisher_z_mean = np.mean(fisher_z)
  157. r_mean = np.tanh(fisher_z_mean)
  158. chi2, p_comb = stats.combine_pvalues(pvs)
  159. print(p, r_mean, p_comb, chi2)
  160. Z, xedges, yedges = np.histogram2d(phasevelo_pair, duty_factor_pair, bins=[50,30], range=[[0,150],[0,1]], density=True)
  161. _ = axs[i].pcolormesh(xedges, yedges, Z.T, cmap='magma', vmin=0, vmax=.21)
  162. axs[i].set_title(p)
  163. axs[i].text(1,.01,f'r: {np.round(r_mean, 2)}; p: {np.format_float_scientific(p_comb, precision=2)}', c='w', )
  164. plt.colorbar(_, cax=axs[-1])
  165. plt.savefig(os.path.join(outpath,f'duty_factor.pdf'), bbox_inches='tight')
  166. plt.show()
  167. # %%
  168. # example bins
  169. fig, axs = plt.subplots(1,10, figsize=(20,2), sharex=True)
  170. s = 100
  171. for i,j in enumerate(range(s,s+10)):
  172. axs[i].imshow(swing_all.iloc[cyc_bouts[j]:cyc_bouts[j+1]][limbs_ord].T, aspect='auto', interpolation='None')
  173. plt.savefig(os.path.join(outpath,'examplecycle.png'), bbox_inches='tight')
  174. # %% [markdown]
  175. # ### cross-similarity
  176. # %%
  177. # wrangle bins for each data type into lists
  178. X_train = [CLbp_all[limbs].iloc[cyc_bouts[j]:cyc_bouts[j+1]].values for j in range(len(cyc_bouts)-1)] #limbs / frontlegs
  179. swing_train = [swing_all[limbs].iloc[cyc_bouts[j]:cyc_bouts[j+1]].values for j in range(len(cyc_bouts)-1)] #limbs / frontlegs
  180. straight_train = [straight_all.iloc[cyc_bouts[j]:cyc_bouts[j+1]].values for j in range(len(cyc_bouts)-1)]
  181. angle_train = [Trkangle_all.iloc[cyc_bouts[j]:cyc_bouts[j+1]].values for j in range(len(cyc_bouts)-1)]
  182. cmsvelo_train = [CMSvelo_all.iloc[cyc_bouts[j]:cyc_bouts[j+1]].values for j in range(len(cyc_bouts)-1)]
  183. cyc_idx_turns = np.concatenate([np.where((cyc_bouts[:-1]<t).values&(cyc_bouts[1:]>t).values)[0] for t in turns_all.values.flatten()])
  184. cyc_trainidx = np.arange(len(cyc_bouts)-1)#[np.array(legs_in_swing) >= 8]
  185. dur_train = [len(x) for x in X_train]
  186. len(dur_train), np.mean(dur_train), np.std(dur_train)
  187. # %%
  188. # resample into same length
  189. ts_resampler = TimeSeriesResampler(sz=15)
  190. X_train = ts_resampler.fit_transform(X_train)
  191. X_train = TimeSeriesScalerMeanVariance().fit_transform(X_train)
  192. # transform bools for swing and straight accordingly
  193. swing_train_new = ts_resampler.transform(swing_train) #TODO check if necessary when below step
  194. swing_train_new = np.round(swing_train_new).astype(int)
  195. straight_train_new = ts_resampler.transform(straight_train) #TODO check if necessary when below step
  196. straight_train_new = np.round(straight_train_new).astype(int)
  197. # %%
  198. # get mean angle and velocity for each bin
  199. angle_train_new = np.vstack([np.nanmean(v) for v in angle_train]).flatten()
  200. cmsvelo_train_new = np.vstack([np.nanmean(v) for v in cmsvelo_train]).flatten()
  201. cmsvelo_train_new.shape
  202. # %%
  203. # compute cross similarity matrix for different sets of bodyparts, using dynamical time warping
  204. dtw_list = []
  205. dtw_range = []
  206. for r in [(0,8), (0,6)]:
  207. x_train = X_train[:,:,r[0]:r[1]]
  208. dtw_dist = metrics.cdist_dtw((x_train), (x_train))
  209. dtw_list.append(dtw_dist)
  210. dtw_range.append(r)
  211. # %%
  212. # plot cross-similarity in comparable way between sets
  213. triu_idx = np.triu_indices(dtw_list[0].shape[0])
  214. dtw_list_max = np.max([np.max(d) for d in dtw_list])
  215. dtw_list_diff_max = np.max([np.max(abs(dtw_list[0]-d)) for d in dtw_list])+1
  216. for d,r in zip(dtw_list,dtw_range):
  217. plt.imshow(d, vmin=0, vmax=dtw_list_max)
  218. plt.colorbar()
  219. plt.savefig(os.path.join(outpath,f'dtw_crosssimilaritydistance_{r[0]}-{r[1]}.pdf'), bbox_inches='tight')
  220. plt.show()
  221. plt.imshow(dtw_list[0]-d,vmin=0, vmax=dtw_list_diff_max)
  222. plt.colorbar()
  223. plt.savefig(os.path.join(outpath,f'dtw_crosssimilaritydistance_{r[0]}-{r[1]}_difffull.pdf'), bbox_inches='tight')
  224. plt.show()
  225. # %%
  226. # calculate the agreement between pairwise cross-similarity matrices using spearman r
  227. agree_mat = np.zeros((len(dtw_list),len(dtw_list)))
  228. for i in range(len(dtw_list)):
  229. for j in range(len(dtw_list)):
  230. dtw_i = dtw_list[i]
  231. dtw_j = dtw_list[j]
  232. rho, p = stats.spearmanr(dtw_i[triu_idx], dtw_j[triu_idx])
  233. agree_mat[i,j] = rho
  234. # plot agreement
  235. plt.imshow(agree_mat, vmin=0, vmax=1)
  236. plt.colorbar()
  237. plt.savefig(os.path.join(outpath,f'dtw_spearmanr.pdf'), bbox_inches='tight')
  238. np.savetxt(os.path.join(outpath,f'dtw_spearmanr.csv'), agree_mat, delimiter=',')
  239. # %% [markdown]
  240. # ### clustering
  241. # %%
  242. # define x_train for one set of bodyparts, redo for others
  243. dtw_dist = dtw_list[0] # [1]
  244. dtw_name = dtw_range[0] # [1]
  245. x_train = X_train[:,:,dtw_name[0]:dtw_name[1]]
  246. outpath_leg = os.path.join(outpath,f'legs{dtw_name[0]}-{dtw_name[1]}')
  247. if not os.path.exists(outpath_leg):
  248. os.makedirs(outpath_leg)
  249. # %%
  250. # embedd bins into lower dim space using umap and similarity matrix as pairwise distance
  251. rng = np.random.default_rng(seed=42) #set random seeds to evaluate randomness in UMAP
  252. # UMAP paramaters
  253. neigh = 30
  254. neg_rate = 30
  255. min_dist = 0
  256. disc_dist = 12
  257. rep_str = 1
  258. # define UMAP
  259. emb = UMAP(metric='precomputed', init='random', n_neighbors=neigh, min_dist=min_dist,
  260. negative_sample_rate=neg_rate,
  261. disconnection_distance=disc_dist,
  262. repulsion_strength=rep_str,
  263. )
  264. # perform embedding multiple times
  265. Xemb_l = []
  266. while len(Xemb_l) < 6:
  267. emb.random_state = rng.integers(1000)
  268. print(emb.random_state)
  269. Xemb = emb.fit_transform(dtw_dist)
  270. Xemb_l.append(Xemb)
  271. # %%
  272. # plot UMAP embeddings
  273. fig, axs = plt.subplots(2, len(Xemb_l)//2, figsize=(10,5))
  274. axs = axs.T.flatten()
  275. for i in range(len(Xemb_l)):
  276. sc = axs[i].scatter(*Xemb_l[i].T, alpha=1, s=1, c=cmsvelo_train_new)
  277. axs[i].axis('equal')
  278. plt.colorbar(sc)
  279. plt.savefig(os.path.join(outpath_leg,f'UMAP_neigh{neigh}_mindist{min_dist}_negrate{neg_rate}_discdist{disc_dist}_repstr{rep_str}_legs{dtw_name[0]}-{dtw_name[1]}.pdf'), bbox_inches='tight')
  280. plt.show()
  281. # %%
  282. # perform clustering on embeddings
  283. # cluster settings
  284. nclust = 4
  285. metr = 'euclidean'
  286. link = 'ward'
  287. # cluster fr all embeddings
  288. cluster_l = []
  289. silscore_l = []
  290. for i in range(len(Xemb_l)):
  291. cluster = AgglomerativeClustering(n_clusters=nclust, metric=metr, linkage=link).fit(Xemb_l[i])
  292. cluster_l.append(cluster)
  293. silscore_l.append(silhouette_score(Xemb_l[i], cluster.labels_))
  294. # plot clustering
  295. plt.rcParams["axes.prop_cycle"] = plt.cycler("color", plt.cm.viridis(np.linspace(0,1,len(np.unique(cluster.labels_)))))
  296. fig, axs = plt.subplots(4, len(Xemb_l)//2, figsize=(10,5), height_ratios=(1,.1,1,.1))
  297. axs = axs.T.flatten()
  298. for i,j in enumerate(range(0,len(Xemb_l)*2,2)):
  299. for c in np.unique(cluster_l[i].labels_):
  300. axs[j].scatter(*Xemb_l[i][cluster_l[i].labels_==c].T, alpha=1,s=2, label=c)
  301. axs[j].axis('equal')
  302. axs[j+1].text(0,0, f'silscore: {np.round(float(silscore_l[i]),4)}')
  303. axs[j+1].axis('off')
  304. plt.legend()
  305. plt.savefig(os.path.join(outpath_leg,f'AgglomClust_ncl{nclust}_metric{metr}_link{link}.pdf'), bbox_inches='tight')
  306. plt.show()
  307. plt.rcdefaults()
  308. # %%
  309. # calculate agreement matrix between clusters
  310. agree_mat = np.zeros((len(Xemb_l),len(Xemb_l)))
  311. for i in range(len(Xemb_l)):
  312. for j in range(len(Xemb_l)):
  313. # get labels for two clusters
  314. labels_i = cluster_l[i].labels_.copy()
  315. labels_j = cluster_l[j].labels_.copy()
  316. # calcualte overlap between clusters to map clusters with same identity
  317. maparr = np.zeros((len(np.unique(labels_i)),len(np.unique(labels_i))))
  318. for c in np.unique(labels_i):
  319. n = np.zeros(len(np.unique(labels_i)))
  320. c_, n_ = np.unique(labels_j[labels_i==c], return_counts=True) # how many of the rows contain the same labels in j as in i
  321. n[c_] = n_
  322. maparr[c] = n/np.sum(n)
  323. # maps clusters with highest overlap, create dict of corresponding clusters
  324. mapper = {k:np.nan for k in np.unique(labels_i)}
  325. for c in np.argsort(np.max(maparr, axis=1))[::-1]: # sorts cluster with highest overlap to map first
  326. m_cand = np.argsort(maparr[c])[::-1] # sorts index of clusters in j, according to overlap with clusters in i
  327. for m in m_cand:
  328. if m in mapper.values(): # checks if cluster already assigned
  329. continue
  330. mapper[c] = m # assigns cluster
  331. break
  332. if not all([o in np.unique(list(mapper.values())) for o in np.unique(labels_i)]):
  333. raise ValueError(f"Not all values are assigned properly, current mapping dictionary is {mapper}")
  334. # rename labels in i
  335. for c in np.unique(labels_i):
  336. labels_i[labels_i==c] = -mapper[c] #first to negative to not merge different clusters
  337. labels_i = abs(labels_i) #revert negative with abs
  338. # after clusters are mapped, calculate agreement
  339. agree_mat[i,j] = np.sum(labels_i == labels_j)/len(labels_i)
  340. # plot agreement
  341. plt.imshow(agree_mat, vmin=0, vmax=1)
  342. plt.colorbar()
  343. plt.savefig(os.path.join(outpath_leg,f'agreement.pdf'), bbox_inches='tight')
  344. np.savetxt(os.path.join(outpath_leg,f'agreement.csv'), agree_mat, delimiter=',')
  345. print(np.mean(agree_mat), np.std(agree_mat))
  346. # %%
  347. # decide for best embedding
  348. best_embedd = 0
  349. Xemb = Xemb_l[best_embedd]
  350. cluster = cluster_l[best_embedd]
  351. # %%
  352. np.savetxt(os.path.join(outpath_leg,f'Xemb.csv'), Xemb, delimiter=',')
  353. np.savetxt(os.path.join(outpath_leg,f'clusterlabels.csv'), cluster.labels_, delimiter=',')
  354. # %%
  355. # get barycenter for different clusters
  356. barycent_clust = {}
  357. for c in np.unique(cluster.labels_):
  358. x_c = X_train[cluster.labels_==c]
  359. barycent_clust[c] = {}
  360. for bp in range(X_train.shape[2]):
  361. barycent_clust[c][bp] = barycenters.softdtw_barycenter(x_c[:,:,bp])
  362. # %%
  363. # plot hildebrandt style heatmap of clusters, after each bin has been time warped to barycenter
  364. fig, axs = plt.subplots(len(np.unique(cluster.labels_)),2, figsize=(6,len(np.unique(cluster.labels_))*2.2), sharey=True)
  365. gridspec = axs.flatten()[0].get_subplotspec().get_gridspec()
  366. subfigs = [fig.add_subfigure(gs) for gs in gridspec]
  367. limbs_ord_num = [6,4,2,0,1,3,5,7]
  368. for c in np.unique(cluster.labels_):
  369. x_train_c = X_train[cluster.labels_==c]
  370. bary_c = np.hstack(list(barycent_clust[c].values()))
  371. x_shift = []
  372. d_shift = []
  373. print(len(x_train_c))
  374. # shift each trace in comparision to barycenter
  375. for x_ in x_train_c:
  376. path,d = metrics.dtw_path(bary_c, x_)
  377. path = np.array(path)
  378. u,ui = np.unique(path[:,0], return_index=True)
  379. path = path[ui]
  380. x_shift.append(x_[path[:,1]])
  381. d_shift.append(d)
  382. x_shift = np.stack(x_shift)
  383. x_shift_mean = np.mean(x_shift, axis=0)
  384. # plot mean
  385. subfigs[0].suptitle(f'mean')
  386. axs[c,0].set_title(f'Cluster {c}')
  387. im = axs[c,0].imshow(x_shift_mean.T, vmin=-1, vmax=1) #[limbs_ord_num]
  388. axs[c,0].set_yticks(range(x_shift_mean.shape[1]))
  389. axs[c,0].set_yticklabels(limbs) #limbs_ord
  390. plt.colorbar(im)
  391. # plot barycenter
  392. subfigs[1].suptitle(f'barycenter')
  393. axs[c,1].set_title(f'Cluster {c}')
  394. im = axs[c,1].imshow(bary_c.T, vmin=-1, vmax=1) #[limbs_ord_num]
  395. plt.colorbar(im)
  396. plt.savefig(os.path.join(outpath_leg,f'hildeheatmap_c{c}.pdf'), bbox_inches='tight')
  397. plt.show()
  398. # %%
  399. # plot the movement wave of each leg for the different clusters
  400. limb_color_list = list(pd.Series(bp_color_dict)[limbs])
  401. for c in np.unique(cluster.labels_):
  402. rndm = np.random.choice(np.arange(len(cluster.labels_))[cluster.labels_==c], min(500, len(cluster.labels_[cluster.labels_==c])))
  403. bary_c = np.hstack(list(barycent_clust[c].values()))
  404. x_rndm = []
  405. d_rndm = []
  406. for r in rndm:
  407. path,d = metrics.dtw_path(bary_c[:,dtw_name[0]:dtw_name[1]], x_train[r])
  408. path = np.array(path)
  409. u,ui = np.unique(path[:,0], return_index=True)
  410. path = path[ui]
  411. x_rndm.append(X_train[r,path[:,1]])
  412. d_rndm.append(d)
  413. plt.rcParams["axes.prop_cycle"] = plt.cycler("color", plt.cm.viridis(np.linspace(0,1,len(rndm))))
  414. fig = plt.figure(layout="constrained", figsize=(4,10))
  415. gs = GridSpec(5, 2, figure=fig)
  416. axs = []
  417. for i in range(4):
  418. axs.append(fig.add_subplot(gs[i, 0]))
  419. axs.append(fig.add_subplot(gs[i, 1]))
  420. axs.append(fig.add_subplot(gs[i+1, :2]))
  421. axs = np.array(axs)
  422. plt.suptitle(f'Cluster {c}')
  423. r_i = np.argsort(d_rndm)[::-1]
  424. for i in range(X_train.shape[2]):
  425. for j,r in enumerate(r_i):
  426. x_r = x_rndm[r][:,i]
  427. axs[i].plot(x_r, alpha=.1, zorder=j)
  428. axs[i].plot(barycent_clust[c][i], ls=':', c=limb_color_list[i], zorder=len(rndm)+1, lw=3)
  429. axs[-1].plot(barycent_clust[c][i], ls=':', c=limb_color_list[i], lw=5)
  430. plt.savefig(os.path.join(outpath_leg,f'gaitwave_c{c}.pdf'), bbox_inches='tight')
  431. plt.show()
  432. plt.rcdefaults()
  433. # %% [markdown]
  434. # ### Statistics
  435. # %%
  436. # create a new dataframe o extract statistics
  437. turns_train_new = np.zeros(len(cluster.labels_))
  438. turns_train_new[cyc_idx_turns] = 1
  439. label_tracktype = pd.DataFrame([cluster.labels_, cmsvelo_train_new, angle_train_new, np.round(np.mean(straight_train_new, axis=1)).astype(int).flatten(), turns_train_new], index=['label','velocity','angle','straight','turn']).T
  440. label_tracktype_rec = label_tracktype.groupby([cyc_rec.reset_index(drop=True),label_tracktype['label']]).mean() # group by biological replicant and label
  441. # %%
  442. # ANOVA
  443. velosamples = []
  444. anglesamples = []
  445. for name, gr in label_tracktype_rec.groupby('label'):
  446. if name < 0:
  447. continue
  448. velosamples.append(gr['velocity'].dropna())
  449. anglesamples.append(gr['angle'].dropna())
  450. stats.f_oneway(*velosamples), stats.f_oneway(*anglesamples),
  451. # %%
  452. # tukey_hsd
  453. velores = stats.tukey_hsd(*velosamples)
  454. velo_sig_comb=np.unique(np.array(np.where(velores.pvalue < .05)).T, axis=0)
  455. velo_sig_combuniq=np.unique((np.sort(velo_sig_comb, axis=1)), axis=0)
  456. velo_pv_combuniq=velores.pvalue[velo_sig_combuniq[:,0],velo_sig_combuniq[:,1]]
  457. angleres = stats.tukey_hsd(*anglesamples)
  458. angle_sig_comb=np.unique(np.array(np.where(angleres.pvalue < .05)).T, axis=0)
  459. angle_sig_combuniq=np.unique((np.sort(angle_sig_comb, axis=1)), axis=0)
  460. angle_pv_combuniq=angleres.pvalue[angle_sig_combuniq[:,0],angle_sig_combuniq[:,1]]
  461. velores.pvalue, angleres.pvalue
  462. # %%
  463. def sigbracket_loc(x0, x1, y, h):
  464. return (x0,y), (x0,y+h), (x1,y+h), (x1,y)
  465. # %%
  466. # Plot boxplot for cluster (animal mean) for velocity and angle
  467. fig, axs = plt.subplots(2, figsize=(7,7))
  468. for name, gr in label_tracktype_rec.groupby('label'):
  469. if name < 0:
  470. continue
  471. axs[0].boxplot(gr['velocity'].dropna(), positions=[name], showfliers=False)
  472. axs[0].scatter(np.full_like(gr['velocity'].dropna(),name-.3), gr['velocity'].dropna(), c='k')
  473. axs[1].boxplot(gr['angle'].dropna(), positions=[name], showfliers=False)
  474. axs[1].scatter(np.full_like(gr['angle'].dropna(),name-.3), gr['angle'].dropna(), c='k')
  475. for pv in range(len(velo_pv_combuniq)):
  476. p0,p1,p2,p3 = sigbracket_loc(velo_sig_combuniq[pv][0],velo_sig_combuniq[pv][1], 90+(10*velo_sig_combuniq[pv][0]),5)
  477. axs[0].plot(*np.array([p0,p1,p2,p3]).T)
  478. for pv in range(len(angle_sig_combuniq)):
  479. p0,p1,p2,p3 = sigbracket_loc(angle_sig_combuniq[pv][0],angle_sig_combuniq[pv][1], .15+(.1*angle_sig_combuniq[pv][0]),.03)
  480. axs[1].plot(*np.array([p0,p1,p2,p3]).T)
  481. axs[0].set_ylim(0,100)
  482. axs[1].set_ylim(0,0.4)
  483. plt.savefig(os.path.join(outpath_leg,f'clusterdist_velo-angle.pdf'), bbox_inches='tight')
  484. plt.show()
  485. # %%
  486. # plot usage and normalized usage of cluster during turn
  487. counts = pd.DataFrame([], index=np.unique(cluster.labels_))
  488. for name, gr in label_tracktype.groupby('turn'):
  489. count, bin_edge = np.histogram(gr['label'], bins=len(np.unique(cluster.labels_)), range=(0,max(np.unique(cluster.labels_))+1))
  490. counts.loc[bin_edge[:-1].astype(int),name] = count
  491. if name == 0:
  492. continue
  493. plt.hist(gr['label'], histtype='bar', bins=len(np.unique(cluster.labels_)), range=(0,max(np.unique(cluster.labels_))+1))
  494. plt.savefig(os.path.join(outpath_leg,f'clusteruse_turn.pdf'), bbox_inches='tight')
  495. plt.show()
  496. plt.bar(counts.loc[0:].index, counts.loc[0:,1]/counts.loc[0:,0], .9)
  497. plt.savefig(os.path.join(outpath_leg,f'clusteruse_turn_norm.pdf'), bbox_inches='tight')
  498. # %%
  499. # save cluster labels for each recording
  500. all_label_org = pd.DataFrame([])
  501. for f in os.listdir(inpath):
  502. if expID in f and '.' not in f:
  503. rec_label = np.full(len(rec_cycinfo[f]), np.nan)
  504. rec_idx = [t for t in rec_cycinfo[f]['idx'] if t <= len(cluster.labels_)-1]
  505. rec_label[:len(rec_idx)] = cluster.labels_[rec_idx]
  506. rec_label_org = np.concatenate([np.repeat(rec_label[i], rec_cycinfo[f]['dur'][i]) for i in range(len(rec_label))]).astype(int)
  507. print(f, np.unique(rec_label_org, return_counts=True))
  508. np.savetxt(os.path.join(outpath_leg,f'{f}_gait.csv'), rec_label_org, delimiter=',', fmt='%i')
  509. rec_label_org = pd.DataFrame(rec_label_org)
  510. rec_label_org.index = pd.MultiIndex.from_product([[f],rec_label_org.index])
  511. all_label_org = pd.concat([all_label_org, rec_label_org])
  512. # %%

tardigrade_cluster.ipynb at commit 6841dc7, no license · at the source

Overview

  1. Max Planck Research Group Neural Information Flow, Max Planck Institute for Neurobiology of Behavior—Caesar, Bonn, Germany
  2. International Max Planck Research School for Brain and Behavior, Bonn, Germany
  3. Max Planck Research Group Genetics of Behavior, Max Planck Institute for Neurobiology of Behavior—Caesar, Bonn, Germany
Journal: Nature communications, volume 17, issue 1, article 4412
Dates: received 2 May 2025; accepted 23 April 2026; published online 19 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41467-026-72710-3 · PMID 42156370 · PMCID PMC13187449 · OpenAlex W4409044563
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: histology / microscopy (modality), optical imaging (calcium, voltage, 2-photon) (modality), drosophila (organism), C. elegans (organism)
Methods: Statistics, Smoothing, state filtering, decompositions, Machine learning, Evoked potentials, Connectivity, fMRI & imaging, Single-unit activity, calcium imaging
Keywords: Wide-field fluorescence microscopy, Ca2+ imaging, Behavioural methods
MeSH: Microscopy, Fluorescence*, Animals, Caenorhabditis elegans, Calcium, Drosophila melanogaster, Larva, Muscles, Neurons (* major topic)
Topic: Advanced Fluorescence Microscopy Techniques (Biophysics, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: EC | Horizon 2020 Framework Programme (EU Framework Programme for Research and Innovation H2020) (101098722); NSF | Directorate for Mathematical &amp; Physical Sciences | Division of Physics (PHY) (PHY-1748958)
Citations: cited by 1 paper (Europe PMC); 73 references in the paper

Abstract

We present a modular epifluorescence tracking microscope which enables ratiometric imaging of muscles, neurons, and other structures in moving animals. The microscope is assembled entirely from commercial parts within 3 h, making the system broadly accessible. Leveraging the improved brightness and bleaching characteristics of recent genetically encoded indicators and fluorophores, the simple microscope is even suitable for calcium imaging of neurons in behaving animals, as we demonstrate in C. elegans. We also show how muscle dynamics in D. melanogaster larvae can be analyzed and how dual color fluorescence tracking elucidates inter-species interactions by visualizing both predatory nematodes and their prey. Finally, we showcase a configuration for brightfield imaging by tracking tardigrade gait as an example of utility for non-labeled species. The affordability of the hardware and ease of use of the accompanying software make this a suitable tool for education in addition to its use in research.

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 6 matches between paragraphs and lines of code.

scholz-lab.github.io/glowtracker

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: the link answers
Software Heritage: not checked
Found in: the text, “Hardware: Modular design enables multiple imagin”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 28 September 2026: the link answers (HTTP 200)
  • 28 September 2026: the link answers (HTTP 200)

scholz-lab/Macroscope-Paper

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 6841dc791e28c7d1bcfdcc2ab4940f0b6899e933, 23 March 2026
Languages: Jupyter (10), Python (5)
Size: 18 files, 15 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, 10 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (15 files), pandas (14 files), Matplotlib (13 files), SciPy (10 files), scikit-image (7 files), scikit-learn (4 files), Pillow (1 file), statsmodels (1 file), UMAP (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
16 files

scholz-lab/GlowTracker

License: GPL-3.0
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 5585048feccaee94ba5a2fd4e7f2c9059f0126a8, 15 July 2026
Languages: Python (29)
Size: 72 files, 29 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, license file, environment (pyproject.toml), continuous integration
Not found: CITATION.cff, tests, documentation
Tools: NumPy (17 files), Matplotlib (10 files), OpenCV (8 files), scikit-image (5 files), pandas (3 files), SciPy (2 files), Pillow (1 file), tifffile (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
31 files

Code availability

The Glowtracker software is available on GitHub https://github.com/scholz-lab/GlowTracker and installable via the Python package manager pip as “glowtracker”. Further documentation and instructions are available at https://scholz-lab.github.io/GlowTracker. At the time of publication, the version of GlowTracker was glowtracker 0.11.0.

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:

  • 3 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 44 scripts, each with its path and the digest of its content;
  • 6 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 Statement

The data underlying this manuscript are shown in the figures and supplementary tables. Extended behavioral and imaging data are available at https://osf.io/rpfny/. Code for generating the figures is deposited under https://github.com/scholz-lab/Macroscope-Paper.

The Glowtracker software is available on GitHub https://github.com/scholz-lab/GlowTracker and installable via the Python package manager pip as “glowtracker”. Further documentation and instructions are available at https://scholz-lab.github.io/GlowTracker. At the time of publication, the version of GlowTracker was glowtracker 0.11.0.

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 1, 28 September 2026: the first record

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

Cite

This paper

Ramahefarivo, E., Böger, L., Saichol, T., Shomali, B., Alvarez, L., & Scholz, M. (2026). A modular multi-color fluorescence microscope for simultaneous tracking of cellular activity and behavior. Nature communications, 17(1), 4412. https://doi.org/10.1038/s41467-026-72710-3

BibTeX

@article{ramahefarivo2026modular,
author = {Ramahefarivo, Euphrasie and Böger, Leonard and Saichol, Takkasila and Shomali, Behzad and Alvarez, Luis and Scholz, Monika},
title = {{A modular multi-color fluorescence microscope for simultaneous tracking of cellular activity and behavior}},
journal = {Nature communications},
year = {2026},
month = may,
volume = {17},
number = {1},
pages = {4412},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-72710-3},
url = {https://doi.org/10.1038/s41467-026-72710-3},
pmid = {42156370},
pmcid = {PMC13187449}
}

RIS

TY - JOUR
AU - Ramahefarivo, Euphrasie
AU - Böger, Leonard
AU - Saichol, Takkasila
AU - Shomali, Behzad
AU - Alvarez, Luis
AU - Scholz, Monika
TI - A modular multi-color fluorescence microscope for simultaneous tracking of cellular activity and behavior
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/05/19
VL - 17
IS - 1
SP - 4412
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-72710-3
UR - https://doi.org/10.1038/s41467-026-72710-3
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-72710-3",
"type": "article-journal",
"title": "A modular multi-color fluorescence microscope for simultaneous tracking of cellular activity and behavior",
"container-title": "Nature communications",
"author": [
{
"family": "Ramahefarivo",
"given": "Euphrasie"
},
{
"family": "Böger",
"given": "Leonard"
},
{
"family": "Saichol",
"given": "Takkasila"
},
{
"family": "Shomali",
"given": "Behzad"
},
{
"family": "Alvarez",
"given": "Luis"
},
{
"family": "Scholz",
"given": "Monika"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "4412",
"DOI": "10.1038/s41467-026-72710-3",
"PMID": "42156370",
"PMCID": "PMC13187449",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-72710-3",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
19
]
]
}
}

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-72709-w [code]
An epifluorescence microscope design for naturalistic behavior and cellular activity in freely moving Caenorhabditis elegans.
Journal: Nature communications
In common: tifffile, OpenCV, scikit-image, 5 other tools, C. elegans, histology / microscopy, 12 references
[2] doi:10.1038/s41593-026-02257-5 [code]
Neural sequences underlying directed turning in Caenorhabditis elegans.
Journal: Nature neuroscience
In common: tifffile, Pillow, pandas, 3 other tools, C. elegans, 7 references
[3] doi:10.1038/s41467-026-72437-1 [code]
High-speed whole-brain imaging in Drosophila.
Journal: Nature communications
In common: OpenCV, scikit-image, Pillow, 6 other tools, drosophila, 5 references
[4] doi:10.1371/journal.pcbi.1013441 [code]
Large vision model framework for automated C. elegans analysis: From static morphometry to dynamic neural activity.
Journal: PLoS computational biology
In common: tifffile, OpenCV, scikit-image, 5 other tools, C. elegans, optical imaging (calcium, voltage, 2-photon), 3 references
[5] doi:10.1111/ejn.70582 [code]
Multifiber Array-Based Photometry System for Multiregional Functional Mapping in the Mouse Brain.
Journal: The European journal of neuroscience
In common: tifffile, OpenCV, scikit-image, 5 other tools, optical imaging (calcium, voltage, 2-photon), 3 references
[6] doi:10.1016/j.isci.2026.116206 [code]
Gut distension evokes rapid neural dynamics in vagal and hindbrain populations of larval zebrafish.
Journal: iScience
In common: tifffile, OpenCV, scikit-image, 7 other tools, optical imaging (calcium, voltage, 2-photon), 1 reference
[7] doi: [code]
Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement control
Journal: eLife
In common: tifffile, OpenCV, scikit-image, 7 other tools, optical imaging (calcium, voltage, 2-photon), 1 reference
[8] 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: tifffile, UMAP, OpenCV, 7 other tools, 1 reference
[9] doi:10.7554/elife.109717 [code]
Retrosplenial cortex enables context-dependent goal-directed sensorimotor transformation.
Journal: eLife
In common: tifffile, OpenCV, scikit-image, 7 other tools, 1 reference
[10] doi:10.1038/s41467-026-75352-7 [code]
Mechanosensory encoding of surface mechanics optimizes locomotion.
Journal: Nature communications
In common: tifffile, OpenCV, scikit-image, 4 other tools, C. elegans, 2 references

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.