OSCR

AgRP neuron activity predicts and tracks the glycemic response to oral glucose.

Code ↔ Paper

The paper beside its authors' code: matches between them have not been computed for this paper yet.

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 · 753 lines · 39 KB · MIT

  1. # -*- coding: utf-8 -*-
  2. """
  3. Created on Fri Aug 21 13:33:46 2020
  4. @author: nwh5j
  5. """
  6. #To do: save "snapshots" of the subset of hdf5 files used when making a jupyter report
  7. #want to be able to run through photometry data and save the jupyter report
  8. #look at https://github.com/LernerLab/GuPPy/blob/main/GuPPy/savingInputParameters.ipynb
  9. import tdt
  10. import numpy as np
  11. from scipy.ndimage.filters import uniform_filter1d, median_filter
  12. import pandas as pd
  13. import os
  14. import matplotlib.pyplot as plt
  15. import matplotlib.gridspec as gridspec
  16. import matplotlib as mpl
  17. from math import sqrt
  18. import time
  19. import re
  20. import tables as tb
  21. import xlsxwriter
  22. def time2sec(time):
  23. """ Input is a string in the format: "h,m,s" i.e. "0,2,3" """
  24. time = [int(x) for x in time.split(',')]
  25. seconds = (time[0] * 60**2) + (time[1] * 60) + time[2]
  26. return seconds
  27. def xlsx2dataframe(xlsx_file, path=None, sheet_list=False):
  28. """Sheets should just be an index. path=os.getcwd() is probably wha tyou want to default to"""
  29. if path is None:
  30. path = os.getcwd()
  31. for root, dirs, files in os.walk(path, topdown=True): #maybe add path option here for raw recordings
  32. if xlsx_file in files:
  33. path_xlsx = r"{}".format(root + "/" + xlsx_file)
  34. xlsx_file = pd.ExcelFile(path_xlsx) #xlsx_file gets reassigned here
  35. sheet_names = xlsx_file.sheet_names
  36. df_list = []
  37. for i, sheet in enumerate(sheet_names):
  38. if sheet_list == False:
  39. df_list.append(xlsx_file.parse(sheet_name=sheet, header=0))
  40. else:
  41. if i in sheet_list or sheet in sheet_list:
  42. df_list.append(xlsx_file.parse(sheet_name=sheet, header=0))
  43. exp_df = pd.concat(df_list, ignore_index=True, sort=False)
  44. exp_df.dropna(how='all') #drop empty rows
  45. exp_df['Date'] = exp_df['Date'].dt.strftime("%m/%d/%y")
  46. index_list = []
  47. for i, row in exp_df.iterrows():
  48. if type(row["File"]) != str:
  49. print("Empty excel line at row ", i + 1)
  50. pass
  51. else:
  52. filename = re.sub("-", "_", row["File"])
  53. identity = str(row["Mouse"]) + "_from_" + filename
  54. if identity[0] in "0123456789": #Need this for mice with no letter in front
  55. identity = "Mouse" + identity
  56. else:
  57. identity = str(row["Mouse"]) + "_from_" + filename
  58. index_list.append(identity)
  59. exp_df.index = index_list
  60. # have line that checks that the mouse is actually in the file as a failsafe
  61. return exp_df
  62. def generate_recording(file_path, mouse_channel, channel_config, down_hz, smoothing_method="moving avg", smoothing_window=1):
  63. """TDT formatting: https://www.tdt.com/docs/sdk/offline-data-analysis/offline-data-python/00_Intro/"""
  64. data = tdt.read_block(file_path)
  65. data_channels = set(data.streams.keys())
  66. processed_traces = []
  67. for count, i in enumerate(channel_config["colors"]):
  68. possible_color_channels = channel_config[i][mouse_channel]
  69. color_channel = list(data_channels.intersection(possible_color_channels))[0]
  70. trace_raw = data.streams[color_channel].data
  71. raw_hz = data.streams[color_channel].fs
  72. if smoothing_method == "moving avg":
  73. trace_smooth = uniform_filter1d(trace_raw, size=smoothing_window*(1000)) #rolling mean filter
  74. elif smoothing_method == "median": # median filter algo is slower
  75. trace_smooth = median_filter(trace_raw, size=smoothing_window*(1000))
  76. else:
  77. trace_smooth = trace_raw
  78. trace_final = trace_smooth[0:(len(trace_smooth)-1):int(raw_hz/down_hz)]
  79. if count == 0:
  80. time_vec = np.linspace(0, (len(trace_final) - 1)/(down_hz * 60), len(trace_final))
  81. processed_traces.append(time_vec)
  82. processed_traces.append(trace_final)
  83. # *** to do: make it channel agnostic. For now time_vec, gcamp470, isos405 order should stay the same
  84. return processed_traces
  85. def extract_experiment(exp_df, channel_config, hdf5_name, xlsx_path=None, hdf5_path=None, down_hz=1):
  86. """Recording files refer to the TDT folders that have the recording files within."""
  87. if xlsx_path is None:
  88. xlsx_path = os.getcwd()
  89. if hdf5_path is None:
  90. hdf5_path = os.getcwd()
  91. hdf5_name = hdf5_name + ".h5"
  92. hdf5_title = "Photometry Data: " + hdf5_name
  93. with tb.open_file(hdf5_name, 'a', title=hdf5_title) as hdf5_file:
  94. missing_hdf5recordings_bool = False
  95. missing_hdf5recordings = {}
  96. for identity, row in exp_df.iterrows():
  97. recording_file = row["File"]
  98. if ("/" + identity) not in hdf5_file:
  99. missing_hdf5recordings[recording_file] = missing_hdf5recordings.get(recording_file, [])
  100. missing_hdf5recordings[recording_file].append(identity)
  101. print("")
  102. if len(missing_hdf5recordings.keys()) > 0: #if greater than 0 it means there are missing recordings
  103. raw_recordings_paths = {}
  104. for root, dirs, files in os.walk(xlsx_path, topdown=True): #maybe add path option here for raw recordings
  105. if "StoresListing.txt" in files:
  106. recording_file = os.path.split(root)[-1]
  107. if recording_file in missing_hdf5recordings.keys(): #tells you if recordings not in hdf5 file but is in a path that can be uploaded
  108. path = root
  109. raw_recordings_paths[recording_file] = path
  110. missing_raw_recordings_bool = False # *** Change this to set intersection
  111. for recording_file, identities in missing_hdf5recordings.items():
  112. if recording_file not in raw_recordings_paths.keys():
  113. missing_raw_recordings_bool = True
  114. if missing_raw_recordings_bool == True: # *** Change this to set intersection
  115. print(hdf5_name + " is missing recordings which it needs to be up to date based on excel data sheet.\n")
  116. print("The recordings that are not in working directory or subfolders:")
  117. for recording_file, identities in missing_hdf5recordings.items():
  118. if recording_file not in raw_recordings_paths.keys():
  119. print(recording_file)
  120. print("")
  121. question_continue = "Do you want to continue? Enter y or n: "
  122. answer_continue = input(question_continue)
  123. if answer_continue == "y":
  124. pass
  125. elif answer_continue == "n":
  126. hdf5_file.close()
  127. return None
  128. else:
  129. print("Input should be y or n")
  130. hdf5_file.close()
  131. return None
  132. if len(missing_hdf5recordings.keys()) > 0:
  133. print("Based on xlsx data sheet " + hdf5_name + " is missing these recordings which are ready to be added: ")
  134. recordings_2b_added = set()
  135. for recording_file, identities in missing_hdf5recordings.items():
  136. if recording_file in raw_recordings_paths.keys():
  137. recordings_2b_added.update(identities)
  138. print(" and ".join(identities))
  139. question_hdf5_update = "Update " + hdf5_name + " to include recordings? Enter y or n: "
  140. answer_hdf5_update = input(question_hdf5_update)
  141. print("")
  142. if answer_hdf5_update == "y":
  143. for identity in recordings_2b_added:
  144. recording_name = str(exp_df.loc[identity]["File"]) #this appeared as a pandas.core.series.Series in a certain instance why??
  145. recording_group_name = exp_df.loc[identity]["Group"]
  146. recording_path = raw_recordings_paths[recording_name]
  147. mouse_channel = exp_df.loc[identity]["Channel"]
  148. print("Adding " + identity + " downsampled to " + str(down_hz) + "hz")
  149. print("from group " + recording_group_name)
  150. recording_data = generate_recording(recording_path, mouse_channel, channel_config, down_hz)
  151. print("")
  152. recording_group = hdf5_file.create_group("/", str(identity), "")
  153. recording_group._v_attrs.identity = identity
  154. recording_group._v_attrs.group = recording_group_name
  155. hdf5_file.create_array(recording_group, "timevec", recording_data[0], ("Time vector " + str(down_hz) + "hz"))
  156. hdf5_file.create_array(recording_group, "gcamp470", recording_data[1], ("Gcamp470 " + str(down_hz) + "hz"))
  157. hdf5_file.create_array(recording_group, "isos405", recording_data[2], ("Isos405 " + str(down_hz) + "hz"))
  158. #except:
  159. # return recording_data
  160. elif answer_hdf5_update == "n":
  161. pass
  162. else:
  163. print("Input should be y or n")
  164. else:
  165. print("All recordings present in " + hdf5_name)
  166. recordings_dict = {}
  167. for rec_identity in hdf5_file.walk_groups():
  168. if "identity" in rec_identity._v_attrs:
  169. recordings_dict[rec_identity._v_attrs.identity] = (rec_identity.timevec.read(),
  170. rec_identity.gcamp470.read(),
  171. rec_identity.isos405.read())
  172. return recordings_dict
  173. def extract_intervention(full_recording, start_time, experiment_len, deltapercent,
  174. buff_len, buff_offset, ema_smooth, ema_strength, start_buff=True, sampling_hz=1):
  175. gcamp470 = full_recording[1]
  176. isos405 = full_recording[2]
  177. full_traces = [gcamp470, isos405]
  178. start_time = time2sec(start_time) * sampling_hz #convert time format to sec to data point in time
  179. experiment_len = int(experiment_len * 60 * sampling_hz) #convert min to sec to data point scaled
  180. buff_len = int(buff_len * 60 * sampling_hz)
  181. buff_offset = int(buff_offset * 60 * sampling_hz)
  182. extracted_traces = []
  183. if start_buff == True: #the start buffer window prior to intervention is for normalization/visualization purposes
  184. for trace in full_traces:
  185. extracted_trace = np.array(trace[(start_time - buff_len - buff_offset):(start_time + experiment_len + 1)])
  186. if ema_smooth == True:
  187. extracted_trace = pd.DataFrame(extracted_trace)
  188. extracted_trace = extracted_trace.ewm(span=(60*sampling_hz*ema_strength)).mean()
  189. extracted_trace = extracted_trace.values.tolist()
  190. extracted_trace = [element for sublist in extracted_trace for element in sublist]
  191. extracted_trace = np.array(extracted_trace)
  192. #formatted_time_vec = np.linspace(-(buff_len + buff_offset)/60, (len((extracted_trace) - (buff_len + buff_offset)))/60, num=len(extracted_trace))
  193. formatted_time_vec = np.linspace(-(buff_len + buff_offset)/(60 * sampling_hz), experiment_len/(60 * sampling_hz), num=len(extracted_trace))
  194. mean2normalize2 = np.mean(trace[(start_time - buff_len - buff_offset):(start_time - buff_offset)])
  195. trace_norm = (extracted_trace / mean2normalize2) - 1
  196. if deltapercent == True:
  197. trace_norm = trace_norm * 100
  198. trace_data = (formatted_time_vec, trace_norm, extracted_trace)
  199. extracted_traces.append(trace_data)
  200. else:
  201. for trace in full_traces:
  202. extracted_trace = np.array(trace[start_time:(start_time + experiment_len)])
  203. if ema_smooth == True:
  204. extracted_trace = pd.DataFrame(extracted_trace)
  205. extracted_trace = extracted_trace.ewm(span=(60*sampling_hz*ema_strength)).mean()
  206. extracted_trace = extracted_trace.values.tolist()
  207. extracted_trace = [element for sublist in extracted_trace for element in sublist]
  208. extracted_trace = np.array(extracted_trace)
  209. formatted_time_vec = np.linspace(0, len(extracted_trace)/(60 * sampling_hz), num=len(extracted_trace))
  210. mean2normalize2 = np.mean(extracted_trace)
  211. trace_norm = (extracted_trace / mean2normalize2) - 1
  212. if deltapercent == True: #****
  213. trace_norm = trace_norm * 100
  214. trace_data = (formatted_time_vec, trace_norm, extracted_trace) #This is the time vector, deltaF over F, trace
  215. extracted_traces.append(trace_data)
  216. gcamp470, isos405 = extracted_traces
  217. gcamp470_isocorr_norm = gcamp470[1] - isos405[1]
  218. gcamp460_isocorr_timevec = gcamp470[0]
  219. gcamp470_isocorr = (gcamp460_isocorr_timevec, gcamp470_isocorr_norm)
  220. return gcamp470_isocorr, gcamp470, isos405
  221. def collate_traces(traces_list):
  222. traces = []
  223. for trace in traces_list:
  224. traces.append(trace[1])
  225. time_vec = traces_list[0][0] #take tme vec from first trace
  226. mean_of_traces = np.mean(traces, axis=0)
  227. stderr_of_traces = np.std(traces, axis=0)/sqrt(len(traces))
  228. return time_vec, mean_of_traces, stderr_of_traces
  229. def group_epochs_extraction(exp_df, recordings_dict, epoch_list, sampling_hz=1, ema_smooth=True, ema_strength=1, deltapercent=True):
  230. """epoch1 = ["Epoch Name from group_df", epoch_length, normbufferlen_min, normbufferoffset_min]
  231. epoch_list = [epoch1, epoch2, epoch3, etc]"""
  232. epoch_dict = {}
  233. reverse_epoch_dict = {}
  234. for identity, recording in recordings_dict.items():
  235. if identity not in exp_df.index:
  236. pass
  237. else:
  238. # create full length trace
  239. first_epoch_name, epoch_length, epoch_bufflen, epoch_buffoffset = epoch_list[0] #takes info from first epoch so should be chronological
  240. full_start = exp_df.loc[identity, first_epoch_name]
  241. full_length =(len(recording[0]) - time2sec(full_start))/(60 * sampling_hz) #NEED TO check to make sure time vector is accurately aligned with data sampling
  242. full_trace = extract_intervention(recording, full_start, full_length, deltapercent, epoch_bufflen, epoch_buffoffset, ema_smooth, ema_strength)
  243. epoch_dict[identity] = epoch_dict.get(identity, {})
  244. epoch_dict[identity]["Full Trace"] = epoch_dict[identity].get("Full Trace", {})
  245. epoch_dict[identity]["Full Trace"] = (full_trace, full_start)
  246. reverse_epoch_dict["Full Trace"] = reverse_epoch_dict.get("Full Trace", {})
  247. reverse_epoch_dict["Full Trace"][identity] = reverse_epoch_dict["Full Trace"].get(identity, {})
  248. reverse_epoch_dict["Full Trace"][identity] = (full_trace, full_start)
  249. for epoch_data in epoch_list:
  250. epoch_name, epoch_length, epoch_bufflen, epoch_buffoffset = epoch_data
  251. epoch_start = exp_df.loc[identity, epoch_name]
  252. epoch_trace = extract_intervention(recording, epoch_start, epoch_length, deltapercent, epoch_bufflen, epoch_buffoffset, ema_smooth, ema_strength)
  253. full_epoch_length = (epoch_length + epoch_bufflen + epoch_buffoffset) * 60 * sampling_hz
  254. if full_epoch_length > len(epoch_trace[0][0]):
  255. print(identity + " is shorter than " + epoch_name + " length! It is " + str(len(epoch_trace[0][0])) + " but needs to be " + str(full_epoch_length))
  256. epoch_dict[identity][epoch_name] = epoch_dict[identity].get(epoch_name, {})
  257. epoch_dict[identity][epoch_name] = (epoch_trace, epoch_start)
  258. reverse_epoch_dict[epoch_name] = reverse_epoch_dict.get(epoch_name, {})
  259. reverse_epoch_dict[epoch_name][identity] = reverse_epoch_dict[epoch_name].get(identity, {})
  260. reverse_epoch_dict[epoch_name][identity] = (epoch_trace, epoch_start)
  261. return epoch_dict, reverse_epoch_dict
  262. def inspect_recordings(exp_df, r_epoch_dict, show=True):
  263. """new one"""
  264. full_traces = r_epoch_dict["Full Trace"]
  265. for identity, full_trace in full_traces.items():
  266. fig1 = plt.figure(figsize=(15,12))
  267. rawtraces_plt = fig1.add_subplot(211)
  268. rawtraces_plt.set_ylabel(r'Fluorescence Units', fontsize=25, labelpad=15)
  269. rawtraces_plt.plot(full_trace[0][1][0], full_trace[0][1][2], label="GCaMP", linewidth=2, color="green")
  270. rawtraces_plt.plot(full_trace[0][2][0], full_trace[0][2][2], label="UV", linewidth=2, color="magenta")
  271. rawtraces_plt.legend(loc="upper left", fontsize="large", shadow=True)
  272. rawtraces_plt.axes.get_xaxis().set_visible(False)
  273. #axvspan
  274. deltaF_plt = fig1.add_subplot(212)
  275. deltaF_plt.set_ylabel(r'% $\Delta$F/F', fontsize=25, labelpad=15)
  276. deltaF_plt.set_xlabel("Minutes", fontsize=25, labelpad=15)
  277. deltaF_plt.plot(full_trace[0][1][0], full_trace[0][1][1], label="Gcamp_original", linewidth=2, color="seagreen") #shows isosbestic trace
  278. deltaF_plt.plot(full_trace[0][1][0], full_trace[0][0][1], label="Gcamp_corrected", linewidth=2, color="red")
  279. deltaF_plt.legend(loc="upper left", fontsize="large", shadow=True)
  280. deltaF_plt.set_ybound((-50, 50))
  281. deltaF_plt.set_axisbelow(True)
  282. full_start = time2sec(r_epoch_dict["Full Trace"][identity][1])
  283. for epoch_name, epoch_data in r_epoch_dict.items():
  284. if epoch_name != "Full Trace":
  285. epoch_start = time2sec(epoch_data[identity][1])
  286. deltaF_plt.axvline(x=(epoch_start-full_start)/60, color = "maroon", linestyle="--", zorder=1)
  287. deltaF_plt.axhline(y=0, color = "k", linestyle="-", linewidth=1, zorder=1)
  288. group = str(exp_df.loc[identity, "Group"])
  289. mouse = str(exp_df.loc[identity, "Mouse"])
  290. date = exp_df.loc[identity, "Date"]
  291. rawtraces_plt.tick_params(axis="x", labelsize=15)
  292. rawtraces_plt.tick_params(axis="y", labelsize=15)
  293. deltaF_plt.tick_params(axis="x", labelsize=15)
  294. deltaF_plt.tick_params(axis="y", labelsize=15)
  295. fig_title = group + " <" + str(mouse) + "> (" + str(date) + ")"
  296. fig1.suptitle(fig_title, fontsize=25, fontweight="bold")
  297. fig1.tight_layout()
  298. if show == True:
  299. plt.show()
  300. def average_epochs(sub_df, epoch_dict, epoch_list, channel = "gcamp_isocorr"):
  301. """Takes epochs and for each situation where a mouse has two recordings for one "group""
  302. experimental condition, it will average them together. For example, if there are a number
  303. of PBS traces. Returns a new epoch dict along with a data frame to be used for the graphing"""
  304. group_mouse_means_df = pd.DataFrame()
  305. group_mouse_means_df.insert(0, "Group",'')
  306. group_mouse_means_df.insert(1, "Mouse",'')
  307. list1 = list(sub_df.index)
  308. list2 = list(epoch_dict.keys())
  309. channel_dict = {"gcamp_isocorr":0, "gcamp":1, "isos":2} #consider renaming channel, since two different meanings appear in codebase
  310. epoch_dict_averaged = {} #averages same mice within each group
  311. for epoch in epoch_list:
  312. epoch_dict_averaged[epoch[0]] = epoch_dict_averaged.get(epoch[0], {})
  313. for identity1 in list1:
  314. if identity1 in list2:
  315. list2collate = []
  316. mouse = sub_df.loc[identity1]["Mouse"]
  317. group_mouse_means_df["Mouse"] = mouse
  318. group = sub_df.loc[identity1]["Group"]
  319. group_mouse_means_df["Group"] = group
  320. for identity2 in list2:
  321. if (mouse == sub_df.loc[identity2]["Mouse"]) and (group == sub_df.loc[identity2]["Group"]):
  322. list2collate.append(identity2)
  323. for epoch in epoch_list:
  324. trace_list = []
  325. epoch_name = epoch[0]
  326. epoch_dict_averaged[epoch_name][group] = epoch_dict_averaged[epoch_name].get(group, {})
  327. epoch_dict_averaged[epoch_name][group][mouse] = epoch_dict_averaged[epoch_name][group].get(mouse, [])
  328. for identity in list2collate:
  329. traces = epoch_dict[identity][epoch_name][0][channel_dict[channel]] #modified this from 0
  330. trace_list.append(traces[0:2]) #includes just time and deltaff traces
  331. time_vec, mean_of_traces, stderr_of_traces = collate_traces(trace_list)
  332. epoch_dict_averaged[epoch_name][group][mouse] = [time_vec, mean_of_traces]
  333. for identity in list2collate:
  334. list2.remove(identity) #remove them from list so you don't redundantly add them
  335. else:
  336. pass
  337. return epoch_dict_averaged
  338. def averagedepochs_to_excel(excel_name, epoch_dict_averaged, groups2compare):
  339. """Take an average reverse epoch dict (Epochs -> Individual Traces) and convert to dataframe.
  340. Name excel_name as a "string" "without using spaces or slashes or special characters.
  341. example: avgepochdata_to_excel("epochdata.xlsx", avg_epoch_dict, groups2compare)"""
  342. workbook = xlsxwriter.Workbook(excel_name) #needs to include .xlsx
  343. for epoch_name, exp_group_data in epoch_dict_averaged.items():
  344. worksheet = workbook.add_worksheet(epoch_name)
  345. n = 0
  346. for exp_group_name, mice_data in exp_group_data.items():
  347. for mouse_name, epoch_trace in mice_data.items(): #epoch_trace includes [time, deltaf]
  348. if n == 0:
  349. column_content = ["Time", ""]
  350. column_content.extend(epoch_trace[0])
  351. worksheet.write_column(0, n, column_content)
  352. n = n + 1
  353. column_content = [exp_group_name, mouse_name]
  354. column_content.extend(epoch_trace[1])
  355. worksheet.write_column(0, n, column_content)
  356. n = n + 1
  357. workbook.close()
  358. def auc_finder(avg_epoch_dict, epoch_name, start_minute, end_minute):
  359. group_auc_dict = {}
  360. for epoch, groups in avg_epoch_dict.items():
  361. if epoch == epoch_name:
  362. group_auc_dict = {}
  363. for group, mice in groups.items():
  364. group_auc_dict[group] = ()
  365. auc_list = []
  366. for mouse, trace_data in mice.items():
  367. start_min_ind = 0
  368. end_min_ind = 0
  369. time_vec, trace_vec = trace_data
  370. for i in range(len(time_vec)):
  371. if time_vec[i] == start_minute:
  372. start_min_ind = i
  373. if int(time_vec[i]) == end_minute:
  374. end_min_ind = i
  375. break
  376. auc_time_vec = time_vec[start_min_ind:end_min_ind + 1]
  377. auc_trace_vec = trace_vec[start_min_ind:end_min_ind + 1]
  378. auc_time_vec = auc_time_vec.tolist()
  379. auc_trace_vec = auc_trace_vec.tolist()
  380. auc = np.trapz(auc_trace_vec, auc_time_vec) #performs auc
  381. auc_list.append(auc)
  382. group_auc_dict[group] = (auc_time_vec, auc_list)
  383. return group_auc_dict
  384. def average_window(epoch_dict_averaged, groups2compare, epoch_name, start_minute, end_minute, down_hz=1):
  385. """times are in minutes"""
  386. epoch_dict_groups = epoch_dict_averaged[epoch_name]
  387. average_window_dict = {}
  388. for group in groups2compare:
  389. mice_data = epoch_dict_groups[group]
  390. average_window_dict[group] = average_window_dict.get(group, [])
  391. for mouse, traces in mice_data.items():
  392. time_trace = traces[0]
  393. gcamp_trace = traces[1]
  394. time_point_start = np.where(time_trace==float(start_minute))
  395. time_point_start = int(list(time_point_start)[0])
  396. time_point_end = np.where(time_trace==float(end_minute))
  397. time_point_end = int(list(time_point_end)[0])
  398. gcamp_window = gcamp_trace[time_point_start:time_point_end + 1]
  399. gcamp_window_mean = gcamp_window.mean()
  400. average_window_dict[group].append((mouse, gcamp_window_mean))
  401. return average_window_dict
  402. def plot_epochs(exp_df, reverse_epoch_dict, groups2compare, exp_df_column="Group", deltapercent=True):
  403. """Group legend follows order in groups2compare. group_traces is for the main graph
  404. comparing all groups whereas group_trace is for the within group graphs."""
  405. for epoch_name, identities in reverse_epoch_dict.items():
  406. if epoch_name != "Full Trace":
  407. group_epoch_dict = {}
  408. for group in groups2compare:
  409. for identity, trace_data in identities.items():
  410. if exp_df.loc[identity, "Group"] != group:
  411. pass
  412. else:
  413. group_epoch_dict[group] = group_epoch_dict.get(group, [])
  414. group_epoch_dict[group].append((identity, trace_data[0][0])) #just getting the deltaF trace
  415. fig3 = plt.figure(figsize=(15,12))
  416. title = epoch_name + " Photometry Period"
  417. fig3.suptitle(title, fontsize="30", fontweight="bold")
  418. group_traces_plt = fig3.add_subplot(211)
  419. if deltapercent==True:
  420. group_traces_plt.set_ylabel(r'% $\Delta$F/F', fontsize="x-large")
  421. else:
  422. group_traces_plt.set_ylabel(r'$\Delta$F/F', fontsize="x-large")
  423. group_traces_plt.set_xlabel("Minutes", fontsize="x-large")
  424. for group_name, traces_info in group_epoch_dict.items():
  425. fig2 = plt.figure(figsize=(15,12))
  426. individual_traces_plt = fig2.add_subplot(211)
  427. group_trace_plt = fig2.add_subplot(212)
  428. if deltapercent==True:
  429. group_trace_plt.set_ylabel(r'% $\Delta$F/F', fontsize="x-large")
  430. else:
  431. group_traces_plt.set_ylabel(r'$\Delta$F/F', fontsize="x-large")
  432. group_trace_plt.set_ylabel(r'$\Delta$F/F', fontsize="x-large")
  433. group_trace_plt.set_xlabel("Minutes", fontsize="x-large")
  434. traces_data = []
  435. for identity, trace_data in traces_info:
  436. traces_data.append(trace_data)
  437. time_axis, trace = trace_data
  438. individual_traces_plt.plot(time_axis, trace, linewidth=0.8, label=exp_df.loc[identity,"Mouse"])
  439. individual_traces_plt.legend(loc="lower left", fontsize="medium", shadow=True)
  440. individual_traces_plt.axes.get_xaxis().set_visible(False)
  441. time_axis, mean_trace, stderr_trace = collate_traces(traces_data)
  442. group_trace_plt.plot(time_axis, mean_trace, linewidth=1, label="Mean Trace + Std.Error", color="darkcyan")
  443. group_trace_plt.fill_between(time_axis, mean_trace+stderr_trace, mean_trace-stderr_trace, alpha=0.4, color="salmon")
  444. group_trace_plt.legend(loc="lower left", fontsize="medium", shadow=True)
  445. group_traces_plt.plot(time_axis, mean_trace, linewidth=1, label=group_name)
  446. group_traces_plt.fill_between(time_axis, mean_trace+stderr_trace, mean_trace-stderr_trace, alpha=0.35)
  447. fig2.suptitle((group_name + " (" + epoch_name + ")"), fontsize="30", fontweight="bold")
  448. fig2.tight_layout()
  449. group_traces_plt.legend(loc="upper right", fontsize="large", shadow=True)
  450. fig3.tight_layout()
  451. def plot_permouse_interventioncompare(exp_df, reverse_epoch_dict, groups2compare, mice2plot, exp_df_column="Group"):
  452. for mouse in mice2plot:
  453. for epoch_name, identities in reverse_epoch_dict.items():
  454. if epoch_name != "Full Trace":
  455. group_epoch_dict = {}
  456. for identity, trace_data in identities.items():
  457. if exp_df.loc[identity]["Mouse"] != mouse:
  458. print("searching for " + exp_df.loc[identity]["Mouse"] + " but is " + mouse)
  459. pass
  460. else:
  461. group_name = exp_df.loc[identity, exp_df_column]
  462. if group_name in groups2compare:
  463. #print(identity + " " + group_name)
  464. group_epoch_dict[group_name] = group_epoch_dict.get(group_name, [])
  465. #mouse_name = exp_df.loc[identity, "Mouse"]
  466. group_epoch_dict[group_name].append((identity, trace_data[0][0])) #just getting the deltaF trace
  467. print("else")
  468. fig3 = plt.figure(figsize=(15,12))
  469. fig3.suptitle((mouse + " " + epoch_name), fontsize="27", fontweight="bold")
  470. group_traces_plt = fig3.add_subplot(211)
  471. group_traces_plt.set_ylabel(r'$\Delta$F/F', fontsize="x-large")
  472. group_traces_plt.set_xlabel("Minutes", fontsize="x-large")
  473. group_traces_plt.legend(loc="lower left", fontsize="large", shadow=True)
  474. fig3.tight_layout()
  475. def plot_averagedepochs(avg_epoch_dict, groups2compare):
  476. """Group legend follows order in groups2compare"""
  477. for epoch_name, groups in avg_epoch_dict.items():
  478. if epoch_name != "Full Trace":
  479. group_epoch_dict = {}
  480. for group, mice in groups.items():
  481. if group in groups2compare:
  482. for mouse, trace_data in mice.items():
  483. group_epoch_dict[group] = group_epoch_dict.get(group, [])
  484. group_epoch_dict[group].append((mouse, trace_data))
  485. fig3 = plt.figure(figsize=(15,12))
  486. fig3.suptitle(epoch_name, fontsize="30", fontweight="bold")
  487. group_traces_plt = fig3.add_subplot(211)
  488. group_traces_plt.set_ylabel(r'$\Delta$F/F', fontsize="x-large")
  489. group_traces_plt.set_xlabel("Minutes", fontsize="x-large")
  490. for group_name, traces_info in group_epoch_dict.items():
  491. fig2 = plt.figure(figsize=(15,12))
  492. individual_traces_plt = fig2.add_subplot(211)
  493. group_trace_plt = fig2.add_subplot(212)
  494. group_trace_plt.set_ylabel(r'$\Delta$F/F', fontsize="x-large")
  495. group_trace_plt.set_xlabel("Minutes", fontsize="x-large")
  496. traces_data = []
  497. for mouse, trace_data in traces_info:
  498. traces_data.append(trace_data)
  499. time_axis, trace = trace_data
  500. individual_traces_plt.plot(time_axis, trace, linewidth=0.8, label=mouse)
  501. individual_traces_plt.legend(loc="lower left", fontsize="medium", shadow=True)
  502. individual_traces_plt.axes.get_xaxis().set_visible(False)
  503. time_axis, mean_trace, stderr_trace = collate_traces(traces_data)
  504. group_trace_plt.plot(time_axis, mean_trace, linewidth=1, label="Mean Trace + Std.Error", color="darkcyan")
  505. group_trace_plt.fill_between(time_axis, mean_trace+stderr_trace, mean_trace-stderr_trace, alpha=0.4, color="salmon")
  506. group_trace_plt.legend(loc="lower right", fontsize="medium", shadow=True)
  507. group_traces_plt.plot(time_axis, mean_trace, linewidth=1, label=group_name)
  508. group_traces_plt.fill_between(time_axis, mean_trace+stderr_trace, mean_trace-stderr_trace, alpha=0.35)
  509. fig2.suptitle((group_name + " (" + epoch_name + ")"), fontsize="30", fontweight="bold")
  510. fig2.tight_layout()
  511. group_traces_plt.legend(loc="lower right", fontsize="large", shadow=True)
  512. fig3.tight_layout()
  513. def plot_averagedepochs_with_heat(epochs, avg_epoch_dict, groups2compare, heatmap_dim, my_cmap=None):
  514. """Group legend follows order in groups2compare"""
  515. for epoch_name, groups in avg_epoch_dict.items():
  516. if epoch_name in epochs.keys():
  517. group_epoch_dict = {}
  518. for group, mice in groups.items():
  519. if group in groups2compare:
  520. for mouse, trace_data in mice.items():
  521. group_epoch_dict[group] = group_epoch_dict.get(group, [])
  522. group_epoch_dict[group].append((mouse, trace_data))
  523. all_groups_plt = plt.figure(figsize=(15,12))
  524. all_groups_plt.suptitle(epoch_name, fontsize="30", fontweight="bold")
  525. group_traces_plt = all_groups_plt.add_subplot(211)
  526. group_traces_plt.set_ylabel(r'$\Delta$F/F %', fontsize="x-large")
  527. group_traces_plt.set_xlabel("Minutes", fontsize="x-large")
  528. for group_name, traces_info in group_epoch_dict.items():
  529. fig2 = plt.figure(figsize=(15,12))
  530. individual_traces_plt = fig2.add_subplot(211)
  531. group_trace_plt = fig2.add_subplot(212)
  532. group_trace_plt.set_ylabel(r'$\Delta$F/F %', fontsize="x-large")
  533. group_trace_plt.set_xlabel("Minutes", fontsize="x-large")
  534. mice_list = []
  535. traces_data = []
  536. for mouse, trace_data in traces_info:
  537. mice_list.append(mouse)
  538. traces_data.append(trace_data)
  539. time_axis, trace = trace_data
  540. individual_traces_plt.plot(time_axis, trace, linewidth=0.8, label=mouse)
  541. traces_fluorescence = [trace for time_axis, trace in traces_data]
  542. time_axis = traces_data[0][0]
  543. fig_width = heatmap_dim[0]
  544. fig_height = heatmap_dim[1] #len(traces_fluorescence)
  545. fig_heattraces = plt.figure(figsize=(fig_width, (fig_height))) #fig_heattraces = plt.figure(figsize=(15,3))
  546. spec_heattraces = gridspec.GridSpec(ncols=fig_width, nrows=len(traces_fluorescence) + 2,
  547. wspace=0, hspace=0, figure=fig_heattraces)
  548. row = 0
  549. subplot_dict = {}
  550. for trace in traces_fluorescence:
  551. trace = np.expand_dims(trace, axis=0)
  552. subplot_dict[row] = fig_heattraces.add_subplot(spec_heattraces[row, 0:])
  553. if row != len(traces_fluorescence) - 1: #last row you want ticks
  554. subplot_dict[row].tick_params(
  555. axis='both', # changes apply to the x-axis
  556. which='both', # both major and minor ticks are affected
  557. bottom=False, # ticks along the bottom edge are off
  558. top=False, # ticks along the top edge are off
  559. left=True,
  560. labelleft=True,
  561. labelbottom=False # labels along the bottom edge are off)
  562. )
  563. elif row == len(traces_fluorescence) - 1:
  564. subplot_dict[row].tick_params(
  565. axis='x', # changes apply to the x-axis
  566. which='both', # both major and minor ticks are affected
  567. bottom=True, # ticks along the bottom edge are off
  568. top=False, # ticks along the top edge are off
  569. labelbottom=True, # labels along the bottom edge are off
  570. labelsize=30
  571. )
  572. vmax = epochs[epoch_name][0]
  573. vmin = epochs[epoch_name][1]
  574. norm_exp = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
  575. aspect = "auto"
  576. subplot_dict[row].imshow(trace, norm=norm_exp, cmap = my_cmap, interpolation='bicubic', aspect=aspect,
  577. extent =[time_axis[0], time_axis[-1], (-10 * 1/len(traces_fluorescence)), (10 * 1/len(traces_fluorescence))])
  578. subplot_dict[row].get_yaxis().set_visible(False)
  579. row = row + 1
  580. cbarax = fig_heattraces.add_subplot(spec_heattraces[-1, int(fig_width/4) :int(fig_width*3/4)])
  581. cbar = fig_heattraces.colorbar(mpl.cm.ScalarMappable(norm=norm_exp, cmap=my_cmap), cax=cbarax, orientation="horizontal") #use_gridspec=True
  582. cbarax.tick_params(labelsize=20)
  583. fig_heattraces.tight_layout(pad=0)
  584. individual_traces_plt.legend(loc="lower left", fontsize="medium", shadow=True)
  585. individual_traces_plt.axes.get_xaxis().set_visible(False)
  586. individual_traces_plt.set_ybound((-60, 20))
  587. time_axis, mean_trace, stderr_trace = collate_traces(traces_data)
  588. group_trace_plt.plot(time_axis, mean_trace, linewidth=1, label="Mean Trace + Std.Error", color="darkcyan")
  589. group_trace_plt.fill_between(time_axis, mean_trace+stderr_trace, mean_trace-stderr_trace, alpha=0.4, color="salmon")
  590. group_trace_plt.legend(loc="upper right", fontsize="small", shadow=True)
  591. group_trace_plt.set_ybound((-60, 20))
  592. group_traces_plt.plot(time_axis, mean_trace, linewidth=1, label=group_name)
  593. group_traces_plt.fill_between(time_axis, mean_trace+stderr_trace, mean_trace-stderr_trace, alpha=0.35)
  594. group_traces_plt.set_ybound((-60, 30))
  595. fig2.suptitle((group_name + " (" + epoch_name + ")"), fontsize="30", fontweight="bold")
  596. group_traces_plt.legend(loc="lower left", fontsize="large", shadow=True)
  597. all_groups_plt.tight_layout()

