OSCR

Visuomotor decision-making through multifeature convergence in the larval zebrafish hindbrain.

Code ↔ Paper

26 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 26 matches
  1. [1] § Results › Neuron morphology of computational units refines model structure ↔ figure5_S8_photoactivations.py, lines 1099–1156 · score 0.98 · stratum album centrale, stratum griseum centrale, griseum superficiale, stratum fibrosum, intermediate hypothalamus, interpeduncular nucleus
  2. [2] § Methods › KDE binary mask generation and mapzebrain atlas-based anatomical analysis ↔ figure5_S8_photoactivations.py, lines 868–953 · score 0.92 · neurons project posterior, neurons project ventral, front pathway, neurons project anterior, lateral pathway, tectal neuron
  3. [3] § Methods › KDE binary mask generation and mapzebrain atlas-based anatomical analysis ↔ figure5_S8_photoactivations.py, lines 868–953 · score 0.90 · lateral_cross_neurons, front pathway, local_ipsi, anterior hindbrain neurons, lateral pathway, tectal neurons
  4. [4] § Methods › Synthetic data simulation ↔ figure3_S3_S4_imaging_modelbased.py, lines 1830–1906 · score 0.88 · added Gaussian noise, create synthetic, GCaMP, model node, activity trace, Poisson
  5. [5] § Results › Neuron morphology of computational units refines model structure ↔ figure3_S3_S4_imaging_modelbased.py, lines 2197–2239 · score 0.83 · tectal neuropil, dorsal thalamus, luminance increase detectors, luminance decrease detectors, luminance change detectors, intermediate
  6. [6] § Methods › Additive and winner-takes-all steady-state models and fitting strategy ↔ figure1_behavior_overview.py, lines 164–188 · score 0.74 · winner takes, model hypothesis, stimulus model, additive model, bias, guess
  7. [7] § Results › Model-predicted activity is represented across the brain ↔ figure3_S3_S4_imaging_modelbased.py, lines 2197–2239 · score 0.74 · superior dorsal medulla, periventricular layer, oblongata stripe, model node, logical statements, multifeature integrators
  8. [8] § Methods › KDE binary mask generation and mapzebrain atlas-based anatomical analysis ↔ figure5_S8_photoactivations.py, lines 1099–1156 · score 0.70 · superior dorsal medulla, oblongata stripe, soma, masks, mapzebrain, ZBRAIN
  9. [9] § Methods › KDE binary mask generation and mapzebrain atlas-based anatomical analysis ↔ figure5_S8_photoactivations.py, lines 786–824 · score 0.70 · local_ipsi, project anterior, contra, posterior, hemisphere, ventral
  10. [10] § Results › Neuron morphology of computational units refines model structure ↔ figure5_S8_photoactivations.py, lines 16–164 · score 0.70 · Reference brain regions, neuron morphology, full brain, Scale bar, cerebellum, pa
  11. [11] § Methods › Threshold-and-width-based psychometric curve fitting ↔ figureS1_psychometricfits.py, lines 38–150 · score 0.69 · psychometric curve, curve_fit, single fish, guess, width, threshold
  12. [12] § Results › Excitation and inhibition are largely balanced across motion and lateral luminance processing neurons ↔ figure4-S7/figure4_S7_hcr.py, lines 402–546 · score 0.67 · G8s pre, G8s post, situ image, overlay, channel, vglut
  13. [13] § Results › A three-pathway model captures behavioral dynamics to multifeature stimulus configurations ↔ figure3_S3_S4_imaging_modelbased.py, lines 69–201 · score 0.67 · luminance change pathway, motion pathway, integrated motion, mutual, decayed, repulsive
  14. [14] § Methods › Model fitting ↔ figure2_S2_behavior_tempdynamics.py, lines 1304–1362 · score 0.66 · unifeature training, training rounds, fitted parameter, median, split, MSE
  15. [15] § Results › A three-pathway model captures behavioral dynamics to multifeature stimulus configurations ↔ figure2_S2_behavior_tempdynamics.py, lines 1045–1170 · score 0.65 · black backgrounds, model fitting, lumi change, SEM, rows, training
  16. [16] § Results › A three-pathway model captures behavioral dynamics to multifeature stimulus configurations ↔ figure2_S2_behavior_tempdynamics.py, lines 405–476 · score 0.65 · luminance change pathway, motion pathway, integrated motion, mutual, repulsive, sum
  17. [17] § Results › Addition of multifeature sensory cues captures sensorimotor decision-making ↔ figure2_S2_behavior_tempdynamics.py, lines 1419–1457 · score 0.64 · congruent stimuli, conflicting stimuli, luminance cues, lateral luminance, Swims, behavior
  18. [18] § Methods › Behavior data analysis ↔ figure1_behavior_overview.py, lines 105–125 · score 0.64 · right bouts, interbout interval, orientation change, behavior, fish
  19. [19] § Results › Addition of multifeature sensory cues captures sensorimotor decision-making ↔ figure1_behavior_overview.py, lines 459–594 · score 0.63 · relative interbout interval, individual fish, luminance stimuli, ns, orientation, behavior
  20. [20] § Methods › Imaging analysis with linear regression ↔ figure3_S3_S4_imaging_modelbased.py, lines 69–201 · score 0.61 · GCaMP, model predictions, linregress, decay, convolving, kernel
  21. [21] § Methods › HCR-FISH analysis ↔ figure4-S7/figure4_S7_hcr.py, lines 402–546 · score 0.59 · volume stacks, situ imaging, ANTs, HCR
  22. [22] § Methods › Behavior data analysis ↔ figure1_behavior_overview.py, lines 459–594 · score 0.57 · leftward stimuli, rightward stimuli, flipped, orientation, behavior, conflicts
  23. [23] § Results › Better separability of congruent than conflicting stimuli in low-dimensional space ↔ figure2_S2_behavior_tempdynamics.py, lines 1419–1457 · score 0.57 · congruent stimuli, conflicting stimuli, lateral luminance, cues, behavioral, model
  24. [24] § Methods › Behavior ↔ figure1_behavior_overview.py, lines 597–644 · score 0.56 · interbout interval, orientation change, raw, bouts, window, Behavior
  25. [25] § Results › A three-pathway model captures behavioral dynamics to multifeature stimulus configurations ↔ figure2_S2_behavior_tempdynamics.py, lines 1218–1260 · score 0.54 · unifeature trained model, multifeature trained model, pathways, behavior, motion, stimuli
  26. [26] § Methods › Additive network model ↔ figure2_S2_behavior_tempdynamics.py, lines 270–334 · score 0.52 · weighted sum, integrated motion, repulsive, attractive, luminance change, Additive

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 · 1,558 lines · 100 KB · no license · 7 matches

  1. import numpy as np
  2. from pathlib import Path
  3. import pandas as pd
  4. from multifeature_integration_paper.figure_helper import Figure
  5. from scipy.optimize import curve_fit
  6. from scipy.stats import ttest_ind
  7. from sklearn.model_selection import GroupShuffleSplit
  8. from scipy.ndimage import convolve1d
  9. from multifeature_integration_paper.useful_small_funcs import cohens_d, rolling_end_window, fill_nans
  10. def get_stim_input(folder_name, stim_len, stim_type, zero_coh=0.1):
  11. '''
  12. This function obtains the input to the models for each experiment stimulus.
  13. :param folder_name: Name of the experiment stimulus.
  14. :param stim_len: Length of the stimulus in 0.1s steps (e.g. a 25s stimulus is written here as 250).
  15. :param stim_type: Stimulus type (Motion, Photo, Same or Oppo)
  16. :param zero_coh: To avoid divisions by zero we set the perceived motion at 0% coherence to a small value.
  17. :return: Four input arrays: motion_left, motion_right, luminance_left, luminance_right.
  18. '''
  19. # Get the experiment specific parameters of when motion start and stops, luminance starts and stops. The strength factors measured from the data,
  20. # as well as the luminance factors pre/post stimulus and on the bright and dark side of the stimulus.
  21. if folder_name == 'converted_phototaxis_dotmotion_integration_simultaneous_white': # Simultaneous
  22. mot_on, mot_off, lumi_on, lumi_off = [50, 200, 50, 200]
  23. mot_fac, lumi_fac = [1., 1.]
  24. lumi_pre, lumi_bright, lumi_dark = [1., 1., 0.]
  25. elif folder_name == 'phototaxis_dotmotion_simultaneous_low_Sep': # Simultaneous low
  26. mot_on, mot_off, lumi_on, lumi_off = [200, 400, 200, 400]
  27. mot_fac, lumi_fac = [0.12, 1.17]
  28. lumi_pre, lumi_bright, lumi_dark = [1., 1., 0.]
  29. elif folder_name == 'converted_phototaxis_dotmotion_integration_peppersalt': # Peppersalt
  30. mot_on, mot_off, lumi_on, lumi_off = [50, 150, 150, 250]
  31. mot_fac, lumi_fac = [0.37, 0.19]
  32. lumi_pre, lumi_bright, lumi_dark = [0.7, 1., 0.]
  33. elif folder_name == 'phototaxis_dotmotion_reverse_Sep': # Reverse
  34. mot_on, mot_off, lumi_on, lumi_off = [150, 250, 50, 150]
  35. mot_fac, lumi_fac = [0.41, 1.29]
  36. lumi_pre, lumi_bright, lumi_dark = [1., 1., 0.]
  37. elif folder_name == 'phototaxis_dotmotion_white_Sep': # White
  38. mot_on, mot_off, lumi_on, lumi_off = [50, 150, 150, 250]
  39. mot_fac, lumi_fac = [0.33, 0.64]
  40. lumi_pre, lumi_bright, lumi_dark = [1., 1., 0.]
  41. elif folder_name == 'converted_phototaxis_dotmotion_integration_long':
  42. mot_on, mot_off, lumi_on, lumi_off = [50, 250, 250, 450]
  43. mot_fac, lumi_fac = [0.90, 1.00]
  44. lumi_pre, lumi_bright, lumi_dark = [0., 0.7, 0.]
  45. elif folder_name == 'beh_simultaneous_blackwhite_white':
  46. mot_on, mot_off, lumi_on, lumi_off = [50, 200, 50, 200]
  47. mot_fac, lumi_fac = [0.82, 0.53]
  48. lumi_pre, lumi_bright, lumi_dark = [1., 1., 0.]
  49. elif folder_name == 'beh_simultaneous_blackwhite_black':
  50. mot_on, mot_off, lumi_on, lumi_off = [50, 200, 50, 200]
  51. mot_fac, lumi_fac = [1.02, 1.08]
  52. lumi_pre, lumi_bright, lumi_dark = [0., 1., 0.]
  53. elif folder_name == 'phototaxis_dotmotion_integration_halfoverlap':
  54. mot_on, mot_off, lumi_on, lumi_off = [100, 250, 50, 200]
  55. mot_fac, lumi_fac = [1.00, 1.65]
  56. lumi_pre, lumi_bright, lumi_dark = [1., 1., 0.]
  57. elif folder_name == 'beh_simultaneous_10only':
  58. mot_on, mot_off, lumi_on, lumi_off = [50, 200, 50, 200]
  59. mot_fac, lumi_fac = [0.13, 1.04]
  60. lumi_pre, lumi_bright, lumi_dark = [1., 1., 0.]
  61. elif folder_name == 'imaging_prediction':
  62. mot_on, mot_off, lumi_on, lumi_off = [100, 400, 100, 400]
  63. mot_fac, lumi_fac = [1., 1.]
  64. lumi_pre, lumi_bright, lumi_dark = [0.3, 1., 0.]
  65. # Get the four inputs for the given stimulus type.
  66. if stim_type == 'Motion':
  67. left_input_mot = mot_fac * np.concatenate(
  68. (zero_coh * np.ones(mot_on), 1. * np.ones(mot_off - mot_on), zero_coh * np.ones(stim_len - mot_off)))
  69. right_input_mot = mot_fac * zero_coh * np.ones(stim_len)
  70. left_input_lumi = lumi_fac * lumi_pre * np.ones(stim_len)
  71. right_input_lumi = lumi_fac * lumi_pre * np.ones(stim_len)
  72. elif stim_type == 'Photo':
  73. left_input_mot = mot_fac * zero_coh * np.ones(stim_len)
  74. right_input_mot = mot_fac * zero_coh * np.ones(stim_len)
  75. left_input_lumi = lumi_fac * np.concatenate((lumi_pre * np.ones(lumi_on),
  76. lumi_bright * np.ones(lumi_off - lumi_on),
  77. lumi_pre * np.ones(stim_len - lumi_off)))
  78. right_input_lumi = lumi_fac * np.concatenate((lumi_pre * np.ones(lumi_on),
  79. lumi_dark * np.ones(lumi_off - lumi_on),
  80. lumi_pre * np.ones(stim_len - lumi_off)))
  81. elif stim_type == 'Same':
  82. left_input_mot = mot_fac * np.concatenate(
  83. (zero_coh * np.ones(mot_on), 1. * np.ones(mot_off - mot_on), zero_coh * np.ones(stim_len - mot_off)))
  84. right_input_mot = mot_fac * zero_coh * np.ones(stim_len)
  85. left_input_lumi = lumi_fac * np.concatenate((lumi_pre * np.ones(lumi_on),
  86. lumi_bright * np.ones(lumi_off - lumi_on),
  87. lumi_pre * np.ones(stim_len - lumi_off)))
  88. right_input_lumi = lumi_fac * np.concatenate((lumi_pre * np.ones(lumi_on),
  89. lumi_dark * np.ones(lumi_off - lumi_on),
  90. lumi_pre * np.ones(stim_len - lumi_off)))
  91. elif stim_type == 'Oppo':
  92. left_input_mot = mot_fac * np.concatenate(
  93. (zero_coh * np.ones(mot_on), 1. * np.ones(mot_off - mot_on), zero_coh * np.ones(stim_len - mot_off)))
  94. right_input_mot = mot_fac * zero_coh * np.ones(stim_len)
  95. left_input_lumi = lumi_fac * np.concatenate((lumi_pre * np.ones(lumi_on),
  96. lumi_dark * np.ones(lumi_off - lumi_on),
  97. lumi_pre * np.ones(stim_len - lumi_off)))
  98. right_input_lumi = lumi_fac * np.concatenate((lumi_pre * np.ones(lumi_on),
  99. lumi_bright * np.ones(lumi_off - lumi_on),
  100. lumi_pre * np.ones(stim_len - lumi_off)))
  101. return left_input_mot, right_input_mot, left_input_lumi, right_input_lumi
  102. def avg_mot_lumi(model_input, tau_mot=4.26, tau_ph_eye=12.66, tau_drive=6.46, w_mot=2.783, w_attractor_pos=0.126):
  103. '''
  104. This function contains the additive model with motion and luminance level.
  105. :param model_input: List of 5 input arrays (time, motion left, motion right, luminance left, luminance right).
  106. :param tau_mot: Timeconstant of the motion integrator.
  107. :param tau_ph_eye: Timeconstant of the luminance level integrator.
  108. :param tau_drive: Timeconstant of the multifeature integrator.
  109. :param w_mot: Weight of the motion pathway
  110. :param w_attractor_pos: Weight of the luminance level pathway.
  111. :return: Curve matching the percentage left swims over time.
  112. '''
  113. # We add some baseline activity to both multifeature nodes to avoid division by zero. Since both nodes contain this baseline, it doesn't affect the percentage left swims.
  114. baseline = 1
  115. time, left_input_mot, right_input_mot, left_input_ph, right_input_ph = model_input
  116. # Integrate motion
  117. exp_kernel_mot = np.concatenate((np.zeros(150), 1/tau_mot * np.exp(-np.linspace(0, 150, 151) / tau_mot)))
  118. exp_kernel_mot = exp_kernel_mot / np.sum(exp_kernel_mot)
  119. left_integrated_mot = convolve1d(left_input_mot, exp_kernel_mot)
  120. right_integrated_mot = convolve1d(right_input_mot, exp_kernel_mot)
  121. # Integrate luminance for each eye
  122. exp_kernel_ph_eye = np.concatenate((np.zeros(150), 1/tau_ph_eye * np.exp(-np.linspace(0, 150, 151) / tau_ph_eye)))
  123. exp_kernel_ph_eye = exp_kernel_ph_eye / np.sum(exp_kernel_ph_eye)
  124. left_integrated_ph = convolve1d(left_input_ph, exp_kernel_ph_eye)
  125. right_integrated_ph = convolve1d(right_input_ph, exp_kernel_ph_eye)
  126. # Linear weighted sum going ino the multifeature node (drive)
  127. drive_to_left = w_mot * left_integrated_mot + baseline + w_attractor_pos * left_integrated_ph
  128. drive_to_right = w_mot * right_integrated_mot + baseline + w_attractor_pos * right_integrated_ph
  129. # For integration of the drive
  130. exp_kernel_drive = np.concatenate((np.zeros(150), 1 / tau_drive * np.exp(-np.linspace(0, 150, 151) / tau_drive)))
  131. exp_kernel_drive = exp_kernel_drive / np.sum(exp_kernel_drive)
  132. left_integrated_drive = convolve1d(drive_to_left, exp_kernel_drive)
  133. right_integrated_drive = convolve1d(drive_to_right, exp_kernel_drive)
  134. # Get the ratio of leftward swims
  135. swims_to_left_tot = ((left_integrated_drive - right_integrated_drive) / (
  136. left_integrated_drive + right_integrated_drive) + 1) / 2
  137. # Apply a rolling window to match the data preprocessing. And transform ratios to percentages.
  138. swims_to_left_tot_rw = rolling_end_window(swims_to_left_tot, 20)
  139. swims_to_left_tot_rw = swims_to_left_tot_rw * 100
  140. swims_to_left_tot_rw[np.isinf(swims_to_left_tot_rw)] = 50
  141. swims_to_left_tot_rw[np.isnan(swims_to_left_tot_rw)] = 50
  142. return swims_to_left_tot_rw
  143. def avg_mot_change(model_input, tau_mot=4.26, tau_ph_rep=12.66, tau_drive=6.46, w_mot=2.783, w_repulsor_pos=1.857):
  144. '''
  145. This function contains the additive model with motion and changes in luminance.
  146. :param model_input: List of 5 input arrays (time, motion left, motion right, luminance left, luminance right).
  147. :param tau_mot: Timeconstant of the motion integrator.
  148. :param tau_ph_rep: Timeconstant of the integrator in the luminance change pathway.
  149. :param tau_drive: Timeconstant of the multifeature integrator.
  150. :param w_mot: Weight of the motion pathway
  151. :param w_repulsor_pos: Weight of the luminance change pathway.
  152. :return: Curve matching the percentage left swims over time.
  153. '''
  154. # We add some baseline activity to both multifeature nodes to avoid division by zero. Since both nodes contain this baseline, it doesn't affect the percentage left swims.
  155. baseline = 1
  156. time, left_input_mot, right_input_mot, left_input_ph, right_input_ph = model_input
  157. # Integrate motion
  158. exp_kernel_mot = np.concatenate((np.zeros(150), 1 / tau_mot * np.exp(-np.linspace(0, 150, 151) / tau_mot)))
  159. exp_kernel_mot = exp_kernel_mot / np.sum(exp_kernel_mot)
  160. left_integrated_mot = convolve1d(left_input_mot, exp_kernel_mot)
  161. right_integrated_mot = convolve1d(right_input_mot, exp_kernel_mot)
  162. # Calculate repulsion force
  163. exp_kernel_ph_rep = np.concatenate((np.zeros(150), 1 / tau_ph_rep * np.exp(-np.linspace(0, 150, 151) / tau_ph_rep)))
  164. exp_kernel_ph_rep = exp_kernel_ph_rep / np.sum(exp_kernel_ph_rep)
  165. left_integrated_rep = convolve1d(left_input_ph, exp_kernel_ph_rep)
  166. right_integrated_rep = convolve1d(right_input_ph, exp_kernel_ph_rep)
  167. dark_left = np.clip(left_integrated_rep - left_input_ph, 0, np.inf)
  168. dark_right = np.clip(right_integrated_rep - right_input_ph, 0, np.inf)
  169. bright_left = np.clip(left_input_ph - left_integrated_rep, 0, np.inf)
  170. bright_right = np.clip(right_input_ph - right_integrated_rep, 0, np.inf)
  171. repulsion_from_left = bright_left + dark_left
  172. repulsion_from_right = bright_right + dark_right
  173. # Linear weighted sum going ino the multifeature node (drive)
  174. drive_to_left = w_mot * left_integrated_mot + w_repulsor_pos * repulsion_from_right + baseline
  175. drive_to_right = w_mot * right_integrated_mot + w_repulsor_pos * repulsion_from_left + baseline
  176. # For integration of the drive
  177. exp_kernel_drive = np.concatenate((np.zeros(150), 1 / tau_drive * np.exp(-np.linspace(0, 150, 151) / tau_drive)))
  178. exp_kernel_drive = exp_kernel_drive / np.sum(exp_kernel_drive)
  179. left_integrated_drive = convolve1d(drive_to_left, exp_kernel_drive)
  180. right_integrated_drive = convolve1d(drive_to_right, exp_kernel_drive)
  181. # Get the ratio of leftward swims
  182. swims_to_left_tot = ((left_integrated_drive - right_integrated_drive) / (
  183. left_integrated_drive + right_integrated_drive) + 1) / 2
  184. # Apply a rolling window to match the data preprocessing. And transform ratios to percentages.
  185. swims_to_left_tot_rw = rolling_end_window(swims_to_left_tot, 20)
  186. swims_to_left_tot_rw = swims_to_left_tot_rw * 100
  187. swims_to_left_tot_rw[np.isinf(swims_to_left_tot_rw)] = 50
  188. swims_to_left_tot_rw[np.isnan(swims_to_left_tot_rw)] = 50
  189. return swims_to_left_tot_rw
  190. def avg_lumi_change(model_input, tau_ph_eye=12.66, tau_ph_rep=12.66, tau_drive=6.46, w_attractor_pos=0.126, w_repulsor_pos=1.857):
  191. '''
  192. This function contains the additive model with luminance level and changes in luminance.
  193. :param model_input: List of 5 input arrays (time, motion left, motion right, luminance left, luminance right).
  194. :param tau_ph_eye: Timeconstant of the luminance level integrator.
  195. :param tau_ph_rep: Timeconstant of the integrator in the luminance change pathway.
  196. :param tau_drive: Timeconstant of the multifeature integrator.
  197. :param w_attractor_pos: Weight of the luminance level pathway.
  198. :param w_repulsor_pos: Weight of the luminance change pathway.
  199. :return: Curve matching the percentage left swims over time.
  200. '''
  201. # We add some baseline activity to both multifeature nodes to avoid division by zero. Since both nodes contain this baseline, it doesn't affect the percentage left swims.
  202. baseline = 1
  203. time, left_input_mot, right_input_mot, left_input_ph, right_input_ph = model_input
  204. # Integrate luminance for each eye
  205. exp_kernel_ph_eye = np.concatenate((np.zeros(150), 1 / tau_ph_eye * np.exp(-np.linspace(0, 150, 151) / tau_ph_eye)))
  206. exp_kernel_ph_eye = exp_kernel_ph_eye / np.sum(exp_kernel_ph_eye)
  207. left_integrated_ph = convolve1d(left_input_ph, exp_kernel_ph_eye)
  208. right_integrated_ph = convolve1d(right_input_ph, exp_kernel_ph_eye)
  209. # Calculate repulsion force
  210. exp_kernel_ph_rep = np.concatenate((np.zeros(150), 1 / tau_ph_rep * np.exp(-np.linspace(0, 150, 151) / tau_ph_rep)))
  211. exp_kernel_ph_rep = exp_kernel_ph_rep / np.sum(exp_kernel_ph_rep)
  212. left_integrated_rep = convolve1d(left_input_ph, exp_kernel_ph_rep)
  213. right_integrated_rep = convolve1d(right_input_ph, exp_kernel_ph_rep)
  214. dark_left = np.clip(left_integrated_rep - left_input_ph, 0, np.inf)
  215. dark_right = np.clip(right_integrated_rep - right_input_ph, 0, np.inf)
  216. bright_left = np.clip(left_input_ph - left_integrated_rep, 0, np.inf)
  217. bright_right = np.clip(right_input_ph - right_integrated_rep, 0, np.inf)
  218. repulsion_from_left = bright_left + dark_left
  219. repulsion_from_right = bright_right + dark_right
  220. # Linear weighted sum going ino the multifeature node (drive)
  221. drive_to_left = w_repulsor_pos * repulsion_from_right + baseline + w_attractor_pos * left_integrated_ph
  222. drive_to_right = w_repulsor_pos * repulsion_from_left + baseline + w_attractor_pos * right_integrated_ph
  223. # For integration of the drive
  224. exp_kernel_drive = np.concatenate((np.zeros(150), 1 / tau_drive * np.exp(-np.linspace(0, 150, 151) / tau_drive)))
  225. exp_kernel_drive = exp_kernel_drive / np.sum(exp_kernel_drive)
  226. left_integrated_drive = convolve1d(drive_to_left, exp_kernel_drive)
  227. right_integrated_drive = convolve1d(drive_to_right, exp_kernel_drive)
  228. # Get the ratio of leftward swims
  229. swims_to_left_tot = ((left_integrated_drive - right_integrated_drive) / (
  230. left_integrated_drive + right_integrated_drive) + 1) / 2
  231. # Apply a rolling window to match the data preprocessing. And transform ratios to percentages.
  232. swims_to_left_tot_rw = rolling_end_window(swims_to_left_tot, 20)
  233. swims_to_left_tot_rw = swims_to_left_tot_rw * 100
  234. swims_to_left_tot_rw[np.isinf(swims_to_left_tot_rw)] = 50
  235. swims_to_left_tot_rw[np.isnan(swims_to_left_tot_rw)] = 50
  236. return swims_to_left_tot_rw
  237. def avg_mot_lumi_change(model_input, tau_mot=4.26, tau_ph_eye=12.66, tau_ph_rep=12.66, tau_drive=6.46,
  238. w_mot=2.783, w_attractor_pos=0.126, w_repulsor_pos=1.857):
  239. '''
  240. This function contains the additive model with motion, luminance level and changes in luminance.
  241. :param model_input: List of 5 input arrays (time, motion left, motion right, luminance left, luminance right).
  242. :param tau_mot: Timeconstant of the motion integrator.
  243. :param tau_ph_eye: Timeconstant of the luminance level integrator.
  244. :param tau_ph_rep: Timeconstant of the integrator in the luminance change pathway.
  245. :param tau_drive: Timeconstant of the multifeature integrator.
  246. :param w_mot: Weight of the motion pathway
  247. :param w_attractor_pos: Weight of the luminance level pathway.
  248. :param w_repulsor_pos: Weight of the luminance change pathway.
  249. :return: Curve matching the percentage left swims over time.
  250. '''
  251. # We add some baseline activity to both multifeature nodes to avoid division by zero. Since both nodes contain this baseline, it doesn't affect the percentage left swims.
  252. baseline = 1
  253. time, left_input_mot, right_input_mot, left_input_ph, right_input_ph = model_input
  254. # Integrate motion
  255. exp_kernel_mot = np.concatenate((np.zeros(150), 1 / tau_mot * np.exp(-np.linspace(0, 150, 151) / tau_mot)))
  256. exp_kernel_mot = exp_kernel_mot / np.sum(exp_kernel_mot)
  257. left_integrated_mot = convolve1d(left_input_mot, exp_kernel_mot)
  258. right_integrated_mot = convolve1d(right_input_mot, exp_kernel_mot)
  259. # Integrate luminance for each eye
  260. exp_kernel_ph_eye = np.concatenate((np.zeros(150), 1 / tau_ph_eye * np.exp(-np.linspace(0, 150, 151) / tau_ph_eye)))
  261. exp_kernel_ph_eye = exp_kernel_ph_eye / np.sum(exp_kernel_ph_eye)
  262. left_integrated_ph = convolve1d(left_input_ph, exp_kernel_ph_eye)
  263. right_integrated_ph = convolve1d(right_input_ph, exp_kernel_ph_eye)
  264. # Calculate repulsion force
  265. exp_kernel_ph_rep = np.concatenate((np.zeros(150), 1 / tau_ph_rep * np.exp(-np.linspace(0, 150, 151) / tau_ph_rep)))
  266. exp_kernel_ph_rep = exp_kernel_ph_rep / np.sum(exp_kernel_ph_rep)
  267. left_integrated_rep = convolve1d(left_input_ph, exp_kernel_ph_rep)
  268. right_integrated_rep = convolve1d(right_input_ph, exp_kernel_ph_rep)
  269. dark_left = np.clip(left_integrated_rep - left_input_ph, 0, np.inf)
  270. dark_right = np.clip(right_integrated_rep - right_input_ph, 0, np.inf)
  271. bright_left = np.clip(left_input_ph - left_integrated_rep, 0, np.inf)
  272. bright_right = np.clip(right_input_ph - right_integrated_rep, 0, np.inf)
  273. repulsion_from_left = bright_left + dark_left
  274. repulsion_from_right = bright_right + dark_right
  275. # Linear weighted sum going ino the multifeature node (drive)
  276. drive_to_left = w_mot * left_integrated_mot + w_repulsor_pos * repulsion_from_right + baseline + w_attractor_pos * left_integrated_ph
  277. drive_to_right = w_mot * right_integrated_mot + w_repulsor_pos * repulsion_from_left + baseline + w_attractor_pos * right_integrated_ph
  278. # For integration of the drive
  279. exp_kernel_drive = np.concatenate((np.zeros(150), 1 / tau_drive * np.exp(-np.linspace(0, 150, 151) / tau_drive)))
  280. exp_kernel_drive = exp_kernel_drive / np.sum(exp_kernel_drive)
  281. left_integrated_drive = convolve1d(drive_to_left, exp_kernel_drive)
  282. right_integrated_drive = convolve1d(drive_to_right, exp_kernel_drive)
  283. # Get the ratio of leftward swims
  284. swims_to_left_tot = ((left_integrated_drive - right_integrated_drive) / (
  285. left_integrated_drive + right_integrated_drive) + 1) / 2
  286. # Apply a rolling window to match the data preprocessing. And transform ratios to percentages.
  287. swims_to_left_tot_rw = rolling_end_window(swims_to_left_tot, 20)
  288. swims_to_left_tot_rw = swims_to_left_tot_rw * 100
  289. swims_to_left_tot_rw[np.isinf(swims_to_left_tot_rw)] = 50
  290. swims_to_left_tot_rw[np.isnan(swims_to_left_tot_rw)] = 50
  291. return swims_to_left_tot_rw
  292. def avg_mot_lumi_change_full_node_outputs(model_input, tau_mot=4.26, tau_ph_eye=12.66, tau_ph_rep=12.66, tau_drive=6.46,
  293. w_mot=2.783, w_attractor_pos=0.126, w_repulsor_pos=1.857):
  294. '''
  295. This function contains the additive model with motion, luminance level and changes in luminance. It returns the contributions of several nodes.
  296. :param model_input: List of 5 input arrays (time, motion left, motion right, luminance left, luminance right).
  297. :param tau_mot: Timeconstant of the motion integrator.
  298. :param tau_ph_eye: Timeconstant of the luminance level integrator.
  299. :param tau_ph_rep: Timeconstant of the integrator in the luminance change pathway.
  300. :param tau_drive: Timeconstant of the multifeature integrator.
  301. :param w_mot: Weight of the motion pathway
  302. :param w_attractor_pos: Weight of the luminance level pathway.
  303. :param w_repulsor_pos: Weight of the luminance change pathway.
  304. :return: motion_left-, motion_right-, lumi_left-, lumi_right-, change_left-, change_right-, multifeature_left-, multifeature_right- contributions, Curve matching the percentage left swims over time.
  305. '''
  306. # We add some baseline activity to both multifeature nodes to avoid division by zero. Since both nodes contain this baseline, it doesn't affect the percentage left swims.
  307. baseline = 1
  308. time, left_input_mot, right_input_mot, left_input_ph, right_input_ph = model_input
  309. # Integrate motion
  310. exp_kernel_mot = np.concatenate((np.zeros(150), 1 / tau_mot * np.exp(-np.linspace(0, 150, 151) / tau_mot)))
  311. exp_kernel_mot = exp_kernel_mot / np.sum(exp_kernel_mot)
  312. left_integrated_mot = convolve1d(left_input_mot, exp_kernel_mot)
  313. right_integrated_mot = convolve1d(right_input_mot, exp_kernel_mot)
  314. # Integrate luminance for each eye
  315. exp_kernel_ph_eye = np.concatenate((np.zeros(150), 1 / tau_ph_eye * np.exp(-np.linspace(0, 150, 151) / tau_ph_eye)))
  316. exp_kernel_ph_eye = exp_kernel_ph_eye / np.sum(exp_kernel_ph_eye)
  317. left_integrated_ph = convolve1d(left_input_ph, exp_kernel_ph_eye)
  318. right_integrated_ph = convolve1d(right_input_ph, exp_kernel_ph_eye)
  319. # Calculate repulsion force
  320. exp_kernel_ph_rep = np.concatenate((np.zeros(150), 1 / tau_ph_rep * np.exp(-np.linspace(0, 150, 151) / tau_ph_rep)))
  321. exp_kernel_ph_rep = exp_kernel_ph_rep / np.sum(exp_kernel_ph_rep)
  322. left_integrated_rep = convolve1d(left_input_ph, exp_kernel_ph_rep)
  323. right_integrated_rep = convolve1d(right_input_ph, exp_kernel_ph_rep)
  324. dark_left = np.clip(left_integrated_rep - left_input_ph, 0, np.inf)
  325. dark_right = np.clip(right_integrated_rep - right_input_ph, 0, np.inf)
  326. bright_left = np.clip(left_input_ph - left_integrated_rep, 0, np.inf)
  327. bright_right = np.clip(right_input_ph - right_integrated_rep, 0, np.inf)
  328. repulsion_from_left = bright_left + dark_left
  329. repulsion_from_right = bright_right + dark_right
  330. # Linear weighted sum going into the multifeature (drive) node.
  331. drive_to_left = w_mot * left_integrated_mot + w_repulsor_pos * repulsion_from_right + baseline + w_attractor_pos * left_integrated_ph
  332. drive_to_right = w_mot * right_integrated_mot + w_repulsor_pos * repulsion_from_left + baseline + w_attractor_pos * right_integrated_ph
  333. # For integration of the drive
  334. exp_kernel_drive = np.concatenate((np.zeros(150), 1 / tau_drive * np.exp(-np.linspace(0, 150, 151) / tau_drive)))
  335. exp_kernel_drive = exp_kernel_drive / np.sum(exp_kernel_drive)
  336. left_integrated_drive = convolve1d(drive_to_left, exp_kernel_drive)
  337. right_integrated_drive = convolve1d(drive_to_right, exp_kernel_drive)
  338. # Get the ratio of leftward swims
  339. swims_to_left_tot = ((left_integrated_drive - right_integrated_drive) / (
  340. left_integrated_drive + right_integrated_drive) + 1) / 2
  341. # Apply a rolling window to match the data preprocessing. And transform ratios to percentages.
  342. swims_to_left_tot_rw = rolling_end_window(swims_to_left_tot, 20)
  343. swims_to_left_tot_rw = swims_to_left_tot_rw * 100
  344. swims_to_left_tot_rw[np.isinf(swims_to_left_tot_rw)] = 50
  345. swims_to_left_tot_rw[np.isnan(swims_to_left_tot_rw)] = 50
  346. return (w_mot * left_integrated_mot, w_mot * right_integrated_mot,
  347. w_attractor_pos * left_integrated_ph, w_attractor_pos * right_integrated_ph,
  348. w_repulsor_pos * repulsion_from_left, w_repulsor_pos * repulsion_from_right,
  349. left_integrated_drive, right_integrated_drive, swims_to_left_tot_rw)
  350. def avg_mot_lumi_change_withmotinhib(model_input, tau_mot=4.26, tau_ph_eye=12.66, tau_ph_rep=12.66, tau_drive=6.46,
  351. w_mot=2.783, w_attractor_pos=0.126, w_repulsor_pos=1.857, w_mot_inhib=0):
  352. '''
  353. This function contains the additive model with motion, luminance level and changes in luminance. Additionally, it contains a mutually inhibitory connection between the left and right motion integrators.
  354. :param model_input: List of 5 input arrays (time array, motion left, motion right, luminance left, luminance right).
  355. :param tau_mot: Timeconstant of the motion integrator.
  356. :param tau_ph_eye: Timeconstant of the luminance level integrator.
  357. :param tau_ph_rep: Timeconstant of the integrator in the luminance change pathway.
  358. :param tau_drive: Timeconstant of the multifeature integrator.
  359. :param w_mot: Weight of the motion pathway
  360. :param w_attractor_pos: Weight of the luminance level pathway.
  361. :param w_repulsor_pos: Weight of the luminance change pathway.
  362. :param w_mot_inhib: Relative weight of the motion inhibition.
  363. :return: Curve matching the percentage left swims over time.
  364. '''
  365. # We add some baseline activity to both multifeature nodes to avoid division by zero. Since both nodes contain this baseline, it doesn't affect the percentage left swims.
  366. baseline = 1
  367. time, left_input_mot, right_input_mot, left_input_ph, right_input_ph = model_input
  368. # Integrate motion
  369. exp_kernel_mot = np.concatenate((np.zeros(150), 1 / tau_mot * np.exp(-np.linspace(0, 150, 151) / tau_mot)))
  370. exp_kernel_mot = exp_kernel_mot / np.sum(exp_kernel_mot)
  371. left_integrated_mot = convolve1d(left_input_mot, exp_kernel_mot)
  372. right_integrated_mot = convolve1d(right_input_mot, exp_kernel_mot)
  373. # Compute the mutual inhibition between the motion nodes (and make sure they remain positive).
  374. left_integrated_mot_preinhib = left_integrated_mot
  375. left_integrated_mot = left_integrated_mot - w_mot_inhib * right_integrated_mot
  376. right_integrated_mot = right_integrated_mot - w_mot_inhib * left_integrated_mot_preinhib
  377. left_integrated_mot[left_integrated_mot < 0] = 0
  378. right_integrated_mot[right_integrated_mot < 0] = 0
  379. # Integrate luminance for each eye
  380. exp_kernel_ph_eye = np.concatenate((np.zeros(150), 1 / tau_ph_eye * np.exp(-np.linspace(0, 150, 151) / tau_ph_eye)))
  381. exp_kernel_ph_eye = exp_kernel_ph_eye / np.sum(exp_kernel_ph_eye)
  382. left_integrated_ph = convolve1d(left_input_ph, exp_kernel_ph_eye)
  383. right_integrated_ph = convolve1d(right_input_ph, exp_kernel_ph_eye)
  384. # Calculate repulsion force
  385. exp_kernel_ph_rep = np.concatenate((np.zeros(150), 1 / tau_ph_rep * np.exp(-np.linspace(0, 150, 151) / tau_ph_rep)))
  386. exp_kernel_ph_rep = exp_kernel_ph_rep / np.sum(exp_kernel_ph_rep)
  387. left_integrated_rep = convolve1d(left_input_ph, exp_kernel_ph_rep)
  388. right_integrated_rep = convolve1d(right_input_ph, exp_kernel_ph_rep)
  389. dark_left = np.clip(left_integrated_rep - left_input_ph, 0, np.inf)
  390. dark_right = np.clip(right_integrated_rep - right_input_ph, 0, np.inf)
  391. bright_left = np.clip(left_input_ph - left_integrated_rep, 0, np.inf)
  392. bright_right = np.clip(right_input_ph - right_integrated_rep, 0, np.inf)
  393. repulsion_from_left = bright_left + dark_left
  394. repulsion_from_right = bright_right + dark_right
  395. # Compute the linear weighted sum into the multifeature node (drive).
  396. drive_to_left = w_mot * left_integrated_mot + w_repulsor_pos * repulsion_from_right + baseline + w_attractor_pos * left_integrated_ph
  397. drive_to_right = w_mot * right_integrated_mot + w_repulsor_pos * repulsion_from_left + baseline + w_attractor_pos * right_integrated_ph
  398. # For integration of the drive
  399. exp_kernel_drive = np.concatenate((np.zeros(150), 1 / tau_drive * np.exp(-np.linspace(0, 150, 151) / tau_drive)))
  400. exp_kernel_drive = exp_kernel_drive / np.sum(exp_kernel_drive)
  401. left_integrated_drive = convolve1d(drive_to_left, exp_kernel_drive)
  402. right_integrated_drive = convolve1d(drive_to_right, exp_kernel_drive)
  403. # Calculate the ratio of leftward swims.
  404. swims_to_left_tot = ((left_integrated_drive - right_integrated_drive) / (
  405. left_integrated_drive + right_integrated_drive) + 1) / 2
  406. # Apply a rollowing window to match the data processing step. and transform ratios to percentage.
  407. swims_to_left_tot_rw = rolling_end_window(swims_to_left_tot, 20)
  408. swims_to_left_tot_rw = swims_to_left_tot_rw * 100
  409. swims_to_left_tot_rw[np.isinf(swims_to_left_tot_rw)] = 50
  410. swims_to_left_tot_rw[np.isnan(swims_to_left_tot_rw)] = 50
  411. return swims_to_left_tot_rw
  412. def get_data(folder_name, stim_len_timepoints, stim_name, path_to_folders, nsplits=3, debug=False):
  413. '''
  414. This function retrieves the data of the specified experiments and stimulus types.
  415. :param folder_names: List of experiment stimuli to train on.
  416. :param stim_len_timepoints: List of the length of each stimulus in 0.1s steps (e.g. a 25s stimulus is written as 250 in this list).
  417. :param stim_names: List of stimulus types to train on (e.g. Motion, Photo, Same and Oppo).
  418. :param path_to_folders: path to the folder containing all experiments.
  419. :param nsplits: Number of splits to split the data in, one of the splits will be used as testing data, the rest will be used as training data. Default is 3.
  420. :param debug: Print how many fish are used for testing and how many for training.
  421. :return: The data: mean training values, SEM training values, mean testing values, SEM testing values.
  422. '''
  423. # Load the analysed dataframe.
  424. try:
  425. path_to_local_folder = rf'{path_to_folders}\{folder_name}'
  426. df_path = f'{path_to_local_folder}/data_analysed.hdf5'
  427. df = pd.read_hdf(df_path)
  428. except:
  429. path_to_local_folder = rf'{path_to_folders}\{folder_name}\Analysis'
  430. df_path = f'{path_to_local_folder}/data_analysed.hdf5'
  431. df = pd.read_hdf(df_path)
  432. # Group the data into the splits.
  433. gkf = GroupShuffleSplit(n_splits=nsplits)
  434. datasplitss = {}
  435. datasplitss.update({f"{folder_name}": [(train_ids, test_ids) for train_ids, test_ids in
  436. gkf.split(df.index.unique('experiment_ID'),
  437. groups=df.index.unique('experiment_ID').tolist())]})
  438. # Create the two separate dataframes for the training and testing data.
  439. train_df = df[np.isin(df.index.get_level_values('experiment_ID'),
  440. df.index.unique('experiment_ID')[datasplitss[f'{folder_name}'][0][0]])]
  441. test_df = df[np.isin(df.index.get_level_values('experiment_ID'),
  442. df.index.unique('experiment_ID')[datasplitss[f'{folder_name}'][0][1]])]
  443. if debug:
  444. print(f'{folder_name} - {stim_name}: ')
  445. print(f'Train N FISH: {len(train_df.index.unique("experiment_ID"))}')
  446. print(f'Test N FISH: {len(test_df.index.unique("experiment_ID"))}\n')
  447. # Extract the mean and sem of percentage_left swims for the specific stimulus type. Both for the training and testing data set.
  448. stimulus_data = train_df.xs(stim_name, level='stimulus_name')
  449. data_mean = stimulus_data.groupby('window_time').mean()['percentage_left']
  450. data_sem = stimulus_data.groupby('window_time').std()['percentage_left'] / np.sqrt(
  451. len(train_df.index.unique('experiment_ID')))
  452. stimulus_data_test = test_df.xs(stim_name, level='stimulus_name')
  453. data_mean_test = stimulus_data_test.groupby('window_time').mean()['percentage_left']
  454. data_sem_test = stimulus_data_test.groupby('window_time').std()['percentage_left'] / np.sqrt(
  455. len(test_df.index.unique('experiment_ID')))
  456. # This is needed in case there were no bouts in the first 0.1 s (this happens). We need to make sure the length of the dataframe matches across stimulus types.
  457. for iter in range(10):
  458. if len(data_mean) < stim_len_timepoints:
  459. data_mean = pd.concat([pd.Series([0.5]), data_mean])
  460. data_sem = pd.concat([pd.Series([0.]), data_sem])
  461. if len(data_mean_test) < stim_len_timepoints:
  462. data_mean_test = pd.concat([pd.Series([0.5]), data_mean_test])
  463. data_sem_test = pd.concat([pd.Series([0.]), data_sem_test])
  464. # Replace first 0,5 seconds of each stimulus with a flat line - there are not enough bouts in this initial time for a good percentage estimate.
  465. # And since the stimulus input is always 0 here - it won't affect the model fitting.
  466. data_mean[:0.5] = 0.5
  467. data_sem[:0.5] = 0.05
  468. data_mean_test[:0.5] = 0.5
  469. data_sem_test[:0.5] = 0.05
  470. data_mean = data_mean * 100
  471. data_sem = data_sem * 100
  472. data_mean_test = data_mean_test * 100
  473. data_sem_test = data_sem_test * 100
  474. # Fill any potential nans since scipy's curve-fit can't handle it.
  475. data_mean = fill_nans(data_mean)
  476. data_sem = fill_nans(data_sem)
  477. data_mean_test = fill_nans(data_mean_test)
  478. data_sem_test = fill_nans(data_sem_test)
  479. return data_mean, data_sem, data_mean_test, data_sem_test
  480. def train_model_once(model_func, folder_names, stim_len_timepoints, stim_names, path_to_folders, subfig=None):
  481. '''
  482. This function trains the model once.
  483. :param model_func: name of the model, e.g. avg_mot_lumi_change.
  484. :param folder_names: List of experiment stimuli to train on.
  485. :param stim_len_timepoints: List of the length of each stimulus in 0.1s steps (e.g. a 25s stimulus is written as 250 in this list).
  486. :param stim_names: List of stimulus types to train on (e.g. Motion, Photo, Same and Oppo).
  487. :param path_to_folders: Path the folder containing all experiment folders.
  488. :param subfig: If not None plot the data and model fit from the training. This is not used anymore in the paper.
  489. :return: best model parameters, best_mse, list_of_folder_ids
  490. '''
  491. n_loops = len(stim_len_timepoints) * len(stim_names)
  492. list_of_folder_ids = []
  493. list_of_stim_ids = []
  494. # Loop over randomly drawn stimulus combinations.
  495. for i in range(n_loops):
  496. random_folder_id = np.random.randint(len(folder_names))
  497. random_stim_id = np.random.randint(len(stim_names))
  498. list_of_folder_ids = np.append(list_of_folder_ids, random_folder_id)
  499. list_of_stim_ids = np.append(list_of_stim_ids, random_stim_id)
  500. # Get the data, split into a training (2/3 of the data) and a testing (1/3 of the data) set.
  501. # Note that training is only based on the mean, the sem we only use for the plotting of the data.
  502. data_mean, data_sem, data_mean_test, data_sem_test = get_data(folder_names[random_folder_id],
  503. stim_len_timepoints[random_folder_id],
  504. stim_names[random_stim_id],
  505. path_to_folders=path_to_folders)
  506. # Get the input into the model
  507. left_mot_input, right_mot_input, left_lumi_input, right_lumi_input = get_stim_input(
  508. folder_names[random_folder_id], stim_len_timepoints[random_folder_id], stim_names[random_stim_id])
  509. time = np.linspace(0, (len(data_mean) - 1) / 10, len(data_mean))
  510. # Collect the model input, data mean and sem for training and testing, folder and stim ids.
  511. if i == 0:
  512. model_input = [time, left_mot_input, right_mot_input, left_lumi_input, right_lumi_input]
  513. data_full = data_mean
  514. data_sem_full = data_sem
  515. data_full_test = data_mean_test
  516. data_sem_full_test = data_sem_test
  517. folder_ids = random_folder_id * np.ones(stim_len_timepoints[random_folder_id])
  518. stim_ids = random_stim_id * np.ones(stim_len_timepoints[random_folder_id])
  519. else:
  520. model_input = np.hstack(
  521. (model_input, [time, left_mot_input, right_mot_input, left_lumi_input, right_lumi_input]))
  522. data_full = np.append(data_full, data_mean)
  523. data_full_test = np.append(data_full_test, data_mean_test)
  524. data_sem_full = np.append(data_sem_full, data_sem)
  525. data_sem_full_test = np.append(data_sem_full_test, data_sem_test)
  526. folder_ids = np.append(folder_ids, random_folder_id * np.ones(stim_len_timepoints[random_folder_id]))
  527. stim_ids = np.append(stim_ids, random_stim_id * np.ones(stim_len_timepoints[random_folder_id]))
  528. # We add a chunk of 5s between the different stimuli to allow the model to return back to baseline before starting the next stimulus.
  529. if i < n_loops - 1:
  530. model_input = np.hstack((model_input, [np.zeros(50), np.zeros(50), np.zeros(50), np.ones(50), np.ones(50)]))
  531. data_full = np.append(data_full, 50 * np.ones(50))
  532. data_full_test = np.append(data_full_test, 50 * np.ones(50))
  533. data_sem_full = np.append(data_sem_full, 0 * np.ones(50))
  534. data_sem_full_test = np.append(data_sem_full_test, 0 * np.ones(50))
  535. folder_ids = np.append(folder_ids, -1 * np.ones(50))
  536. stim_ids = np.append(stim_ids, -1 * np.ones(50))
  537. # Find the best_mse across 5 model fitting with different initital parameter guesses.
  538. # Usually scipy's curve-fit finds the best fit immediately, but since it sometimes gets stuck and returns a warning 'optimal parameters not found', we give it 5 tries to be quite sure it always finds the true best fit.
  539. best_mse = 1000
  540. for c in range(5):
  541. # Define the initial parameters, as well as the bounds for each model option (timeconstants are bound between 0 and 100, weights are bound between 0 and 25).
  542. if model_func == avg_mot_lumi:
  543. tau_mot = np.random.uniform(0.1, 100)
  544. tau_ph_eye = np.random.uniform(0.1, 100)
  545. tau_drive = np.random.uniform(0.1, 100)
  546. w_mot = np.random.uniform(0.1, 25)
  547. w_attractor_pos = np.random.uniform(0.1, 25)
  548. p0 = [tau_mot, tau_ph_eye, tau_drive, w_mot, w_attractor_pos,]
  549. bounds = ([0, 0, 0, 0, 0], [100, 100, 100, 25, 25])
  550. elif model_func == avg_mot_change:
  551. tau_mot = np.random.uniform(0.1, 100)
  552. tau_ph_rep = np.random.uniform(0.1, 100)
  553. tau_drive = np.random.uniform(0.1, 100)
  554. w_mot = np.random.uniform(0.1, 25)
  555. w_repulsor_pos = np.random.uniform(0.1, 25)
  556. p0 = [tau_mot, tau_ph_rep, tau_drive, w_mot, w_repulsor_pos,]
  557. bounds = ([0, 0, 0, 0, 0], [100, 100, 100, 25, 25])
  558. elif model_func == avg_lumi_change:
  559. tau_ph_eye = np.random.uniform(0.1, 100)
  560. tau_ph_rep = np.random.uniform(0.1, 100)
  561. tau_drive = np.random.uniform(0.1, 100)
  562. w_attractor_pos = np.random.uniform(0.1, 25)
  563. w_repulsor_pos = np.random.uniform(0.1, 25)
  564. p0 = [tau_ph_eye, tau_ph_rep, tau_drive, w_attractor_pos, w_repulsor_pos]
  565. bounds = ([0, 0, 0, 0, 0], [100, 100, 100, 25, 25])
  566. elif model_func == avg_mot_lumi_change:
  567. tau_mot = np.random.uniform(0.1, 100)
  568. tau_ph_eye = np.random.uniform(0.1, 100)
  569. tau_ph_rep = np.random.uniform(0.1, 100)
  570. tau_drive = np.random.uniform(0.1, 100)
  571. w_mot = np.random.uniform(0.1, 25)
  572. w_attractor_pos = np.random.uniform(0.1, 25)
  573. w_repulsor_pos = np.random.uniform(0.1, 25)
  574. p0 = [tau_mot, tau_ph_eye, tau_ph_rep, tau_drive, w_mot, w_attractor_pos, w_repulsor_pos,]
  575. bounds = ([0, 0, 0, 0, 0, 0, 0], [100, 100, 100, 100, 25, 25, 25])
  576. elif model_func == avg_mot_lumi_change_withmotinhib:
  577. tau_mot = np.random.uniform(0.1, 100)
  578. tau_ph_eye = np.random.uniform(0.1, 100)
  579. tau_ph_rep = np.random.uniform(0.1, 100)
  580. tau_drive = np.random.uniform(0.1, 100)
  581. w_mot = np.random.uniform(0.1, 25)
  582. w_attractor_pos = np.random.uniform(0.1, 25)
  583. w_repulsor_pos = np.random.uniform(0.1, 25)
  584. w_mot_inhib = np.random.uniform(0.001, 0.999)
  585. p0 = [tau_mot, tau_ph_eye, tau_ph_rep, tau_drive, w_mot, w_attractor_pos, w_repulsor_pos, w_mot_inhib]
  586. bounds = ([0, 0, 0, 0, 0, 0, 0, 0], [100, 100, 100, 100, 25, 25, 25, 1])
  587. # Find the best fit between the model and the training data.
  588. try:
  589. popt, pcov = curve_fit(model_func, model_input, data_full, p0=p0, bounds=bounds)
  590. except:
  591. print('Warning optimal params not found, using initial params instead.')
  592. popt = p0
  593. # Calculate the MSE between the left-out testing data and the model.
  594. test_mse = np.nanmean(np.square(model_func(model_input, *popt) - data_full_test))
  595. # Update the best MSE and parameter set.
  596. if test_mse < best_mse:
  597. best_mse = test_mse
  598. best_popt = popt
  599. # If subfig is not None, plot the data with the model fit. Since the stimuli are drawn randomly and differ in length, we cannot know before how long the x-axis should be. We give a warning if not all data is shown.
  600. if subfig is not None:
  601. if len(data_full_test) > 7500:
  602. print(f'Warning: Length of data outside xmax (not the full plot will be visible) {len(data_full_test)}. ')
  603. subfig.draw_line(np.arange(len(data_full_test)), data_full_test, yerr=data_sem_full_test, lc='tab:blue', lw=1,
  604. eafc='#AEC7E8', eaalpha=1.0, ealw=1, eaec='#AEC7E8')
  605. subfig.draw_line(np.arange(len(data_full_test)), model_func(model_input, *best_popt), lc='k', lw=1.)
  606. subfig.draw_line([len(data_full_test)-150, len(data_full_test)], [21, 21], lc='k')
  607. subfig.draw_text(len(data_full_test)-75, 15, '15s')
  608. subfig.draw_text(0, 105, 'Model training on 20x random stimuli M or L', textlabel_ha='left')
  609. return best_popt, best_mse, list_of_folder_ids
  610. def train_model_full(model_func, folder_names, stim_len_timepoints, stim_names, path_to_folders, subfig=None, train_loops=10, debug=False):
  611. '''
  612. This function trains the model on all stimuli in folder_names and all stimulus_types in stim_names.
  613. :param model_func: Name of the model to train, e.g. avg_mot_lumi_change
  614. :param folder_names: List of experiment stimuli to train on.
  615. :param stim_len_timepoints: List of the length of each stimulus in 0.1s steps (e.g. a 25s stimulus is written as 250 in this list).
  616. :param stim_names: List of stimulus types to train on (e.g. Motion, Photo, Same and Oppo).
  617. :param path_to_folders: path to folder containing all experiment folders.
  618. :param subfig: Subfigure to draw the model training fit. This is not used anymore in the paper.
  619. :param train_loops: Number of training-rounds.
  620. :param debug: If True print all parameters, the best parameter set and all training MSEs.
  621. :return: All parameter sets, all training MSEs, a list of the lists with folder-ids used in each round.
  622. '''
  623. # Inititalize the best_test_mse at a high-value to be beaten by the model fits. initialize lists of all values to return.
  624. best_test_mse = 1000
  625. all_params = []
  626. all_mse = []
  627. list_of_lists_of_folder_ids = []
  628. # Loop over training rounds and train the model.
  629. for t in range(train_loops):
  630. print(f'Training round {t}')
  631. popt, test_mse, folder_ids = train_model_once(model_func,
  632. folder_names,
  633. stim_len_timepoints,
  634. stim_names,
  635. path_to_folders,
  636. subfig=subfig)
  637. list_of_lists_of_folder_ids = np.append(list_of_lists_of_folder_ids, folder_ids)
  638. all_params = np.append(all_params, popt)
  639. all_mse = np.append(all_mse, test_mse)
  640. # Update the best mse and parameter set.
  641. if test_mse < best_test_mse:
  642. best_test_mse = test_mse
  643. best_params = popt
  644. if debug:
  645. print(best_params)
  646. print(all_params)
  647. print(all_mse)
  648. return all_params, all_mse, list_of_lists_of_folder_ids
  649. def test_model_once(model_func, params, folder_names, stim_len_timepoints, stim_names, path_to_folders, subfig_data=None, subfig_model=None, training_folder_ids=None, pick_stim_ids=None, pl_color='k', debug=False):
  650. '''
  651. This function tests the model fit once.
  652. This is related to most of the subpanels in Figure 2 and S2.
  653. :param model_func: Name of the model, e.g. avg_mot_lumi_change.
  654. :param params: List of model parameters to test.
  655. :param folder_names: List of experiment stimuli to test.
  656. :param stim_len_timepoints: List with the length of each stimulus in 0.1s (a stimulus of 25 seconds should be written as 250 in this list).
  657. :param stim_names: List of stimulus-types to test, e.g. Motion, Photo, Same, Oppo.
  658. :param path_to_folders: Path to folder containing all experiment folders.
  659. :param subfig_data: Subfigure to plot the data in.
  660. :param subfig_model: Subfigure to plot the model in.
  661. :param training_folder_ids: Default None. If specified it contains a list of integers indicating the experiment stimuli to test (used in Fig. 2g to match the drawn cartoons).
  662. :param pick_stim_ids: Default None. If specified it contains a list of integers indicating the stimulus types to test (used in Fig. 2g to match the drawn cartoons).
  663. :param pl_color: Color to use for the model plot.
  664. :param debug: if True print the test_mse for each fit.
  665. :return: test_mse (mean-squared-error between the model and the data).
  666. '''
  667. # Loop over all stimuli and stimulus-types to test (this can be randomly drawn, or following the list if training_folder_ids and/or pick_stim_ids are not None.
  668. list_of_stim_ids = []
  669. for i in range(len(stim_len_timepoints) * len(stim_names)):
  670. if training_folder_ids is None:
  671. folder_id = i % len(stim_len_timepoints)
  672. else:
  673. folder_id = int(training_folder_ids[i])
  674. if pick_stim_ids is None and training_folder_ids is None:
  675. stim_id = int(i / len(stim_len_timepoints))
  676. elif pick_stim_ids is None:
  677. stim_id = np.random.choice(np.arange(len(stim_names)))
  678. else:
  679. stim_id = int(pick_stim_ids[i])
  680. # Append the list of stimulus ids.
  681. list_of_stim_ids = np.append(list_of_stim_ids, stim_id)
  682. # Load the data of a specific stimulus and stimulus-type.
  683. _, _, data_mean, data_sem = get_data(folder_names[folder_id],
  684. stim_len_timepoints[folder_id],
  685. stim_names[stim_id],
  686. path_to_folders=path_to_folders,
  687. nsplits=3)
  688. # Get the model input for this stimulus and stimulus-type.
  689. left_mot_input, right_mot_input, left_lumi_input, right_lumi_input = get_stim_input(folder_names[folder_id],
  690. stim_len_timepoints[
  691. folder_id],
  692. stim_names[stim_id])
  693. time = np.linspace(0, (len(data_mean) - 1) / 10, len(data_mean))
  694. #Collect the model_inputs, mean of the data, sem of the data, stimulus-ids (folder_ids) and stimulus-types.
  695. if i == 0:
  696. model_input = [time, left_mot_input, right_mot_input, left_lumi_input, right_lumi_input]
  697. data_full = data_mean
  698. data_sem_full = data_sem
  699. # The in_stim_tracker keeps track of which data should be plotted (we stick 5s pieces of flat data between the stimuli to allow any longer timescale model
  700. # fits to return to baseline before starting the next stim. These 5s pieces are skipped in the plotting based on in_stim_tracker.
  701. in_stim_tracker = np.zeros(len(data_mean))
  702. folder_ids = folder_id * np.ones(stim_len_timepoints[folder_id])
  703. stim_ids = stim_id * np.ones(stim_len_timepoints[folder_id])
  704. else:
  705. model_input = np.hstack(
  706. (model_input, [time, left_mot_input, right_mot_input, left_lumi_input, right_lumi_input]))
  707. data_full = np.append(data_full, data_mean)
  708. data_sem_full = np.append(data_sem_full, data_sem)
  709. in_stim_tracker = np.append(in_stim_tracker, np.zeros(len(data_mean)))
  710. folder_ids = np.append(folder_ids, folder_id * np.ones(stim_len_timepoints[folder_id]))
  711. stim_ids = np.append(stim_ids, stim_id * np.ones(stim_len_timepoints[folder_id]))
  712. # Here we attach a 5s piece between each stimulus to allow all models to return back to baseline before the next stimulus starts.
  713. if i < len(stim_len_timepoints) * len(stim_names) - 1:
  714. model_input = np.hstack((model_input, [np.zeros(50), np.zeros(50), np.zeros(50), np.ones(50), np.ones(50)]))
  715. data_full = np.append(data_full, 50 * np.ones(50))
  716. data_sem_full = np.append(data_sem_full, 0 * np.ones(50))
  717. in_stim_tracker = np.append(in_stim_tracker, np.ones(50))
  718. folder_ids = np.append(folder_ids, -1 * np.ones(50))
  719. stim_ids = np.append(stim_ids, -1 * np.ones(50))
  720. # Calculate the entire Mean-squared-error between the data and the model.
  721. test_mse = np.nanmean(np.square(model_func(model_input, *params) - data_full))
  722. if debug:
  723. print(test_mse)
  724. # Plot the data if subfig_data is not None.
  725. if subfig_data is not None:
  726. print('length of data', len(data_full))
  727. data_full[in_stim_tracker.astype(bool)] = np.nan
  728. data_sem_full[in_stim_tracker.astype(bool)] = np.nan
  729. subfig_data.draw_line(np.arange(len(data_full)), data_full, yerr=data_sem_full, lc='k', lw=1,
  730. eafc='#404040', eaalpha=1.0, ealw=1, eaec='#404040')
  731. subfig_data.draw_line([len(data_full)-150, len(data_full)], [21, 21], lc='k')
  732. subfig_data.draw_text(len(data_full)-75, 15, '15s')
  733. # Plot the model fit if subfig_model is not None.
  734. if subfig_model is not None:
  735. model_output = model_func(model_input, *params)
  736. model_output[in_stim_tracker.astype(bool)] = np.nan
  737. subfig_model.draw_line(np.arange(len(data_full)), model_output, lc=pl_color, lw=1.)
  738. return test_mse
  739. def plot_model_contribution_single_experiment(model_func, params, folder_name, stim_len_timepoints, stim_name, path_to_folders, subfig_contributions_left=None, subfig_contributions_right=None):
  740. '''
  741. This function plots the contributions of the main 3 nodes to the model output.
  742. This is related to figure S2c.
  743. :param model_func: Name of the model function to use, e.g. avg_mot_lumi_change.
  744. :param params: Model parameters, in case of the avg_mot_lumi_change model these are the four weigths and three timeconstants.
  745. :param folder_name: Folder name of the experiment to plot.
  746. :param stim_len_timepoints: Length of the stimulus in 0.1s steps (a 25s stimulus is written as 250 here).
  747. :param stim_name: The name of the stimulus type (e.g. Motion, Photo, Same, Oppo)
  748. :param path_to_folders: Path to folder containing all experiment folders.
  749. :param subfig_contributions_left: subfigure to plot the contributions to the leftward multifeature integrator.
  750. :param subfig_contributions_right: Subfigure to plot the contributions to the rightward multifeature integrator (with flipped y-axis).
  751. '''
  752. # Load the data of one specific experiment (folder_name) and one specific stimulus type (Motion, Photo, Same or Oppo).
  753. _, _, data_mean, data_sem = get_data(folder_name,
  754. stim_len_timepoints,
  755. stim_name,
  756. path_to_folders=path_to_folders,
  757. nsplits=3)
  758. # Load the input to the model for the same folder_name and stimulus type (stim_name).
  759. left_mot_input, right_mot_input, left_lumi_input, right_lumi_input = get_stim_input(folder_name,
  760. stim_len_timepoints,
  761. stim_name)
  762. time = np.linspace(0, (len(data_mean) - 1) / 10, len(data_mean))
  763. model_input = [time, left_mot_input, right_mot_input, left_lumi_input, right_lumi_input]
  764. # Plot the contributions of the left/right motion integrators, left/right luminance integrators and left/right luminance change detectors.
  765. if subfig_contributions_left is not None or subfig_contributions_right is not None:
  766. mot_left, mot_right, ph_left, ph_right, diff_left, diff_right, drive_left, drive_right, model_output = model_func(model_input, *params)
  767. subfig_contributions_left.draw_line(np.arange(len(mot_left)), mot_left, lc='#359B73', lw=1.)
  768. # We flip the y-axis of the rightward contributions for better visualization
  769. subfig_contributions_right.draw_line(np.arange(len(mot_right)), -mot_right, lc='#359B73', lw=1.)
  770. subfig_contributions_left.draw_line(np.arange(len(ph_left)), ph_left, lc='#E69F00', lw=1.)
  771. subfig_contributions_right.draw_line(np.arange(len(ph_right)), -ph_right, lc='#E69F00', lw=1.)
  772. # Note that the leftward change detectors feeds into the right multifeature integrator and is therefore flipped.
  773. subfig_contributions_right.draw_line(np.arange(len(diff_left)), -diff_left, lc='#D55E00', lw=1.)
  774. subfig_contributions_left.draw_line(np.arange(len(diff_right)), diff_right, lc='#D55E00', lw=1.)
  775. return
  776. def sub_plot_temp_dyn_data(path_to_analysed_data, subfigs):
  777. '''
  778. This function plots the behavior curves over time.
  779. This is related to figure 2a-d
  780. :param path_to_analysed_data: the path to the combined dataframe for figure 2a-d. Each row in this dataframe contains the percentage left within a rolling window.
  781. :param subfigs: list of subfigures to plot the decision curves over time.
  782. '''
  783. # Load the data
  784. analysed_df = pd.read_hdf(path_to_analysed_data)
  785. # Looping over the four stimulus-types (only-motion, only-luminance, congruent, and conflicting).
  786. stim_names = ['Mot', 'Lumi', 'Same', 'Oppo']
  787. plot_names = ['only-motion (M)', 'only-luminance (L)', 'congruent (M=L)', 'conflicting (M\u2260L)']
  788. for st, pl_name, plot in zip(stim_names, plot_names, subfigs):
  789. # Select and then plot the data of the current stimulus with white background during pre- and post stim.
  790. stim = f'{st} W'
  791. stim_df = analysed_df.xs(stim, level='stimulus_name')
  792. n_fish = len(stim_df.index.unique('experiment_ID'))
  793. grouped_df = stim_df.groupby('window_time')
  794. mean = grouped_df['percentage_left'].mean()[0.5:]
  795. std = grouped_df['percentage_left'].std()[0.5:]
  796. sem = std / np.sqrt(n_fish)
  797. binned_time = mean.index.unique('window_time')
  798. plot.draw_line(x=binned_time, y=mean, yerr=sem, lc='#676767', eafc='#989898', eaalpha=1.0, lw=1, ealw=1, eaec='#989898')
  799. # Select and then plot the data of the current stimulus with black background during pre- and post stim.
  800. stim = f'{st} B'
  801. stim_df = analysed_df.xs(stim, level='stimulus_name')
  802. n_fish = len(stim_df.index.unique('experiment_ID'))
  803. grouped_df = stim_df.groupby('window_time')
  804. mean = grouped_df['percentage_left'].mean()[0.5:]
  805. std = grouped_df['percentage_left'].std()[0.5:]
  806. sem = std / np.sqrt(n_fish)
  807. binned_time = mean.index.unique('window_time')
  808. plot.draw_line(x=binned_time, y=mean, yerr=sem, lc='k', eafc='#404040', alpha=0.5, eaalpha=0.5, lw=1, ealw=1, eaec='#404040')
  809. plot.draw_text(12.5, 1.25, f'{pl_name}')
  810. subfigs[-1].draw_line([20, 25], [0.26, 0.26], lc='k')
  811. subfigs[-1].draw_text(22.5, 0.20, '5s')
  812. return
  813. def sub_plot_modelling_example_traces(plot_model_ADD_example, plot_model_ADD_only_example, plot_data_example, path_to_folders):
  814. '''
  815. This function plots the example traces of the multifeature and unifeature fitting strategies, as well as the behavioral data.
  816. This is related to figure 2g.
  817. :param plot_model_ADD_example: Subfigure to plot the multifeature fitted model.
  818. :param plot_model_ADD_only_example: Subfigure to plot the unifeature fiteed model.
  819. :param plot_data_example: Subfigure to plot the fish data.
  820. :param path_to_folders: path to folder containing all experiment folders.
  821. '''
  822. # We define the multifeature training stimuli.
  823. folder_names_half = ['converted_phototaxis_dotmotion_integration_simultaneous_white', # Simultaneous
  824. 'phototaxis_dotmotion_white_Sep', # White
  825. 'phototaxis_dotmotion_simultaneous_low_Sep', # Simultaneous low
  826. 'converted_phototaxis_dotmotion_integration_peppersalt', # peppersalt
  827. 'converted_phototaxis_dotmotion_integration_long'] # long
  828. stim_len_timepoints_half = np.array([250, 300, 450, 350, 600])
  829. stim_names_half = ['Motion', 'Photo', 'Same', 'Oppo']
  830. # We define the unifeature training stimuli.
  831. folder_names_onlyML = ['converted_phototaxis_dotmotion_integration_simultaneous_white', # Simultaneous
  832. 'phototaxis_dotmotion_simultaneous_low_Sep', # Simultaneous low
  833. 'converted_phototaxis_dotmotion_integration_peppersalt', # Peppersalt
  834. 'phototaxis_dotmotion_reverse_Sep', # Reverse
  835. 'phototaxis_dotmotion_white_Sep', # White
  836. 'converted_phototaxis_dotmotion_integration_long', # Long
  837. 'beh_simultaneous_blackwhite_black', # Simultaneous black
  838. 'beh_simultaneous_blackwhite_white', # Simultaneous white
  839. 'phototaxis_dotmotion_integration_halfoverlap', # Halfoverlap
  840. 'beh_simultaneous_10only'] # Simultaneous verylow
  841. stim_len_timepoints_onlyML = np.array([250, 450, 350, 300, 300, 600, 250, 250, 350, 250])
  842. stim_names_onlyML = ['Motion', 'Photo',]
  843. # We define the common testing stimuli to make sure neither model is trained on this data.
  844. folder_names_test = ['beh_simultaneous_blackwhite_white', # simultaneous white
  845. 'phototaxis_dotmotion_reverse_Sep', # reverse
  846. 'beh_simultaneous_blackwhite_black', # simultaneous black
  847. 'phototaxis_dotmotion_integration_halfoverlap', # Halfoverlap
  848. 'beh_simultaneous_10only'] #simultaneous very low
  849. stim_len_timepoints_test = np.array([250, 300, 250, 350, 250])
  850. stim_names_test = ['Same', 'Oppo']
  851. # We hardcoded the random stimulus order to align with the stimulus cartoons.
  852. testing_folder_ids = np.array([0, 1, 2, 3, 4, 3, 1, 0, 4, 2])
  853. testing_stim_ids = np.array([1, 0, 0, 1, 1, 0, 1, 0, 0, 1])
  854. total_time = 450 + np.sum(stim_len_timepoints_test[testing_folder_ids.astype(int)])
  855. print('total time example traces is:, ', total_time)
  856. # Training the multifeature fitted model
  857. model_params, model_train_mse, _ = train_model_full(model_func=avg_mot_lumi_change,
  858. folder_names=folder_names_half,
  859. stim_len_timepoints=stim_len_timepoints_half,
  860. stim_names=stim_names_half,
  861. path_to_folders=path_to_folders,
  862. subfig=None,
  863. train_loops=1)
  864. model_params = np.array(model_params).reshape(-1, 7)
  865. # Testing the multifeature fitted model; Note that in this case there is only 1 set of parameters, because we trained n=1 train_loops.
  866. model_test_mse = []
  867. for params in model_params:
  868. model_test_mse = np.append(model_test_mse, test_model_once(model_func=avg_mot_lumi_change,
  869. params=params,
  870. folder_names=folder_names_test,
  871. stim_len_timepoints=stim_len_timepoints_test,
  872. stim_names=stim_names_test,
  873. path_to_folders=path_to_folders,
  874. subfig_data=None,
  875. subfig_model=plot_model_ADD_example,
  876. training_folder_ids=testing_folder_ids,
  877. pick_stim_ids=testing_stim_ids,
  878. pl_color='#808080'))
  879. # Training the unifeature fitted model
  880. model_params, model_train_mse, _ = train_model_full(model_func=avg_mot_lumi_change,
  881. folder_names=folder_names_onlyML,
  882. stim_len_timepoints=stim_len_timepoints_onlyML,
  883. stim_names=stim_names_onlyML,
  884. path_to_folders=path_to_folders,
  885. subfig=None,
  886. train_loops=1)
  887. model_params = np.array(model_params).reshape(-1, 7)
  888. # Testing the unifeature fitted model; Note that in this case there is only 1 set of parameters, because we trained n=1 train_loops.
  889. # The fish data will also be plotted in this, since subfig_data in the test_model_once function is not None.
  890. model_test_mse = []
  891. for params in model_params:
  892. model_test_mse = np.append(model_test_mse, test_model_once(model_func=avg_mot_lumi_change,
  893. params=params,
  894. folder_names=folder_names_test,
  895. stim_len_timepoints=stim_len_timepoints_test,
  896. stim_names=stim_names_test,
  897. path_to_folders=path_to_folders,
  898. subfig_data=plot_data_example,
  899. subfig_model=plot_model_ADD_only_example,
  900. training_folder_ids=testing_folder_ids,
  901. pick_stim_ids=testing_stim_ids,
  902. pl_color='cyan'))
  903. return
  904. def sub_plot_example_traces_model_fit(path_to_analysed_data, subfiga, subfigb, subfigcs, subfigds, subfiges, subfigfs, path_to_folders):
  905. '''
  906. This figure plots the data and model fit of the decision-curves over time, as well as the node contributions.
  907. This is related to Fig. S2b,c.
  908. :param path_to_analysed_data: the path to the combined dataframe for figure 2a-d. Each row in this dataframe contains the percentage left within a rolling window.
  909. :param subfiga: Subfigure containing the decision curves over time for stimuli with a white-background pre- and post-stimulus.
  910. :param subfigb: Subfigure containing the decision curves over time for stimuli with a black-background pre- and post-stimulus.
  911. :param subfigcs: List of subfigures containing the leftward node contributions to stimuli with a white-background pre- and post-stimulus.
  912. :param subfigds: List of subfigures containing the rightward node contributions to stimuli with a white-background pre- and post-stimulus.
  913. :param subfiges: List of subfigures containing the leftward node contributions to stimuli with a black-background pre- and post-stimulus.
  914. :param subfigfs: List of subfigures containing the rightward node contributions to stimuli with a black-background pre- and post-stimulus.
  915. :param path_to_folders: path to folder containing all experiment folders.
  916. '''
  917. # These model parameters are printed by sub_plot_modelling_overview_mse_tau_w, to save time in creating this figure we hard-coded them here.
  918. model_params = [8, 52, 13, 11, 3.9, 0.9, 4]
  919. # Add a white line to highlight 50%, or the decision-baseline.
  920. subfiga.draw_line([0, 1150], [50, 50], lc='w', lw=1.5)
  921. subfigb.draw_line([0, 1150], [50, 50], lc='w', lw=1.5)
  922. # Load the data.
  923. analysed_df = pd.read_hdf(path_to_analysed_data)
  924. # The stimulus names relate to only-motion, only-luminance, congruent, conflicting.
  925. stim_names = ['Mot', 'Lumi', 'Same', 'Oppo']
  926. # The x-starts are used to sequentially plot all stimuli within the same subfigure.
  927. x_starts = [0, 300, 600, 900, 1200, 1500, 1800, 2100]
  928. for st, x_start in zip(stim_names, x_starts):
  929. # The W indicates the white background pre- and post stimulus.
  930. stim = f'{st} W'
  931. stim_df = analysed_df.xs(stim, level='stimulus_name')
  932. n_fish = len(stim_df.index.unique('experiment_ID'))
  933. grouped_df = stim_df.groupby('window_time')
  934. # We extract the mean and sem decision curves. We ignore the first 0.5 second of data, since there the windows are not complete and contain less data causing the percentages to fluctuate.
  935. mean = grouped_df['percentage_left'].mean()[0.5:]
  936. std = grouped_df['percentage_left'].std()[0.5:]
  937. sem = std / np.sqrt(n_fish)
  938. binned_time = mean.index.unique('window_time') * 10 + x_start
  939. subfiga.draw_line(x=binned_time, y=mean*100, yerr=sem*100, lc='#676767', eafc='#989898', eaalpha=1.0, lw=1, ealw=1, eaec='#989898')
  940. # The B the black background pre- and post stimulus.
  941. stim = f'{st} B'
  942. stim_df = analysed_df.xs(stim, level='stimulus_name')
  943. n_fish = len(stim_df.index.unique('experiment_ID'))
  944. grouped_df = stim_df.groupby('window_time')
  945. # We extract the mean and sem decision curves. We ignore the first 0.5 second of data, since there the windows are not complete and contain less data causing the percentages to fluctuate.
  946. mean = grouped_df['percentage_left'].mean()[0.5:]
  947. std = grouped_df['percentage_left'].std()[0.5:]
  948. sem = std / np.sqrt(n_fish)
  949. binned_time = mean.index.unique('window_time') * 10 + x_start
  950. subfigb.draw_line(x=binned_time, y=mean*100, yerr=sem*100, lc='k', eafc='#404040', alpha=0.5, eaalpha=0.5, lw=1, ealw=1, eaec='#404040')
  951. # Add the model fit to the plot with white background
  952. folder_names_test = ['beh_simultaneous_blackwhite_white', ]
  953. stim_len_timepoints_test = np.array([250])
  954. stim_names_test = ['Motion', 'Photo', 'Same', 'Oppo']
  955. testing_folder_ids = np.array([0, 0, 0, 0])
  956. testing_stim_ids = np.array([0, 1, 2, 3, 0, 1, 2, 3])
  957. test_model_once(model_func=avg_mot_lumi_change,
  958. params=model_params,
  959. folder_names=folder_names_test,
  960. stim_len_timepoints=stim_len_timepoints_test,
  961. stim_names=stim_names_test,
  962. path_to_folders=path_to_folders,
  963. subfig_data=None,
  964. subfig_model=subfiga,
  965. training_folder_ids=testing_folder_ids,
  966. pick_stim_ids=testing_stim_ids,
  967. pl_color='cyan')
  968. # Plot the model contributions of all three main pathways - motion, luminance level, change in luminance- to the white background traces.
  969. for i in range(len(stim_names_test)):
  970. plot_model_contribution_single_experiment(model_func=avg_mot_lumi_change_full_node_outputs,
  971. params=model_params,
  972. folder_name=folder_names_test[0],
  973. stim_len_timepoints=stim_len_timepoints_test[0],
  974. stim_name=stim_names_test[i],
  975. path_to_folders=path_to_folders,
  976. subfig_contributions_left=subfigcs[i],
  977. subfig_contributions_right=subfigds[i])
  978. ## Add the model fit to the plot with black background
  979. folder_names_test = ['beh_simultaneous_blackwhite_black', # simultaneous black
  980. ]
  981. stim_len_timepoints_test = np.array([250])
  982. stim_names_test = ['Motion', 'Photo', 'Same', 'Oppo']
  983. testing_folder_ids = np.array([0, 0, 0, 0])
  984. testing_stim_ids = np.array([0, 1, 2, 3, 0, 1, 2, 3])
  985. test_model_once(model_func=avg_mot_lumi_change,
  986. params=model_params,
  987. folder_names=folder_names_test,
  988. stim_len_timepoints=stim_len_timepoints_test,
  989. stim_names=stim_names_test,
  990. path_to_folders=path_to_folders,
  991. subfig_data=None,
  992. subfig_model=subfigb,
  993. training_folder_ids=testing_folder_ids,
  994. pick_stim_ids=testing_stim_ids,
  995. pl_color='cyan')
  996. # Plot the model contributions of all three main pathways - motion, luminance level, change in luminance- to the black background traces.
  997. for i in range(len(stim_names_test)):
  998. plot_model_contribution_single_experiment(model_func=avg_mot_lumi_change_full_node_outputs,
  999. params=model_params,
  1000. folder_name=folder_names_test[0],
  1001. stim_len_timepoints=stim_len_timepoints_test[0],
  1002. stim_name=stim_names_test[i],
  1003. path_to_folders=path_to_folders,
  1004. subfig_contributions_left=subfiges[i],
  1005. subfig_contributions_right=subfigfs[i])
  1006. subfigb.draw_line([1100, 1150], [22, 22], lc='k')
  1007. subfigb.draw_text(1125, 15, '5s')
  1008. subfigfs[-1].draw_line([200, 250], [-4, -4], lc='k')
  1009. subfigfs[-1].draw_text(225, -5, '5s')
  1010. return
  1011. def sub_plot_modelling_overview_mse_tau_w(n_training_rounds, subfig_mse_btm, subfig_mse_top, subfig_mse_sup, subfig_tau, subfig_w, path_to_folders):
  1012. '''
  1013. This function plots the full overview of the model performance across multifeature and unifeature training/testing strategies, the fitted timeconstants and the fitted weights.
  1014. This is related to Fig. 2h,i,j and Fig. S2d.
  1015. :param n_training_rounds: Integer number of training iterations.
  1016. :param subfig_mse_btm: Subfigure of the mean-squared-error of all model and training alterantives. Up to MSE=75 (the y-axis is split to accommodate for the very high error of the silenced motion-model).
  1017. :param subfig_mse_top: Subfigure of the mean-squared-error of all model and training alterantives. Up from MSE=75 (the y-axis is split to accommodate for the very high error of the silenced motion-model).
  1018. :param subfig_mse_sup: Subfigure of the mean-squared error of alternative models in the supplemental information.
  1019. :param subfig_tau: Subfigure of the fitted timeconstant distributions
  1020. :param subfig_w: Subfigure of the fitted weight distributions.
  1021. :param path_to_folders: path to folder containing all experiment folders.
  1022. :return:
  1023. '''
  1024. # We define the multifeature training stimuli.
  1025. folder_names_half = ['converted_phototaxis_dotmotion_integration_simultaneous_white', # Simultaneous
  1026. 'phototaxis_dotmotion_white_Sep', # White
  1027. 'phototaxis_dotmotion_simultaneous_low_Sep', # simul low
  1028. 'converted_phototaxis_dotmotion_integration_peppersalt', # peppersalt
  1029. 'converted_phototaxis_dotmotion_integration_long'] # long
  1030. stim_len_timepoints_half = np.array([250, 300, 450, 350, 600])
  1031. stim_names_half = ['Motion', 'Photo', 'Same', 'Oppo']
  1032. # We define the unifeature training stimuli.
  1033. folder_names_onlyML = ['converted_phototaxis_dotmotion_integration_simultaneous_white', # Simultaneous
  1034. 'phototaxis_dotmotion_simultaneous_low_Sep', # Simultaneous low
  1035. 'converted_phototaxis_dotmotion_integration_peppersalt', # Peppersalt
  1036. 'phototaxis_dotmotion_reverse_Sep', # Reverse
  1037. 'phototaxis_dotmotion_white_Sep', # White
  1038. 'converted_phototaxis_dotmotion_integration_long', # Long
  1039. 'beh_simultaneous_blackwhite_black', # Simultaneous black
  1040. 'beh_simultaneous_blackwhite_white', # Simultaneous white
  1041. 'phototaxis_dotmotion_integration_halfoverlap', # Halfoverlap
  1042. 'beh_simultaneous_10only'] # Simultaneous verylow
  1043. stim_len_timepoints_onlyML = np.array([250, 450, 350, 300, 300, 600, 250, 250, 350, 250])
  1044. stim_names_onlyML = ['Motion', 'Photo', ]
  1045. # We define the multifeature testing stimuli.
  1046. folder_names_half_test = ['beh_simultaneous_blackwhite_white', # simultaneous white
  1047. 'phototaxis_dotmotion_reverse_Sep', # Reverse
  1048. 'beh_simultaneous_blackwhite_black', # simultaneous black
  1049. 'phototaxis_dotmotion_integration_halfoverlap', # Halfoverlap
  1050. 'beh_simultaneous_10only'] #simulataneous very low
  1051. stim_len_timepoints_half_test = np.array([250, 300, 250, 350, 250])
  1052. stim_names_half_test = ['Motion', 'Photo', 'Same', 'Oppo']
  1053. # We define the unifeature testing stimuli.
  1054. folder_names_onlyML_test = ['converted_phototaxis_dotmotion_integration_simultaneous_white', # Simultaneous
  1055. 'phototaxis_dotmotion_simultaneous_low_Sep', # Simultaneous low
  1056. 'converted_phototaxis_dotmotion_integration_peppersalt', # Peppersalt
  1057. 'phototaxis_dotmotion_reverse_Sep', # Reverse
  1058. 'phototaxis_dotmotion_white_Sep', # White
  1059. 'converted_phototaxis_dotmotion_integration_long', # Long
  1060. 'beh_simultaneous_blackwhite_black', # Simultaneous black
  1061. 'beh_simultaneous_blackwhite_white', # Simultaneous white
  1062. 'phototaxis_dotmotion_integration_halfoverlap', # Halfoverlap
  1063. 'beh_simultaneous_10only'] # Simultaneous verylow
  1064. stim_len_timepoints_onlyML_test = np.array([250, 450, 350, 300, 300, 600, 250, 250, 350, 250])
  1065. stim_names_onlyML_test = ['Same', 'Oppo', ]
  1066. # Prepare lists to store the model performance of 8 models. For in the main figure: 2x original model with multifeature and unifeature training; 3x one of the pathways silenced. For in the supplemental figure: original, no multifeature integration, mutual inhibtion between motion integrators.
  1067. model_test_mses = [[]] * 8
  1068. print(f'Running model avg_mot_lumi_change multifeature training')
  1069. # Training the original model using multifeature training/testing
  1070. model_params, model_train_mse, all_folder_ids = train_model_full(model_func=avg_mot_lumi_change,
  1071. folder_names=folder_names_half,
  1072. stim_len_timepoints=stim_len_timepoints_half,
  1073. stim_names=stim_names_half,
  1074. path_to_folders=path_to_folders,
  1075. subfig=None,
  1076. train_loops=n_training_rounds)
  1077. # Re-arranging the 7 fitted parameters.
  1078. model_params = np.array(model_params).reshape(-1, 7)
  1079. for param in range(7):
  1080. print('Final param means: ')
  1081. print(f'param {param}: {model_params[:, param]}')
  1082. # Testing the performance of each of the n_trainings_round fits.
  1083. all_folder_ids = np.array(all_folder_ids).reshape(-1, 20)
  1084. for params, folder_ids in zip(model_params, all_folder_ids):
  1085. model_test_mses[0] = np.append(model_test_mses[0],
  1086. test_model_once(model_func=avg_mot_lumi_change, params=params,
  1087. folder_names=folder_names_half_test,
  1088. stim_len_timepoints=stim_len_timepoints_half_test,
  1089. stim_names=stim_names_half_test,
  1090. path_to_folders=path_to_folders,
  1091. subfig_data=None, subfig_model=None,
  1092. training_folder_ids=folder_ids))
  1093. # Plotting the model performance of the original model (note the mse-subfigure has a split y-axes at 75).
  1094. btm_idx = np.array(model_test_mses[0]) < 75
  1095. top_idx = np.array(model_test_mses[0]) >= 75
  1096. if np.sum(btm_idx) > 0:
  1097. subfig_mse_btm.draw_scatter(np.zeros(len(model_test_mses[0]))[btm_idx] + np.random.uniform(-0.4, 0.4, len(model_test_mses[0]))[btm_idx], model_test_mses[0][btm_idx], pc='cadetblue', ec='cadetblue')
  1098. if np.sum(top_idx) > 0:
  1099. subfig_mse_top.draw_scatter(np.zeros(len(model_test_mses[0]))[top_idx] + np.random.uniform(-0.4, 0.4, len(model_test_mses[0]))[top_idx], model_test_mses[0][top_idx], pc='cadetblue', ec='cadetblue')
  1100. if np.nanmedian(model_test_mses[0]) < 75:
  1101. subfig_mse_btm.draw_line([-0.4, 0.4], [np.nanmedian(model_test_mses[0]), np.nanmedian(model_test_mses[0])], lc='k', lw='1')
  1102. else:
  1103. subfig_mse_top.draw_line([-0.4, 0.4], [np.nanmedian(model_test_mses[0]), np.nanmedian(model_test_mses[0])], lc='k', lw='1')
  1104. # Plotting the model parameters and the median value (split into 4 timeconstants and 3 weights).
  1105. for param in range(7):
  1106. if param < 4:
  1107. subfig_tau.draw_scatter(2*param * np.ones(len(model_params[:, param])) + np.random.uniform(-0.4, 0.4, len(model_params[:, param])), model_params[:, param], pc='cadetblue', ec='cadetblue')
  1108. subfig_tau.draw_line([2*param - 0.4, 2*param + 0.4], [np.nanmedian(model_params[:, param]), np.nanmedian(model_params[:, param])], lc='k', lw='1')
  1109. else:
  1110. subfig_w.draw_scatter((2*param - 8) * np.ones(len(model_params[:, param])) + np.random.uniform(-0.4, 0.4, len(model_params[:, param])), model_params[:, param], pc='cadetblue', ec='cadetblue')
  1111. subfig_w.draw_line([2*param - 8 - 0.4, 2*param - 8 + 0.4], [np.nanmedian(model_params[:, param]), np.nanmedian(model_params[:, param])], lc='k', lw='1')
  1112. # Loop over all main-figure alternative models to train them using unifeature training/testing and plot their performance, and only for the original model, parameters.
  1113. for model_counter, (modelfunction, pl_color, param_size) in enumerate(zip(
  1114. [avg_mot_lumi_change, avg_mot_lumi, avg_mot_change, avg_lumi_change],
  1115. ['cyan', '#D55E00', '#E69F00', '#359B73'],
  1116. [7, 5, 5, 5])):
  1117. print(f'Running model {model_counter} unifeature training')
  1118. # Training the model using unifeature training/testing
  1119. model_params, model_train_mse, all_folder_ids = train_model_full(model_func=modelfunction,
  1120. folder_names=folder_names_onlyML,
  1121. stim_len_timepoints=stim_len_timepoints_onlyML,
  1122. stim_names=stim_names_onlyML,
  1123. path_to_folders=path_to_folders,
  1124. subfig=None,
  1125. train_loops=n_training_rounds)
  1126. # Re-arranging the 5 or 7 fitted parameters.
  1127. model_params = np.array(model_params).reshape(-1, param_size)
  1128. for param in range(param_size):
  1129. print('Final param means: ')
  1130. print(f'param {param}: {model_params[:, param]}')
  1131. # Testing the performance of each of the n_trainings_round fits.
  1132. all_folder_ids = np.array(all_folder_ids).reshape(-1, 20)
  1133. for params, folder_ids in zip(model_params, all_folder_ids):
  1134. model_test_mses[model_counter+1] = np.append(model_test_mses[model_counter+1],
  1135. test_model_once(model_func=modelfunction, params=params,
  1136. folder_names=folder_names_onlyML_test,
  1137. stim_len_timepoints=stim_len_timepoints_onlyML_test,
  1138. stim_names=stim_names_onlyML_test,
  1139. path_to_folders=path_to_folders,
  1140. subfig_data=None, subfig_model=None,
  1141. training_folder_ids=folder_ids))
  1142. # Plotting the model performance of the model (note the mse-subfigure has a split y-axes at 75).
  1143. btm_idx = np.array(model_test_mses[model_counter+1]) < 75
  1144. top_idx = np.array(model_test_mses[model_counter+1]) >= 75
  1145. if np.sum(btm_idx) > 0:
  1146. subfig_mse_btm.draw_scatter((model_counter + 1) * np.ones(len(model_test_mses[model_counter+1]))[btm_idx] + np.random.uniform(-0.4, 0.4, len(model_test_mses[model_counter+1]))[btm_idx],
  1147. model_test_mses[model_counter+1][btm_idx], pc=pl_color, ec=pl_color)
  1148. if np.sum(top_idx) > 0:
  1149. subfig_mse_top.draw_scatter((model_counter + 1) * np.ones(len(model_test_mses[model_counter+1]))[top_idx] + np.random.uniform(-0.4, 0.4, len(model_test_mses[model_counter+1]))[top_idx],
  1150. model_test_mses[model_counter+1][top_idx], pc=pl_color, ec=pl_color)
  1151. if np.nanmedian(model_test_mses[model_counter+1]) < 75:
  1152. subfig_mse_btm.draw_line([model_counter + 0.6, model_counter + 1.4], [np.nanmedian(model_test_mses[model_counter+1]), np.nanmedian(model_test_mses[model_counter+1])], lc='k', lw='1')
  1153. else:
  1154. subfig_mse_top.draw_line([model_counter + 0.6, model_counter + 1.4], [np.nanmedian(model_test_mses[model_counter+1]), np.nanmedian(model_test_mses[model_counter+1])], lc='k', lw='1')
  1155. # Only for the original model: Plotting the model parameters and the median value (split into 4 timeconstants and 3 weights).
  1156. if model_counter == 0:
  1157. for param in range(param_size):
  1158. if param < 4:
  1159. subfig_tau.draw_scatter((2*param+1) * np.ones(len(model_params[:, param])) + np.random.uniform(-0.4, 0.4, len(model_params[:, param])), model_params[:, param], pc=pl_color, ec=pl_color)
  1160. subfig_tau.draw_line([2*param+1-0.4, 2*param+1+0.4], [np.nanmedian(model_params[:, param]), np.nanmedian(model_params[:, param])], lc='k', lw='1')
  1161. print(f'Param {param}: {np.nanmedian(model_params[:, param])}')
  1162. else:
  1163. subfig_w.draw_scatter((2*param - 7) * np.ones(len(model_params[:, param])) + np.random.uniform(-0.4, 0.4, len(model_params[:, param])), model_params[:, param], pc=pl_color, ec=pl_color)
  1164. subfig_w.draw_line([2*param-7-0.4, 2*param-7+0.4], [np.nanmedian(model_params[:, param]), np.nanmedian(model_params[:, param])], lc='k', lw='1')
  1165. print(f'Param {param}: {np.nanmedian(model_params[:, param])}')
  1166. # Loop over all supplemental-figure alternative models to train them using unifeature training/testing and plot their performance.
  1167. for model_counter, (modelfunction, pl_color, param_size) in enumerate(zip(
  1168. [avg_mot_lumi_change, avg_mot_lumi_change_withmotinhib],
  1169. ['cyan', 'green'],
  1170. [7, 8])):
  1171. print(f'Running model {model_counter} unifeature training')
  1172. # Training the model using unifeature training/testing
  1173. model_params, model_train_mse, all_folder_ids = train_model_full(model_func=modelfunction,
  1174. folder_names=folder_names_onlyML,
  1175. stim_len_timepoints=stim_len_timepoints_onlyML,
  1176. stim_names=stim_names_onlyML,
  1177. path_to_folders=path_to_folders,
  1178. subfig=None,
  1179. train_loops=n_training_rounds)
  1180. # Re-arranging the 6, 7 or 8 fitted parameters.
  1181. model_params = np.array(model_params).reshape(-1, param_size)
  1182. for param in range(param_size):
  1183. print('Final param means: ')
  1184. print(f'param {param}: {model_params[:, param]}')
  1185. # Testing the performance of each of the n_trainings_round fits.
  1186. all_folder_ids = np.array(all_folder_ids).reshape(-1, 20)
  1187. for params, folder_ids in zip(model_params, all_folder_ids):
  1188. model_test_mses[model_counter+5] = np.append(model_test_mses[model_counter+5],
  1189. test_model_once(model_func=modelfunction, params=params,
  1190. folder_names=folder_names_onlyML_test,
  1191. stim_len_timepoints=stim_len_timepoints_onlyML_test,
  1192. stim_names=stim_names_onlyML_test,
  1193. path_to_folders=path_to_folders,
  1194. subfig_data=None, subfig_model=None,
  1195. training_folder_ids=folder_ids))
  1196. # Plotting the model performance of the model.
  1197. subfig_mse_sup.draw_scatter((model_counter + 1) * np.ones(len(model_test_mses[model_counter+5])) + np.random.uniform(-0.4, 0.4, len(model_test_mses[model_counter+5])),
  1198. model_test_mses[model_counter+5], pc=pl_color, ec=pl_color)
  1199. subfig_mse_sup.draw_line([model_counter + 0.6, model_counter + 1.4], [np.nanmedian(model_test_mses[model_counter+5]), np.nanmedian(model_test_mses[model_counter+5])], lc='k', lw='1')
  1200. # Do the statistical comparisons between the main-figure model fits using a t-test. The pval is compared to a Bonferonni corrected threshold.
  1201. for model1, model2, h in zip([0, 1, 1, 1],
  1202. [1, 2, 3, 4],
  1203. [80, 110, 140, 180]):
  1204. subfig_mse_top.draw_line([model1, model1, model2, model2], [h-1, h, h, h-1], lc='k')
  1205. _, pval = ttest_ind(model_test_mses[model1], model_test_mses[model2])
  1206. if pval < 0.001/4:
  1207. subfig_mse_top.draw_text((model1+model2)/2, h+2, '***')
  1208. elif pval < 0.01/4:
  1209. subfig_mse_top.draw_text((model1+model2)/2, h+2, '**')
  1210. elif pval < 0.05/4:
  1211. subfig_mse_top.draw_text((model1+model2)/2, h+2, '*')
  1212. else:
  1213. subfig_mse_top.draw_text((model1+model2)/2, h+4, 'ns')
  1214. effect_size = cohens_d(model_test_mses[model1], model_test_mses[model2])
  1215. print(f'model1 {model1} vs model2 {model2}, pval {pval}: Cohen D effect size {effect_size}')
  1216. # Do the statistical comparisons between the supplemental-figure model fits using a t-test. The pval is compared to a Bonferonni corrected threshold.
  1217. for model1, model2, h in zip([5,],
  1218. [6,],
  1219. [60,]):
  1220. subfig_mse_sup.draw_line([model1 - 4, model1 - 4, model2 - 4, model2 - 4], [h-1, h, h, h-1], lc='k')
  1221. _, pval = ttest_ind(model_test_mses[model1], model_test_mses[model2])
  1222. if pval < 0.001/2:
  1223. subfig_mse_sup.draw_text((model1-4+model2-4)/2, h+2, '***')
  1224. elif pval < 0.01/2:
  1225. subfig_mse_sup.draw_text((model1-4+model2-4)/2, h+2, '**')
  1226. elif pval < 0.05/2:
  1227. subfig_mse_sup.draw_text((model1-4+model2-4)/2, h+2, '*')
  1228. else:
  1229. subfig_mse_sup.draw_text((model1-4+model2-4)/2, h+4, 'ns')
  1230. effect_size = cohens_d(model_test_mses[model1], model_test_mses[model2])
  1231. print(f'model1 {model1} vs model2 {model2}, pval {pval}: Cohen D effect size {effect_size}')
  1232. return
  1233. if __name__ == '__main__':
  1234. # A general note for this file: We changed the names of the model nodes later for the paper. In this code we use moslty the old names:
  1235. # multifeature integrator used to be drive, luminance change detector used to be diff, luminance level integrator used to be lumi, luminance increase detector used to be bright, and luminance decrease detector used to be dark. Motion integrator was always motion.
  1236. # In the stimuli, lateral luminance cue is often referred to as Photo (from phototaxis stimulus), congruent stimuli are referred to as Same, conflicting stimuli as Oppo.
  1237. # Provide the path to save the figures.
  1238. fig_save_path = 'C:/users/katja/Desktop/fig_2.pdf'
  1239. supfig_save_path = 'C:/users/katja/Desktop/fig_S2.pdf'
  1240. # Provide the path to the figure_2 folder.
  1241. fig_2_folder_path = r'Z:\Bahl lab member directories\Katja\paper_data\figure_2'
  1242. # Get the paths to the combined dataframes for figure 2a-d.
  1243. path_to_analysed = Path(fr'{fig_2_folder_path}\data_analysed.hdf5')
  1244. # Get the path to the folder containing all experiment folders.
  1245. path_to_folders = Path(fr'{fig_2_folder_path}')
  1246. # Here we define the figures and subpanel outlines (e.g. the limits, ticks and labels of the axes) beloning to figure 2 and S2.
  1247. fig = Figure(fig_width=18, fig_height=17)
  1248. subfig = Figure(fig_width=18, fig_height=8)
  1249. # Fig. 2a
  1250. plot_motion = fig.create_plot(xpos=1, ypos=13.5, plot_height=2, plot_width=3.5, errorbar_area=True,
  1251. xmin=0, xmax=25, yl='left swims (%)', ymin=0.25,
  1252. ymax=0.9, yticks=[0.30, 0.50, 0.70, 0.90], yticklabels=['30', '50', '70', '90'], hlines=[0.5],
  1253. helper_lines_lc='w', helper_lines_dashes=(2, 0), helper_lines_lw=1, vspans=[[5, 20, 'lightgray', 1.0], ])
  1254. # Fig. 2b
  1255. plot_lumi = fig.create_plot(xpos=5, ypos=13.5, plot_height=2, plot_width=3.5, errorbar_area=True,
  1256. xmin=0, xmax=25, ymin=0.25,
  1257. ymax=0.9, yticks=[0.30, 0.50, 0.70, 0.90], yticklabels=['', '', '', ''], hlines=[0.5],
  1258. helper_lines_lc='w', helper_lines_dashes=(2, 0), helper_lines_lw=1, vspans=[[5, 20, 'lightgray', 1.0], ])
  1259. # Fig. 2c
  1260. plot_same = fig.create_plot(xpos=9, ypos=13.5, plot_height=2, plot_width=3.5, errorbar_area=True,
  1261. xmin=0, xmax=25, ymin=0.25,
  1262. ymax=0.9, yticks=[0.30, 0.50, 0.70, 0.90], yticklabels=['', '', '', ''], hlines=[0.5],
  1263. helper_lines_lc='w', helper_lines_dashes=(2, 0), helper_lines_lw=1, vspans=[[5, 20, 'lightgray', 1.0], ])
  1264. # Fig. 2d
  1265. plot_oppo = fig.create_plot(xpos=13, ypos=13.5, plot_height=2, plot_width=3.5, errorbar_area=True,
  1266. xmin=0, xmax=25, ymin=0.25,
  1267. ymax=0.9, yticks=[0.30, 0.50, 0.70, 0.90], yticklabels=['', '', '', ''], hlines=[0.5],
  1268. helper_lines_lc='w', helper_lines_dashes=(2, 0), helper_lines_lw=1, vspans=[[5, 20, 'lightgray', 1.0], ])
  1269. # Fig. 2g
  1270. plot_model_ADD_example = fig.create_plot(xpos=1, ypos=9, plot_height=1.4, plot_width=7, errorbar_area=True,
  1271. xmin=0, xmax=3250, ymin=20, ymax=90, yticks=[50, 75], yl='l (%)')
  1272. plot_model_ADD_only_example = fig.create_plot(xpos=1, ypos=8., plot_height=1.4, plot_width=7, errorbar_area=True,
  1273. xmin=0, xmax=3250, ymin=20, ymax=90, yticks=[50, 75], yl='left swims (%)')
  1274. plot_model_data_example = fig.create_plot(xpos=1, ypos=6.5, plot_height=1.4, plot_width=7, errorbar_area=True,
  1275. xmin=0, xmax=3250, ymin=20, ymax=90, yticks=[25, 50, 75], yl='left swims (%)')
  1276. # Fig. S2b
  1277. plot_white_example = subfig.create_plot(xpos=1, ypos=2.5, plot_height=1.4, plot_width=6.5, errorbar_area=True,
  1278. xmin=0, xmax=1150, ymin=20, ymax=90, yticks=[25, 50, 75], yl='left swims (%)',
  1279. vspans=[[50, 200, 'lightgray', 1.0], [350, 500, 'lightgray', 1.0], [650, 800, 'lightgray', 1.0], [950, 1100, 'lightgray', 1.0]])
  1280. plot_black_example = subfig.create_plot(xpos=8.5, ypos=2.5, plot_height=1.4, plot_width=6.5, errorbar_area=True,
  1281. xmin=0, xmax=1150, ymin=20, ymax=90, yticks=[25, 50, 75],
  1282. vspans=[[50, 200, 'lightgray', 1.0], [350, 500, 'lightgray', 1.0], [650, 800, 'lightgray', 1.0], [950, 1100, 'lightgray', 1.0]])
  1283. # Fig. S2c
  1284. plot_contributions_examples_white_left = [[]] * 4
  1285. plot_contributions_examples_white_right = [[]] * 4
  1286. plot_contributions_examples_black_left = [[]] * 4
  1287. plot_contributions_examples_black_right = [[]] * 4
  1288. for i in range(4):
  1289. if i == 0:
  1290. plot_contributions_examples_white_left[i] = subfig.create_plot(xpos=1 + 1.7*i, ypos=1.3, plot_height=0.7, plot_width=1.4, errorbar_area=True,
  1291. xmin=0, xmax=250, ymin=-0.2, ymax=4.5, yticks=[0, 2, 4],
  1292. yticklabels=['0', '2', '4'], yl='node activity (au)',
  1293. vspans=[[50, 200, 'lightgray', 1.0], ])
  1294. plot_contributions_examples_white_right[i] = subfig.create_plot(xpos=1 + 1.7*i, ypos=0.5, plot_height=0.7, plot_width=1.4, errorbar_area=True,
  1295. xmin=0, xmax=250, ymin=-4.5, ymax=0.2, yticks=[-4, -2, 0],
  1296. yticklabels=['4', '2', '0'],
  1297. vspans=[[50, 200, 'lightgray', 1.0], ])
  1298. plot_contributions_examples_black_left[i] = subfig.create_plot(xpos=8.5 + 1.7*i, ypos=1.3, plot_height=0.7, plot_width=1.4, errorbar_area=True,
  1299. xmin=0, xmax=250, ymin=-0.2, ymax=4.5, yticks=[0, 2, 4],
  1300. yticklabels=['0', '2', '4'],
  1301. vspans=[[50, 200, 'lightgray', 1.0], ])
  1302. plot_contributions_examples_black_right[i] = subfig.create_plot(xpos=8.5 + 1.7*i, ypos=0.5, plot_height=0.7, plot_width=1.4, errorbar_area=True,
  1303. xmin=0, xmax=250, ymin=-4.5, ymax=0.2, yticks=[-4, -2, 0],
  1304. yticklabels=['4', '2', '0'],
  1305. vspans=[[50, 200, 'lightgray', 1.0], ])
  1306. else:
  1307. plot_contributions_examples_white_left[i] = subfig.create_plot(xpos=1 + 1.7*i, ypos=1.3, plot_height=0.7, plot_width=1.4, errorbar_area=True,
  1308. xmin=0, xmax=250, ymin=-0.2, ymax=4.5,
  1309. vspans=[[50, 200, 'lightgray', 1.0], ])
  1310. plot_contributions_examples_white_right[i] = subfig.create_plot(xpos=1 + 1.7*i, ypos=0.5, plot_height=0.7, plot_width=1.4, errorbar_area=True,
  1311. xmin=0, xmax=250, ymin=-4.5, ymax=0.2,
  1312. vspans=[[50, 200, 'lightgray', 1.0], ])
  1313. plot_contributions_examples_black_left[i] = subfig.create_plot(xpos=8.5 + 1.7*i, ypos=1.3, plot_height=0.7, plot_width=1.4, errorbar_area=True,
  1314. xmin=0, xmax=250, ymin=-0.2, ymax=4.5,
  1315. vspans=[[50, 200, 'lightgray', 1.0], ])
  1316. plot_contributions_examples_black_right[i] = subfig.create_plot(xpos=8.5 + 1.7*i, ypos=0.5, plot_height=0.7, plot_width=1.4, errorbar_area=True,
  1317. xmin=0, xmax=250, ymin=-4.5, ymax=0.2,
  1318. vspans=[[50, 200, 'lightgray', 1.0], ])
  1319. # Fig. 2h
  1320. plot_mse_overview_bottom = fig.create_plot(xpos=9, ypos=7.5, plot_height=1.25, plot_width=2.25,
  1321. xmin=-1, xmax=5, ymin=0, ymax=75, yticks=[10, 20, 30, 40, 50], yl='MSE',
  1322. xticks=[0, 1, 2, 3, 4], xticklabels=['multifeature\ntesting', 'unifeature\ntesting', 'X lumi change',
  1323. 'X lumi level', 'X motion'], xticklabels_rotation=90)
  1324. plot_mse_overview_top = fig.create_plot(xpos=9, ypos=8.75, plot_height=1.25, plot_width=2.25,
  1325. xmin=-1, xmax=5, ymin=75, ymax=210, yticks=[100, 150, 200])
  1326. # Fig. S2d
  1327. plot_mse_overview_sup = subfig.create_plot(xpos=16, ypos=2.5, plot_height=1.6, plot_width=2,
  1328. xmin=0, xmax=3, ymin=0, ymax=65, yticks=[10, 20, 30, 40, 50], yl='MSE',
  1329. xticks=[1, 3], xticklabels=['unifeature\ntesting', '+ Mot inhi'], xticklabels_rotation=90)
  1330. # Fig. 2i
  1331. plot_timeconstants_overview = fig.create_plot(xpos=12, ypos=7.5, plot_height=2.5, plot_width=2.25,
  1332. xmin=-1, xmax=8, ymin=0, ymax=31, yticks=[0, 5, 10, 15, 20, 25, 30],
  1333. yticklabels=['0', '0.5', '1.0', '1.5', '2.0', '2.5', '3.0'], yl='time constant (s)',
  1334. xticks=[0.5, 2.5, 4.5, 6.5], xticklabels=['\u03C4 motion', '\u03C4 lumi level', '\u03C4 lumi change', '\u03C4 multifeature'],
  1335. xticklabels_rotation=90)
  1336. # Fig. 2j
  1337. plot_weights_overview = fig.create_plot(xpos=15, ypos=7.5, plot_height=2.5, plot_width=2.25,
  1338. xmin=-1, xmax=6, ymin=0, ymax=3.5, yticks=[0, 1, 2, 3], yl='weight',
  1339. xticks=[0.5, 2.5, 4.5], xticklabels=['w motion', 'w lumi level', 'w lumi change'],
  1340. xticklabels_rotation=90)
  1341. print('Plot supplemental figure example data with model fit and node contributions.')
  1342. sub_plot_example_traces_model_fit(path_to_analysed, plot_white_example, plot_black_example, plot_contributions_examples_white_left, plot_contributions_examples_white_right, plot_contributions_examples_black_left, plot_contributions_examples_black_right, path_to_folders)
  1343. print('Plotting temporal data')
  1344. sub_plot_temp_dyn_data(path_to_analysed, [plot_motion, plot_lumi, plot_same, plot_oppo])
  1345. print('Plotting example traces')
  1346. sub_plot_modelling_example_traces(plot_model_ADD_example, plot_model_ADD_only_example, plot_model_data_example, path_to_folders)
  1347. print('Plotting model overview')
  1348. sub_plot_modelling_overview_mse_tau_w(n_training_rounds=25, subfig_mse_btm=plot_mse_overview_bottom, subfig_mse_top=plot_mse_overview_top, subfig_mse_sup=plot_mse_overview_sup, subfig_tau=plot_timeconstants_overview, subfig_w=plot_weights_overview, path_to_folders=path_to_folders)
  1349. fig.save(fig_save_path)
  1350. subfig.save(supfig_save_path)
  1351. # Note that Figure 2e-f and S2a only contain explanatory cartoons without actual data and therefore are not part of this code.

