OSCR

Indirect pathway neurons in the tail of the striatum regulate inhibitory control over sensory driven behavior.

Code ↔ Paper

7 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 7 matches
  1. [1] § MATERIALS AND METHODS › Neural encoding model ↔ PhotometryAnalysis/kernelGLM.m, lines 52–112 · score 0.68 · withholding boxcar, trialStart, contiguous, lick rate, ridge, intercept
  2. [2] § MATERIALS AND METHODS › Neural encoding model ↔ PhotometryAnalysis/PhotometryHelperFunctions/build_design_matrix.m, lines 139–218 · score 0.67 · photometry sample, licks occurring, response window, boxcar, lick rate, regressor
  3. [3] § MATERIALS AND METHODS › Neural encoding model ↔ PhotometryAnalysis/PhotometryHelperFunctions/trial_browser.m, lines 196–288 · score 0.61 · withholding boxcar, trialStart, lick rate, regressor, intercept, predictors
  4. [4] § MATERIALS AND METHODS › Neural encoding model ↔ PhotometryAnalysis/kernelGLM.m, lines 3875–3930 · score 0.54 · Benjamini Hochberg FDR
  5. [5] § MATERIALS AND METHODS › Photometry analysis ↔ PhotometryAnalysis/kernelGLM.m, lines 3875–3930 · score 0.53 · Benjamini Hochberg, discovery, FDR, Photometry
  6. [6] § MATERIALS AND METHODS › Photometry analysis ↔ photometry_data_prep.m, lines 190–230 · score 0.51 · cubic polynomial, debleached, filtered, fitting, Photometry
  7. [7] § MATERIALS AND METHODS › Neural encoding model ↔ PhotometryAnalysis/kernelGLM.m, lines 52–112 · score 0.51 · kernel windows, boxcar, lick rate, instruction, predictors, event

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