fibphoflow.py at commit dde0864, under MIT · at the source

Overview

Authors: Micaela Glat1, Anna J Bowen1, Yang Gou1, Elizabeth Giering1,2, Jarrad M Scarlett1,3, Gregory J Morton1, Michael W Schwartz1
ORCID iDs: Micaela Glat
  1. University of Washington Medicine Diabetes Institute, Department of Medicine, Seattle, WA, 98109, USA
  2. Veterans Affairs Puget Sound Health Care System, Seattle, WA, 98108, USA
  3. Department of Pediatric Gastroenterology and Hepatology, Seattle Children's Hospital, Seattle, WA, 98145, USA
Institutions: University of Washington (United States); VA Puget Sound Health Care System (United States); Seattle Children's Hospital (United States)
Journal: Molecular metabolism, volume 108, article 102372
Dates: received 2 April 2026; accepted 10 April 2026; published online 17 April 2026; in print June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.molmet.2026.102372 · PMID 42002224 · PMCID PMC13141783 · OpenAlex W7154743481
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: optical imaging (calcium, voltage, 2-photon) (modality), mouse (organism), cellular / molecular (subfield)
Methods: Spectral & time-frequency, Connectivity, Statistics, Machine learning, Preprocessing, Single-unit activity, calcium imaging, Smoothing, state filtering, decompositions
Keywords: AgRP neurons, Anticipatory, Glycemia, Continuous glucose monitoring, Fiber photometry
MeSH: Agouti-Related Protein*, Blood Glucose*, Glucose*, Neurons*, Administration, Oral, Animals, Hypothalamus, Male, Mice (* major topic)
Topic: Regulation of Appetite and Obesity (Endocrine and Autonomic Systems, Neuroscience), according to OpenAlex
Funding: NIDDK NIH HHS (P30 DK017047, R01 DK089056)
Citations: not cited yet (Europe PMC); 41 references in the paper