figure2_S2_behavior_tempdynamics.py at commit c321f06, no license · at the source

Overview

  1. Department of Biology, University of Konstanz,Konstanz, Germany
  2. Centre for the Advanced Study of Collective Behaviour,Konstanz, Germany
  3. International Max Planck Research School for Quantitative Behaviour, Ecology and Evolution (IMPRS/QBEE); Max Planck Institute of Animal Behavior,Konstanz, Germany
  4. Present Address: Medical Research Council Laboratory of Molecular Biology,Cambridge, UK
  5. Max Planck Institute for Biological Intelligence,Martinsried, Germany
  6. Max Planck Institute of Animal Behavior,Konstanz, Germany
  7. Zukunftskolleg,Konstanz, Germany
Journal: Nature communications, volume 17, issue 1, article 2024
Dates: received 20 August 2025; accepted 5 February 2026; published online 5 March 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41467-026-69633-4 · PMID 41786700 · PMCID PMC12963418 · OpenAlex W7133646064
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: zebrafish (organism), cognitive (subfield)
Methods: Connectivity, Statistics, Smoothing, state filtering, decompositions, Evoked potentials, Machine learning, Single-unit activity, calcium imaging
Keywords: Neural circuits, Decision, Sensory processing, Motion detection
MeSH: Decision Making*, Rhombencephalon*, Zebrafish*, Animals, Larva, Neurons, Photic Stimulation, Phototaxis (* major topic)
Topic: Zebrafish Biomedical Research Applications (Cell Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Citations: cited by 5 papers (Europe PMC); 89 references in the paper

Abstract

Animals continuously extract and evaluate diverse sensory information from the environment to guide behavior. Yet, how neural circuits integrate multiple, potentially conflicting, inputs remains poorly understood. Here, we use larval zebrafish to address this question, leveraging their robust optomotor response to coherent random dot motion and phototaxis towards light. We demonstrate that animals employ an additive behavioral algorithm of three visual features: motion coherence, luminance level, and changes in luminance. Using brain-wide two-photon imaging, we identify the loci of these computations, with the anterior hindbrain emerging as a multifeature integration hub. Through single-cell neurotransmitter and morphological analyses of functionally identified neurons, we characterize potential connections within and across computational nodes. These experiments reveal three parallel and converging computational pathways, matching our behavioral results. Our study provides a mechanistic brain-wide account of how a vertebrate brain integrates multiple features to drive sensorimotor decisions, bridging behavioral algorithms with their neural implementation.

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

Repository

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

bahl-lab-konstanz/multifeature_integration_paper

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: c321f06836f6fa4a0fb849969854927c75df28bc, 23 February 2026
Languages: Python (12)
Size: 20 files, 12 scripts
Software Heritage: not archived
Found in: DataCite
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (12 files), pandas (9 files), h5py (8 files), Matplotlib (7 files), SciPy (7 files), scikit-learn (2 files), imageio (1 file), Pillow (1 file), seaborn (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
13 files

Code availability

Analysis and model code have been archived and are publicly available in KonData with the identifier 10.48606/6jhypme0jeu49raa.

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:

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

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

Data availability

The behavior and preprocessed imaging data reported in this study are publicly available in KonData with the identifier 10.48606/tus7hs1zv77phbtv. The preprocessed imaging data contains the raw fluorescent traces for each neuron, as well as the averaged imaging stack. The raw imaging data itself is too large to upload to KonData (~ 3TB). This data will be provided upon request without restrictions. We will respond to requests within 10 working days, and shared data will remain available as long as requested.

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

Recorded: type, language, journal, volume, issue, pages, dates, 8 authors, 4 keywords, 8 MeSH terms, 3 funders, 86 references.

Cite

This paper

Slangewal, K., Aimon, S., Capelle, M. Q., Kämpf, F., Naumann, H., Slanchev, K., Baier, H., & Bahl, A. (2026). Visuomotor decision-making through multifeature convergence in the larval zebrafish hindbrain. Nature communications, 17(1), 2024. https://doi.org/10.1038/s41467-026-69633-4

BibTeX

@article{slangewal2026visuomotor,
author = {Slangewal, Katja and Aimon, Sophie and Capelle, Maxim Q. and Kämpf, Florian and Naumann, Heike and Slanchev, Krasimir and Baier, Herwig and Bahl, Armin},
title = {{Visuomotor decision-making through multifeature convergence in the larval zebrafish hindbrain}},
journal = {Nature communications},
year = {2026},
month = mar,
volume = {17},
number = {1},
pages = {2024},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-69633-4},
url = {https://doi.org/10.1038/s41467-026-69633-4},
pmid = {41786700},
pmcid = {PMC12963418}
}

RIS

TY - JOUR
AU - Slangewal, Katja
AU - Aimon, Sophie
AU - Capelle, Maxim Q.
AU - Kämpf, Florian
AU - Naumann, Heike
AU - Slanchev, Krasimir
AU - Baier, Herwig
AU - Bahl, Armin
TI - Visuomotor decision-making through multifeature convergence in the larval zebrafish hindbrain
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/03/05
VL - 17
IS - 1
SP - 2024
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-69633-4
UR - https://doi.org/10.1038/s41467-026-69633-4
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-69633-4",
"type": "article-journal",
"title": "Visuomotor decision-making through multifeature convergence in the larval zebrafish hindbrain",
"container-title": "Nature communications",
"author": [
{
"family": "Slangewal",
"given": "Katja"
},
{
"family": "Aimon",
"given": "Sophie"
},
{
"family": "Capelle",
"given": "Maxim Q."
},
{
"family": "Kämpf",
"given": "Florian"
},
{
"family": "Naumann",
"given": "Heike"
},
{
"family": "Slanchev",
"given": "Krasimir"
},
{
"family": "Baier",
"given": "Herwig"
},
{
"family": "Bahl",
"given": "Armin"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "2024",
"DOI": "10.1038/s41467-026-69633-4",
"PMID": "41786700",
"PMCID": "PMC12963418",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-69633-4",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
5
]
]
}
}

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-76267-z [code]
Modulation of habenula axon terminals supports action-outcome associations in larval zebrafish.
Journal: Nature communications
In common: imageio, Pillow, seaborn, 5 other tools, zebrafish, 11 references
[2] doi:10.1016/j.isci.2026.116190
Behavioral algorithms of ontogenetic switching in larval and juvenile zebrafish phototaxis.
Journal: iScience
In common: zebrafish, 12 references, author Armin Bahl
[3] doi:10.7554/elife.100880 [code]
An applicable and efficient retrograde monosynaptic circuit mapping tool for larval zebrafish.
Journal: eLife
In common: h5py, seaborn, scikit-learn, 4 other tools, zebrafish, 7 references
[4] doi:10.1038/s41467-026-71371-6
Molecular and functional dissection using CaMPARI-seq reveals the neuronal organization for dissociating optic flow-dependent behaviors.
Journal: Nature communications
In common: zebrafish, 10 references
[5] 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: Pillow, seaborn, scikit-learn, 4 other tools, zebrafish, 6 references
[6] 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: h5py, scikit-learn, pandas, 3 other tools, zebrafish, 5 references
[7] doi:10.1038/s41467-026-72222-0 [code]
Eye movement kinematics reveal novel circadian organization of sleep substates.
Journal: Nature communications
In common: zebrafish, 7 references
[8] doi:10.1038/s41592-026-03179-7 [code]
Voltage imaging of neurons distributed across entire brains of larval zebrafish.
Journal: Nature methods
In common: h5py, SciPy, Matplotlib, 1 other tool, zebrafish, 5 references
[9] doi:10.1038/s41467-026-71640-4
Neural temporal scaling accounts for robust hunting behavior across temperatures.
Journal: Nature communications
In common: zebrafish, 7 references
[10] doi:10.1016/j.isci.2026.117136 [code]
Complex pectoral-fin-driven locomotion underlies refined visuomotor behavior in the cichlid &lt;i&gt;Astatotilapia burtoni&lt;/i&gt;.
Journal: iScience
In common: scikit-learn, pandas, SciPy, 2 other tools, zebrafish, 4 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.