MATLAB · 3,960 lines · 125 KB · no license · 4 matches

  1. %Kernel GLM
  2. %We would like to acknowledge the use of OpenAI’s ChatGPT (versions 4o, 5,
  3. % 5.1, 5.2) as an auxiliary tool for code optimization.
  4. %% Import data
  5. streamsPath = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\EvanStorage\streams.csv";
  6. trialsPath = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\EvanStorage\trials.csv";
  7. sessionsPath = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\EvanStorage\sessions.csv";
  8. licksPath = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\EvanStorage\licks.csv";
  9. signalsPath = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\EvanStorage\signals2.h5";
  10. streams = readtable(streamsPath, "TextType", "string");
  11. trials = readtable(trialsPath, "TextType", "string");
  12. sessions = readtable(sessionsPath, "TextType", "string");
  13. licks = readtable(licksPath, "TextType", "string");
  14. % Exclude Nrxn1a
  15. streams = streams(streams.nrxn ==0,:);
  16. trials = trials(trials.nrxn == 0,:);
  17. sessions = sessions(sessions.nrxn ==0,:);
  18. licks = licks(licks.nrxn ==0,:);
  19. %
  20. % Exclude mistargeted animals
  21. %DATA QUALITY/CLEANING EXCLUSIONS + REASONS:
  22. % Exclude:
  23. % o a2ac213 right - Reason: no right fiber
  24. % o d1c176 left - Reason: mistargeted (see histology)
  25. % o d1c209 left - Reason: mistargeted (see histology)
  26. % o d1c217 left - Reason: mistargeted (see histology)
  27. exclusions = table( ...
  28. ["a2ac213"; "d1c176"; "d1c209"; "d1c217"], ...
  29. ["R"; "L"; "L"; "L"], ...
  30. ["no right fiber";
  31. "mistargeted (histology)";
  32. "mistargeted (histology)";
  33. "mistargeted (histology)"], ...
  34. 'VariableNames', {'animal_id','side','reason'});
  35. streams.include = true(height(streams),1);
  36. streams.exclude_reason = strings(height(streams),1);
  37. for k = 1:height(exclusions)
  38. mask = (streams.animal_id == exclusions.animal_id(k)) & ...
  39. (streams.side == exclusions.side(k));
  40. streams.include(mask) = false;
  41. streams.exclude_reason(mask) = exclusions.reason(k);
  42. end
  43. streams = streams(streams.include == 1,:);
  44. % no need to exclude trials or sessions because these are all unilateral
  45. % exclusions. IF THIS CHANGES, WE NEED TO RECODE.
  46. %% ===== Kernel-GLM config (edit here only) =====
  47. cfg = struct();
  48. % H5 + signal choice (IMPORTANT: use debleached, not normalized/zscored)
  49. cfg.h5file = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\EvanStorage\signals2.h5";
  50. cfg.signal_field = "path_debleached"; % <-- streams.(cfg.signal_field); debleached is the non-z-scored one. normalized is the z-scored one
  51. cfg.time_field = "path_time";
  52. % Sampling / binning
  53. cfg.dt = 0.025; % seconds; 0.025 = 40Hz, our recording rate
  54. cfg.time_units = "seconds"; % for sanity
  55. % Effect coding (±0.5)
  56. cfg.code.high = +0.5; cfg.code.low = -0.5;
  57. cfg.code.nogo = +0.5; cfg.code.go = -0.5;
  58. % Event kernel windows (seconds, relative to event)
  59. cfg.win.trialStart = [-1.0, 2.0]; %ITI length is predictable
  60. cfg.win.tone = [0.0, 1.5];
  61. cfg.win.outcome = [0.0, 2.0]; % applies to outcome+ and outcome-
  62. cfg.win.firstLick = [-0.2, 1.2];
  63. % Basis sizes (splines) per kernel (easy to tune)
  64. cfg.K.trialStart = 7;
  65. cfg.K.tone = 6; % for 1.5s you could use 5–6; keep 6 unless you see wiggle
  66. cfg.K.outcome = 6;
  67. cfg.K.firstLick = 6;
  68. % Lick-rate definition (continuous OR binarized covariate - modify below)
  69. cfg.lickRate.win_s = 0.3; % 200 ms causal window
  70. cfg.lickRate.causal = false; % use past-only window; if false, looks at licks between t-(win_s/2) and t+(win_s/2).
  71. cfg.lickRate.zscore = true; % recommended for ridge stability; ignored when binarized
  72. cfg.lickRate.clip_prctile = 99.5; % optional: clip extreme bursts
  73. cfg.lickRate.binarize = false; % reports out licking state - i.e., licking vs. not licking currently
  74. % Withholding definition (trial-anchored state)
  75. cfg.withhold.value = 1; % boxcar amplitude (keep 1, don't effect-code this)
  76. % Ridge regularization + CV
  77. cfg.ridge.do_cv = true;
  78. cfg.ridge.kfold = 5;
  79. cfg.ridge.blocked_time_cv = true; % contiguous time blocks
  80. cfg.ridge.lambdas = logspace(0, 6, 50); % editable
  81. % Which predictors to include
  82. cfg.use.trialStart = true;
  83. cfg.use.tone_main = true;
  84. cfg.use.tone_F = true; %F is frequency (high / low)
  85. cfg.use.tone_I = false; %I is instruction (go / nogo) %REDUNDANT WITH F, IT'S EITHER F or -F -- degenerate
  86. cfg.use.tone_FI = false; %REDUNDANT WITH F, degenerate
  87. cfg.use.outcome_hit = true; % reward==1
  88. cfg.use.outcome_neg = false; % reward==0
  89. cfg.use.outcome_FA = true;
  90. cfg.use.outcome_CRMiss = true;
  91. cfg.use.firstLick = false;
  92. cfg.use.lickRate = true;
  93. cfg.use.withholdingBoxcar = false;
  94. cfg.basis.type = "rcos"; % "rcos" (default) or "spline" later if you want
  95. cfg.addIntercept = true; % global intercept column (recommended)
  96. %% RUN KERNEL GLM ACROSS ALL STREAMS
  97. % Assumes you already have in workspace:
  98. % streams (table), trials (table), cfg (struct)
  99. % And helper functions on path:
  100. % make_rcos_basis, event_kernel_block, tone_blocks, trial_modulators,
  101. % block_idx, reconstruct_kernel
  102. assert(isfield(cfg,'h5file') && isfile(cfg.h5file), "cfg.h5file not found.");
  103. if ~isfield(cfg,'addIntercept'); cfg.addIntercept = true; end
  104. % Which streams to run
  105. runMask = (streams.include==1) & (streams.has_signal==1);
  106. idxList = find(runMask);
  107. nS = numel(idxList);
  108. fprintf("KernelGLM: running %d streams...\n", nS);
  109. % pick any representative stream index
  110. i0 = idxList(1);
  111. stream = streams(i0,:);
  112. row = licks(licks.session_uid == stream.session_uid, :);
  113. lickTimes = row.lick_times; % numeric vector
  114. template = fit_one_stream_kernelGLM(streams(i0,:), trials, cfg,lickTimes);
  115. fits = repmat(template, nS, 1);
  116. for si = 1:nS
  117. i = idxList(si);
  118. stream = streams(i,:);
  119. row = licks(licks.session_uid == stream.session_uid, :);
  120. lickTimes = row.lick_times; % numeric vector
  121. try
  122. fitToFit = fit_one_stream_kernelGLM(stream, trials, cfg, lickTimes);
  123. fits(si) = fitToFit;
  124. fprintf("[%d/%d] OK %s (%s %s)\n", si, nS, ...
  125. string(stream.stream_uid), string(stream.animal_id), string(stream.cell_type));
  126. catch ME
  127. fprintf("[%d/%d] FAIL %s : %s\n", si, nS, ...
  128. string(stream.stream_uid), ME.message);
  129. fits(si) = struct();
  130. fits(si).failed = true;
  131. fits(si).error = ME;
  132. fits(si).meta = struct( ...
  133. 'stream_uid', string(stream.stream_uid), ...
  134. 'session_uid', string(stream.session_uid), ...
  135. 'animal_id', string(stream.animal_id), ...
  136. 'cell_type', string(stream.cell_type));
  137. end
  138. end
  139. % Build a compact summary table
  140. summary = build_kernelGLM_summary(fits);
  141. % Save
  142. fitsLNonCausal = fits;
  143. outFile = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\EvanStorage\kernelGLM_fitsLNonCausal_allStreams_debleached.mat";
  144. save(outFile, "fitsLNonCausal", "summary", "cfg", "-v7.3");
  145. fprintf("Saved: %s\n", outFile);
  146. %% Post hoc filter to separate Training vs. ALL-Expert
  147. analyzeWhich = "training"; %toggle "training" or "expert"
  148. excelPath = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\General_workspaces\Day1ExpertDates.xlsx";
  149. filter_sessions_day1_expert(excelPath, analyzeWhich, fits, licks, sessions, streams, summary, trials, ...
  150. "assignInCaller", true);
  151. %% Review kernels
  152. figure;
  153. for i = 1:numel(fits)
  154. clf;
  155. t = tiledlayout(2,3, ...
  156. 'TileSpacing','compact', ...
  157. 'Padding','compact');
  158. title(t, sprintf('Fit %d', i));
  159. nexttile; plot(fits(i).kernels.trialStart); title("trialStart");
  160. nexttile; plot(fits(i).kernels.tone_main); title("tone main");
  161. nexttile; plot(fits(i).kernels.tone_F); title("tone F");
  162. % nexttile; plot(fits(i).kernels.tone_I); title("tone I");
  163. % nexttile; plot(fits(i).kernels.tone_FI); title("tone FI");
  164. nexttile; plot(fits(i).kernels.out_hit); title("out hit");
  165. nexttile; plot(fits(i).kernels.out_FA); title("out FA");
  166. nexttile; plot(fits(i).kernels.out_CRMiss); title("out CR Miss");
  167. %nexttile; plot(fits(i).kernels.firstLick); title("firstLick");
  168. disp("Press any key or click to continue...");
  169. waitforbuttonpress;
  170. end
  171. %% Plot outputs
  172. %build datasets for each cell type
  173. KDa2a = kernel_build_dataset(fits, sessions, 'CellType',"a2a", 'Unit',"animal_hemi", 'UseOnlyGood',false);
  174. KDd1 = kernel_build_dataset(fits, sessions, 'CellType',"d1", 'Unit',"animal_hemi", 'UseOnlyGood',false);
  175. % Define colors for plotting
  176. a2aColor = [255,51,153];
  177. d1Color = [51,51,153];
  178. hitColor = [0.20 0.65 0.35];
  179. missColor = [0.20 0.70 0.75];
  180. FAColor = [0.80 0.25 0.30];
  181. CRColor = [0.35 0.30 0.70];
  182. goColor = [0.25 0.60 0.40];
  183. nogoColor = [0.75 0.30 0.30];
  184. highToneColor = [0.25 0.70 0.75];
  185. lowToneColor = [0.90 0.65 0.20];
  186. C = my_plot_colors();
  187. %% plot tone kernels by high / low
  188. figure;
  189. tiledlayout(1,2);
  190. nexttile;
  191. S_d1_hi = kernel_plot(KDd1, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'Title',"d1 High tone",'Color', highToneColor, 'FaceAlpha', 0.15);
  192. S_d1_lo = kernel_plot(KDd1, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5], 'Title',"d1 Low tone",'Color', lowToneColor, 'FaceAlpha', 0.15);
  193. text(0.98, 0.95, 'High Frequencies', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', highToneColor,'FontWeight','bold','FontSize', 12);
  194. text(0.98, 0.88, 'Low Frequencies', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', lowToneColor,'FontWeight','bold','FontSize', 12);
  195. title('D1+ SPN');
  196. xlim([0,1]);
  197. grid off;
  198. S_hi = S_d1_hi;
  199. S_lo = S_d1_lo;
  200. D = S_hi.M - S_lo.M; % time × nAnimals
  201. muD = mean(D,2,'omitnan');
  202. B = 10000;
  203. [nT, nA] = size(D);
  204. maxstat = nan(1,B);
  205. for b = 1:B
  206. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  207. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  208. muDp = mean(Dp,2,'omitnan');
  209. maxstat(b) = max(abs(muDp));
  210. end
  211. thr = prctile(maxstat, 95);
  212. sig = abs(muD) > thr;
  213. ci_lo_d1_fr = muD-thr;
  214. ci_hi_d1_fr = muD + thr;
  215. muD_d1_fr = muD;
  216. yl = ylim;
  217. ybar = yl(2) - 0.02*range(yl);
  218. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  219. nexttile;
  220. S_a2a_hi = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'Title',"a2a High tone",'Color', highToneColor, 'FaceAlpha', 0.15);
  221. S_a2a_lo = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5], 'Title',"a2a Low tone",'Color', lowToneColor, 'FaceAlpha', 0.15);
  222. text(0.98, 0.95, 'High Frequencies', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', highToneColor,'FontWeight','bold','FontSize', 12);
  223. text(0.98, 0.88, 'Low Frequencies', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', lowToneColor,'FontWeight','bold','FontSize', 12);
  224. title('A2A+ SPN');
  225. xlim([0,1]);
  226. grid off;
  227. S_hi = S_a2a_hi;
  228. S_lo = S_a2a_lo;
  229. D = S_hi.M - S_lo.M; % time × nAnimals
  230. muD = mean(D,2,'omitnan');
  231. B = 10000;
  232. [nT, nA] = size(D);
  233. maxstat = nan(1,B);
  234. for b = 1:B
  235. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  236. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  237. muDp = mean(Dp,2,'omitnan');
  238. maxstat(b) = max(abs(muDp));
  239. end
  240. thr = prctile(maxstat, 95);
  241. sig = abs(muD) > thr;
  242. ci_lo_a2a_fr = muD-thr;
  243. ci_hi_a2a_fr = muD + thr;
  244. muD_a2a_fr = muD;
  245. yl = ylim;
  246. ybar = yl(2) - 0.02*range(yl);
  247. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  248. %% plot tone kernels by go / no go
  249. figure;
  250. tiledlayout(1,2);
  251. nexttile;
  252. S_d1_go = kernel_plot(KDd1, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', goColor, 'FaceAlpha', 0.15);
  253. S_d1_nogo = kernel_plot(KDd1, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  254. grid off;
  255. text(0.98, 0.95, 'Go', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 12);
  256. text(0.98, 0.88, 'No Go', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 12);
  257. title('D1+ SPN');
  258. xlim([0,1]);
  259. S_hi = S_d1_nogo;
  260. S_lo = S_d1_go;
  261. D = S_hi.M - S_lo.M; % time × nAnimals
  262. muD = mean(D,2,'omitnan');
  263. B = 10000;
  264. [nT, nA] = size(D);
  265. maxstat = nan(1,B);
  266. for b = 1:B
  267. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  268. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  269. muDp = mean(Dp,2,'omitnan');
  270. maxstat(b) = max(abs(muDp));
  271. end
  272. thr = prctile(maxstat, 95);
  273. sig = abs(muD) > thr;
  274. ci_lo_d1 = muD-thr;
  275. ci_hi_d1 = muD + thr;
  276. muD_d1 = muD;
  277. yl = ylim;
  278. ybar = yl(2) - 0.02*range(yl);
  279. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  280. nexttile;
  281. S_a2a_go = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', goColor, 'FaceAlpha', 0.15);
  282. S_a2a_nogo = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  283. grid off;
  284. text(0.98, 0.95, 'Go', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 12);
  285. text(0.98, 0.88, 'No Go', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 12);
  286. title('A2A+ SPN');
  287. xlim([0,1]);
  288. S_hi = S_a2a_nogo;
  289. S_lo = S_a2a_go;
  290. D = S_hi.M - S_lo.M; % time × nAnimals
  291. muD = mean(D,2,'omitnan');
  292. B = 10000;
  293. [nT, nA] = size(D);
  294. maxstat = nan(1,B);
  295. for b = 1:B
  296. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  297. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  298. muDp = mean(Dp,2,'omitnan');
  299. maxstat(b) = max(abs(muDp));
  300. end
  301. thr = prctile(maxstat, 95);
  302. sig = abs(muD) > thr;
  303. ci_lo_a2a = muD-thr;
  304. ci_hi_a2a = muD + thr;
  305. muD_a2a = muD;
  306. yl = ylim;
  307. ybar = yl(2) - 0.02*range(yl);
  308. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  309. %% plot diffs
  310. t = 0:0.025:1.5;
  311. t = t';
  312. W = (t >= 0) & (t <= 0.5);
  313. figure; tiledlayout(1,2);
  314. nexttile;hold on
  315. plot(t, muD_d1_fr, 'k', 'LineWidth', 2)
  316. fill([t; flipud(t)], ...
  317. [ci_lo_d1_fr; flipud(ci_hi_d1_fr)], ...
  318. 'k', 'FaceAlpha', 0.25, 'EdgeColor','none')
  319. yline(0,'--')
  320. xline(0,'--')
  321. xlabel('Time from tone (s)')
  322. ylabel('High − Low (a.u.)')
  323. title("D1+ SPN");
  324. box off; grid off
  325. xlim([0,1]);
  326. nexttile;hold on
  327. plot(t, muD_a2a_fr, 'k', 'LineWidth', 2)
  328. fill([t; flipud(t)], ...
  329. [ci_lo_a2a_fr; flipud(ci_hi_a2a_fr)], ...
  330. 'k', 'FaceAlpha', 0.25, 'EdgeColor','none')
  331. yline(0,'--')
  332. xline(0,'--')
  333. xlabel('Time from tone (s)')
  334. ylabel('High − Low (a.u.)')
  335. title("A2A+ SPN");
  336. box off; grid off
  337. xlim([0,1]);
  338. figure; tiledlayout(1,2);
  339. nexttile;hold on
  340. plot(t, muD_d1, 'k', 'LineWidth', 2)
  341. fill([t; flipud(t)], ...
  342. [ci_lo_d1; flipud(ci_hi_d1)], ...
  343. 'k', 'FaceAlpha', 0.25, 'EdgeColor','none')
  344. yline(0,'--')
  345. xline(0,'--')
  346. xlabel('Time from tone (s)')
  347. ylabel('NoGo − Go (a.u.)')
  348. title("D1+ SPN");
  349. box off; grid off
  350. xlim([0,1]);
  351. nexttile;hold on
  352. plot(t, muD_a2a, 'k', 'LineWidth', 2)
  353. fill([t; flipud(t)], ...
  354. [ci_lo_a2a; flipud(ci_hi_a2a)], ...
  355. 'k', 'FaceAlpha', 0.25, 'EdgeColor','none')
  356. yline(0,'--')
  357. xline(0,'--')
  358. xlabel('Time from tone (s)')
  359. ylabel('NoGo − Go (a.u.)')
  360. title("A2A+ SPN");
  361. box off; grid off
  362. xlim([0,1]);
  363. %% AUC analysis - terciles, per animal
  364. %go no go D1, A2A
  365. [dD1gonogo,dD1gonogo_p] = getAUCsTerciles(S_d1_go.M,S_d1_nogo.M);
  366. [dA2Agonogo,dA2Agonogo_p] = getAUCsTerciles(S_a2a_go.M,S_a2a_nogo.M);
  367. %hi lo D1, A2A
  368. [dD1hilo,dD1hilo_p] = getAUCsTerciles(S_d1_hi.M,S_d1_lo.M);
  369. [dA2Ahilo,dA2Ahilo_p] = getAUCsTerciles(S_a2a_hi.M,S_a2a_lo.M);
  370. % ---- Inputs ----
  371. X{1} = dD1gonogo; P{1} = dD1gonogo_p; titles{1} = 'D1: Go - NoGo';
  372. X{2} = dA2Agonogo; P{2} = dA2Agonogo_p; titles{2} = 'A2A: Go - NoGo';
  373. X{3} = dD1hilo; P{3} = dD1hilo_p; titles{3} = 'D1: Hi - Lo';
  374. X{4} = dA2Ahilo; P{4} = dA2Ahilo_p; titles{4} = 'A2A: Hi - Lo';
  375. figure('Color','w');
  376. tiledlayout(1,4,'TileSpacing','compact','Padding','compact');
  377. %
  378. % % Optional: consistent y-lims across panels
  379. % allVals = cell2mat(cellfun(@(a) a(:), X, 'UniformOutput', false));
  380. % yPad = 0.08 * range(allVals); if yPad==0, yPad = 0.1; end
  381. % yL = [min(allVals)-yPad, max(allVals)+yPad];
  382. for i = 1:4
  383. nexttile;
  384. plotDeltaAUCTerciles_barScatter(X{i}, P{i}, titles{i}, []);
  385. end
  386. %% AUC analysis - two bins, stream-level w/mixed effects
  387. KDd1_stream = KD_fromFitsStreams(fits, streams, KDd1, 'CellType',"d1");
  388. KDa2a_stream = KD_fromFitsStreams(fits, streams, KDa2a, 'CellType',"a2a");
  389. % now RECOMPUTE these using the stream KDs (not the old KDs)
  390. S_d1_go = kernel_plot(KDd1_stream, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'MapToneFTo',"goTone", 'DoPlot',false);
  391. S_d1_nogo = kernel_plot(KDd1_stream, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'MapToneFTo',"nogoTone", 'DoPlot',false);
  392. S_d1_hi = kernel_plot(KDd1_stream, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'MapToneFTo',"acousticHighMinusLow", 'DoPlot',false);
  393. S_d1_lo = kernel_plot(KDd1_stream, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"acousticHighMinusLow", 'DoPlot',false);
  394. S_a2a_go = kernel_plot(KDa2a_stream,'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'MapToneFTo',"goTone", 'DoPlot',false);
  395. S_a2a_nogo = kernel_plot(KDa2a_stream,'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'MapToneFTo',"nogoTone", 'DoPlot',false);
  396. S_a2a_hi = kernel_plot(KDa2a_stream,'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'MapToneFTo',"acousticHighMinusLow", 'DoPlot',false);
  397. S_a2a_lo = kernel_plot(KDa2a_stream,'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"acousticHighMinusLow", 'DoPlot',false);
  398. t = S_d1_go.t; % don’t hardcode
  399. t0 = 0.15; tEnd = 1.15;
  400. tMid = (t0 + tEnd)/2; % or choose a biologically motivated split
  401. dD1gonogo = getAUCs2Bins(t, t0, tMid, tEnd, S_d1_go.M, S_d1_nogo.M);
  402. dA2Agonogo = getAUCs2Bins(t, t0, tMid, tEnd, S_a2a_go.M, S_a2a_nogo.M);
  403. dD1hilo = getAUCs2Bins(t, t0, tMid, tEnd, S_d1_hi.M, S_d1_lo.M);
  404. dA2Ahilo = getAUCs2Bins(t, t0, tMid, tEnd, S_a2a_hi.M, S_a2a_lo.M);
  405. animal_id = string({KDd1_stream.rows.animal_id})';
  406. pD1gonogo = pvalsLME_vsZero(dD1gonogo, animal_id);
  407. pD1hilo = pvalsLME_vsZero(dD1hilo, animal_id);
  408. animal_id = string({KDa2a_stream.rows.animal_id})';
  409. pA2Agonogo = pvalsLME_vsZero(dA2Agonogo, animal_id);
  410. pA2Ahilo = pvalsLME_vsZero(dA2Ahilo, animal_id);
  411. p_all = [ ...
  412. pD1gonogo(:); ...
  413. pA2Agonogo(:); ...
  414. pD1hilo(:); ...
  415. pA2Ahilo(:) ...
  416. ];
  417. [q_all, sig_all, pcrit] = bh_fdr(p_all, 0.05);
  418. q_mat = reshape(q_all, [2 4]); % rows: early/late, cols: panels in the same order
  419. sig_mat = reshape(sig_all,[2 4]);
  420. % ---- Inputs (BH-corrected) ----
  421. X{1} = dD1gonogo; P{1} = q_mat(:,1); titles{1} = 'D1: Go - NoGo';
  422. X{2} = dA2Agonogo; P{2} = q_mat(:,2); titles{2} = 'A2A: Go - NoGo';
  423. X{3} = dD1hilo; P{3} = q_mat(:,3); titles{3} = 'D1: Hi - Lo';
  424. X{4} = dA2Ahilo; P{4} = q_mat(:,4); titles{4} = 'A2A: Hi - Lo';
  425. animal_d1 = string({KDd1_stream.rows.animal_id})';
  426. animal_a2a = string({KDa2a_stream.rows.animal_id})';
  427. figure('Color','w');
  428. tiledlayout(1,4,'TileSpacing','compact','Padding','compact');
  429. nexttile; plotDeltaAUC2Bins_barScatter_animals(dD1gonogo, animal_d1, P{1}, titles{1}, []);
  430. nexttile; plotDeltaAUC2Bins_barScatter_animals(dA2Agonogo, animal_a2a, P{2}, titles{2}, []);
  431. nexttile; plotDeltaAUC2Bins_barScatter_animals(dD1hilo, animal_d1, P{3}, titles{3}, []);
  432. nexttile; plotDeltaAUC2Bins_barScatter_animals(dA2Ahilo, animal_a2a, P{4}, titles{4}, []);
  433. fprintf("\nNOTE!!! The p values shown here are actually BH-corrected q-values across all comparisons\n");
  434. p_all = [ ...
  435. pD1gonogo(:); ...
  436. pA2Agonogo(:)];
  437. [q_all, sig_all, pcrit] = bh_fdr(p_all, 0.05);
  438. q_mat = reshape(q_all, [2 2]); % rows: early/late, cols: panels in the same order
  439. sig_mat = reshape(sig_all,[2 2]);
  440. % ---- Inputs (BH-corrected) ----
  441. X{1} = dD1gonogo; P{1} = q_mat(:,1); titles{1} = 'D1: Go - NoGo';
  442. X{2} = dA2Agonogo; P{2} = q_mat(:,2); titles{2} = 'A2A: Go - NoGo';
  443. animal_d1 = string({KDd1_stream.rows.animal_id})';
  444. animal_a2a = string({KDa2a_stream.rows.animal_id})';
  445. figure('Color','w');
  446. tiledlayout(1,2,'TileSpacing','compact','Padding','compact');
  447. nexttile; plotDeltaAUC2Bins_barScatter_animals(dD1gonogo, animal_d1, P{1}, titles{1}, []);
  448. nexttile; plotDeltaAUC2Bins_barScatter_animals(dA2Agonogo, animal_a2a, P{2}, titles{2}, []);
  449. fprintf("\nNOTE!!! The p values shown here are actually BH-corrected q-values across all comparisons\n");
  450. %
  451. p_all = [ ...
  452. pD1hilo(:); ...
  453. pA2Ahilo(:)];
  454. [q_all, sig_all, pcrit] = bh_fdr(p_all, 0.05);
  455. q_mat = reshape(q_all, [2 2]); % rows: early/late, cols: panels in the same order
  456. sig_mat = reshape(sig_all,[2 2]);
  457. % ---- Inputs (BH-corrected) ----
  458. X{1} = dD1hilo; P{1} = q_mat(:,1); titles{1} = 'D1: Hi - Lo';
  459. X{2} = dA2Ahilo; P{2} = q_mat(:,2); titles{2} = 'A2A: Ho - Lo';
  460. animal_d1 = string({KDd1_stream.rows.animal_id})';
  461. animal_a2a = string({KDa2a_stream.rows.animal_id})';
  462. figure('Color','w');
  463. tiledlayout(1,2,'TileSpacing','compact','Padding','compact');
  464. nexttile; plotDeltaAUC2Bins_barScatter_animals(dD1hilo, animal_d1, P{1}, titles{1}, []);
  465. nexttile; plotDeltaAUC2Bins_barScatter_animals(dA2Ahilo, animal_a2a, P{2}, titles{2}, []);
  466. fprintf("\nNOTE!!! The p values shown here are actually BH-corrected q-values across all comparisons\n");
  467. %% Disaggregate go vs. no go kernels by tone counterbalancing condition
  468. figure;
  469. tiledlayout(2,2);
  470. %D1 high go
  471. nexttile;
  472. S_d1_go_h = kernel_plot(KDd1, 'Which', ["tone_main","tone_F"],'Weights', [1 0.5],'MapToneFTo', "goTone",'GoIsHigh', "high", 'Title', "Go (High tone only)", 'Color', goColor, 'FaceAlpha', 0.15);
  473. S_d1_nogo_h = kernel_plot(KDd1, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'GoIsHigh', "high",'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  474. grid off;
  475. text(0.98, 0.95, 'Go (High)', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 12);
  476. text(0.98, 0.88, 'No Go (Low)', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 12);
  477. title('D1+ SPN');
  478. xlim([0,1]);
  479. S_hi = S_d1_nogo_h;
  480. S_lo = S_d1_go_h;
  481. D = S_hi.M - S_lo.M; % time × nAnimals
  482. muD = mean(D,2,'omitnan');
  483. B = 10000;
  484. [nT, nA] = size(D);
  485. maxstat = nan(1,B);
  486. for b = 1:B
  487. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  488. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  489. muDp = mean(Dp,2,'omitnan');
  490. maxstat(b) = max(abs(muDp));
  491. end
  492. thr = prctile(maxstat, 95);
  493. sig = abs(muD) > thr;
  494. ci_lo_d1 = muD-thr;
  495. ci_hi_d1 = muD + thr;
  496. muD_d1 = muD;
  497. yl = ylim;
  498. ybar = yl(2) - 0.02*range(yl);
  499. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  500. %D1 low go
  501. nexttile;
  502. S_d1_go_l = kernel_plot(KDd1, 'Which', ["tone_main","tone_F"],'Weights', [1 0.5],'MapToneFTo', "goTone",'GoIsHigh', "low", 'Title', "Go (High tone only)", 'Color', goColor, 'FaceAlpha', 0.15);
  503. S_d1_nogo_l = kernel_plot(KDd1, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'GoIsHigh', "low",'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  504. grid off;
  505. text(0.98, 0.95, 'Go (Low)', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 12);
  506. text(0.98, 0.88, 'No Go (High)', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 12);
  507. title('D1+ SPN');
  508. xlim([0,1]);
  509. S_hi = S_d1_nogo_l;
  510. S_lo = S_d1_go_l;
  511. D = S_hi.M - S_lo.M; % time × nAnimals
  512. muD = mean(D,2,'omitnan');
  513. B = 10000;
  514. [nT, nA] = size(D);
  515. maxstat = nan(1,B);
  516. for b = 1:B
  517. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  518. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  519. muDp = mean(Dp,2,'omitnan');
  520. maxstat(b) = max(abs(muDp));
  521. end
  522. thr = prctile(maxstat, 95);
  523. sig = abs(muD) > thr;
  524. ci_lo_d1 = muD-thr;
  525. ci_hi_d1 = muD + thr;
  526. muD_d1 = muD;
  527. yl = ylim;
  528. ybar = yl(2) - 0.02*range(yl);
  529. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  530. %a2a high go
  531. nexttile;
  532. S_a2a_go_h = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5],'MapToneFTo',"goTone",'GoIsHigh', "high", 'Title',"Go vs NoGo",'Color', goColor, 'FaceAlpha', 0.15);
  533. S_a2a_nogo_h = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'GoIsHigh', "high", 'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  534. grid off;
  535. text(0.98, 0.95, 'Go (high)', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 12);
  536. text(0.98, 0.88, 'No Go (low)', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 12);
  537. title('A2A+ SPN');
  538. xlim([0,1]);
  539. S_hi = S_a2a_nogo_h;
  540. S_lo = S_a2a_go_h;
  541. D = S_hi.M - S_lo.M; % time × nAnimals
  542. muD = mean(D,2,'omitnan');
  543. B = 10000;
  544. [nT, nA] = size(D);
  545. maxstat = nan(1,B);
  546. for b = 1:B
  547. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  548. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  549. muDp = mean(Dp,2,'omitnan');
  550. maxstat(b) = max(abs(muDp));
  551. end
  552. thr = prctile(maxstat, 95);
  553. sig = abs(muD) > thr;
  554. ci_lo_a2a = muD-thr;
  555. ci_hi_a2a = muD + thr;
  556. muD_a2a = muD;
  557. yl = ylim;
  558. ybar = yl(2) - 0.02*range(yl);
  559. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  560. %a2a low go
  561. nexttile;
  562. S_a2a_go_l = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5],'MapToneFTo',"goTone",'GoIsHigh', "low", 'Title',"Go vs NoGo",'Color', goColor, 'FaceAlpha', 0.15);
  563. S_a2a_nogo_l = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'GoIsHigh', "low", 'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  564. grid off;
  565. text(0.98, 0.95, 'Go (low)', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 12);
  566. text(0.98, 0.88, 'No Go (high)', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 12);
  567. title('A2A+ SPN');
  568. xlim([0,1]);
  569. S_hi = S_a2a_nogo_l;
  570. S_lo = S_a2a_go_l;
  571. D = S_hi.M - S_lo.M; % time × nAnimals
  572. muD = mean(D,2,'omitnan');
  573. B = 10000;
  574. [nT, nA] = size(D);
  575. maxstat = nan(1,B);
  576. for b = 1:B
  577. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  578. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  579. muDp = mean(Dp,2,'omitnan');
  580. maxstat(b) = max(abs(muDp));
  581. end
  582. thr = prctile(maxstat, 95);
  583. sig = abs(muD) > thr;
  584. ci_lo_a2a = muD-thr;
  585. ci_hi_a2a = muD + thr;
  586. muD_a2a = muD;
  587. yl = ylim;
  588. ybar = yl(2) - 0.02*range(yl);
  589. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  590. %% just look at all kernels - trial start, tone start, hit, FA, and CR/miss
  591. figure;
  592. tiledlayout(2,6);
  593. nexttile;
  594. kernel_plot(KDd1,'Which',"trialStart",'Color', d1Color./255, 'FaceAlpha', 0.15);
  595. nexttile;
  596. kernel_plot(KDd1,'Which',"tone_main",'Color', d1Color./255, 'FaceAlpha', 0.15);
  597. nexttile;
  598. kernel_plot(KDd1, 'Which', "tone_F", 'MapToneFTo', "goTone", 'Color', d1Color./255,'FaceAlpha', 0.15,'Title','Tone-Instruction'); %Go-NoGo
  599. nexttile;
  600. kernel_plot(KDd1,'Which',"out_hit",'Color', d1Color./255, 'FaceAlpha', 0.15);
  601. nexttile;
  602. kernel_plot(KDd1,'Which',"out_FA",'ApplyFARule',true,'MinNFA',10,'Color', d1Color./255, 'FaceAlpha', 0.15);
  603. nexttile;
  604. kernel_plot(KDd1,'Which',"out_CRMiss",'Color', d1Color./255, 'FaceAlpha', 0.15);
  605. nexttile;
  606. kernel_plot(KDa2a,'Which',"trialStart",'Color', a2aColor./255, 'FaceAlpha', 0.15);
  607. nexttile;
  608. kernel_plot(KDa2a,'Which',"tone_main",'Color', a2aColor./255, 'FaceAlpha', 0.15);
  609. nexttile;
  610. kernel_plot(KDa2a, 'Which', "tone_F", 'MapToneFTo', "goTone", 'Color', d1Color./255,'FaceAlpha', 0.15,'Title','Tone-Instruction'); %Go-NoGo
  611. nexttile;
  612. kernel_plot(KDa2a,'Which',"out_hit",'Color', a2aColor./255, 'FaceAlpha', 0.15);
  613. nexttile;
  614. kernel_plot(KDa2a,'Which',"out_FA",'ApplyFARule',true,'MinNFA',10,'Color', a2aColor./255, 'FaceAlpha', 0.15);
  615. nexttile;
  616. kernel_plot(KDa2a,'Which',"out_CRMiss",'Color', a2aColor./255, 'FaceAlpha', 0.15);
  617. %% ===== Kernel-GLM CONTROL MODEL!! config (edit here only) =====
  618. cfg = struct();
  619. % H5 + signal choice (IMPORTANT: use debleached, not normalized/zscored)
  620. cfg.h5file = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\EvanStorage\signals2.h5";
  621. cfg.signal_field = "path_debleached"; % <-- streams.(cfg.signal_field); debleached is the non-z-scored one. normalized is the z-scored one
  622. cfg.time_field = "path_time";
  623. % Sampling / binning
  624. cfg.dt = 0.025; % seconds; 0.025 = 40Hz, our recording rate
  625. cfg.time_units = "seconds"; % for sanity
  626. % Effect coding (±0.5)
  627. cfg.code.high = +0.5; cfg.code.low = -0.5;
  628. cfg.code.nogo = +0.5; cfg.code.go = -0.5;
  629. % Event kernel windows (seconds, relative to event)
  630. cfg.win.trialStart = [-1.0, 2.0]; %ITI length is predictable
  631. cfg.win.tone = [0.0, 1.5];
  632. cfg.win.outcome = [0.0, 2.0]; % applies to outcome+ and outcome-
  633. cfg.win.firstLick = [-0.2, 1.2];
  634. % Basis sizes (splines) per kernel (easy to tune)
  635. cfg.K.trialStart = 7;
  636. cfg.K.tone = 6; % for 1.5s you could use 5–6; keep 6 unless you see wiggle
  637. cfg.K.outcome = 6;
  638. cfg.K.firstLick = 6;
  639. % Ridge regularization + CV
  640. cfg.ridge.do_cv = true;
  641. cfg.ridge.kfold = 5;
  642. cfg.ridge.blocked_time_cv = true; % contiguous time blocks
  643. cfg.ridge.lambdas = logspace(0, 6, 50); % editable
  644. % Which predictors to include
  645. cfg.use.trialStart = true;
  646. cfg.use.tone_main = true;
  647. cfg.use.tone_F = true; %F is frequency (high / low)
  648. cfg.use.tone_I = false; %I is instruction (go / nogo) %REDUNDANT WITH F, IT'S EITHER F or -F -- degenerate
  649. cfg.use.tone_FI = false; %REDUNDANT WITH F, degenerate
  650. cfg.use.outcome_hit = false; % reward==1
  651. cfg.use.outcome_neg = false; % reward==0
  652. cfg.use.outcome_FA = false;
  653. cfg.use.outcome_CRMiss = false;
  654. cfg.use.firstLick = false;
  655. cfg.basis.type = "rcos"; % "rcos" (default) or "spline" later if you want
  656. cfg.addIntercept = true; % global intercept column (recommended)
  657. cfg_B = cfg;
  658. %% RUN KERNEL GLM ACROSS ALL STREAMS
  659. % Assumes you already have in workspace:
  660. % streams (table), trials (table), cfg (struct)
  661. % And helper functions on path:
  662. % make_rcos_basis, event_kernel_block, tone_blocks, trial_modulators,
  663. % block_idx, reconstruct_kernel
  664. assert(isfield(cfg,'h5file') && isfile(cfg.h5file), "cfg.h5file not found.");
  665. if ~isfield(cfg,'addIntercept'); cfg.addIntercept = true; end
  666. % Which streams to run
  667. runMask = (streams.include==1) & (streams.has_signal==1);
  668. idxList = find(runMask);
  669. nS = numel(idxList);
  670. fprintf("KernelGLM: running %d streams...\n", nS);
  671. % pick any representative stream index
  672. i0 = idxList(1);
  673. template = fit_one_stream_kernelGLM(streams(i0,:), trials, cfg);
  674. fitsCtrl = repmat(template, nS, 1);
  675. for si = 1:nS
  676. i = idxList(si);
  677. stream = streams(i,:);
  678. try
  679. fitToFit = fit_one_stream_kernelGLM(stream, trials, cfg);
  680. fitsCtrl(si) = fitToFit;
  681. fprintf("[%d/%d] OK %s (%s %s)\n", si, nS, ...
  682. string(stream.stream_uid), string(stream.animal_id), string(stream.cell_type));
  683. catch ME
  684. fprintf("[%d/%d] FAIL %s : %s\n", si, nS, ...
  685. string(stream.stream_uid), ME.message);
  686. fitsCtrl(si) = struct();
  687. fitsCtrl(si).failed = true;
  688. fitsCtrl(si).error = ME;
  689. fitsCtrl(si).meta = struct( ...
  690. 'stream_uid', string(stream.stream_uid), ...
  691. 'session_uid', string(stream.session_uid), ...
  692. 'animal_id', string(stream.animal_id), ...
  693. 'cell_type', string(stream.cell_type));
  694. end
  695. end
  696. % Build a compact summary table
  697. summaryCtrl = build_kernelGLM_summary(fitsCtrl);
  698. % Save
  699. outFile = "kernelGLM_fitsCtrl_allStreams_debleached.mat";
  700. save(outFile, "fitsCtrl", "summaryCtrl", "cfg", "-v7.3");
  701. fprintf("Saved: %s\n", outFile);
  702. %% build datasets for each cell type
  703. KDCa2a = kernel_build_dataset(fitsCtrl, sessions, 'CellType',"a2a", 'Unit',"animal_hemi", 'UseOnlyGood',true);
  704. KDCd1 = kernel_build_dataset(fitsCtrl, sessions, 'CellType',"d1", 'Unit',"animal_hemi", 'UseOnlyGood',true);
  705. %% Look at no-outcome-model kernels
  706. % just look at all kernels - trial start, tone start, hit, FA, and CR/miss
  707. figure;
  708. tiledlayout(2,3);
  709. nexttile;
  710. kernel_plot(KDCd1,'Which',"trialStart",'Color', d1Color./255, 'FaceAlpha', 0.15);
  711. nexttile;
  712. kernel_plot(KDCd1,'Which',"tone_main",'Color', d1Color./255, 'FaceAlpha', 0.15);
  713. nexttile;
  714. kernel_plot(KDCd1,'Which',"tone_F",'Color', d1Color./255, 'FaceAlpha', 0.15);
  715. nexttile;
  716. kernel_plot(KDCa2a,'Which',"trialStart",'Color', a2aColor./255, 'FaceAlpha', 0.15);
  717. nexttile;
  718. kernel_plot(KDCa2a,'Which',"tone_main",'Color', a2aColor./255, 'FaceAlpha', 0.15);
  719. nexttile;
  720. kernel_plot(KDCa2a,'Which',"tone_F",'Color', a2aColor./255, 'FaceAlpha', 0.15);
  721. %% Go - No Go in NO-OUTCOMES model
  722. %plot tone kernels by go / no go
  723. figure;
  724. tiledlayout(1,2);
  725. nexttile;
  726. S_d1_go = kernel_plot(KDCd1, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', goColor, 'FaceAlpha', 0.15);
  727. S_d1_nogo = kernel_plot(KDCd1, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  728. grid off;
  729. text(0.98, 0.95, 'Go', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 12);
  730. text(0.98, 0.88, 'No Go', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 12);
  731. title('D1+ SPN');
  732. xlim([0,1]);
  733. S_hi = S_d1_nogo;
  734. S_lo = S_d1_go;
  735. D = S_hi.M - S_lo.M; % time × nAnimals
  736. muD = mean(D,2,'omitnan');
  737. B = 10000;
  738. [nT, nA] = size(D);
  739. maxstat = nan(1,B);
  740. for b = 1:B
  741. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  742. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  743. muDp = mean(Dp,2,'omitnan');
  744. maxstat(b) = max(abs(muDp));
  745. end
  746. thr = prctile(maxstat, 95);
  747. sig = abs(muD) > thr;
  748. yl = ylim;
  749. ybar = yl(2) - 0.02*range(yl);
  750. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  751. B = 10000;
  752. [T, nA] = size(D);
  753. boot_mu = nan(T,B);
  754. for b = 1:B
  755. idx = randi(nA,[nA 1]);
  756. boot_mu(:,b) = mean(D(:,idx),2,'omitnan');
  757. end
  758. ci_lo_d1_ctrl = prctile(boot_mu, 2.5, 2);
  759. ci_hi_d1_ctrl = prctile(boot_mu, 97.5, 2);
  760. muD_d1_ctrl = muD;
  761. ci_lo_d1_ctrl = muD-thr;
  762. ci_hi_d1_ctrl = muD + thr;
  763. nexttile;
  764. S_a2a_go = kernel_plot(KDCa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', goColor, 'FaceAlpha', 0.15);
  765. S_a2a_nogo = kernel_plot(KDCa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  766. grid off;
  767. text(0.98, 0.95, 'Go', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 12);
  768. text(0.98, 0.88, 'No Go', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 12);
  769. title('A2A+ SPN');
  770. xlim([0,1]);
  771. S_hi = S_a2a_nogo;
  772. S_lo = S_a2a_go;
  773. D = S_hi.M - S_lo.M; % time × nAnimals
  774. muD = mean(D,2,'omitnan');
  775. B = 10000;
  776. [nT, nA] = size(D);
  777. maxstat = nan(1,B);
  778. for b = 1:B
  779. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  780. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  781. muDp = mean(Dp,2,'omitnan');
  782. maxstat(b) = max(abs(muDp));
  783. end
  784. thr = prctile(maxstat, 95);
  785. sig = abs(muD) > thr;
  786. yl = ylim;
  787. ybar = yl(2) - 0.02*range(yl);
  788. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  789. B = 10000;
  790. [T, nA] = size(D);
  791. boot_mu = nan(T,B);
  792. for b = 1:B
  793. idx = randi(nA,[nA 1]);
  794. boot_mu(:,b) = mean(D(:,idx),2,'omitnan');
  795. end
  796. ci_lo_a2a_ctrl = prctile(boot_mu, 2.5, 2);
  797. ci_hi_a2a_ctrl = prctile(boot_mu, 97.5, 2);
  798. muD_a2a_ctrl = muD;
  799. ci_lo_a2a_ctrl = muD-thr;
  800. ci_hi_a2a_ctrl = muD + thr;
  801. %% plot tone kernels by high / low NO OUTCOME ODEL
  802. figure;
  803. tiledlayout(1,2);
  804. nexttile;
  805. S_d1_hi = kernel_plot(KDCd1, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'Title',"d1 High tone",'Color', highToneColor, 'FaceAlpha', 0.15);
  806. S_d1_lo = kernel_plot(KDCd1, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5], 'Title',"d1 Low tone",'Color', lowToneColor, 'FaceAlpha', 0.15);
  807. text(0.98, 0.95, 'High Frequencies', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', highToneColor,'FontWeight','bold','FontSize', 12);
  808. text(0.98, 0.88, 'Low Frequencies', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', lowToneColor,'FontWeight','bold','FontSize', 12);
  809. title('D1+ SPN');
  810. xlim([0,1]);
  811. grid off;
  812. S_hi = S_d1_hi;
  813. S_lo = S_d1_lo;
  814. D = S_hi.M - S_lo.M; % time × nAnimals
  815. muD = mean(D,2,'omitnan');
  816. B = 10000;
  817. [nT, nA] = size(D);
  818. maxstat = nan(1,B);
  819. for b = 1:B
  820. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  821. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  822. muDp = mean(Dp,2,'omitnan');
  823. maxstat(b) = max(abs(muDp));
  824. end
  825. thr = prctile(maxstat, 95);
  826. sig = abs(muD) > thr;
  827. yl = ylim;
  828. ybar = yl(2) - 0.02*range(yl);
  829. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  830. B = 10000;
  831. [T, nA] = size(D);
  832. boot_mu = nan(T,B);
  833. for b = 1:B
  834. idx = randi(nA,[nA 1]);
  835. boot_mu(:,b) = mean(D(:,idx),2,'omitnan');
  836. end
  837. ci_lo_d1_fr_ctrl = prctile(boot_mu, 2.5, 2);
  838. ci_hi_d1_fr_ctrl = prctile(boot_mu, 97.5, 2);
  839. muD_d1_fr_ctrl = muD;
  840. ci_lo_d1_fr_ctrl = muD-thr;
  841. ci_hi_d1_fr_ctrl = muD + thr;
  842. nexttile;
  843. S_a2a_hi = kernel_plot(KDCa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5], 'Title',"a2a High tone",'Color', highToneColor, 'FaceAlpha', 0.15);
  844. S_a2a_lo = kernel_plot(KDCa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5], 'Title',"a2a Low tone",'Color', lowToneColor, 'FaceAlpha', 0.15);
  845. text(0.98, 0.95, 'High Frequencies', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', highToneColor,'FontWeight','bold','FontSize', 12);
  846. text(0.98, 0.88, 'Low Frequencies', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', lowToneColor,'FontWeight','bold','FontSize', 12);
  847. title('A2A+ SPN');
  848. xlim([0,1]);
  849. grid off;
  850. S_hi = S_a2a_hi;
  851. S_lo = S_a2a_lo;
  852. D = S_hi.M - S_lo.M; % time × nAnimals
  853. muD = mean(D,2,'omitnan');
  854. B = 10000;
  855. [nT, nA] = size(D);
  856. maxstat = nan(1,B);
  857. for b = 1:B
  858. sgn = (rand(nA,1) > 0.5)*2 - 1; % +/-1 per animal
  859. Dp = D .* reshape(sgn, 1, []); % sign-flip columns
  860. muDp = mean(Dp,2,'omitnan');
  861. maxstat(b) = max(abs(muDp));
  862. end
  863. thr = prctile(maxstat, 95);
  864. sig = abs(muD) > thr;
  865. yl = ylim;
  866. ybar = yl(2) - 0.02*range(yl);
  867. plot(S_hi.t(sig), ybar*ones(sum(sig),1), 'k.', 'MarkerSize',10);
  868. B = 10000;
  869. [T, nA] = size(D);
  870. boot_mu = nan(T,B);
  871. for b = 1:B
  872. idx = randi(nA,[nA 1]);
  873. boot_mu(:,b) = mean(D(:,idx),2,'omitnan');
  874. end
  875. ci_lo_a2a_fr_ctrl = prctile(boot_mu, 2.5, 2);
  876. ci_hi_a2a_fr_ctrl = prctile(boot_mu, 97.5, 2);
  877. muD_a2a_fr_ctrl = muD;
  878. ci_lo_a2a_fr_ctrl = muD-thr;
  879. ci_hi_a2a_fr_ctrl = muD + thr;
  880. %% plot diffs - no outcome model
  881. t = 0:0.025:1.5;
  882. t = t';
  883. W = (t >= 0) & (t <= 0.5);
  884. figure; tiledlayout(1,2);
  885. nexttile;hold on
  886. plot(t, muD_d1_fr_ctrl, 'k', 'LineWidth', 2)
  887. fill([t; flipud(t)], ...
  888. [ci_lo_d1_fr_ctrl; flipud(ci_hi_d1_fr_ctrl)], ...
  889. 'k', 'FaceAlpha', 0.25, 'EdgeColor','none')
  890. yline(0,'--')
  891. xline(0,'--')
  892. xlabel('Time from tone (s)')
  893. ylabel('High − Low (a.u.)')
  894. title("D1+ SPN");
  895. box off; grid off
  896. xlim([0,1]);
  897. nexttile;hold on
  898. plot(t, muD_a2a_fr_ctrl, 'k', 'LineWidth', 2)
  899. fill([t; flipud(t)], ...
  900. [ci_lo_a2a_fr_ctrl; flipud(ci_hi_a2a_fr_ctrl)], ...
  901. 'k', 'FaceAlpha', 0.25, 'EdgeColor','none')
  902. yline(0,'--')
  903. xline(0,'--')
  904. xlabel('Time from tone (s)')
  905. ylabel('High − Low (a.u.)')
  906. title("A2A+ SPN");
  907. box off; grid off
  908. xlim([0,1]);
  909. figure; tiledlayout(1,2);
  910. nexttile;hold on
  911. plot(t, muD_d1_ctrl, 'k', 'LineWidth', 2)
  912. fill([t; flipud(t)], ...
  913. [ci_lo_d1_ctrl; flipud(ci_hi_d1_ctrl)], ...
  914. 'k', 'FaceAlpha', 0.25, 'EdgeColor','none')
  915. yline(0,'--')
  916. xline(0,'--')
  917. xlabel('Time from tone (s)')
  918. ylabel('NoGo − Go (a.u.)')
  919. title("D1+ SPN");
  920. box off; grid off
  921. xlim([0,1]);
  922. M_go = S_a2a_go.M;
  923. M_nogo = S_a2a_nogo.M;
  924. auc_go = trapz(t(W), M_go(W,:), 1); % 1 × nAnimals
  925. auc_nogo = trapz(t(W), M_nogo(W,:), 1); % 1 × nAnimals
  926. delta_auc = auc_go - auc_nogo; % paired difference
  927. nexttile;hold on
  928. plot(t, muD_a2a_ctrl, 'k', 'LineWidth', 2)
  929. fill([t; flipud(t)], ...
  930. [ci_lo_a2a_ctrl; flipud(ci_hi_a2a_ctrl)], ...
  931. 'k', 'FaceAlpha', 0.25, 'EdgeColor','none')
  932. yline(0,'--')
  933. xline(0,'--')
  934. xlabel('Time from tone (s)')
  935. ylabel('NoGo − Go (a.u.)')
  936. title("A2A+ SPN");
  937. box off; grid off
  938. xlim([0,1]);
  939. %% Get the deltaCVs
  940. mse_A = nan(numel(fits),1);
  941. mse_B = nan(numel(fits),1);
  942. fits_A = fitsLW;
  943. fits_B = fits;
  944. for i = 1:numel(fits_A)
  945. L = fits_A(i).cv.lambda_star;
  946. lambdas = fits_A(i).cv.lambdas;
  947. [~, idx] = min(abs(lambdas - L));
  948. mse_A(i) = mean(fits_A(i).cv.fold_mse(:,idx));
  949. L = fits_B(i).cv.lambda_star;
  950. lambdas = fits_B(i).cv.lambdas;
  951. [~, idx] = min(abs(lambdas - L));
  952. mse_B(i) = mean(fits_B(i).cv.fold_mse(:,idx));
  953. end
  954. % Sanity checks
  955. assert(height(summary)==numel(mse_A) && numel(mse_A)==numel(mse_B), 'Size mismatch.');
  956. summary.mse_full = mse_A(:);
  957. summary.mse_reduced = mse_B(:);
  958. summary.delta_mse = summary.mse_full - summary.mse_reduced; % negative = full better
  959. % Optional: drop failed rows
  960. if ismember("failed", string(summary.Properties.VariableNames))
  961. summary_use = summary(~summary.failed, :);
  962. else
  963. summary_use = summary;
  964. end
  965. G = groupsummary(summary_use, ["cell_type","animal_id"], "mean", "delta_mse");
  966. G.Properties.VariableNames{'mean_delta_mse'} = 'delta_mse_animal';
  967. cts = ["d1","a2a"]; % fixed order
  968. x = 1:2;
  969. means = nan(1,2);
  970. sems = nan(1,2);
  971. for i = 1:2
  972. v = G.delta_mse_animal(G.cell_type==cts(i));
  973. means(i) = mean(v, 'omitnan');
  974. sems(i) = std(v, 'omitnan') / sqrt(sum(~isnan(v)));
  975. end
  976. figure; hold on
  977. % Bars
  978. bar(x, means);
  979. % Errorbars (SEM; change to CI if you want)
  980. errorbar(x, means, sems, 'k.', 'LineWidth', 1);
  981. % Overlay dots with jitter
  982. rng(0);
  983. for i = 1:2
  984. v = G.delta_mse_animal(G.cell_type==cts(i));
  985. xi = x(i) + 0.08*(rand(size(v))-0.5); % jitter
  986. plot(xi, v, 'k.', 'MarkerSize', 14);
  987. end
  988. % Cosmetics
  989. xticks(x);
  990. xticklabels(cts);
  991. yline(0,'--');
  992. ylabel('\DeltaMSE = MSE_{lick+withhold} - MSE_{base}');
  993. title('Lick and withholding regressors improve fit (animal-level)');
  994. box off; grid off
  995. disp(groupsummary(G, "cell_type", ["mean","median"], "delta_mse_animal"));
  996. fprintf('Frac animals with ΔMSE<0 (full better):\n');
  997. for i = 1:2
  998. v = G.delta_mse_animal(G.cell_type==cts(i));
  999. fprintf('%s: %.2f\n', cts(i), mean(v<0,'omitnan'));
  1000. end
  1001. %% Show how model (vs. reduced) predicts held out data
  1002. pickStream = 158; %pick a stream
  1003. stream = streams(pickStream,:);
  1004. %start with full model
  1005. cfg = cfg_A;
  1006. % ---------- Load t, y ----------
  1007. t = h5read(cfg.h5file, stream.(cfg.time_field){1}); t = t(:);
  1008. y = h5read(cfg.h5file, stream.(cfg.signal_field){1}); y = y(:);
  1009. assert(numel(t)==numel(y), "t and y lengths mismatch.");
  1010. % Optional: remove mean (recommended, helps intercept interpretability)
  1011. y0 = y;
  1012. y = y - mean(y, 'omitnan');
  1013. % ---------- Session trials ----------
  1014. sessTrials = trials(trials.session_uid == stream.session_uid & trials.is_valid == 1, :);
  1015. assert(height(sessTrials) > 10, "Too few valid trials for session.");
  1016. dt = cfg.dt;
  1017. % ---------- Build design matrix X for full (NO intercept here; add later) ----------
  1018. [X, labels, bases, lags] = build_design_matrix(t, sessTrials, cfg);
  1019. % ---------- Blocked time CV for ridge ----------
  1020. Kfold = cfg.ridge.kfold;
  1021. T = numel(y);
  1022. edges = round(linspace(1, T+1, Kfold+1));
  1023. k = 3;
  1024. test_idx = edges(k):edges(k+1)-1;
  1025. train_idx = setdiff(1:T, test_idx);
  1026. Xtr0 = X(train_idx,:);
  1027. Xte0 = X(test_idx,:);
  1028. ytr = y(train_idx);
  1029. yte = y(test_idx);
  1030. % fold-specific standardization (TRAIN ONLY)
  1031. mu = mean(Xtr0, 1);
  1032. sd = std(Xtr0, 0, 1);
  1033. sd(sd==0) = 1;
  1034. Xtr = (Xtr0 - mu) ./ sd;
  1035. Xte = (Xte0 - mu) ./ sd;
  1036. lam = fits(pickStream).cv.lambda_star; % adjust field name
  1037. % Add intercept
  1038. XtrI = [ones(size(Xtr,1),1) Xtr];
  1039. XteI = [ones(size(Xte,1),1) Xte];
  1040. % Ridge penalty matrix: don't penalize intercept
  1041. p = size(XtrI,2);
  1042. P = diag([0; ones(p-1,1)]);
  1043. beta = (XtrI'*XtrI + lam*P) \ (XtrI'*ytr);
  1044. yhat_full = XteI * beta;
  1045. %now do reduced
  1046. cfg = cfg_B;
  1047. % ---------- Load t, y ----------
  1048. t = h5read(cfg.h5file, stream.(cfg.time_field){1}); t = t(:);
  1049. y = h5read(cfg.h5file, stream.(cfg.signal_field){1}); y = y(:);
  1050. assert(numel(t)==numel(y), "t and y lengths mismatch.");
  1051. % Optional: remove mean (recommended, helps intercept interpretability)
  1052. y0 = y;
  1053. y = y - mean(y, 'omitnan');
  1054. % ---------- Session trials ----------
  1055. sessTrials = trials(trials.session_uid == stream.session_uid & trials.is_valid == 1, :);
  1056. assert(height(sessTrials) > 10, "Too few valid trials for session.");
  1057. dt = cfg.dt;
  1058. % ---------- Build design matrix X for full (NO intercept here; add later) ----------
  1059. [X, labels, bases, lags] = build_design_matrix(t, sessTrials, cfg);
  1060. % ---------- Blocked time CV for ridge ----------
  1061. Kfold = cfg.ridge.kfold;
  1062. T = numel(y);
  1063. edges = round(linspace(1, T+1, Kfold+1));
  1064. k = 3;
  1065. test_idx = edges(k):edges(k+1)-1;
  1066. train_idx = setdiff(1:T, test_idx);
  1067. Xtr0 = X(train_idx,:);
  1068. Xte0 = X(test_idx,:);
  1069. ytr = y(train_idx);
  1070. yte = y(test_idx);
  1071. % fold-specific standardization (TRAIN ONLY)
  1072. mu = mean(Xtr0, 1);
  1073. sd = std(Xtr0, 0, 1);
  1074. sd(sd==0) = 1;
  1075. Xtr = (Xtr0 - mu) ./ sd;
  1076. Xte = (Xte0 - mu) ./ sd;
  1077. lam = fitsCtrl(pickStream).cv.lambda_star; % adjust field name
  1078. % Add intercept
  1079. XtrI = [ones(size(Xtr,1),1) Xtr];
  1080. XteI = [ones(size(Xte,1),1) Xte];
  1081. % Ridge penalty matrix: don't penalize intercept
  1082. p = size(XtrI,2);
  1083. P = diag([0; ones(p-1,1)]);
  1084. beta = (XtrI'*XtrI + lam*P) \ (XtrI'*ytr);
  1085. yhat_red = XteI * beta;
  1086. % ---- Plot held-out prediction: actual vs full vs reduced ----
  1087. % Reconstruct held-out time axis
  1088. t_te = t(test_idx); % uses the *current* t (from reduced model load); OK since same stream/h5/dt
  1089. % If you want to be extra safe, save t from the full model block as t_full, and use that instead.
  1090. % Sanity check lengths
  1091. assert(numel(yte) == numel(yhat_full) && numel(yte) == numel(yhat_red), ...
  1092. "Held-out vectors length mismatch.");
  1093. % Compute held-out MSEs (optional but nice for title)
  1094. mse_full = mean((yte - yhat_full).^2, 'omitnan');
  1095. mse_red = mean((yte - yhat_red ).^2, 'omitnan');
  1096. dMSE = mse_full - mse_red;
  1097. % Choose a readable window inside held-out segment (e.g., 30 s)
  1098. win_s = 30; % seconds to show
  1099. Nwin = min(numel(t_te), round(win_s/dt)); % samples
  1100. i0 = 1; % start index within held-out segment (change if you want)
  1101. ii = i0:(i0+Nwin-1);
  1102. figure; tiledlayout(2,1);
  1103. % --- Top: actual + predictions ---
  1104. nexttile; hold on
  1105. plot(t_te(ii) - t_te(ii(1)), yte(ii), 'k', 'LineWidth', 1.25);
  1106. plot(t_te(ii) - t_te(ii(1)), yhat_full(ii),'b', 'LineWidth', 1.25);
  1107. plot(t_te(ii) - t_te(ii(1)), yhat_red(ii), 'r', 'LineWidth', 1.00);
  1108. xlabel('Time within held-out window (s)');
  1109. ylabel('\DeltaF/F (mean-centered)');
  1110. legend({'Actual (held-out)','Full model','No-outcome'}, 'Location','best');
  1111. title(sprintf('Held-out prediction (stream %d, fold %d): MSE_full=%.3g, MSE_red=%.3g, \\Delta=%.3g', ...
  1112. pickStream, k, mse_full, mse_red, dMSE));
  1113. box off; grid off
  1114. % --- Bottom: residuals (shows structure the reduced model misses) ---
  1115. nexttile; hold on
  1116. plot(t_te(ii) - t_te(ii(1)), yte(ii) - yhat_full(ii), 'b', 'LineWidth', 1.1);
  1117. plot(t_te(ii) - t_te(ii(1)), yte(ii) - yhat_red(ii), 'r', 'LineWidth', 1.0);
  1118. yline(0,'k--');
  1119. xlabel('Time within held-out window (s)');
  1120. ylabel('Residual (Actual - Pred)');
  1121. legend({'Full residual','No-outcome residual'}, 'Location','best');
  1122. box off; grid off
  1123. %% Residualized PSTH pipeline
  1124. %cfg = struct();
  1125. cfg.dt = 0.025;
  1126. cfg.h5file = "C:\Users\walki\Box\Filtering Paper\resubmission\Figures\Photometry Figure\Extra_Stuff\EvanStorage\signals2.h5";
  1127. cfg.time_field = "path_time";
  1128. cfg.signal_field = "path_debleached";
  1129. cfg.win.trialStart_extract = [-1 2];
  1130. cfg.win.tone_extract = [-1 2];
  1131. cfg.code.high = +0.5; % HIGH tone
  1132. cfg.code.low = -0.5; % LOW tone
  1133. [allOUT, metricsAll] = resid_psth_pipeline(streams, trials, fits, cfg, licks);
  1134. %% Residualized PSTH sanity checks
  1135. % pick a stream with data
  1136. % k = find(~cellfun('isempty',allOUT),1,'first');
  1137. % S = allOUT{k};
  1138. %
  1139. % figure; plot(S.t_tone, S.psth.tone_byFreq.high); hold on;
  1140. % plot(S.t_tone, S.psth.tone_byFreq.low);
  1141. % xlim([-1 2]); xlabel('Time from tone (s)'); ylabel('\DeltaF/F residual');
  1142. % legend({'High','Low'});
  1143. [i, trRow] = pick_clean_stream_trial(streams, trials, allOUT);
  1144. sanity_plot_one_trial(streams, fits, cfg, trRow, i);
  1145. [i, trRow] = pick_clean_stream_trial(streams, trials, allOUT);
  1146. sanity_roundtrip_identity(streams, fits, cfg, trRow, i);
  1147. sanity_counts(allOUT);
  1148. % Peruse trials
  1149. [i, trRow] = pick_random_clean_stream_trial(streams, trials, allOUT);
  1150. sanity_plot_one_trial(streams, fits, cfg, trRow, i);
  1151. %% D1 A2A Go-No Go Resid PSTH -BASE
  1152. figure;
  1153. tiledlayout(1,2);
  1154. nexttile;
  1155. hold on;
  1156. plot_resid_population(allOUT, ...
  1157. 'Event',"tone", 'CellType',"a2a", 'Tone',"go", ...
  1158. 'Label',"A2A go",'Color',goColor);
  1159. plot_resid_population(allOUT, ...
  1160. 'Event',"tone", 'CellType',"a2a", 'Tone',"nogo", ...
  1161. 'Label',"A2A nogo",'Color',nogoColor);
  1162. xlim([-1 2]); legend;
  1163. xlabel('Time from tone start (s)');
  1164. ylabel('DeltaF/F (Residualized)');
  1165. nexttile; hold on;
  1166. plot_resid_population(allOUT, ...
  1167. 'Event',"tone", 'CellType',"d1", 'Tone',"go", ...
  1168. 'Label',"D1 go",'Color',goColor);
  1169. plot_resid_population(allOUT, ...
  1170. 'Event',"tone", 'CellType',"d1", 'Tone',"nogo", ...
  1171. 'Label',"D1 nogo",'Color',nogoColor);
  1172. xlim([-1 2]); legend;
  1173. xlabel('Time from tone start (s)');
  1174. ylabel('DeltaF/F (Residualized)');
  1175. %% D1 A2A Go-No Go Resid PSTH + Lick + Withhold
  1176. figure;
  1177. tiledlayout(1,2);
  1178. nexttile;
  1179. hold on;
  1180. plot_resid_population(allOUT, ...
  1181. 'Event',"tone_LW", 'CellType',"a2a", 'Tone',"go", ...
  1182. 'Label',"A2A go",'Color',goColor);
  1183. plot_resid_population(allOUT, ...
  1184. 'Event',"tone_LW", 'CellType',"a2a", 'Tone',"nogo", ...
  1185. 'Label',"A2A nogo",'Color',nogoColor);
  1186. xlim([-1 2]); legend;
  1187. xlabel('Time from tone start (s)');
  1188. ylabel('DeltaF/F (Residualized)');
  1189. nexttile; hold on;
  1190. plot_resid_population(allOUT, ...
  1191. 'Event',"tone_LW", 'CellType',"d1", 'Tone',"go", ...
  1192. 'Label',"D1 go",'Color',goColor);
  1193. plot_resid_population(allOUT, ...
  1194. 'Event',"tone_LW", 'CellType',"d1", 'Tone',"nogo", ...
  1195. 'Label',"D1 nogo",'Color',nogoColor);
  1196. xlim([-1 2]); legend;
  1197. xlabel('Time from tone start (s)');
  1198. ylabel('DeltaF/F (Residualized)');
  1199. %
  1200. figure;
  1201. tiledlayout(1,2);
  1202. nexttile;
  1203. hold on;
  1204. plot_resid_population(allOUT, ...
  1205. 'Event',"tone", 'CellType',"a2a", 'Tone',"high", ...
  1206. 'Label',"A2A high",'Color',highToneColor);
  1207. plot_resid_population(allOUT, ...
  1208. 'Event',"tone", 'CellType',"a2a", 'Tone',"low", ...
  1209. 'Label',"A2A low",'Color',lowToneColor);
  1210. xlim([-1 2]); legend;
  1211. xlabel('Time from tone start (s)');
  1212. ylabel('DeltaF/F (Residualized)');
  1213. nexttile; hold on;
  1214. plot_resid_population(allOUT, ...
  1215. 'Event',"tone", 'CellType',"d1", 'Tone',"high", ...
  1216. 'Label',"D1 high",'Color',highToneColor);
  1217. plot_resid_population(allOUT, ...
  1218. 'Event',"tone", 'CellType',"d1", 'Tone',"low", ...
  1219. 'Label',"D1 low",'Color',lowToneColor);
  1220. xlim([-1 2]); legend;
  1221. xlabel('Time from tone start (s)');
  1222. ylabel('DeltaF/F (Residualized)');
  1223. %% D1 A2A Tone-aligned activity by outcome type - BASE
  1224. figure;
  1225. tiledlayout(1,2);
  1226. nexttile;
  1227. hold on;
  1228. plot_resid_population(allOUT, ...
  1229. 'Event',"tone", 'CellType',"a2a", 'Outcome',"hit", ...
  1230. 'Label',"A2A hit",'Color',hitColor);
  1231. plot_resid_population(allOUT, ...
  1232. 'Event',"tone", 'CellType',"a2a", 'Outcome',"cr", ...
  1233. 'Label',"A2A CR",'Color',CRColor);
  1234. plot_resid_population(allOUT, ...
  1235. 'Event',"tone", 'CellType',"a2a", 'Outcome',"fa", ...
  1236. 'Label',"A2A FA",'Color',FAColor);
  1237. xlim([-1 2]); legend;
  1238. xlabel('Time from tone start (s)');
  1239. ylabel('DeltaF/F (Residualized)');
  1240. nexttile; hold on;
  1241. plot_resid_population(allOUT, ...
  1242. 'Event',"tone", 'CellType',"d1", 'Outcome',"hit", ...
  1243. 'Label',"D1 hit",'Color',hitColor);
  1244. plot_resid_population(allOUT, ...
  1245. 'Event',"tone", 'CellType',"d1", 'Outcome',"cr", ...
  1246. 'Label',"D1 CR",'Color',CRColor);
  1247. plot_resid_population(allOUT, ...
  1248. 'Event',"tone", 'CellType',"d1", 'Outcome',"fa", ...
  1249. 'Label',"D1 FA",'Color',FAColor);
  1250. xlim([-1 2]); legend;
  1251. xlabel('Time from tone start (s)');
  1252. ylabel('DeltaF/F (Residualized)');
  1253. %% D1 A2A Tone-aligned activity by outcome type - BASE + Lick + Withholding
  1254. figure;
  1255. tiledlayout(1,2);
  1256. nexttile;
  1257. hold on;
  1258. plot_resid_population(allOUT, ...
  1259. 'Event',"tone_LW", 'CellType',"a2a", 'Outcome',"hit", ...
  1260. 'Label',"A2A hit",'Color',hitColor);
  1261. plot_resid_population(allOUT, ...
  1262. 'Event',"tone_LW", 'CellType',"a2a", 'Outcome',"cr", ...
  1263. 'Label',"A2A CR",'Color',CRColor);
  1264. plot_resid_population(allOUT, ...
  1265. 'Event',"tone_LW", 'CellType',"a2a", 'Outcome',"fa", ...
  1266. 'Label',"A2A FA",'Color',FAColor);
  1267. xlim([-1 2]); legend;
  1268. xlabel('Time from tone start (s)');
  1269. ylabel('DeltaF/F (Residualized)');
  1270. nexttile; hold on;
  1271. plot_resid_population(allOUT, ...
  1272. 'Event',"tone_LW", 'CellType',"d1", 'Outcome',"hit", ...
  1273. 'Label',"D1 hit",'Color',hitColor);
  1274. plot_resid_population(allOUT, ...
  1275. 'Event',"tone_LW", 'CellType',"d1", 'Outcome',"cr", ...
  1276. 'Label',"D1 CR",'Color',CRColor);
  1277. plot_resid_population(allOUT, ...
  1278. 'Event',"tone_LW", 'CellType',"d1", 'Outcome',"fa", ...
  1279. 'Label',"D1 FA",'Color',FAColor);
  1280. xlim([-1 2]); legend;
  1281. xlabel('Time from tone start (s)');
  1282. ylabel('DeltaF/F (Residualized)');
  1283. %
  1284. figure;
  1285. tiledlayout(1,2);
  1286. nexttile;
  1287. hold on;
  1288. plot_resid_population(allOUT, ...
  1289. 'Event',"trialStart", 'CellType',"a2a", 'Outcome',"hit", ...
  1290. 'Label',"A2A hit",'Color',hitColor);
  1291. plot_resid_population(allOUT, ...
  1292. 'Event',"trialStart", 'CellType',"a2a", 'Outcome',"cr", ...
  1293. 'Label',"A2A CR",'Color',CRColor);
  1294. plot_resid_population(allOUT, ...
  1295. 'Event',"trialStart", 'CellType',"a2a", 'Outcome',"fa", ...
  1296. 'Label',"A2A FA",'Color',FAColor);
  1297. xlim([-1 2]); legend;
  1298. xlabel('Time from trial start (s)');
  1299. ylabel('DeltaF/F (Residualized)');
  1300. nexttile; hold on;
  1301. plot_resid_population(allOUT, ...
  1302. 'Event',"trialStart", 'CellType',"d1", 'Outcome',"hit", ...
  1303. 'Label',"D1 hit",'Color',hitColor);
  1304. plot_resid_population(allOUT, ...
  1305. 'Event',"trialStart", 'CellType',"d1", 'Outcome',"cr", ...
  1306. 'Label',"D1 CR",'Color',CRColor);
  1307. plot_resid_population(allOUT, ...
  1308. 'Event',"trialStart", 'CellType',"d1", 'Outcome',"fa", ...
  1309. 'Label',"D1 FA",'Color',FAColor);
  1310. xlim([-1 2]); legend;
  1311. xlabel('Time from trial start (s)');
  1312. ylabel('DeltaF/F (Residualized)');
  1313. %% Comparing base, baseL, baseW and baseLW - full time
  1314. % Requires in workspace:
  1315. % fits, fitsL, fitsW, fitsLW (same length, same stream order)
  1316. % summary (table with animal_id, cell_type, failed)
  1317. %
  1318. % Output:
  1319. % streamTbl, animalTbl
  1320. % plots for ΔMSE and betas
  1321. % ---------- SETTINGS ----------
  1322. USE_BASE_LAMBDA_FOR_ALL = false; % true => evaluate all models at base lambda_star (Option B)
  1323. MIN_STREAMS_PER_ANIMAL = 1; % if you have multiple fibers per animal, can average
  1324. % Beta label strings (adjust if needed)
  1325. LICK_LABEL_CANDIDATES = ["LickRate","lickRate","LICKRATE"];
  1326. WITHHOLD_LABEL_CANDIDATES= ["Withhold","Withholding","withhold","WITHHOLD","WithholdBoxcar"];
  1327. % ---------- BASIC SANITY ----------
  1328. n = numel(fits);
  1329. assert(numel(fitsL)==n && numel(fitsW)==n && numel(fitsLW)==n, "fits arrays differ in length.");
  1330. assert(height(summary)==n, "summary height must match fits length.");
  1331. ok = ~summary.failed;
  1332. fprintf("Streams total: %d | ok: %d | failed: %d\n", n, nnz(ok), nnz(~ok));
  1333. % ---------- Helper: get CV MSE at lambda_star ----------
  1334. get_mse_at_lambda = @(fit, lam) fit.cv.cv_mse( find(fit.cv.lambdas==lam, 1, 'first') );
  1335. % ---------- Compute per-stream MSEs ----------
  1336. mseB = nan(n,1);
  1337. mseL = nan(n,1);
  1338. mseW = nan(n,1);
  1339. mseLW = nan(n,1);
  1340. lamB = nan(n,1);
  1341. lamL = nan(n,1);
  1342. lamW = nan(n,1);
  1343. lamLW= nan(n,1);
  1344. for i = 1:n
  1345. if ~ok(i), continue; end
  1346. lamB(i) = fits(i).cv.lambda_star;
  1347. lamL(i) = fitsL(i).cv.lambda_star;
  1348. lamW(i) = fitsW(i).cv.lambda_star;
  1349. lamLW(i) = fitsLW(i).cv.lambda_star;
  1350. if USE_BASE_LAMBDA_FOR_ALL
  1351. lam = lamB(i);
  1352. mseB(i) = get_mse_at(fits(i), lam);
  1353. mseL(i) = get_mse_at(fitsL(i), lam);
  1354. mseW(i) = get_mse_at(fitsW(i), lam);
  1355. mseLW(i) = get_mse_at(fitsLW(i), lam);
  1356. else
  1357. mseB(i) = get_mse_star(fits(i));
  1358. mseL(i) = get_mse_star(fitsL(i));
  1359. mseW(i) = get_mse_star(fitsW(i));
  1360. mseLW(i) = get_mse_star(fitsLW(i));
  1361. end
  1362. end
  1363. % ---------- ΔMSEs (negative is improvement) ----------
  1364. dL = mseL - mseB; % add lick to base
  1365. dW = mseW - mseB; % add withhold to base
  1366. dLW = mseLW - mseB; % add both to base
  1367. % Incremental adds
  1368. dAddL_givenW = mseLW - mseW; % add lick on top of withhold
  1369. dAddW_givenL = mseLW - mseL; % add withhold on top of lick
  1370. % ---------- Build stream-level table ----------
  1371. streamTbl = table();
  1372. streamTbl.stream_uid = summary.stream_uid;
  1373. streamTbl.session_uid = summary.session_uid;
  1374. streamTbl.animal_id = summary.animal_id;
  1375. streamTbl.cell_type = summary.cell_type;
  1376. streamTbl.ok = ok;
  1377. streamTbl.mseB = mseB;
  1378. streamTbl.mseL = mseL;
  1379. streamTbl.mseW = mseW;
  1380. streamTbl.mseLW = mseLW;
  1381. streamTbl.dL = dL;
  1382. streamTbl.dW = dW;
  1383. streamTbl.dLW = dLW;
  1384. streamTbl.dAddL_givenW = dAddL_givenW;
  1385. streamTbl.dAddW_givenL = dAddW_givenL;
  1386. streamTbl.lamB = lamB;
  1387. streamTbl.lamL = lamL;
  1388. streamTbl.lamW = lamW;
  1389. streamTbl.lamLW = lamLW;
  1390. % ---------- Animal-level aggregation (recommended) ----------
  1391. % Average across streams within animal (and within cell_type if you have both)
  1392. streamOk = streamTbl(streamTbl.ok,:);
  1393. [G, animalKeys, cellKeys] = findgroups(streamOk.animal_id, streamOk.cell_type);
  1394. animalTbl = table();
  1395. animalTbl.animal_id = animalKeys;
  1396. animalTbl.cell_type = cellKeys;
  1397. animalTbl.mseB = splitapply(@mean, streamOk.mseB, G);
  1398. animalTbl.mseL = splitapply(@mean, streamOk.mseL, G);
  1399. animalTbl.mseW = splitapply(@mean, streamOk.mseW, G);
  1400. animalTbl.mseLW = splitapply(@mean, streamOk.mseLW, G);
  1401. animalTbl.dL = splitapply(@mean, streamOk.dL, G);
  1402. animalTbl.dW = splitapply(@mean, streamOk.dW, G);
  1403. animalTbl.dLW = splitapply(@mean, streamOk.dLW, G);
  1404. animalTbl.dAddL_givenW = splitapply(@mean, streamOk.dAddL_givenW, G);
  1405. animalTbl.dAddW_givenL = splitapply(@mean, streamOk.dAddW_givenL, G);
  1406. animalTbl.n_streams = splitapply(@numel, streamOk.dLW, G);
  1407. % optional filter
  1408. animalTbl = animalTbl(animalTbl.n_streams >= MIN_STREAMS_PER_ANIMAL, :);
  1409. % ---------- Normalize deltas (optional but handy) ----------
  1410. animalTbl.fracImprove_L = (animalTbl.mseB - animalTbl.mseL) ./ animalTbl.mseB;
  1411. animalTbl.fracImprove_W = (animalTbl.mseB - animalTbl.mseW) ./ animalTbl.mseB;
  1412. animalTbl.fracImprove_LW = (animalTbl.mseB - animalTbl.mseLW) ./ animalTbl.mseB;
  1413. % ---------- Plot: ΔMSE by model (animal-level dots) ----------
  1414. figure; hold on;
  1415. isD1 = animalTbl.cell_type=="d1";
  1416. isA2A = animalTbl.cell_type=="a2a";
  1417. xD1 = 1; xA2A = 2;
  1418. % Base->Lick
  1419. scatter(xD1*ones(nnz(isD1),1), animalTbl.dL(isD1), 'filled');
  1420. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dL(isA2A),'filled');
  1421. yline(0,'--');
  1422. title("ΔMSE: Base -> Base+Lick (animal means)");
  1423. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1424. ylabel("ΔMSE (neg = better)");
  1425. hold off;
  1426. figure; hold on;
  1427. scatter(xD1*ones(nnz(isD1),1), animalTbl.dW(isD1), 'filled');
  1428. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dW(isA2A),'filled');
  1429. yline(0,'--');
  1430. title("ΔMSE: Base -> Base+Withhold (animal means)");
  1431. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1432. ylabel("ΔMSE (neg = better)");
  1433. hold off;
  1434. figure; hold on;
  1435. scatter(xD1*ones(nnz(isD1),1), animalTbl.dLW(isD1), 'filled');
  1436. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dLW(isA2A),'filled');
  1437. yline(0,'--');
  1438. title("ΔMSE: Base -> Base+Lick+Withhold (animal means)");
  1439. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1440. ylabel("ΔMSE (neg = better)");
  1441. hold off;
  1442. figure; hold on;
  1443. scatter(xD1*ones(nnz(isD1),1), animalTbl.dAddW_givenL(isD1), 'filled');
  1444. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dAddW_givenL(isA2A),'filled');
  1445. yline(0,'--');
  1446. title("ΔMSE: Add Withhold on top of Lick (animal means)");
  1447. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1448. ylabel("ΔMSE (neg = better)");
  1449. hold off;
  1450. figure; hold on;
  1451. scatter(xD1*ones(nnz(isD1),1), animalTbl.dAddL_givenW(isD1), 'filled');
  1452. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dAddL_givenW(isA2A),'filled');
  1453. yline(0,'--');
  1454. title("ΔMSE: Add Lick on top of Withhold (animal means)");
  1455. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1456. ylabel("ΔMSE (neg = better)");
  1457. hold off;
  1458. % ---------- Beta extraction per stream ----------
  1459. betaL = nan(n,1);
  1460. betaW = nan(n,1);
  1461. for i = 1:n
  1462. if ~ok(i), continue; end
  1463. if ~isfield(fitsLW(i),'model') || ~isfield(fitsLW(i).model,'labels'), continue; end
  1464. labels = string(fitsLW(i).model.labels);
  1465. % find lick label
  1466. idxL = find(ismember(labels, LICK_LABEL_CANDIDATES), 1, 'first');
  1467. idxW = find(ismember(labels, WITHHOLD_LABEL_CANDIDATES), 1, 'first');
  1468. if ~isempty(idxL)
  1469. betaL(i) = fitsLW(i).model.beta_hat(1 + idxL);
  1470. end
  1471. if ~isempty(idxW)
  1472. betaW(i) = fitsLW(i).model.beta_hat(1 + idxW);
  1473. end
  1474. end
  1475. streamTbl.betaLick = betaL;
  1476. streamTbl.betaWithhold = betaW;
  1477. % animal-level betas
  1478. streamOk = streamTbl(streamTbl.ok,:);
  1479. [G, animalKeys, cellKeys] = findgroups(streamOk.animal_id, streamOk.cell_type);
  1480. betaTbl = table();
  1481. betaTbl.animal_id = animalKeys;
  1482. betaTbl.cell_type = cellKeys;
  1483. betaTbl.betaLick = splitapply(@mean, streamOk.betaLick, G);
  1484. betaTbl.betaWithhold = splitapply(@mean, streamOk.betaWithhold, G);
  1485. % ---------- Plot betas ----------
  1486. figure; hold on;
  1487. isD1 = betaTbl.cell_type=="d1";
  1488. isA2A = betaTbl.cell_type=="a2a";
  1489. scatter(1*ones(nnz(isD1),1), betaTbl.betaLick(isD1), 'filled');
  1490. scatter(2*ones(nnz(isA2A),1), betaTbl.betaLick(isA2A), 'filled');
  1491. yline(0,'--');
  1492. title("β for LickRate (from Base+Lick+Withhold model; animal means)");
  1493. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1494. ylabel("β (Δsignal per 1 SD lickRate)");
  1495. hold off;
  1496. figure; hold on;
  1497. scatter(1*ones(nnz(isD1),1), betaTbl.betaWithhold(isD1), 'filled');
  1498. scatter(2*ones(nnz(isA2A),1), betaTbl.betaWithhold(isA2A), 'filled');
  1499. yline(0,'--');
  1500. title("β for Withhold (from Base+Lick+Withhold model; animal means)");
  1501. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1502. ylabel("β (state effect; sign meaningful)");
  1503. hold off;
  1504. fprintf("Done. streamTbl, animalTbl, betaTbl in workspace.\n");
  1505. %% Comparing base, baseL, baseW and baseLW (RESPONSE-WINDOW MSE @ each model's lambda_star)
  1506. % Requires in workspace:
  1507. % fitsBase, fitsL, fitsW, fitsLW (same length, same stream order)
  1508. % summary (table with animal_id, cell_type, failed)
  1509. %
  1510. % Output:
  1511. % streamTbl, animalTbl, betaTbl
  1512. % plots for ΔMSE and betas
  1513. % ---------- SETTINGS ----------
  1514. MIN_STREAMS_PER_ANIMAL = 1; % if you have multiple fibers per animal, can average
  1515. % Beta label strings (adjust if needed)
  1516. LICK_LABEL_CANDIDATES = ["LickRate","lickRate","LICKRATE"];
  1517. WITHHOLD_LABEL_CANDIDATES = ["Withhold","Withholding","withhold","WITHHOLD","WithholdBoxcar"];
  1518. % ---------- BASIC SANITY ----------
  1519. n = numel(fitsBase);
  1520. assert(numel(fitsL)==n && numel(fitsW)==n && numel(fitsLW)==n, "fits arrays differ in length.");
  1521. assert(height(summary)==n, "summary height must match fits length.");
  1522. ok = ~summary.failed;
  1523. fprintf("Streams total: %d | ok: %d | failed: %d\n", n, nnz(ok), nnz(~ok));
  1524. % ---------- Helper: cached resp-window MSE at lambda_star ----------
  1525. get_mse_resp_star = @(fit) fit.cv.mse_resp_go_at_lambda_star;
  1526. % ---------- Compute per-stream MSEs (response window only) ----------
  1527. mseB = nan(n,1);
  1528. mseL = nan(n,1);
  1529. mseW = nan(n,1);
  1530. mseLW = nan(n,1);
  1531. for i = 1:n
  1532. if ~ok(i), continue; end
  1533. % Each is a scalar already evaluated at THAT model's lambda_star
  1534. mseB(i) = get_mse_resp_star(fitsBase(i));
  1535. mseL(i) = get_mse_resp_star(fitsL(i));
  1536. mseW(i) = get_mse_resp_star(fitsW(i));
  1537. mseLW(i) = get_mse_resp_star(fitsLW(i));
  1538. end
  1539. % Optional sanity print
  1540. fprintf("NaNs in resp-window MSEs among ok? B=%d L=%d W=%d LW=%d\n", ...
  1541. nnz(isnan(mseB(ok))), nnz(isnan(mseL(ok))), nnz(isnan(mseW(ok))), nnz(isnan(mseLW(ok))));
  1542. % ---------- ΔMSEs (negative is improvement) ----------
  1543. dL = mseL - mseB; % add lick to base
  1544. dW = mseW - mseB; % add withhold to base
  1545. dLW = mseLW - mseB; % add both to base
  1546. % Incremental adds
  1547. dAddL_givenW = mseLW - mseW; % add lick on top of withhold
  1548. dAddW_givenL = mseLW - mseL; % add withhold on top of lick
  1549. % ---------- Build stream-level table ----------
  1550. streamTbl = table();
  1551. streamTbl.stream_uid = summary.stream_uid;
  1552. streamTbl.session_uid = summary.session_uid;
  1553. streamTbl.animal_id = summary.animal_id;
  1554. streamTbl.cell_type = summary.cell_type;
  1555. streamTbl.ok = ok;
  1556. streamTbl.mseB = mseB;
  1557. streamTbl.mseL = mseL;
  1558. streamTbl.mseW = mseW;
  1559. streamTbl.mseLW = mseLW;
  1560. streamTbl.dL = dL;
  1561. streamTbl.dW = dW;
  1562. streamTbl.dLW = dLW;
  1563. streamTbl.dAddL_givenW = dAddL_givenW;
  1564. streamTbl.dAddW_givenL = dAddW_givenL;
  1565. % ---------- Animal-level aggregation (recommended) ----------
  1566. % Average across streams within animal (and within cell_type if you have both)
  1567. streamOk = streamTbl(streamTbl.ok,:);
  1568. [G, animalKeys, cellKeys] = findgroups(streamOk.animal_id, streamOk.cell_type);
  1569. animalTbl = table();
  1570. animalTbl.animal_id = animalKeys;
  1571. animalTbl.cell_type = cellKeys;
  1572. animalTbl.mseB = splitapply(@mean, streamOk.mseB, G);
  1573. animalTbl.mseL = splitapply(@mean, streamOk.mseL, G);
  1574. animalTbl.mseW = splitapply(@mean, streamOk.mseW, G);
  1575. animalTbl.mseLW = splitapply(@mean, streamOk.mseLW, G);
  1576. animalTbl.dL = splitapply(@mean, streamOk.dL, G);
  1577. animalTbl.dW = splitapply(@mean, streamOk.dW, G);
  1578. animalTbl.dLW = splitapply(@mean, streamOk.dLW, G);
  1579. animalTbl.dAddL_givenW = splitapply(@mean, streamOk.dAddL_givenW, G);
  1580. animalTbl.dAddW_givenL = splitapply(@mean, streamOk.dAddW_givenL, G);
  1581. animalTbl.n_streams = splitapply(@numel, streamOk.dLW, G);
  1582. % optional filter
  1583. animalTbl = animalTbl(animalTbl.n_streams >= MIN_STREAMS_PER_ANIMAL, :);
  1584. % ---------- Normalize deltas (optional but handy) ----------
  1585. animalTbl.fracImprove_L = (animalTbl.mseB - animalTbl.mseL) ./ animalTbl.mseB;
  1586. animalTbl.fracImprove_W = (animalTbl.mseB - animalTbl.mseW) ./ animalTbl.mseB;
  1587. animalTbl.fracImprove_LW = (animalTbl.mseB - animalTbl.mseLW) ./ animalTbl.mseB;
  1588. % ---------- Plot: ΔMSE by model (animal-level dots) ----------
  1589. figure; hold on;
  1590. isD1 = animalTbl.cell_type=="d1";
  1591. isA2A = animalTbl.cell_type=="a2a";
  1592. xD1 = 1; xA2A = 2;
  1593. % Base->Lick
  1594. scatter(xD1*ones(nnz(isD1),1), animalTbl.dL(isD1), 'filled');
  1595. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dL(isA2A),'filled');
  1596. yline(0,'--');
  1597. title("ΔMSE (resp win): Base -> Base+Lick (animal means)");
  1598. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1599. ylabel("ΔMSE (neg = better)");
  1600. hold off;
  1601. figure; hold on;
  1602. scatter(xD1*ones(nnz(isD1),1), animalTbl.dW(isD1), 'filled');
  1603. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dW(isA2A),'filled');
  1604. yline(0,'--');
  1605. title("ΔMSE (resp win): Base -> Base+Withhold (animal means)");
  1606. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1607. ylabel("ΔMSE (neg = better)");
  1608. hold off;
  1609. figure; hold on;
  1610. scatter(xD1*ones(nnz(isD1),1), animalTbl.dLW(isD1), 'filled');
  1611. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dLW(isA2A),'filled');
  1612. yline(0,'--');
  1613. title("ΔMSE (resp win): Base -> Base+Lick+Withhold (animal means)");
  1614. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1615. ylabel("ΔMSE (neg = better)");
  1616. hold off;
  1617. figure; hold on;
  1618. scatter(xD1*ones(nnz(isD1),1), animalTbl.dAddW_givenL(isD1), 'filled');
  1619. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dAddW_givenL(isA2A),'filled');
  1620. yline(0,'--');
  1621. title("ΔMSE (resp win): Add Withhold on top of Lick (animal means)");
  1622. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1623. ylabel("ΔMSE (neg = better)");
  1624. hold off;
  1625. figure; hold on;
  1626. scatter(xD1*ones(nnz(isD1),1), animalTbl.dAddL_givenW(isD1), 'filled');
  1627. scatter(xA2A*ones(nnz(isA2A),1),animalTbl.dAddL_givenW(isA2A),'filled');
  1628. yline(0,'--');
  1629. title("ΔMSE (resp win): Add Lick on top of Withhold (animal means)");
  1630. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1631. ylabel("ΔMSE (neg = better)");
  1632. hold off;
  1633. % ---------- Beta extraction per stream ----------
  1634. betaL = nan(n,1);
  1635. betaW = nan(n,1);
  1636. for i = 1:n
  1637. if ~ok(i), continue; end
  1638. if ~isfield(fitsLW(i),'model') || ~isfield(fitsLW(i).model,'labels'), continue; end
  1639. labels = string(fitsLW(i).model.labels);
  1640. % find lick label
  1641. idxL = find(ismember(labels, LICK_LABEL_CANDIDATES), 1, 'first');
  1642. idxW = find(ismember(labels, WITHHOLD_LABEL_CANDIDATES), 1, 'first');
  1643. if ~isempty(idxL)
  1644. betaL(i) = fitsLW(i).model.beta_hat(1 + idxL); % +1 for intercept
  1645. end
  1646. if ~isempty(idxW)
  1647. betaW(i) = fitsLW(i).model.beta_hat(1 + idxW); % +1 for intercept
  1648. end
  1649. end
  1650. streamTbl.betaLick = betaL;
  1651. streamTbl.betaWithhold = betaW;
  1652. % animal-level betas
  1653. streamOk = streamTbl(streamTbl.ok,:);
  1654. [G, animalKeys, cellKeys] = findgroups(streamOk.animal_id, streamOk.cell_type);
  1655. betaTbl = table();
  1656. betaTbl.animal_id = animalKeys;
  1657. betaTbl.cell_type = cellKeys;
  1658. betaTbl.betaLick = splitapply(@mean, streamOk.betaLick, G);
  1659. betaTbl.betaWithhold = splitapply(@mean, streamOk.betaWithhold, G);
  1660. % ---------- Plot betas ----------
  1661. figure; hold on;
  1662. isD1 = betaTbl.cell_type=="d1";
  1663. isA2A = betaTbl.cell_type=="a2a";
  1664. scatter(1*ones(nnz(isD1),1), betaTbl.betaLick(isD1), 'filled');
  1665. scatter(2*ones(nnz(isA2A),1), betaTbl.betaLick(isA2A), 'filled');
  1666. yline(0,'--');
  1667. title("β for LickRate (from Base+Lick+Withhold model; animal means)");
  1668. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1669. ylabel("β (Δsignal per 1 SD lickRate)");
  1670. hold off;
  1671. figure; hold on;
  1672. scatter(1*ones(nnz(isD1),1), betaTbl.betaWithhold(isD1), 'filled');
  1673. scatter(2*ones(nnz(isA2A),1), betaTbl.betaWithhold(isA2A), 'filled');
  1674. yline(0,'--');
  1675. title("β for Withhold (from Base+Lick+Withhold model; animal means)");
  1676. set(gca,'XTick',[1 2],'XTickLabel',{'D1','A2A'});
  1677. ylabel("β (state effect; sign meaningful)");
  1678. hold off;
  1679. fprintf("Done. streamTbl, animalTbl, betaTbl in workspace.\n");
  1680. %% Partial predictions - generate
  1681. out = partialPred_lick_withhold(streams,trials,licks,fits,cfg);
  1682. %% Partial predictions - plot
  1683. withholdColor = [0.85 0.33 0.10];
  1684. lickColor = [0 0 0];
  1685. figure;
  1686. tiledlayout(2,2);
  1687. nexttile;
  1688. S_d1a_nogo = kernel_plot(KDd1, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  1689. plot_partial_component(out, "withhold", "d1", "nogo", ...
  1690. 'Color', withholdColor, ...
  1691. 'XLine0', true);
  1692. plot_partial_component(out, "lick", "d1", "nogo", ...
  1693. 'Color', lickColor, ...
  1694. 'XLine0', true);
  1695. ylim([-.3,1.5]);
  1696. xlim([0,1]);
  1697. title("D1 No Go - Partial model contributions (Xβ)");
  1698. grid off
  1699. text(0.98, 0.97, 'No Go Tone', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 10);
  1700. text(0.98, 0.92, 'Withhold', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', withholdColor,'FontWeight','bold','FontSize', 10);
  1701. text(0.98, 0.87, 'Lick rate', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', lickColor,'FontWeight','bold','FontSize', 10);
  1702. ylabel("Partial prediction (ΔF/F, a.u.)");
  1703. yline(0,'k--');
  1704. nexttile;
  1705. S_d1_go = kernel_plot(KDd1, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', goColor, 'FaceAlpha', 0.15);
  1706. plot_partial_component(out, "withhold", "d1", "go", ...
  1707. 'Color', withholdColor, ...
  1708. 'XLine0', true);
  1709. plot_partial_component(out, "lick", "d1", "go", ...
  1710. 'Color', lickColor, ...
  1711. 'XLine0', true);
  1712. ylim([-.3,1.5]);
  1713. xlim([0,1]);
  1714. title("D1 Go - Partial model contributions (Xβ)");
  1715. grid off
  1716. text(0.98, 0.97, 'Go Tone', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 10);
  1717. text(0.98, 0.92, 'Withhold', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', withholdColor,'FontWeight','bold','FontSize', 10);
  1718. text(0.98, 0.87, 'Lick rate', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', lickColor,'FontWeight','bold','FontSize', 10);
  1719. ylabel("Partial prediction (ΔF/F, a.u.)");
  1720. yline(0,'k--');
  1721. nexttile;
  1722. S_a2a_nogo = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 -0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', nogoColor, 'FaceAlpha', 0.15);
  1723. plot_partial_component(out, "withhold", "a2a", "nogo", ...
  1724. 'Color', withholdColor, ... % orange
  1725. 'XLine0', true);
  1726. plot_partial_component(out, "lick", "a2a", "nogo", ...
  1727. 'Color', lickColor, ...
  1728. 'XLine0', true);
  1729. ylim([-.3,.6]);
  1730. xlim([0,1]);
  1731. title("A2A No Go - Partial model contributions (Xβ)");
  1732. grid off
  1733. text(0.98, 0.97, 'No Go Tone', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', nogoColor,'FontWeight','bold','FontSize', 10);
  1734. text(0.98, 0.92, 'Withhold', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', withholdColor,'FontWeight','bold','FontSize', 10);
  1735. text(0.98, 0.87, 'Lick rate', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', lickColor,'FontWeight','bold','FontSize', 10);
  1736. ylabel("Partial prediction (ΔF/F, a.u.)");
  1737. yline(0,'k--');
  1738. nexttile;
  1739. S_a2a_go = kernel_plot(KDa2a, 'Which',["tone_main","tone_F"], 'Weights',[1 0.5],'MapToneFTo',"goTone",'Title',"Go vs NoGo",'Color', goColor, 'FaceAlpha', 0.15);
  1740. plot_partial_component(out, "withhold", "a2a", "go", ...
  1741. 'Color', withholdColor, ... % orange
  1742. 'XLine0', true);
  1743. plot_partial_component(out, "lick", "a2a", "go", ...
  1744. 'Color', lickColor, ...
  1745. 'XLine0', true);
  1746. ylim([-.3,.6]);
  1747. xlim([0,1]);
  1748. title("A2A Go - Partial model contributions (Xβ)");
  1749. grid off
  1750. text(0.98, 0.97, 'Go Tone', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', goColor,'FontWeight','bold','FontSize', 10);
  1751. text(0.98, 0.92, 'Withhold', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', withholdColor,'FontWeight','bold','FontSize', 10);
  1752. text(0.98, 0.87, 'Lick rate', 'Units','normalized','HorizontalAlignment','right', 'VerticalAlignment','top','Color', lickColor,'FontWeight','bold','FontSize', 10);
  1753. ylabel("Partial prediction (ΔF/F, a.u.)");
  1754. yline(0,'k--')
  1755. %% Local Helper Functions
  1756. function fit = fit_one_stream_kernelGLM(stream, trials, cfg, lickTimes)
  1757. % ---------- Load t, y ----------
  1758. t = h5read(cfg.h5file, stream.(cfg.time_field){1}); t = t(:);
  1759. y = h5read(cfg.h5file, stream.(cfg.signal_field){1}); y = y(:);
  1760. assert(numel(t)==numel(y), "t and y lengths mismatch.");
  1761. % Optional: remove mean (recommended, helps intercept interpretability)
  1762. y0 = y;
  1763. y = y - mean(y, 'omitnan');
  1764. % ---------- Session trials ----------
  1765. sessTrials = trials(trials.session_uid == stream.session_uid & trials.is_valid == 1, :);
  1766. assert(height(sessTrials) > 10, "Too few valid trials for session.");
  1767. % ---------- Build response-window masks (fixed 1.15s post-tone) ----------
  1768. respWin_s = 1.15;
  1769. % Filter trials used to define response windows:
  1770. % - valid (already filtered above, but keep robust)
  1771. % - tone_type is go/nogo (exclude "none")
  1772. % - outcome not "restart"
  1773. tone_type = string(sessTrials.tone_type);
  1774. outcome = string(sessTrials.outcome);
  1775. useTrials = sessTrials.is_valid == 1 & ...
  1776. (tone_type == "go" | tone_type == "nogo") & ...
  1777. (outcome ~= "restart") & ...
  1778. ~isnan(sessTrials.t_tone_start);
  1779. t0s = sessTrials.t_tone_start(useTrials);
  1780. tt = tone_type(useTrials);
  1781. respMask_all = false(size(t));
  1782. respMask_go = false(size(t));
  1783. respMask_nogo = false(size(t));
  1784. for k0 = 1:numel(t0s)
  1785. t0 = t0s(k0);
  1786. t1 = t0 + respWin_s;
  1787. m = (t >= t0) & (t < t1);
  1788. respMask_all = respMask_all | m;
  1789. if tt(k0) == "go"
  1790. respMask_go = respMask_go | m;
  1791. else % "nogo"
  1792. respMask_nogo = respMask_nogo | m;
  1793. end
  1794. end
  1795. % fprintf("RespMask: all=%d go=%d nogo=%d samples (dt=%.3f)\n", ...
  1796. % nnz(respMask_all), nnz(respMask_go), nnz(respMask_nogo), cfg.dt);
  1797. dt = cfg.dt;
  1798. % ---------- Build design matrix X (NO intercept here; add later) ----------
  1799. [X, labels, bases, lags] = build_design_matrix(t, sessTrials, cfg, lickTimes);
  1800. % ---------- Blocked time CV for ridge ----------
  1801. lambdas = cfg.ridge.lambdas(:);
  1802. Kfold = cfg.ridge.kfold;
  1803. T = numel(y);
  1804. edges = round(linspace(1, T+1, Kfold+1));
  1805. fold_mse = nan(Kfold, numel(lambdas));
  1806. fold_mse_resp_all = nan(Kfold, numel(lambdas));
  1807. fold_mse_resp_go = nan(Kfold, numel(lambdas));
  1808. fold_mse_resp_nogo = nan(Kfold, numel(lambdas));
  1809. for k = 1:Kfold
  1810. test_idx = edges(k):edges(k+1)-1;
  1811. train_idx = setdiff(1:T, test_idx);
  1812. Xtr0 = X(train_idx,:);
  1813. Xte0 = X(test_idx,:);
  1814. ytr = y(train_idx);
  1815. yte = y(test_idx);
  1816. % fold-specific standardization (TRAIN ONLY)
  1817. mu = mean(Xtr0, 1);
  1818. sd = std(Xtr0, 0, 1);
  1819. sd(sd==0) = 1;
  1820. Xtr = (Xtr0 - mu) ./ sd;
  1821. Xte = (Xte0 - mu) ./ sd;
  1822. for il = 1:numel(lambdas)
  1823. lam = lambdas(il);
  1824. beta = ridge(ytr, Xtr, lam, 0); % returns [intercept; coefs]
  1825. yhat = beta(1) + Xte * beta(2:end);
  1826. fold_mse(k, il) = mean((yte - yhat).^2);
  1827. % --- Response-window MSE on HELD-OUT samples only ---
  1828. % Map global response mask into the fold's test segment
  1829. m_all = respMask_all(test_idx);
  1830. m_go = respMask_go(test_idx);
  1831. m_nogo = respMask_nogo(test_idx);
  1832. % Pooled response-window MSE
  1833. if any(m_all)
  1834. fold_mse_resp_all(k, il) = mean((yte(m_all) - yhat(m_all)).^2);
  1835. else
  1836. fold_mse_resp_all(k, il) = NaN;
  1837. end
  1838. % Go-only response-window MSE
  1839. if any(m_go)
  1840. fold_mse_resp_go(k, il) = mean((yte(m_go) - yhat(m_go)).^2);
  1841. else
  1842. fold_mse_resp_go(k, il) = NaN;
  1843. end
  1844. % NoGo-only response-window MSE
  1845. if any(m_nogo)
  1846. fold_mse_resp_nogo(k, il) = mean((yte(m_nogo) - yhat(m_nogo)).^2);
  1847. else
  1848. fold_mse_resp_nogo(k, il) = NaN;
  1849. end
  1850. end
  1851. end
  1852. cv_mse = mean(fold_mse, 1);
  1853. [~, bestIdx] = min(cv_mse);
  1854. lambda_star = lambdas(bestIdx);
  1855. cv_mse_resp_all = nanmean(fold_mse_resp_all, 1);
  1856. cv_mse_resp_go = nanmean(fold_mse_resp_go, 1);
  1857. cv_mse_resp_nogo = nanmean(fold_mse_resp_nogo, 1);
  1858. mse_resp_all_at_lambda_star = cv_mse_resp_all(bestIdx);
  1859. mse_resp_go_at_lambda_star = cv_mse_resp_go(bestIdx);
  1860. mse_resp_nogo_at_lambda_star = cv_mse_resp_nogo(bestIdx);
  1861. % after lambda_star is chosen:
  1862. mu_all = mean(X,1);
  1863. sd_all = std(X,0,1);
  1864. sd_all(sd_all==0) = 1;
  1865. Xz = (X - mu_all) ./ sd_all;
  1866. % ---------- Fit final model on all timepoints ----------
  1867. beta_hat = ridge(y, Xz, lambda_star, 0);
  1868. % ---------- Reconstruct kernels (in original signal units) ----------
  1869. K = reconstruct_all_kernels(beta_hat, sd_all, labels, bases);
  1870. % ---------- Package output ----------
  1871. fit = struct();
  1872. fit.failed = false;
  1873. % Meta
  1874. fit.meta = struct( ...
  1875. 'stream_uid', string(stream.stream_uid), ...
  1876. 'session_uid', string(stream.session_uid), ...
  1877. 'animal_id', string(stream.animal_id), ...
  1878. 'cell_type', string(stream.cell_type), ...
  1879. 'side', string(stream.side), ...
  1880. 'fiber', stream.fiber, ...
  1881. 'nrxn', stream.nrxn, ...
  1882. 'go_is_high', stream.go_is_high, ...
  1883. 'task_phase', stream.task_phase, ...
  1884. 'dt', dt, ...
  1885. 'n_time', T, ...
  1886. 't0', t(1), ...
  1887. 't1', t(end), ...
  1888. 'n_trials', height(sessTrials));
  1889. % CV
  1890. fit.cv = struct( ...
  1891. 'lambdas', lambdas, ...
  1892. 'fold_mse', fold_mse, ...
  1893. 'cv_mse', cv_mse, ...
  1894. 'lambda_star', lambda_star, ...
  1895. 'kfold', Kfold, ...
  1896. 'blocked_time_cv', cfg.ridge.blocked_time_cv, ...
  1897. ...
  1898. 'fold_mse_resp_all', fold_mse_resp_all, ...
  1899. 'fold_mse_resp_go', fold_mse_resp_go, ...
  1900. 'fold_mse_resp_nogo', fold_mse_resp_nogo, ...
  1901. 'cv_mse_resp_all', cv_mse_resp_all, ...
  1902. 'cv_mse_resp_go', cv_mse_resp_go, ...
  1903. 'cv_mse_resp_nogo', cv_mse_resp_nogo, ...
  1904. 'mse_resp_all_at_lambda_star', mse_resp_all_at_lambda_star, ...
  1905. 'mse_resp_go_at_lambda_star', mse_resp_go_at_lambda_star, ...
  1906. 'mse_resp_nogo_at_lambda_star', mse_resp_nogo_at_lambda_star, ...
  1907. 'respWin_s', respWin_s ...
  1908. );
  1909. %Model
  1910. fit.model = struct( ...
  1911. 'beta_hat', beta_hat, ...
  1912. 'labels', labels, ...
  1913. 'mu_X', mu_all, ...
  1914. 'sd_X', sd_all, ...
  1915. 'y_mean_removed', true);
  1916. % Kernels
  1917. fit.lags = lags;
  1918. fit.kernels = K;
  1919. % Optional: store y summary
  1920. fit.qc = struct( ...
  1921. 'y_mean', mean(y0,'omitnan'), ...
  1922. 'y_sd', std(y0,0,'omitnan'));
  1923. end
  1924. function summary = build_kernelGLM_summary(fits)
  1925. n = numel(fits);
  1926. stream_uid = strings(n,1);
  1927. animal_id = strings(n,1);
  1928. cell_type = strings(n,1);
  1929. session_uid = strings(n,1);
  1930. lambda_star = nan(n,1);
  1931. failed = false(n,1);
  1932. for i = 1:n
  1933. if isfield(fits(i),'failed') && fits(i).failed
  1934. failed(i) = true;
  1935. if isfield(fits(i),'meta')
  1936. stream_uid(i) = fits(i).meta.stream_uid;
  1937. animal_id(i) = fits(i).meta.animal_id;
  1938. cell_type(i) = fits(i).meta.cell_type;
  1939. session_uid(i) = fits(i).meta.session_uid;
  1940. end
  1941. continue
  1942. end
  1943. stream_uid(i) = fits(i).meta.stream_uid;
  1944. animal_id(i) = fits(i).meta.animal_id;
  1945. cell_type(i) = fits(i).meta.cell_type;
  1946. session_uid(i) = fits(i).meta.session_uid;
  1947. lambda_star(i) = fits(i).cv.lambda_star;
  1948. end
  1949. summary = table(stream_uid, session_uid, animal_id, cell_type, lambda_star, failed);
  1950. end
  1951. function [sig, p, pcrit] = bh_ttest_mask(D, q, min_n)
  1952. % BH correction across time on paired t-test p-values for D(t,animal)
  1953. % D: T × nAnimals difference matrix
  1954. % q: desired FDR (e.g., 0.05)
  1955. % min_n: minimum animals needed at a timepoint to test (e.g., 3)
  1956. if nargin < 2 || isempty(q), q = 0.05; end
  1957. if nargin < 3 || isempty(min_n), min_n = 3; end
  1958. T = size(D,1);
  1959. p = nan(T,1);
  1960. for ii = 1:T
  1961. x = D(ii,:);
  1962. x = x(~isnan(x));
  1963. if numel(x) >= min_n
  1964. [~, p(ii)] = ttest(x, 0); % test mean difference vs 0
  1965. end
  1966. end
  1967. sig = false(T,1);
  1968. pcrit = NaN;
  1969. valid = ~isnan(p);
  1970. pv = p(valid);
  1971. [p_sorted, idx] = sort(pv);
  1972. m = numel(pv);
  1973. thresh = (1:m)'/m * q;
  1974. k = find(p_sorted <= thresh, 1, 'last');
  1975. if ~isempty(k)
  1976. tmp = false(m,1);
  1977. tmp(idx(1:k)) = true;
  1978. sig(valid) = tmp;
  1979. pcrit = p_sorted(k);
  1980. end
  1981. end
  1982. %get AUC differences
  1983. function delta_auc = getDeltaAUC(t,t_min,t_max,S1,S2)
  1984. %t = 0:0.025:1.5;
  1985. t = t';
  1986. %t_min = 0.8;
  1987. %t_max = 1.1;
  1988. W = (t >= t_min) & (t <= t_max);
  1989. %S1 = S_a2a_go.M;
  1990. M_go = S1;
  1991. %S2 = S_a2a_nogo.M;
  1992. M_nogo = S2;
  1993. auc_go = trapz(t(W), M_go(W,:), 1); % 1 × nAnimals
  1994. auc_nogo = trapz(t(W), M_nogo(W,:), 1); % 1 × nAnimals
  1995. delta_auc = auc_go - auc_nogo; % paired difference
  1996. end
  1997. function [delta_auc,delta_p] = getAUCsTerciles(S1,S2)
  1998. delta_auc = zeros(3,size(S1,2));
  1999. delta_auc(1,:) = getDeltaAUC(0:0.025:1.5,0,.3833,S1,S2);
  2000. delta_auc(2,:) = getDeltaAUC(0:0.025:1.5,.3834,.7666,S1,S2);
  2001. delta_auc(3,:) = getDeltaAUC(0:0.025:1.5,.7667,1.15,S1,S2);
  2002. delta_p = zeros(3,1);
  2003. [~, delta_p(1,1), ~, ~] = ttest(delta_auc(1,:), 0);
  2004. [~, delta_p(2,1), ~, ~] = ttest(delta_auc(2,:), 0);
  2005. [~, delta_p(3,1), ~, ~] = ttest(delta_auc(3,:), 0);
  2006. end
  2007. % ---- Helper function ----
  2008. function plotDeltaAUCTerciles_barScatter(dAUC_3xN, p_3, panelTitle, yLims)
  2009. dAUC = dAUC_3xN;
  2010. if size(dAUC,1) ~= 3
  2011. error('Expected dAUC to be 3xN (3 terciles by animals).');
  2012. end
  2013. N = size(dAUC,2);
  2014. p = p_3(:);
  2015. if numel(p) ~= 3
  2016. p = []; % skip p-annotation if unexpected shape
  2017. end
  2018. x = 1:3;
  2019. % mean ± SEM across animals
  2020. m = mean(dAUC, 2, 'omitnan');
  2021. s = std(dAUC, 0, 2, 'omitnan');
  2022. nEff = sum(~isnan(dAUC), 2);
  2023. sem = s ./ max(sqrt(nEff),1);
  2024. hold on;
  2025. % Bars + errorbars
  2026. bar(x, m, 0.7);
  2027. errorbar(x, m, sem, 'LineStyle','none', 'LineWidth', 1);
  2028. % Individual points with jitter
  2029. jitterAmp = 0.12;
  2030. for t = 1:3
  2031. y = dAUC(t,:);
  2032. xj = x(t) + (rand(1,N)-0.5)*2*jitterAmp;
  2033. scatter(xj, y, 28, 'filled', 'MarkerFaceAlpha', 0.85);
  2034. end
  2035. % zero line
  2036. yline(0,'--','LineWidth',1);
  2037. % p-value annotations per tercile (optional)
  2038. if ~isempty(p)
  2039. % Choose an offset scale: use yLims if provided, otherwise use data range
  2040. if nargin >= 4 && ~isempty(yLims)
  2041. ySpan = range(yLims);
  2042. else
  2043. ySpan = max(dAUC(:), [], 'omitnan') - min(dAUC(:), [], 'omitnan');
  2044. if ySpan == 0 || isnan(ySpan), ySpan = 1; end
  2045. end
  2046. yTop = max([m + sem, max(dAUC,[],2,'omitnan')], [], 2) + 0.06*ySpan;
  2047. for t = 1:3
  2048. text(x(t), yTop(t), sprintf('p=%.3g', p(t)), ...
  2049. 'HorizontalAlignment','center', 'FontSize', 9);
  2050. end
  2051. end
  2052. xlim([0.5 3.5]);
  2053. xticks(1:3);
  2054. xticklabels({'0–0.383s','0.383–0.767s','0.767–1.15s'}); % edit if needed
  2055. xtickangle(35);
  2056. ylabel('\DeltaAUC');
  2057. title(panelTitle, 'Interpreter','none');
  2058. if nargin >= 4 && ~isempty(yLims)
  2059. ylim(yLims);
  2060. end
  2061. box off;
  2062. set(gca,'TickDir','out');
  2063. hold off;
  2064. end
  2065. function [i, trRow] = pick_clean_stream_trial(streams, trials, allOUT)
  2066. % Returns stream index i (into fits/streams) and a trial row (from trials table)
  2067. % with tone played and non-truncated outcome.
  2068. idxStreams = find(~cellfun('isempty', allOUT));
  2069. if isempty(idxStreams), error('No non-empty outputs in allOUT.'); end
  2070. for k = 1:numel(idxStreams)
  2071. iCand = idxStreams(k);
  2072. sid = streams.session_uid{iCand};
  2073. Tr = trials(strcmp(trials.session_uid, sid) & trials.is_valid==1, :);
  2074. if isempty(Tr), continue; end
  2075. Tr = sortrows(Tr, "trial_idx");
  2076. tonePlayed = ~(string(Tr.tone_type)=="none" | string(Tr.outcome)=="restart");
  2077. cleanOut = ~(string(Tr.outcome)=="truncated" | string(Tr.outcome)=="restart");
  2078. ok = tonePlayed & cleanOut & ~isnan(Tr.t_tone_start) & ~isnan(Tr.t_outcome) & ~isnan(Tr.t_trial_start);
  2079. if any(ok)
  2080. i = iCand;
  2081. trRow = Tr(find(ok,1,'first'), :);
  2082. return
  2083. end
  2084. end
  2085. error('Could not find a clean trial in any non-empty stream.');
  2086. end
  2087. function sanity_plot_one_trial(streams, fits, cfg, trRow, i)
  2088. % Plots for one stream i and one trial row:
  2089. % trialStart window: raw, pred(tone+out), resid
  2090. % tone window: raw, pred(trialStart+out), resid
  2091. %
  2092. % xlim forced to [-1 2] for both.
  2093. dt = cfg.dt;
  2094. % windows
  2095. tTS = (cfg.win.trialStart_extract(1):dt:cfg.win.trialStart_extract(2))';
  2096. tTone = (cfg.win.tone_extract(1):dt:cfg.win.tone_extract(2))';
  2097. % load time + signal
  2098. t = h5read(cfg.h5file, streams.(cfg.time_field){i}); t = t(:);
  2099. y = h5read(cfg.h5file, streams.(cfg.signal_field){i}); y = y(:);
  2100. % kernels + lags
  2101. K = fits(i).kernels;
  2102. lagsTS = fits(i).lags.trialStart(:);
  2103. lagsTone = fits(i).lags.tone(:);
  2104. lagsOut = fits(i).lags.outcome(:);
  2105. kTS = single(K.trialStart(:));
  2106. kTone = single(K.tone_main(:));
  2107. kToneF = single(K.tone_F(:));
  2108. kHit = single(K.out_hit(:)); kFA = single(K.out_FA(:)); kCRM = single(K.out_CRMiss(:));
  2109. % trial info
  2110. t0 = double(trRow.t_trial_start);
  2111. tt = double(trRow.t_tone_start);
  2112. tout = double(trRow.t_outcome);
  2113. toneType = string(trRow.tone_type); % go/nogo/none
  2114. outType = string(trRow.outcome); % hit/fa/cr/miss/...
  2115. goIsHigh = logical(trRow.go_is_high);
  2116. tonePlayed = ~(toneType=="none" | outType=="restart");
  2117. % ---- A) trialStart window: subtract tone+outcome
  2118. tAbs = t0 + tTS;
  2119. rawTS = extract_segment_dt(t, y, tAbs, dt);
  2120. predOtherTS = zeros(size(tTS),'single');
  2121. if tonePlayed && ~isnan(tt)
  2122. tToneRel = tt - t0;
  2123. if toneType=="go"
  2124. isHigh = goIsHigh;
  2125. elseif toneType=="nogo"
  2126. isHigh = ~goIsHigh;
  2127. else
  2128. isHigh = false;
  2129. end
  2130. codeF = cfg.code.low; if isHigh, codeF = cfg.code.high; end
  2131. predOtherTS = addKernel_dt(predOtherTS, tTS, tToneRel, kTone, lagsTone, dt);
  2132. predOtherTS = addKernel_dt(predOtherTS, tTS, tToneRel, single(codeF)*kToneF, lagsTone, dt);
  2133. end
  2134. if ~isnan(tout) && ~(outType=="restart" | outType=="truncated")
  2135. tOutRel = tout - t0;
  2136. kout = outcome_kernel(outType, kHit, kFA, kCRM);
  2137. predOtherTS = addKernel_dt(predOtherTS, tTS, tOutRel, kout, lagsOut, dt);
  2138. end
  2139. residTS = single(rawTS) - predOtherTS;
  2140. % ---- B) tone window [-1,2]: subtract trialStart+outcome
  2141. if tonePlayed && ~isnan(tt)
  2142. tAbs = tt + tTone;
  2143. rawTone = extract_segment_dt(t, y, tAbs, dt);
  2144. predOtherTone = zeros(size(tTone),'single');
  2145. % trialStart relative to tone
  2146. tTSrel = t0 - tt;
  2147. predOtherTone = addKernel_dt(predOtherTone, tTone, tTSrel, kTS, lagsTS, dt);
  2148. % outcome relative to tone
  2149. if ~isnan(tout) && ~(outType=="restart" | outType=="truncated")
  2150. tOutRel = tout - tt;
  2151. kout = outcome_kernel(outType, kHit, kFA, kCRM);
  2152. predOtherTone = addKernel_dt(predOtherTone, tTone, tOutRel, kout, lagsOut, dt);
  2153. end
  2154. residTone = single(rawTone) - predOtherTone;
  2155. else
  2156. rawTone = nan(size(tTone));
  2157. predOtherTone = nan(size(tTone));
  2158. residTone = nan(size(tTone));
  2159. end
  2160. % ---- Plot
  2161. figure('Name',sprintf('Stream %d, session %s, trial %d', i, string(trRow.session_uid), trRow.trial_idx));
  2162. subplot(2,1,1);
  2163. plot(tTS, rawTS, 'DisplayName','raw'); hold on;
  2164. plot(tTS, predOtherTS, 'DisplayName','pred other (tone+out)');
  2165. plot(tTS, residTS, 'DisplayName','resid (raw - other)');
  2166. xlim([-1 2]); xlabel('Time from trialStart (s)'); ylabel('\DeltaF/F');
  2167. title('TrialStart window'); legend('Location','best'); grid on;
  2168. xTone = tt - t0;
  2169. xOut = tout - t0;
  2170. xline(xTone, '--', 'tone');
  2171. xline(xOut, '--', 'outcome');
  2172. subplot(2,1,2);
  2173. plot(tTone, rawTone, 'DisplayName','raw'); hold on;
  2174. plot(tTone, predOtherTone, 'DisplayName','pred other (trialStart+out)');
  2175. plot(tTone, residTone, 'DisplayName','resid (raw - other)');
  2176. xlim([-1 2]); xlabel('Time from tone (s)'); ylabel('\DeltaF/F');
  2177. title('Tone window'); legend('Location','best'); grid on;
  2178. xTS = t0 - tt;
  2179. xOut2 = tout - tt;
  2180. xline(xTS, '--', 'trialStart');
  2181. xline(xOut2,'--', 'outcome');
  2182. end
  2183. % ---- small helpers used above
  2184. function kout = outcome_kernel(outType, kHit, kFA, kCRM)
  2185. if outType=="hit", kout = kHit;
  2186. elseif outType=="fa", kout = kFA;
  2187. else, kout = kCRM; end
  2188. end
  2189. function ySeg = extract_segment_dt(t, y, tAbs, dt)
  2190. t0 = t(1);
  2191. idx = round((tAbs - t0)/dt) + 1;
  2192. ySeg = nan(size(tAbs), 'like', y);
  2193. valid = idx>=1 & idx<=numel(y);
  2194. ySeg(valid) = y(idx(valid));
  2195. end
  2196. function yhat = addKernel_dt(yhat, tWin, tEventRel, k, lags, dt)
  2197. tContrib = tEventRel + lags;
  2198. idx = round((tContrib - tWin(1))/dt) + 1;
  2199. valid = idx>=1 & idx<=numel(tWin);
  2200. yhat(idx(valid)) = yhat(idx(valid)) + k(valid);
  2201. end
  2202. function sanity_roundtrip_identity(streams, fits, cfg, trRow, i)
  2203. % Checks:
  2204. % trialStart: raw ≈ resid + pred(tone+out)
  2205. % tone: raw ≈ resid + pred(trialStart+out)
  2206. % Prints max abs error within window.
  2207. dt = cfg.dt;
  2208. tTS = (cfg.win.trialStart_extract(1):dt:cfg.win.trialStart_extract(2))';
  2209. tTone = (cfg.win.tone_extract(1):dt:cfg.win.tone_extract(2))';
  2210. % load time + signal
  2211. t = h5read(cfg.h5file, streams.(cfg.time_field){i}); t = t(:);
  2212. y = h5read(cfg.h5file, streams.(cfg.signal_field){i}); y = y(:);
  2213. % kernels + lags
  2214. K = fits(i).kernels;
  2215. lagsTS = fits(i).lags.trialStart(:);
  2216. lagsTone = fits(i).lags.tone(:);
  2217. lagsOut = fits(i).lags.outcome(:);
  2218. kTS = single(K.trialStart(:));
  2219. kTone = single(K.tone_main(:));
  2220. kToneF = single(K.tone_F(:));
  2221. kHit = single(K.out_hit(:)); kFA = single(K.out_FA(:)); kCRM = single(K.out_CRMiss(:));
  2222. % trial info
  2223. t0 = double(trRow.t_trial_start);
  2224. tt = double(trRow.t_tone_start);
  2225. tout = double(trRow.t_outcome);
  2226. toneType = string(trRow.tone_type);
  2227. outType = string(trRow.outcome);
  2228. goIsHigh = logical(trRow.go_is_high);
  2229. tonePlayed = ~(toneType=="none" | outType=="restart");
  2230. % ---------- trialStart window
  2231. rawTS = extract_segment_dt(t, y, t0 + tTS, dt);
  2232. predOtherTS = zeros(size(tTS),'single');
  2233. if tonePlayed && ~isnan(tt)
  2234. tToneRel = tt - t0;
  2235. if toneType=="go", isHigh = goIsHigh;
  2236. elseif toneType=="nogo", isHigh = ~goIsHigh;
  2237. else, isHigh = false; end
  2238. codeF = cfg.code.low; if isHigh, codeF = cfg.code.high; end
  2239. predOtherTS = addKernel_dt(predOtherTS, tTS, tToneRel, kTone, lagsTone, dt);
  2240. predOtherTS = addKernel_dt(predOtherTS, tTS, tToneRel, single(codeF)*kToneF, lagsTone, dt);
  2241. end
  2242. if ~isnan(tout) && ~(outType=="restart" | outType=="truncated")
  2243. predOtherTS = addKernel_dt(predOtherTS, tTS, tout - t0, outcome_kernel(outType,kHit,kFA,kCRM), lagsOut, dt);
  2244. end
  2245. residTS = single(rawTS) - predOtherTS;
  2246. reconTS = residTS + predOtherTS;
  2247. errTS = max(abs(double(rawTS) - double(reconTS)), [], 'omitnan');
  2248. % ---------- tone window
  2249. if tonePlayed && ~isnan(tt)
  2250. rawTone = extract_segment_dt(t, y, tt + tTone, dt);
  2251. predOtherTone = zeros(size(tTone),'single');
  2252. predOtherTone = addKernel_dt(predOtherTone, tTone, t0 - tt, kTS, lagsTS, dt);
  2253. if ~isnan(tout) && ~(outType=="restart" | outType=="truncated")
  2254. predOtherTone = addKernel_dt(predOtherTone, tTone, tout - tt, outcome_kernel(outType,kHit,kFA,kCRM), lagsOut, dt);
  2255. end
  2256. residTone = single(rawTone) - predOtherTone;
  2257. reconTone = residTone + predOtherTone;
  2258. errTone = max(abs(double(rawTone) - double(reconTone)), [], 'omitnan');
  2259. else
  2260. errTone = NaN;
  2261. end
  2262. fprintf('Round-trip max abs error (trialStart window): %.3g\n', errTS);
  2263. fprintf('Round-trip max abs error (tone window): %.3g\n', errTone);
  2264. end
  2265. function sanity_counts(allOUT)
  2266. S = allOUT(~cellfun('isempty',allOUT));
  2267. for k=1:numel(S)
  2268. O = S{k};
  2269. % tone totals
  2270. nAll = getfield_safe(O.nTrials,"tone_all",NaN);
  2271. nGo = getfield_safe(O.nTrials,"tone_byAction","go",0);
  2272. nNo = getfield_safe(O.nTrials,"tone_byAction","nogo",0);
  2273. nHi = getfield_safe(O.nTrials,"tone_byFreq","high",0);
  2274. nLo = getfield_safe(O.nTrials,"tone_byFreq","low",0);
  2275. if ~isnan(nAll)
  2276. if nAll ~= (nGo+nNo)
  2277. fprintf('Stream %s: tone_all (%d) != go+nogo (%d)\n', O.stream_uid, nAll, nGo+nNo);
  2278. end
  2279. if nAll ~= (nHi+nLo)
  2280. fprintf('Stream %s: tone_all (%d) != high+low (%d)\n', O.stream_uid, nAll, nHi+nLo);
  2281. end
  2282. end
  2283. end
  2284. disp('Count sanity done.');
  2285. end
  2286. function v = getfield_safe(s, f1, f2, f3, default)
  2287. % allows nested fields; usage:
  2288. % getfield_safe(O.nTrials,"tone_byAction","go",0)
  2289. try
  2290. if nargin==3
  2291. v = s.(f1); return;
  2292. elseif nargin==4
  2293. v = s.(f1).(f2); return;
  2294. elseif nargin==5
  2295. v = s.(f1).(f2).(f3); return;
  2296. end
  2297. catch
  2298. v = default;
  2299. end
  2300. end
  2301. function [i, trRow] = pick_random_clean_stream_trial(streams, trials, allOUT)
  2302. % candidate streams with non-empty outputs
  2303. idxStreams = find(~cellfun('isempty', allOUT));
  2304. if isempty(idxStreams)
  2305. error('No non-empty streams in allOUT.');
  2306. end
  2307. % shuffle streams
  2308. idxStreams = idxStreams(randperm(numel(idxStreams)));
  2309. for k = 1:numel(idxStreams)
  2310. iCand = idxStreams(k);
  2311. sid = streams.session_uid{iCand};
  2312. % all valid trials for this session
  2313. Tr = trials(strcmp(trials.session_uid, sid) & trials.is_valid==1, :);
  2314. if isempty(Tr), continue; end
  2315. % clean trial criteria
  2316. tonePlayed = ~(string(Tr.tone_type)=="none" | string(Tr.outcome)=="restart");
  2317. cleanOut = ~(string(Tr.outcome)=="restart" | string(Tr.outcome)=="truncated");
  2318. ok = tonePlayed & cleanOut & ...
  2319. ~isnan(Tr.t_trial_start) & ...
  2320. ~isnan(Tr.t_tone_start) & ...
  2321. ~isnan(Tr.t_outcome);
  2322. if any(ok)
  2323. TrOK = Tr(ok, :);
  2324. trRow = TrOK(randi(height(TrOK)), :); % random trial from this session
  2325. i = iCand;
  2326. return
  2327. end
  2328. end
  2329. error('Could not find any clean (stream, trial) pair.');
  2330. end
  2331. function C = my_plot_colors()
  2332. % All colors normalized to [0,1]
  2333. C.cell.a2a = [255,51,153] / 255;
  2334. C.cell.d1 = [51,51,153] / 255;
  2335. C.outcome.hit = [0.20 0.65 0.35];
  2336. C.outcome.miss = [0.20 0.70 0.75];
  2337. C.outcome.fa = [0.80 0.25 0.30];
  2338. C.outcome.cr = [0.35 0.30 0.70];
  2339. C.action.go = [0.25 0.60 0.40];
  2340. C.action.nogo = [0.75 0.30 0.30];
  2341. C.freq.high = [0.25 0.70 0.75];
  2342. C.freq.low = [0.90 0.65 0.20];
  2343. end
  2344. % Robust: if exact equality fails due to float representation
  2345. function idx = find_lambda_idx(lambdas, lam)
  2346. [~, idx] = min(abs(lambdas - lam));
  2347. end
  2348. function mse_star = get_mse_star(fit)
  2349. idx = find_lambda_idx(fit.cv.lambdas(:), fit.cv.lambda_star);
  2350. mse_star = fit.cv.cv_mse(idx);
  2351. end
  2352. function mse_at = get_mse_at(fit, lam)
  2353. idx = find_lambda_idx(fit.cv.lambdas(:), lam);
  2354. mse_at = fit.cv.cv_mse(idx);
  2355. end
  2356. function out = partialPred_lick_withhold(streams,trials,licks,fits,cfg)
  2357. % Computes partial predictions y_lick(t)=beta_L*lickRate(t)
  2358. % and y_withhold(t)=beta_W*withhold(t), aligns to tone onset [0,1.15],
  2359. % averages: stream -> animal -> cell_type, split go/nogo.
  2360. dt = cfg.dt;
  2361. win = [0 1.15];
  2362. t_rel = (win(1):dt:win(2))';
  2363. cellTypes = ["a2a","d1"];
  2364. conds = ["go","nogo"];
  2365. % per celltype/cond: map animal_id -> struct with fields lick_list, withhold_list
  2366. A = struct();
  2367. for ct = cellTypes
  2368. for c = conds
  2369. A.(ct).(c) = containers.Map();
  2370. end
  2371. end
  2372. for i = 1:height(streams)
  2373. if ismember('include', streams.Properties.VariableNames) && ~streams.include(i), continue; end
  2374. ct = lower(string(streams.cell_type(i)));
  2375. if ~ismember(ct, cellTypes), continue; end
  2376. % model betas
  2377. mdl = fits(i).model;
  2378. labels = string(mdl.labels(:));
  2379. beta_all = double(mdl.beta_hat(:));
  2380. if cfg.addIntercept
  2381. beta = beta_all(2:end);
  2382. else
  2383. beta = beta_all;
  2384. end
  2385. idxL = find(labels=="LickRate",1);
  2386. idxW = find(labels=="WithholdBoxcar",1);
  2387. if isempty(idxL) || isempty(idxW), continue; end
  2388. bL = beta(idxL);
  2389. bW = beta(idxW);
  2390. % time vector for this stream
  2391. t = h5read(cfg.h5file, streams.(cfg.time_field)(i));
  2392. t = double(t(:));
  2393. % ---------- Build lickRate PER STREAM (this is the critical line) ----------
  2394. lkMask = (string(licks.session_uid) == string(streams.session_uid(i))) & ...
  2395. (string(licks.animal_id) == string(streams.animal_id(i))) & ...
  2396. (lower(string(licks.cell_type)) == lower(string(streams.cell_type(i))));
  2397. lickTimes = double(licks.lick_times(lkMask));
  2398. lickRate = buildLickRate(t, lickTimes, cfg); % <-- called here, per stream
  2399. yL = bL * lickRate;
  2400. % ---------- Build withhold boxcar PER STREAM ----------
  2401. trMaskAll = (string(trials.session_uid) == string(streams.session_uid(i))) & ...
  2402. (string(trials.animal_id) == string(streams.animal_id(i))) & ...
  2403. (lower(string(trials.cell_type)) == lower(string(streams.cell_type(i)))) & ...
  2404. logical(trials.is_valid) & ...
  2405. (trials.tone_type=="go" | trials.tone_type=="nogo");
  2406. withhold = zeros(size(t));
  2407. idxTrAll = find(trMaskAll)';
  2408. for k = idxTrAll
  2409. t0 = trials.t_tone_start(k);
  2410. t1 = trials.t_outcome(k);
  2411. if isnan(t0) || isnan(t1) || t1<=t0, continue; end
  2412. withhold(t>=t0 & t<t1) = cfg.withhold.value;
  2413. end
  2414. yW = bW * withhold;
  2415. % ---------- Align to tone onset and average within stream, per condition ----------
  2416. for cond = conds
  2417. trMask = trMaskAll & (trials.tone_type == cond);
  2418. if nnz(trMask) < 10, continue; end
  2419. Lmat = [];
  2420. Wmat = [];
  2421. for k = find(trMask)'
  2422. t0 = trials.t_tone_start(k);
  2423. % sample exactly on t0 + t_rel (robust to minor dt jitter)
  2424. tq = t0 + t_rel;
  2425. Lsnip = interp1(t, yL, tq, 'linear', 'extrap');
  2426. Wsnip = interp1(t, yW, tq, 'linear', 'extrap');
  2427. Lmat = [Lmat, Lsnip];
  2428. Wmat = [Wmat, Wsnip];
  2429. end
  2430. streamMeanL = mean(Lmat, 2, 'omitnan');
  2431. streamMeanW = mean(Wmat, 2, 'omitnan');
  2432. animal = string(streams.animal_id(i));
  2433. M = A.(ct).(cond);
  2434. if ~isKey(M, animal)
  2435. M(animal) = struct('lick_list', [], 'withhold_list', []);
  2436. end
  2437. s = M(animal);
  2438. s.lick_list = [s.lick_list, streamMeanL];
  2439. s.withhold_list = [s.withhold_list, streamMeanW];
  2440. M(animal) = s;
  2441. A.(ct).(cond) = M;
  2442. end
  2443. end
  2444. % ---------- Hierarchical averaging: stream -> animal -> cell type ----------
  2445. out = struct();
  2446. out.t_rel = t_rel;
  2447. for ct = cellTypes
  2448. for cond = conds
  2449. M = A.(ct).(cond);
  2450. if M.Count == 0, continue; end
  2451. animals = keys(M);
  2452. L_anim = [];
  2453. W_anim = [];
  2454. for a = 1:numel(animals)
  2455. s = M(animals{a});
  2456. % average across streams within animal
  2457. L_anim(:,a) = mean(s.lick_list, 2, 'omitnan');
  2458. W_anim(:,a) = mean(s.withhold_list, 2, 'omitnan');
  2459. end
  2460. % equal-animal weighting
  2461. out.(ct).(cond).lick = mean(L_anim, 2, 'omitnan');
  2462. out.(ct).(cond).withhold = mean(W_anim, 2, 'omitnan');
  2463. out.(ct).(cond).lick_byAnimal = L_anim;
  2464. out.(ct).(cond).withhold_byAnimal = W_anim;
  2465. out.(ct).(cond).animal_ids = string(animals);
  2466. end
  2467. end
  2468. end
  2469. % =================== LOCAL FUNCTION ===================
  2470. function lickRate = buildLickRate(t, lickTimes, cfg)
  2471. dt = median(diff(t));
  2472. win = cfg.lickRate.win_s;
  2473. wSamp = max(1, round(win/dt));
  2474. lickTrain = zeros(numel(t),1);
  2475. if ~isempty(lickTimes)
  2476. idx = round((lickTimes - t(1))/dt) + 1;
  2477. idx = idx(idx>=1 & idx<=numel(t));
  2478. lickTrain = accumarray(idx,1,[numel(t),1],@sum,0);
  2479. end
  2480. if cfg.lickRate.causal
  2481. lickCount = filter(ones(wSamp,1),1,lickTrain);
  2482. else
  2483. lickCount = movsum(lickTrain,[floor(wSamp/2), ceil(wSamp/2)], 'Endpoints','shrink');
  2484. end
  2485. lickRate = lickCount / (wSamp*dt);
  2486. if isfield(cfg.lickRate,'clip_prctile') && cfg.lickRate.clip_prctile < 100
  2487. hi = prctile(lickRate, cfg.lickRate.clip_prctile);
  2488. lickRate = min(lickRate, hi);
  2489. end
  2490. if isfield(cfg.lickRate,'zscore') && cfg.lickRate.zscore
  2491. m = mean(lickRate,'omitnan');
  2492. s = std(lickRate,0,'omitnan'); if s==0, s=1; end
  2493. lickRate = (lickRate-m)/s;
  2494. end
  2495. end
  2496. function [h, stats] = plot_partial_component(out, component, cellType, cond, varargin)
  2497. % [h, stats] = plot_partial_component(...)
  2498. % Mean ± SEM plot for partial predictions, with customizable color.
  2499. component = lower(string(component));
  2500. cellType = lower(string(cellType));
  2501. cond = lower(string(cond));
  2502. validComp = ["lick","withhold"];
  2503. validCT = ["a2a","d1"];
  2504. validCond = ["go","nogo"];
  2505. assert(ismember(component, validComp));
  2506. assert(ismember(cellType, validCT));
  2507. assert(ismember(cond, validCond));
  2508. p = inputParser;
  2509. p.addParameter('ax', [], @(x) isempty(x) || ishghandle(x));
  2510. p.addParameter('LineWidth', 2, @isscalar);
  2511. p.addParameter('FaceAlpha', 0.2, @isscalar);
  2512. p.addParameter('ShowSEM', true, @islogical);
  2513. p.addParameter('Label', "", @(x) isstring(x) || ischar(x));
  2514. p.addParameter('XLine0', false, @islogical);
  2515. p.addParameter('ReturnOnly', false, @islogical);
  2516. p.addParameter('Color', [], @(x) isempty(x) || (isnumeric(x) && numel(x)==3));
  2517. p.parse(varargin{:});
  2518. opt = p.Results;
  2519. if isempty(opt.ax), opt.ax = gca; end
  2520. ax = opt.ax;
  2521. % ---- fetch data ----
  2522. field = component + "_byAnimal";
  2523. X = out.(cellType).(cond).(field); % T x Nanimals
  2524. t = out.t_rel(:);
  2525. % ---- compute stats ----
  2526. mu = mean(X, 2, 'omitnan');
  2527. sd = std(X, 0, 2, 'omitnan');
  2528. n = sum(~all(isnan(X),1));
  2529. se = sd ./ max(1, sqrt(n));
  2530. stats = struct('t', t, 'mu', mu, 'se', se, 'n', n, ...
  2531. 'component', component, 'cellType', cellType, 'cond', cond);
  2532. h = struct('band', [], 'line', [], 'x0', []);
  2533. if opt.ReturnOnly
  2534. return
  2535. end
  2536. axes(ax); %#ok<LAXES>
  2537. hold(ax, 'on');
  2538. if opt.XLine0
  2539. h.x0 = xline(ax, 0, '-');
  2540. end
  2541. % ---- SEM band ----
  2542. if opt.ShowSEM
  2543. xx = [t; flipud(t)];
  2544. yy = [mu - se; flipud(mu + se)];
  2545. h.band = fill(ax, xx, yy, 1, 'LineStyle', 'none');
  2546. if ~isempty(opt.Color)
  2547. set(h.band, 'FaceColor', opt.Color);
  2548. end
  2549. set(h.band, 'FaceAlpha', opt.FaceAlpha);
  2550. end
  2551. % ---- mean line ----
  2552. if isempty(opt.Color)
  2553. h.line = plot(ax, t, mu, 'LineWidth', opt.LineWidth);
  2554. else
  2555. h.line = plot(ax, t, mu, 'LineWidth', opt.LineWidth, 'Color', opt.Color);
  2556. end
  2557. % ---- legend label ----
  2558. if strlength(string(opt.Label)) > 0
  2559. h.line.DisplayName = string(opt.Label);
  2560. else
  2561. h.line.DisplayName = sprintf('%s %s %s (n=%d)', upper(cellType), upper(cond), component, n);
  2562. end
  2563. end
  2564. function S = normalize_sarah_table(T)
  2565. % Expect columns: Animal, Date, Time
  2566. % Date format: dd-mm-yy (e.g., 02-12-24)
  2567. % Time format: HH_MM_SS (e.g., 11_09_58)
  2568. % Normalize column names robustly
  2569. vars = lower(string(T.Properties.VariableNames));
  2570. iA = find(vars=="animal",1); if isempty(iA), iA = find(contains(vars,"animal"),1); end
  2571. iD = find(vars=="date",1); if isempty(iD), iD = find(contains(vars,"date"),1); end
  2572. iT = find(vars=="time",1); if isempty(iT), iT = find(contains(vars,"time"),1); end
  2573. assert(~isempty(iA)&&~isempty(iD)&&~isempty(iT), 'Could not find Animal/Date/Time columns.');
  2574. animal = string(T{:,iA});
  2575. date_s = string(T{:,iD});
  2576. time_s = string(T{:,iT});
  2577. % parse date: dd-mm-yy
  2578. d = datetime(date_s); % let MATLAB infer format
  2579. % parse time: HH_MM_SS (underscores)
  2580. time_s = replace(time_s,"_",":");
  2581. tt = datetime(time_s, 'InputFormat','HH:mm:ss');
  2582. % combine date+time into one datetime
  2583. dt = datetime(year(d), month(d), day(d), hour(tt), minute(tt), second(tt));
  2584. % build session_stamp: eYYYYMMDDHHMMSS
  2585. stamp = "e" + string(dt, 'yyyyMMddHHmmss');
  2586. S = table(animal, stamp, 'VariableNames', {'animal_id','session_stamp'});
  2587. end
  2588. function varargout = filter_sessions_day1_expert(excelPath, keepMode, fits, varargin)
  2589. %FILTER_SESSIONS_DAY1_EXPERT Filter tables + fits by training(day1) or expert(>=expert_date).
  2590. %
  2591. % Training rule: session_date == day1 (per animal)
  2592. % Expert rule: session_date >= expert_date (per animal)
  2593. %
  2594. % Inputs:
  2595. % excelPath : path to xlsx with columns: animal_id, day1, expert_date
  2596. % keepMode : "training" or "expert"
  2597. % fits : struct array OR scalar struct with row-aligned fields (same length as summary)
  2598. % varargin : one or more tables (e.g., licks, sessions, streams, summary, trials, ...)
  2599. %
  2600. % Name-Value:
  2601. % "assignInCaller" : overwrite caller vars (default false)
  2602. % "requireMapping" : if true, drop rows lacking an excel mapping (default true)
  2603. % "dateSource" : "sessions" or "summary" (default "sessions" if provided else "summary")
  2604. % "excludeDay1FromExpert" : if true, expert keeps >= expert_date BUT excludes day1 (default true)
  2605. % "verbose" : print counts (default true)
  2606. %
  2607. % Outputs (in order):
  2608. % same tables you passed in (same order), then fits, then info
  2609. %
  2610. % Example:
  2611. % [licks,sessions,streams,summary,trials,fits,info] = ...
  2612. % filter_sessions_day1_expert(excelPath,"expert",fits,licks,sessions,streams,summary,trials);
  2613. % ----------------- parse name-value from varargin -----------------
  2614. % allow trailing name-value pairs after tables
  2615. assignInCaller = false;
  2616. requireMapping = true;
  2617. dateSource = ""; % decide later
  2618. excludeDay1FromExpert = true;
  2619. verbose = true;
  2620. % Split varargin into (tables) and (name-value)
  2621. isNVStart = find(cellfun(@(x) ischar(x) || (isstring(x) && isscalar(x)), varargin), 1, 'first');
  2622. % Heuristic: name-value starts at first string that matches a known parameter name
  2623. known = ["assignInCaller","requireMapping","dateSource","excludeDay1FromExpert","verbose"];
  2624. nvIdx = [];
  2625. for k = 1:numel(varargin)
  2626. if (ischar(varargin{k}) || (isstring(varargin{k}) && isscalar(varargin{k}))) ...
  2627. && any(strcmpi(string(varargin{k}), known))
  2628. nvIdx = k;
  2629. break;
  2630. end
  2631. end
  2632. if ~isempty(nvIdx)
  2633. tableArgs = varargin(1:nvIdx-1);
  2634. nvArgs = varargin(nvIdx:end);
  2635. else
  2636. tableArgs = varargin;
  2637. nvArgs = {};
  2638. end
  2639. % parse NV
  2640. if ~isempty(nvArgs)
  2641. if mod(numel(nvArgs),2) ~= 0
  2642. error("Name-value arguments must come in pairs.");
  2643. end
  2644. for i = 1:2:numel(nvArgs)
  2645. name = lower(string(nvArgs{i}));
  2646. val = nvArgs{i+1};
  2647. switch name
  2648. case "assignincaller"
  2649. assignInCaller = logical(val);
  2650. case "requiremapping"
  2651. requireMapping = logical(val);
  2652. case "datesource"
  2653. dateSource = lower(string(val));
  2654. case "excludeday1fromexpert"
  2655. excludeDay1FromExpert = logical(val);
  2656. case "verbose"
  2657. verbose = logical(val);
  2658. otherwise
  2659. error("Unknown parameter: %s", name);
  2660. end
  2661. end
  2662. end
  2663. keepMode = lower(string(keepMode));
  2664. if ~any(keepMode == ["training","expert"])
  2665. error('keepMode must be "training" or "expert".');
  2666. end
  2667. % ----------------- validate table inputs -----------------
  2668. if isempty(tableArgs)
  2669. error("Pass at least one table (e.g., summary, sessions, ...).");
  2670. end
  2671. nTables = numel(tableArgs);
  2672. tblNames = strings(1,nTables);
  2673. for i = 1:nTables
  2674. if ~istable(tableArgs{i})
  2675. error("Argument %d after fits is not a table.", i);
  2676. end
  2677. nm = string(inputname(i+3)); % (1)excelPath (2)keepMode (3)fits then tables
  2678. if strlength(nm)==0
  2679. % if user passed an expression, we can't overwrite by name
  2680. nm = "table" + i;
  2681. end
  2682. tblNames(i) = nm;
  2683. end
  2684. % Find the summary table among inputs (needed to filter fits)
  2685. isSummary = strcmpi(tblNames, "summary");
  2686. if ~any(isSummary)
  2687. error('One of the table inputs must be named "summary" (pass your summary table variable as "summary").');
  2688. end
  2689. summaryIdx = find(isSummary, 1, 'first');
  2690. summaryTbl = tableArgs{summaryIdx};
  2691. % choose dateSource
  2692. if strlength(dateSource)==0
  2693. if any(strcmpi(tblNames, "sessions"))
  2694. dateSource = "sessions";
  2695. else
  2696. dateSource = "summary";
  2697. end
  2698. end
  2699. srcIdx = find(strcmpi(tblNames, dateSource), 1, 'first');
  2700. if isempty(srcIdx)
  2701. error('dateSource="%s" requested, but you did not pass a table named "%s".', dateSource, dateSource);
  2702. end
  2703. srcTbl = tableArgs{srcIdx};
  2704. % check required vars in source table
  2705. needVars = ["session_uid","animal_id"];
  2706. for v = needVars
  2707. if ~ismember(v, string(srcTbl.Properties.VariableNames))
  2708. error('Source table "%s" must contain variable "%s".', dateSource, v);
  2709. end
  2710. end
  2711. % ----------------- read and normalize excel mapping -----------------
  2712. map = readtable(excelPath, "TextType", "string");
  2713. reqMapVars = ["animal_id","day1","expert_date"];
  2714. for v = reqMapVars
  2715. if ~ismember(v, string(map.Properties.VariableNames))
  2716. error('Excel file must contain columns: animal_id, day1, expert_date. Missing: %s', v);
  2717. end
  2718. end
  2719. map.animal_id = string(map.animal_id);
  2720. map.day1 = local_to_yyyymmdd_num(map.day1);
  2721. map.expert_date = local_to_yyyymmdd_num(map.expert_date);
  2722. % ----------------- extract session date YYYYMMDD from session_uid -----------------
  2723. session_uid = string(srcTbl.session_uid);
  2724. sessionDateNum = local_extract_yyyymmdd_from_session_uid(session_uid); % Nx1 double
  2725. srcAnimal = string(srcTbl.animal_id);
  2726. [tfMap, loc] = ismember(srcAnimal, map.animal_id);
  2727. if requireMapping
  2728. mappedMask = tfMap;
  2729. else
  2730. mappedMask = true(size(tfMap));
  2731. end
  2732. day1Num = nan(size(sessionDateNum));
  2733. expertNum = nan(size(sessionDateNum));
  2734. day1Num(tfMap) = map.day1(loc(tfMap));
  2735. expertNum(tfMap) = map.expert_date(loc(tfMap));
  2736. isTraining = (sessionDateNum == day1Num);
  2737. isExpert = (sessionDateNum >= expertNum);
  2738. switch keepMode
  2739. case "training"
  2740. keepSessionMask = mappedMask & isTraining;
  2741. case "expert"
  2742. if excludeDay1FromExpert
  2743. keepSessionMask = mappedMask & isExpert & ~isTraining;
  2744. else
  2745. keepSessionMask = mappedMask & isExpert;
  2746. end
  2747. end
  2748. keepSessionUIDs = unique(session_uid(keepSessionMask));
  2749. % ----------------- filter each passed table by session_uid -----------------
  2750. filteredTables = tableArgs;
  2751. keptRowsPerTable = zeros(1,nTables);
  2752. droppedRowsPerTable = zeros(1,nTables);
  2753. for i = 1:nTables
  2754. T = tableArgs{i};
  2755. if ismember("session_uid", string(T.Properties.VariableNames))
  2756. su = string(T.session_uid);
  2757. keepRow = ismember(su, keepSessionUIDs);
  2758. filteredTables{i} = T(keepRow, :);
  2759. keptRowsPerTable(i) = nnz(keepRow);
  2760. droppedRowsPerTable(i) = height(T) - nnz(keepRow);
  2761. else
  2762. % If a table doesn't have session_uid, we leave it untouched.
  2763. filteredTables{i} = T;
  2764. keptRowsPerTable(i) = height(T);
  2765. droppedRowsPerTable(i) = 0;
  2766. end
  2767. end
  2768. % ----------------- filter fits by summary rows (fits aligns 1:1 with summary) -----------------
  2769. newSummary = filteredTables{summaryIdx};
  2770. keepSummaryRows = true(height(summaryTbl),1);
  2771. if ismember("session_uid", string(summaryTbl.Properties.VariableNames))
  2772. keepSummaryRows = ismember(string(summaryTbl.session_uid), keepSessionUIDs);
  2773. else
  2774. error('summary table must contain "session_uid" to filter fits consistently.');
  2775. end
  2776. fitsFiltered = local_filter_fits_by_mask(fits, keepSummaryRows);
  2777. % ----------------- info struct -----------------
  2778. info = struct();
  2779. info.keepMode = keepMode;
  2780. info.dateSource = dateSource;
  2781. info.excludeDay1FromExpert = excludeDay1FromExpert;
  2782. info.nSessionsKept = numel(keepSessionUIDs);
  2783. info.tableNames = tblNames;
  2784. info.keptRowsPerTable = keptRowsPerTable;
  2785. info.droppedRowsPerTable = droppedRowsPerTable;
  2786. info.nSummaryBefore = height(summaryTbl);
  2787. info.nSummaryAfter = height(newSummary);
  2788. info.nFitsBefore = local_len_fits(fits);
  2789. info.nFitsAfter = local_len_fits(fitsFiltered);
  2790. if verbose
  2791. fprintf("[filter_sessions_day1_expert] keep=%s | dateSource=%s | sessions kept=%d\n", ...
  2792. keepMode, dateSource, info.nSessionsKept);
  2793. for i = 1:nTables
  2794. fprintf(" %s: %d -> %d (dropped %d)\n", tblNames(i), height(tableArgs{i}), height(filteredTables{i}), droppedRowsPerTable(i));
  2795. end
  2796. fprintf(" fits: %d -> %d\n", info.nFitsBefore, info.nFitsAfter);
  2797. end
  2798. % ----------------- optionally overwrite caller variables -----------------
  2799. if assignInCaller
  2800. for i = 1:nTables
  2801. nm = tblNames(i);
  2802. % only overwrite if we have a real variable name
  2803. if ~startsWith(nm, "table")
  2804. assignin("caller", nm, filteredTables{i});
  2805. end
  2806. end
  2807. % overwrite fits if caller provided a variable name
  2808. fitsName = string(inputname(3));
  2809. if strlength(fitsName) > 0
  2810. assignin("caller", fitsName, fitsFiltered);
  2811. end
  2812. % also drop info in caller (optional; name is stable)
  2813. assignin("caller", "filter_info", info);
  2814. end
  2815. % ----------------- outputs -----------------
  2816. % return tables in the same order passed, then fits, then info
  2817. varargout = [filteredTables, {fitsFiltered}, {info}];
  2818. end
  2819. % ================= local helpers =================
  2820. function y = local_to_yyyymmdd_num(x)
  2821. % Convert excel column to numeric YYYYMMDD safely (string/numeric/datetime)
  2822. if isdatetime(x)
  2823. y = year(x)*10000 + month(x)*100 + day(x);
  2824. y = double(y);
  2825. elseif isnumeric(x)
  2826. y = double(x);
  2827. else
  2828. xs = string(x);
  2829. xs = strtrim(xs);
  2830. xs(xs=="") = "NaN";
  2831. y = double(xs);
  2832. end
  2833. end
  2834. function d = local_extract_yyyymmdd_from_session_uid(session_uid)
  2835. % session_uid like "e20241126113002_a1" -> 20241126
  2836. % We take characters 2..9 if present; else regexp fallback.
  2837. d = nan(numel(session_uid),1);
  2838. s = string(session_uid);
  2839. % fast path: char 2..9 are digits
  2840. okLen = strlength(s) >= 9;
  2841. cand = extractBetween(s(okLen), 2, 9);
  2842. isDig = ~isnan(str2double(cand));
  2843. d(okLen) = str2double(cand);
  2844. % fallback for anything weird
  2845. need = isnan(d);
  2846. if any(need)
  2847. for i = find(need').'
  2848. tok = regexp(s(i), '^e(\d{8})', 'tokens', 'once');
  2849. if ~isempty(tok)
  2850. d(i) = str2double(tok{1});
  2851. end
  2852. end
  2853. end
  2854. if any(isnan(d))
  2855. bad = find(isnan(d), 1, 'first');
  2856. error('Failed to parse YYYYMMDD from session_uid at index %d: "%s"', bad, s(bad));
  2857. end
  2858. end
  2859. function fitsOut = local_filter_fits_by_mask(fitsIn, keepMask)
  2860. % Support common fits storage patterns.
  2861. if isstruct(fitsIn)
  2862. if numel(fitsIn) == numel(keepMask)
  2863. % struct array, 1 element per row
  2864. fitsOut = fitsIn(keepMask);
  2865. return;
  2866. else
  2867. % scalar struct with fields that are row-aligned vectors/cells
  2868. fitsOut = fitsIn;
  2869. fns = fieldnames(fitsIn);
  2870. for k = 1:numel(fns)
  2871. v = fitsIn.(fns{k});
  2872. try
  2873. if isnumeric(v) || islogical(v)
  2874. if size(v,1) == numel(keepMask)
  2875. fitsOut.(fns{k}) = v(keepMask,:);
  2876. end
  2877. elseif iscell(v) || isstring(v)
  2878. if size(v,1) == numel(keepMask)
  2879. fitsOut.(fns{k}) = v(keepMask,:);
  2880. end
  2881. end
  2882. catch
  2883. % leave field unchanged if indexing fails
  2884. end
  2885. end
  2886. return;
  2887. end
  2888. else
  2889. error("fits must be a struct (either struct array or scalar struct with row-aligned fields).");
  2890. end
  2891. end
  2892. function n = local_len_fits(f)
  2893. if isstruct(f)
  2894. n = numel(f);
  2895. else
  2896. n = NaN;
  2897. end
  2898. end
  2899. function [delta_auc, delta_p] = getAUCs2Bins(t, t0, tMid, tEnd, S1, S2)
  2900. delta_auc = zeros(2, size(S1,2));
  2901. delta_auc(1,:) = getDeltaAUC(t, t0, tMid, S1, S2);
  2902. delta_auc(2,:) = getDeltaAUC(t, tMid, tEnd, S1, S2);
  2903. delta_p = zeros(2,1);
  2904. [~, delta_p(1)] = ttest(delta_auc(1,:), 0);
  2905. [~, delta_p(2)] = ttest(delta_auc(2,:), 0);
  2906. end
  2907. function p = pvalsLME_vsZero(dAUC_2xN, animal_id)
  2908. % p is 2x1 p-values for intercept vs 0 in each bin
  2909. p = nan(2,1);
  2910. animal = categorical(animal_id);
  2911. for b = 1:2
  2912. y = dAUC_2xN(b,:)';
  2913. T = table(y, animal, 'VariableNames', {'dAUC','animal'});
  2914. lme = fitlme(T, 'dAUC ~ 1 + (1|animal)');
  2915. coef = lme.Coefficients;
  2916. p(b) = coef.pValue(strcmp(coef.Name,'(Intercept)'));
  2917. end
  2918. end
  2919. function p = pvalsLME_vsZero_bins(D_BxN, animal_id)
  2920. % D_BxN: B x N (bins x streams), each column corresponds to a stream
  2921. % animal_id: N x 1 or 1 x N identifiers (strings/cellstr/etc.)
  2922. % Returns p: B x 1 p-values for intercept vs 0 per bin
  2923. animal_id = string(animal_id(:));
  2924. animal = categorical(animal_id);
  2925. B = size(D_BxN,1);
  2926. N = size(D_BxN,2);
  2927. assert(numel(animal)==N, 'animal_id must have length N = number of columns in D_BxN.');
  2928. p = nan(B,1);
  2929. for b = 1:B
  2930. y = D_BxN(b,:)';
  2931. T = table(y, animal, 'VariableNames', {'y','animal'});
  2932. lme = fitlme(T, 'y ~ 1 + (1|animal)');
  2933. coef = lme.Coefficients;
  2934. p(b) = coef.pValue(strcmp(coef.Name,'(Intercept)'));
  2935. end
  2936. end
  2937. function plotDeltaAUC2Bins_barScatter(dAUC_2xN, p_2, panelTitle, yLims)
  2938. if size(dAUC_2xN,1) ~= 2
  2939. error('Expected dAUC to be 2xN (2 bins by units).');
  2940. end
  2941. N = size(dAUC_2xN,2);
  2942. x = 1:2;
  2943. % mean ± SEM
  2944. m = mean(dAUC_2xN, 2, 'omitnan');
  2945. s = std(dAUC_2xN, 0, 2, 'omitnan');
  2946. nEff = sum(~isnan(dAUC_2xN), 2);
  2947. sem = s ./ max(sqrt(nEff),1);
  2948. hold on;
  2949. bar(x, m, 0.7);
  2950. errorbar(x, m, sem, 'LineStyle','none', 'LineWidth', 1);
  2951. % individual points
  2952. jitterAmp = 0.12;
  2953. for b = 1:2
  2954. y = dAUC_2xN(b,:);
  2955. xj = x(b) + (rand(1,N)-0.5)*2*jitterAmp;
  2956. scatter(xj, y, 28, 'filled', 'MarkerFaceAlpha', 0.85);
  2957. end
  2958. yline(0,'--','LineWidth',1);
  2959. % p-value annotation
  2960. if nargin >= 2 && ~isempty(p_2)
  2961. ySpan = max(dAUC_2xN(:),[],'omitnan') - min(dAUC_2xN(:),[],'omitnan');
  2962. if ySpan==0 || isnan(ySpan), ySpan=1; end
  2963. yTop = max([m + sem, max(dAUC_2xN,[],2,'omitnan')], [], 2) + 0.06*ySpan;
  2964. for b = 1:2
  2965. text(x(b), yTop(b), sprintf('p=%.3g', p_2(b)), ...
  2966. 'HorizontalAlignment','center', 'FontSize', 9);
  2967. end
  2968. end
  2969. xlim([0.5 2.5]);
  2970. xticks(1:2);
  2971. xticklabels({'early','late'});
  2972. xtickangle(35);
  2973. ylabel('\DeltaAUC');
  2974. title(panelTitle, 'Interpreter','none');
  2975. if nargin >= 4 && ~isempty(yLims)
  2976. ylim(yLims);
  2977. end
  2978. box off;
  2979. set(gca,'TickDir','out');
  2980. hold off;
  2981. end
  2982. function [dAUC_animal, animals] = collapseToAnimalMeans(dAUC_stream, animal_id)
  2983. % dAUC_stream: B x Nstreams
  2984. % animal_id: Nstreams x 1 (or 1 x Nstreams)
  2985. % dAUC_animal: B x Nanimals (mean across streams within animal)
  2986. animal_id = string(animal_id(:)); % force column
  2987. N = size(dAUC_stream,2);
  2988. assert(numel(animal_id)==N, 'animal_id length must match # columns in dAUC');
  2989. animals = unique(animal_id, 'stable');
  2990. nA = numel(animals);
  2991. B = size(dAUC_stream,1);
  2992. dAUC_animal = nan(B, nA);
  2993. for a = 1:nA
  2994. idx = (animal_id == animals(a));
  2995. dAUC_animal(:,a) = mean(dAUC_stream(:,idx), 2, 'omitnan');
  2996. end
  2997. end
  2998. function plotDeltaAUC2Bins_barScatter_animals(dAUC_stream_2xN, animal_id, p_2, panelTitle, yLims)
  2999. % collapse streams -> per-animal means
  3000. animal_id = string(animal_id(:));
  3001. animals = unique(animal_id, 'stable');
  3002. B = size(dAUC_stream_2xN,1);
  3003. nA = numel(animals);
  3004. dAUC = nan(B, nA);
  3005. for a = 1:nA
  3006. idx = animal_id == animals(a);
  3007. dAUC(:,a) = mean(dAUC_stream_2xN(:,idx), 2, 'omitnan');
  3008. end
  3009. x = 1:2;
  3010. % mean ± SEM across animals
  3011. m = mean(dAUC, 2, 'omitnan');
  3012. s = std(dAUC, 0, 2, 'omitnan');
  3013. nEff = sum(~isnan(dAUC), 2);
  3014. sem = s ./ max(sqrt(nEff),1);
  3015. hold on;
  3016. bar(x, m, 0.7);
  3017. errorbar(x, m, sem, 'LineStyle','none', 'LineWidth', 1);
  3018. yline(0,'--','LineWidth',1);
  3019. % per-animal dots (no lines, I promise)
  3020. jitterAmp = 0.10;
  3021. for b = 1:2
  3022. xj = x(b) + (rand(1,nA)-0.5)*2*jitterAmp;
  3023. scatter(xj, dAUC(b,:), 36, 'filled', 'MarkerFaceAlpha', 0.85);
  3024. end
  3025. % p-value annotation
  3026. if nargin >= 3 && ~isempty(p_2) && numel(p_2)==2
  3027. ySpan = max(dAUC(:),[],'omitnan') - min(dAUC(:),[],'omitnan');
  3028. if ySpan==0 || isnan(ySpan), ySpan=1; end
  3029. yTop = max([m + sem, max(dAUC,[],2,'omitnan')], [], 2) + 0.06*ySpan;
  3030. for b = 1:2
  3031. text(x(b), yTop(b), sprintf('p=%.3g', p_2(b)), ...
  3032. 'HorizontalAlignment','center', 'FontSize', 9);
  3033. end
  3034. end
  3035. xlim([0.5 2.5]);
  3036. xticks(1:2);
  3037. xticklabels({'early','late'});
  3038. xtickangle(35);
  3039. ylabel('\DeltaAUC');
  3040. title(sprintf('%s (n=%d animals)', panelTitle, nA), 'Interpreter','none');
  3041. if nargin >= 5 && ~isempty(yLims)
  3042. ylim(yLims);
  3043. end
  3044. box off;
  3045. set(gca,'TickDir','out');
  3046. hold off;
  3047. end
  3048. function [qvals, sig, pcrit] = bh_fdr(pvals, q)
  3049. % bh_fdr: Benjamini-Hochberg FDR correction for a vector of p-values
  3050. % pvals: vector (m x 1 or 1 x m)
  3051. % q: desired FDR level (default 0.05)
  3052. %
  3053. % qvals: BH-adjusted q-values (same shape as pvals)
  3054. % sig: boolean mask of discoveries at level q
  3055. % pcrit: critical p threshold (largest p called significant); NaN if none
  3056. if nargin < 2 || isempty(q), q = 0.05; end
  3057. p = pvals(:);
  3058. m = numel(p);
  3059. % handle NaNs (keep them as NaN in qvals; ignore in procedure)
  3060. valid = ~isnan(p);
  3061. pv = p(valid);
  3062. m0 = numel(pv);
  3063. qvals = nan(size(p));
  3064. sig = false(size(p));
  3065. pcrit = NaN;
  3066. if m0 == 0
  3067. qvals = reshape(qvals, size(pvals));
  3068. sig = reshape(sig, size(pvals));
  3069. return
  3070. end
  3071. [p_sorted, order] = sort(pv, 'ascend');
  3072. % BH "step-up" thresholding
  3073. thresh = (1:m0)'/m0 * q;
  3074. k = find(p_sorted <= thresh, 1, 'last');
  3075. if ~isempty(k)
  3076. sig_sorted = false(m0,1);
  3077. sig_sorted(1:k) = true;
  3078. sig_valid = false(m0,1);
  3079. sig_valid(order) = sig_sorted;
  3080. sig(valid) = sig_valid;
  3081. pcrit = p_sorted(k);
  3082. end
  3083. % BH-adjusted q-values (monotone)
  3084. q_sorted = p_sorted .* (m0 ./ (1:m0)');
  3085. q_sorted = min( cummin(flipud(q_sorted)), 1 );
  3086. q_sorted = flipud(q_sorted);
  3087. q_valid = nan(m0,1);
  3088. q_valid(order) = q_sorted;
  3089. qvals(valid) = q_valid;
  3090. % reshape to match input
  3091. qvals = reshape(qvals, size(pvals));
  3092. sig = reshape(sig, size(pvals));
  3093. end
  3094. function pk = peak_anchor_topk(t, M, tPeakWin, tAnchor, k)
  3095. % t: nTime x 1 (or 1 x nTime)
  3096. % M: nTime x nStreams
  3097. % pk: 1 x nStreams
  3098. if nargin < 3 || isempty(tPeakWin), tPeakWin = [0 0.4]; end
  3099. if nargin < 4 || isempty(tAnchor), tAnchor = 0; end
  3100. if nargin < 5 || isempty(k), k = 5; end
  3101. t = t(:);
  3102. assert(size(M,1) == numel(t), 'M must be time x nStreams.');
  3103. ixP = t>=tPeakWin(1) & t<=tPeakWin(2);
  3104. assert(any(ixP), 'No samples in peak window.');
  3105. [~, i0] = min(abs(t - tAnchor));
  3106. X = M(ixP, :).'; % nStreams x nWin
  3107. pkPeak = mean(maxk(X, k, 2), 2); % nStreams x 1 (robust peak)
  3108. pk0 = M(i0,:).'; % nStreams x 1
  3109. pk = (pkPeak - pk0).'; % 1 x nStreams
  3110. end
  3111. function dpk = getDeltaPeak(t, M1, M2, tPeakWin, tAnchor, k)
  3112. pk1 = peak_anchor_topk(t, M1, tPeakWin, tAnchor, k);
  3113. pk2 = peak_anchor_topk(t, M2, tPeakWin, tAnchor, k);
  3114. dpk = pk1 - pk2; % 1 x nStreams
  3115. end

kernelGLM.m at commit 253d2fe, no license · at the source

Overview

  1. Department of Neuroscience, Perelman School of Medicine, University of Pennsylvania, Philadelphia, PA 19104, USA
  2. Neuroscience Graduate Group, Perelman School of Medicine, University of Pennsylvania, Philadelphia, PA 19104, USA
  3. Autism Spectrum Program of Excellence, University of Pennsylvania, Philadelphia, PA 19104, USA
  4. Medical Scientist Training Program, University of Pennsylvania, Philadelphia, PA 19104, USA
Institutions: University of Pennsylvania (United States)
Journal: Science advances, volume 12, issue 31, article eaeb5352
Dates: received 16 August 2025; accepted 18 June 2026; published online 31 July 2026; in print July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1126/sciadv.aeb5352 · PMID 42536750 · PMCID PMC13426436 · OpenAlex W4413345849
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: mouse (organism), cognitive (subfield)
Methods: Spectral & time-frequency, Statistics, Machine learning, Single-unit activity, calcium imaging
MeSH: Behavior, Animal*, Corpus Striatum*, Neurons*, Animals, Male, Medium Spiny Neurons, Mice, Neurexins (* major topic)
Topic: Neurotransmitter Receptor Influence on Behavior (Cellular and Molecular Neuroscience, Neuroscience), according to OpenAlex
Funding: NIMH NIH HHS (R01 MH136354, F30 MH136699); NINDS NIH HHS (F31 NS130989)
Citations: not cited yet (Europe PMC); 102 references in the paper

Abstract

Inhibitory control, the ability to withhold action in certain contexts, is behaviorally essential. Disrupted inhibitory control is linked to various neuropsychiatric symptoms, making it critical to understand the underlying neural basis. We examined how the tail of the striatum (TS), a major basal ganglia sensory hub, regulates actions to sensory stimuli. Mice performed an auditory Go/NoGo task, while we recorded TS neuron activity. Both spiny neuron subtypes were recruited during target and nontarget sounds, but nontarget sounds uniquely engaged persistent indirect pathway neuron activity. Temporarily silencing this activity increased errors to nontarget stimuli, indicating a role in suppressing inappropriate action. In mice deficient for Neurexin1α, a gene linked to ASD and ADHD, TS indirect pathway recruitment was reduced, and these mice demonstrated auditory-specific inhibitory control deficits that were ameliorated by boosting indirect pathway excitability. These findings highlight a subcortical target to potentially improve attentional and behavioral regulation in neurodevelopmental disorders.

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

Repositories

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

Zenodo 20089479

License: CC-BY-4.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 12 files
Software Heritage: not checked
Found in: “Data, code, and materials availability:”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
61 files

Fuccillo-Lab/Ferrigno-Iliakis_SciAdv2026

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 253d2fe27626b5050ffef1c27b9f224f752b1318, 18 May 2026
Languages: MATLAB (60)
Size: 62 files, 60 scripts
Software Heritage: not archived
Found in: “Data, code, and materials availability:”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
61 files

The paper's code and data availability statement is in the Data section.

Tracing map

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

What the map holds:

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

All data and MATLAB code needed to evaluate the conclusions in the paper are available for public use on Zenodo under the following link: https://doi.org/10.5281/zenodo.20089479. Any future updates to the MATLAB code will be available for public use on the Fuccillo Lab GitHub (github.com/Fuccillo-Lab/Ferrigno-Iliakis_SciAdv2026). This study did not generate new materials.

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

Recorded: type, language, journal, volume, issue, pages, dates, 7 authors, 8 MeSH terms, 2 funders, 100 references.

Cite

This paper

Ferrigno, S. M., Iliakis, E., Zhang, N., Pandey, S., Galanaugh, J., Muhsinov, J., & Fuccillo, M. V. (2026). Indirect pathway neurons in the tail of the striatum regulate inhibitory control over sensory driven behavior. Science advances, 12(31), eaeb5352. https://doi.org/10.1126/sciadv.aeb5352

BibTeX

@article{ferrigno2026indirect,
author = {Ferrigno, Sarah M and Iliakis, Evan and Zhang, Nathan and Pandey, Saurabh and Galanaugh, Jamie and Muhsinov, Jonibek and Fuccillo, Marc V},
title = {{Indirect pathway neurons in the tail of the striatum regulate inhibitory control over sensory driven behavior}},
journal = {Science advances},
year = {2026},
month = jul,
volume = {12},
number = {31},
pages = {eaeb5352},
publisher = {American Association for the Advancement of Science},
issn = {2375-2548},
doi = {10.1126/sciadv.aeb5352},
url = {https://doi.org/10.1126/sciadv.aeb5352},
pmid = {42536750},
pmcid = {PMC13426436}
}

RIS

TY - JOUR
AU - Ferrigno, Sarah M
AU - Iliakis, Evan
AU - Zhang, Nathan
AU - Pandey, Saurabh
AU - Galanaugh, Jamie
AU - Muhsinov, Jonibek
AU - Fuccillo, Marc V
TI - Indirect pathway neurons in the tail of the striatum regulate inhibitory control over sensory driven behavior
T2 - Science advances
J2 - Sci Adv
PY - 2026
DA - 2026/07/31
VL - 12
IS - 31
SP - eaeb5352
SN - 2375-2548
PB - American Association for the Advancement of Science
DO - 10.1126/sciadv.aeb5352
UR - https://doi.org/10.1126/sciadv.aeb5352
LA - en
ER -

CSL-JSON

{
"id": "10.1126/sciadv.aeb5352",
"type": "article-journal",
"title": "Indirect pathway neurons in the tail of the striatum regulate inhibitory control over sensory driven behavior",
"container-title": "Science advances",
"author": [
{
"family": "Ferrigno",
"given": "Sarah M"
},
{
"family": "Iliakis",
"given": "Evan"
},
{
"family": "Zhang",
"given": "Nathan"
},
{
"family": "Pandey",
"given": "Saurabh"
},
{
"family": "Galanaugh",
"given": "Jamie"
},
{
"family": "Muhsinov",
"given": "Jonibek"
},
{
"family": "Fuccillo",
"given": "Marc V"
}
],
"container-title-short": "Sci Adv",
"volume": "12",
"issue": "31",
"page": "eaeb5352",
"DOI": "10.1126/sciadv.aeb5352",
"PMID": "42536750",
"PMCID": "PMC13426436",
"ISSN": "2375-2548",
"publisher": "American Association for the Advancement of Science",
"URL": "https://doi.org/10.1126/sciadv.aeb5352",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
31
]
]
}
}

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.1126/sciadv.aee3529 [code]
Learning dynamically regulates stimulus discrimination of ventral striatal D1 receptor expressing neurons.
Journal: Science advances
In common: mouse, 7 references
[2] doi:10.1016/j.isci.2026.116598
Complementary δ2-protocadherin expression delineates parallel basal ganglia circuits in primates.
Journal: iScience
In common: 6 references
[3] doi:10.1126/sciadv.aed9386 [code]
Shared striatal neurons exhibit context-specific dynamics for internally and externally driven actions.
Journal: Science advances
In common: mouse, 5 references
[4] doi:10.1038/s41467-026-77168-x [code]
Cholinergic-dependent dopamine signals in mouse dorsomedial striatum are regulated by frontal but not sensory cortices.
Journal: Nature communications
In common: mouse, 5 references
[5] doi:10.1113/ep094001
Distinct electrophysiology and dopaminergic modulation of medium spiny neurons across divisions of the tail striatum and the dorsolateral striatum.
Journal: Experimental physiology
In common: 5 references
[6] doi:10.1126/sciadv.aef3715
A cortical output channel for perceptual categorization.
Journal: Science advances
In common: cognitive, mouse, 4 references
[7] doi:10.1038/s41467-026-75945-2 [code]
Neural dynamics for working memory and evidence integration during olfactory navigation in Drosophila.
Journal: Nature communications
In common: shadedErrorBar, Curve Fitting Toolbox, Signal Processing Toolbox, 1 other tool, cognitive
[8] doi:10.1016/j.isci.2026.116240 [code]
Physical activity enhances theta-periodicity of visual attentional allocation.
Journal: iScience
In common: shadedErrorBar, Curve Fitting Toolbox, Signal Processing Toolbox, 1 other tool, cognitive
[9] doi:10.1016/j.isci.2026.117187 [code]
Functional and structural characterization of dendritic spine pathology in a mouse model of tauopathy.
Journal: iScience
In common: shadedErrorBar, Curve Fitting Toolbox, Signal Processing Toolbox, 1 other tool, mouse
[10] doi:10.1002/glia.70141 [code]
Conservation of Neuron-Astrocyte Correlated Activity in Developing Sensory Pathways.
Journal: Glia
In common: shadedErrorBar, Curve Fitting Toolbox, Signal Processing Toolbox, 1 other tool, mouse

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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