Abstract

Hypothalamic AgRP neurons are uniquely responsive to nutritional cues and play an important role in fuel homeostasis. To investigate the temporal relationship between the activity of these neurons and the glycemic response to an oral glucose load, we simultaneously monitored AgRP neuron activity (by fiber photometry in AgRP-IRES-cre mice) and the arterial glucose level, both before and after oral gavage (OG) of either water or glucose (0.5–2.5 g/kg). We report that the AgRP neuron response to an OG glucose load can be subdivided into two functionally distinct phases – one that begins prior to glucose delivery and a second that extends from peak inhibition through the return towards baseline. The ‘first phase’ appears to be anticipatory in nature and is also predictive of subsequent changes in glycemia, suggesting a role in the handling of an oral glucose load. To analyze the relationship between the second phase response and changes of glycemia, we employed a model that allows residual activity to be removed subsequent to the ‘first phase’ component. This analysis reveals that unlike the first phase, the degree of residual inhibition – the second phase – tracks the glycemic response. Moreover, this response is temporally aligned with the blood glucose (BG) rate of change (which is predictive of future BG levels), with AgRP neurons lagging BG rate of change by ∼5 min. We conclude that the AgRP neuron response to an oral glucose challenge consists of two distinct phases, each with its own determinants and metabolic implications: an initial anticipatory component that is predictive of the subsequent glycemic response, and a second phase that tracks the rate of BG change.

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

Repository

Its files are read in the Code ↔ Paper reader above.

nikhayes/fibphoflow

License: MIT
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Commit: dde086457be2aa31925fbf7891b86629dd748603, 19 December 2023
Languages: Python (3)
Size: 10 files, 3 scripts
Software Heritage: archived
Found in: the text, “Fiber photometry analysis”
Holds: README, license file, environment (environment.yml)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (2 files), pandas (2 files), Matplotlib (1 file), SciPy (1 file), seaborn (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers
  • 29 September 2026: the link answers
5 files

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:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 3 scripts, each with its path and the digest of its content;
  • no match between paragraphs and code yet;
  • 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

No dataset and no data link were found in the paper.

Data availability

I have shared the link to my data

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

Recorded: type, language, journal, volume, pages, dates, 7 authors, 5 keywords, 9 MeSH terms, 1 funder, 41 references.

Cite

This paper

Glat, M., Bowen, A. J., Gou, Y., Giering, E., Scarlett, J. M., Morton, G. J., & Schwartz, M. W. (2026). AgRP neuron activity predicts and tracks the glycemic response to oral glucose. Molecular metabolism, 108, 102372. https://doi.org/10.1016/j.molmet.2026.102372

BibTeX

@article{glat2026agrp,
author = {Glat, Micaela and Bowen, Anna J and Gou, Yang and Giering, Elizabeth and Scarlett, Jarrad M and Morton, Gregory J and Schwartz, Michael W},
title = {{AgRP neuron activity predicts and tracks the glycemic response to oral glucose}},
journal = {Molecular metabolism},
year = {2026},
month = apr,
volume = {108},
pages = {102372},
publisher = {Elsevier},
issn = {2212-8778},
doi = {10.1016/j.molmet.2026.102372},
url = {https://doi.org/10.1016/j.molmet.2026.102372},
pmid = {42002224},
pmcid = {PMC13141783}
}

RIS

TY - JOUR
AU - Glat, Micaela
AU - Bowen, Anna J
AU - Gou, Yang
AU - Giering, Elizabeth
AU - Scarlett, Jarrad M
AU - Morton, Gregory J
AU - Schwartz, Michael W
TI - AgRP neuron activity predicts and tracks the glycemic response to oral glucose
T2 - Molecular metabolism
J2 - Mol Metab
PY - 2026
DA - 2026/04/17
VL - 108
SP - 102372
SN - 2212-8778
PB - Elsevier
DO - 10.1016/j.molmet.2026.102372
UR - https://doi.org/10.1016/j.molmet.2026.102372
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.molmet.2026.102372",
"type": "article-journal",
"title": "AgRP neuron activity predicts and tracks the glycemic response to oral glucose",
"container-title": "Molecular metabolism",
"author": [
{
"family": "Glat",
"given": "Micaela"
},
{
"family": "Bowen",
"given": "Anna J"
},
{
"family": "Gou",
"given": "Yang"
},
{
"family": "Giering",
"given": "Elizabeth"
},
{
"family": "Scarlett",
"given": "Jarrad M"
},
{
"family": "Morton",
"given": "Gregory J"
},
{
"family": "Schwartz",
"given": "Michael W"
}
],
"container-title-short": "Mol Metab",
"volume": "108",
"page": "102372",
"DOI": "10.1016/j.molmet.2026.102372",
"PMID": "42002224",
"PMCID": "PMC13141783",
"ISSN": "2212-8778",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.molmet.2026.102372",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
17
]
]
}
}

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.1111/jne.70202 [code]
Altered responses to ghrelin and food cues in AgRP neurons during pregnancy and lactation.
Journal: Journal of neuroendocrinology
In common: pandas, SciPy, Matplotlib, 1 other tool, mouse, cellular / molecular, 7 references
[2] doi:10.1038/s41467-026-75650-0 [code]
Reduced melanocortin tone modulates feeding during pregnancy in mice.
Journal: Nature communications
In common: mouse, 5 references
[3] doi:10.1016/j.stem.2026.05.005 [code]
Generation of human appetite-regulating neurons and tanycytes from pluripotent stem cells.
Journal: Cell stem cell
In common: seaborn, pandas, SciPy, 2 other tools, cellular / molecular, 2 references
[4] doi:10.1038/s41467-026-76242-8 [code]
Whole-brain, all-optical interrogation of neuronal dynamics underlying gut and vascular interoception in zebrafish.
Journal: Nature communications
In common: pandas, SciPy, Matplotlib, 1 other tool, 2 references
[5] doi:10.7554/elife.109717 [code]
Retrosplenial cortex enables context-dependent goal-directed sensorimotor transformation.
Journal: eLife
In common: seaborn, pandas, SciPy, 2 other tools, mouse, 1 reference
[6] doi:10.1016/j.isci.2026.115552 [code]
Physical and cognitive contributions to fatigue perception: The interplay between local muscle fatigue and sensory prediction error.
Journal: iScience
In common: seaborn, pandas, SciPy, 2 other tools, 1 reference
[7] doi: [code]
Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement control
Journal: eLife
In common: seaborn, pandas, SciPy, 2 other tools, optical imaging (calcium, voltage, 2-photon), mouse
[8] doi:10.1016/j.xpro.2026.104678 [code]
Protocol for longitudinal two-photon calcium imaging and holographic optogenetic manipulation to investigate memory in mice.
Journal: STAR protocols
In common: seaborn, pandas, SciPy, 2 other tools, optical imaging (calcium, voltage, 2-photon), mouse
[9] doi:10.7554/elife.110277 [code]
Functional imaging of nine distinct neuronal populations under a miniscope in freely behaving animals.
Journal: eLife
In common: seaborn, pandas, SciPy, 2 other tools, optical imaging (calcium, voltage, 2-photon), mouse
[10] doi:10.1002/advs.202519479 [code]
Diminished Signal-to-Noise Ratio Disrupts Somatosensory Population Encoding and Drives Tactile Hyposensitivity in the Fmr1&lt;sup&gt;-/y&lt;/sup&gt; Autism Model.
Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)
In common: seaborn, pandas, SciPy, 2 other tools, optical imaging (calcium, voltage, 2-photon), mouse

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.