figureDiskUpsideDown20190527gin.m 54 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477
  1. clear all
  2. close all
  3. grating = displayPattern(360/22);
  4. velocity = displayCosine(12.5);
  5. [leftdisk,leftparam] = displayDiskStimuli('left');
  6. [rightdisk,rightparam] = displayDiskStimuli('right');
  7. singledisk = displayDiskStimuli('single');
  8. [diskhandle,diskdata,fitdata] = gainAnalysisDisk('Resolution',50);
  9. variant = 3; % 1 = regular, 2 = rotated, 3 = combined/corrected %%%%% !!!! %%%%%
  10. hd.collage.fg = figure();
  11. hd.collage.fg.Name = 'Sphere manuscript (Fig 6); OKR gain dependence on stimulus location';
  12. hd.collage.fg.Color = [1 1 1];
  13. hd.collage.fg.Position = [0 40 1070 960];
  14. % Populate the combined figure from previously generated partial figures...
  15. hd.collage.ax(1,1) = copyobj(grating.pattern.ax(1), hd.collage.fg);
  16. hd.collage.ax(1,2) = copyobj(velocity.cosine.ax(1), hd.collage.fg);
  17. hd.collage.ax(1,3) = copyobj(leftdisk.default.ax(1), hd.collage.fg);
  18. hd.collage.ax(1,4) = copyobj(rightdisk.default.ax(1), hd.collage.fg);
  19. hd.collage.ax(1,5) = copyobj(singledisk.default.ax(1), hd.collage.fg);
  20. % hd.collage.ax(2,1) = copyobj(diskdata.gain.ax(7), hd.collage.fg);
  21. hd.collage.ax(2,1) = axes;
  22. for subpanel = 2:4
  23. hd.collage.ax(2,subpanel) = copyobj(diskhandle.gain(variant).ax(subpanel-1), hd.collage.fg);
  24. end
  25. for subpanel = 5:7
  26. hd.collage.ax(2,subpanel) = copyobj(hd.collage.ax(2,subpanel-3), hd.collage.fg);
  27. end
  28. % ...get rid of the partial figures once pillaged...
  29. % close(1:7)
  30. % ...then adjust the size, position and other properties of those objects.
  31. [hd.collage.ax(1,1:5).Units, hd.collage.ax(2,1:7).Units] = deal('pixels');
  32. hd.collage.ax(1,1).Position = [100 850 180 70];
  33. hd.collage.ax(1,2).Position = [130 730 140 55];
  34. hd.collage.ax(1,3).Position = [350 730 200 200];
  35. hd.collage.ax(1,4).Position = hd.collage.ax(1,3).Position + [250 0 0 0];
  36. hd.collage.ax(1,5).Position = hd.collage.ax(1,3).Position + [520 0 0 0];
  37. hd.collage.ax(2,1).Position = [ 50 240 50 160];
  38. hd.collage.ax(2,2).Position = [ 90 280 350 350];
  39. for subpanel = 3:4
  40. hd.collage.ax(2,subpanel).Position = hd.collage.ax(2,subpanel-1).Position + [320 0 0 0];
  41. end
  42. for subpanel = 5:7
  43. hd.collage.ax(2,subpanel).Position = hd.collage.ax(2,subpanel-3).Position - [0 280 0 0];
  44. end
  45. darkgreyscale = (.8:.01:.99)' * [1 1 1];
  46. lightgreyscale = (.9:.01:.99)' * [1 1 1];
  47. [hd.collage.ax(1,3:4).Colormap] = deal(lightgreyscale);
  48. [hd.collage.ax(1,5).Colormap] = deal(darkgreyscale);
  49. % % Shared properties of all axis objects anywhere:
  50. [hd.collage.ax(1,1:5).FontSize, ...
  51. hd.collage.ax(2,1:7).FontSize] = deal(15);
  52. [hd.collage.ax(1,1:5).LabelFontSizeMultiplier, ...
  53. hd.collage.ax(1,1:5).TitleFontSizeMultiplier, ...
  54. hd.collage.ax(2,1:7).LabelFontSizeMultiplier, ...
  55. hd.collage.ax(2,1:7).TitleFontSizeMultiplier] = deal(1);
  56. % Create content of first subpanel of panel b (colorbar):
  57. hd.collage.ax(2,1).CLim = hd.collage.ax(2,2).CLim;
  58. hd.collage.ob(2,1,1) = colorbar;
  59. hd.collage.ob(2,1,1).Label.String = 'OKR gain';
  60. hd.collage.ob(2,1,1).Label.Units = 'normalized';
  61. hd.collage.ob(2,1,1).AxisLocation = 'in';
  62. hd.collage.ax(2,1).Visible = 'off';
  63. hd.collage.ob(2,1,1).Units = 'pixels';
  64. hd.collage.ob(2,1,1).Position(3) = 9;
  65. hd.collage.ob(2,1,1).FontSize = hd.collage.ax(2,1).FontSize;
  66. hd.collage.ob(2,1,1).Label.Position = [-.8 1 1] .* hd.collage.ob(2,1,1).Label.Position;
  67. % Add and format titles to panel a:
  68. for panel = 1
  69. hd.collage.ax(panel,1).Title.String = 'stimulus pattern';
  70. hd.collage.ax(panel,3).Title.String = '----------------------- stimulus crop -----------------------';
  71. hd.collage.ax(panel,3).Title.Interpreter = 'Latex';
  72. % hd.collage.ax(panel,4).Title.String = {'stimulus crop','(right hemisphere)'};
  73. hd.collage.ax(panel,2).XLabel.String = 'time (sec)';
  74. hd.collage.ax(panel,2).YLabel.String = {'velocity','(deg/sec)'};
  75. hd.collage.ax(panel,3).XLabel.String = 'left hemisphere';
  76. hd.collage.ax(panel,4).XLabel.String = 'right hemisphere';
  77. [hd.collage.ax(panel,3).XLabel.Visible, ...
  78. hd.collage.ax(panel,4).XLabel.Visible] = deal('on');
  79. hd.collage.ax(panel,5).Title.String = 'stimulus';
  80. for subpanel = 1:5
  81. hd.collage.ax(panel,subpanel).Title.FontWeight = 'normal';
  82. [hd.collage.ax(panel,subpanel).Title.Units, ...
  83. hd.collage.ax(panel,subpanel).XLabel.Units] = deal('pixels');
  84. end
  85. % hd.collage.ax(panel,1).Title.Position(2) = hd.collage.ax(panel,1).Position(4) - 14;
  86. hd.collage.ax(panel,1).XLabel.Position(2) = hd.collage.ax(panel,1).XLabel.Position(2) + 25;
  87. % hd.collage.ax(panel,2).XLabel.Position(2) = hd.collage.ax(panel,2).XLabel.Position(2) - 65;
  88. hd.collage.ax(panel,2).XLabel.Position(2) = hd.collage.ax(panel,2).XLabel.Position(2) + 25;
  89. hd.collage.ax(panel,3).Title.Position(1) = hd.collage.ax(1,3).Title.Position(1) + 126;
  90. for subpanel = 3:4
  91. hd.collage.ax(panel,subpanel).Title.Position(2) = hd.collage.ax(panel,subpanel).Position(4) - 2;
  92. hd.collage.ax(panel,subpanel).XLabel.Position(2) = hd.collage.ax(panel,subpanel).XLabel.Position(2) + 23;
  93. end
  94. % hd.collage.ax(panel,5).Title.Position(2) = hd.collage.ax(panel,4).Position(4) + 2;
  95. end
  96. % axes(hd.collage.ax(1,3))
  97. % leftline = text(hd.collage.ax(1,3).Title.Position(1) - 50, ...
  98. % hd.collage.ax(1,3).Title.Position(2) + 20, ...
  99. % '-----------------------','Interpreter','Latex','HorizontalAlignment','right','Units','pixels')
  100. % Force visibility of axis ticks on bottom left (sine) plot of panel a:
  101. % hd.collage.ax(1,2).XLim = [-13 13];
  102. hd.collage.ax(1,2).YLim = [-13 13];
  103. % Format titles on top row of panels b to d:
  104. for subpanel = 2:4
  105. hd.collage.ax(2,subpanel).Title.Units = 'pixels';
  106. hd.collage.ax(2,subpanel).Title.Position(2) = hd.collage.ax(2,subpanel).Title.Position(2) - 25;
  107. end
  108. % Correct for odd treatment of indices (or their absence):
  109. hd.collage.ax(2,3).Title.Position(2) = hd.collage.ax(2,3).Title.Position(2) + 9;
  110. % Remove titles from bottom row of panels b to d:
  111. for subpanel = 5:7
  112. hd.collage.ax(2,subpanel).Title.Visible = 'off';
  113. end
  114. % % Make fish on panels b to d more visible by adjusting sphere alpha:
  115. % [hd.collage.ax(
  116. % Adjust camera perspective on the lower row of panels b to d:
  117. [hd.collage.ax(2,5:7).CameraPosition] = deal([19.5 0 0]);
  118. % Display panel lettering:
  119. hd.collage.ax(4,1) = axes;
  120. hd.collage.ax(4,1).Color = 'none';
  121. hd.collage.ax(4,1).Position = [0 0 1 1];
  122. hd.collage.ax(4,1).Units = 'pixels';
  123. hd.collage.ax(4,1).Visible = 'off';
  124. hold on
  125. hd.collage.lt(1) = text(hd.collage.ax(1,1).Position(1) - 85 , ...
  126. hd.collage.ax(1,1).Position(2) + hd.collage.ax(1,1).Position(4) + 20, ...
  127. 'a', 'Units', 'pixels');
  128. hd.collage.lt(2) = text(hd.collage.ax(2,2).Position(1) - 50, ...
  129. hd.collage.ax(2,2).Position(2) + hd.collage.ax(2,2).Position(4) - 10, ...
  130. 'b', 'Units', 'pixels');
  131. hd.collage.lt(3) = text(hd.collage.ax(2,3).Position(1) + 35, ...
  132. hd.collage.ax(2,3).Position(2) + hd.collage.ax(2,3).Position(4) - 10, ...
  133. 'c', 'Units', 'pixels');
  134. hd.collage.lt(4) = text(hd.collage.ax(2,4).Position(1) + 35, ...
  135. hd.collage.ax(2,4).Position(2) + hd.collage.ax(2,4).Position(4) - 10, ...
  136. 'd', 'Units', 'pixels');
  137. hold off
  138. [hd.collage.lt(:).FontSize] = deal(15);
  139. [hd.collage.lt(:).FontWeight] = deal('bold');
  140. % plotAsymmetry(diskdata(variant))
  141. % mercatorPanelOldStimuli20190527
  142. mercatorPanelOldStimuli20190616
  143. delete(hd.collage.ax(1,1:2)) % remove panels about basic stimulus pattern
  144. close(1:8)
  145. for subpanel = 3:4
  146. hd.collage.ax(1,subpanel).Position = hd.collage.ax(1,subpanel).Position - [200 0 0 0];
  147. end
  148. hd.collage.ax(1,5).Position = hd.collage.ax(1,5).Position - [100 0 0 0];
  149. % [hd.collage.ax(1:2,1:2).Units,hd.collage.ax(3:5,:).Units] = deal('pixels');
  150. % [hd.collage.ax(1:2,1:2).FontSize,hd.collage.ax(3:5,:).FontSize] = deal(15);
  151. % [hd.collage.ax(1:2,1:2).LabelFontSizeMultiplier, ...
  152. % hd.collage.ax(1:2,1:2).TitleFontSizeMultiplier, ...
  153. % hd.collage.ax(3:5,:).LabelFontSizeMultiplier, ...
  154. % hd.collage.ax(3:5,:).TitleFontSizeMultiplier] = deal(1);
  155. function plotAsymmetry(diskdata)
  156. col.barface = [.2 .8 .4];
  157. col.neutralbarface = .6*[1 1 1];
  158. col.baredge = 'none';
  159. col.errorbar = .3*[1 1 1];
  160. hd.asymindex.fg = figure();
  161. hd.asymindex.fg.Color = [1 1 1];
  162. hd.asymindex.fg.Position = [0 100 1900 390];
  163. hd.asymindex.ax(1,1) = axes();
  164. % hd.asymindex.ax(1,2) = axes();
  165. [hd.asymindex.ax(:,:).Units] = deal('pixels');
  166. hd.asymindex.ax(1,1).Position = [90 50 1780 300];
  167. y5a = diskdata.asymmetry.A1;
  168. y5b = diskdata.asymmetry.A2;
  169. % e5a = mscdisk.botheyes.stdgain;
  170. % e5b = upside.botheyes.stdgain;
  171. e5a = NaN(size(y5a));
  172. e5b = NaN(size(y5b));
  173. pvalue.asymmetry = signrank(y5a,y5b);
  174. x5a = (1:numel(y5a))' - .2;
  175. x5b = (1:numel(y5b))' + .2;
  176. axes(hd.asymindex.ax(1,1))
  177. hold on
  178. hd.asymindex.barregular = bar(x5a,y5a);
  179. hd.asymindex.barcontrol = bar(x5b,y5b);
  180. hd.asymindex.errorregular = errorbar(x5a,y5a,e5a,'LineStyle','none');
  181. hd.asymindex.errorcontrol = errorbar(x5b,y5b,e5b,'LineStyle','none');
  182. hd.asymindex.explanation = text(24,.351,{'biological asymmetry shown in green', ...
  183. '(error bars show s.e.m.)'},'HorizontalAlignment','center');
  184. hd.asymindex.pvalue = text(36,.351,{['p = ',num2str(pvalue.asymmetry)], ...
  185. '(Wilcoxon signed-rank test)'},'HorizontalAlignment','center');
  186. hold off
  187. [hd.asymindex.ax(1,:).XTick] = deal([]);
  188. hd.asymindex.barregular.FaceColor = col.neutralbarface;
  189. hd.asymindex.barcontrol.FaceColor = col.barface;
  190. [hd.asymindex.barregular.BarWidth, ...
  191. hd.asymindex.barcontrol.BarWidth] = deal(.35);
  192. [hd.asymindex.errorregular.Color, ...
  193. hd.asymindex.errorcontrol.Color] = deal(col.errorbar);
  194. [hd.asymindex.errorregular.LineWidth, ...
  195. hd.asymindex.errorcontrol.LineWidth] = deal(1);
  196. hd.asymindex.ax(1,1).XLim = [7 46];
  197. % hd.asymindex.ax(1,1).YLim = hd.asymindex.ax(1,2).YLim;
  198. hd.asymindex.ax(1,1).YLabel.String = 'mean OKR gain (pooled across eyes)';
  199. % hd.asymindex.ax(1,2).YLabel.String = 'mean OKR gain (right eye)';
  200. for panel = 1:1
  201. hd.asymindex.ax(1,panel).YLabel.Units = 'pixels';
  202. hd.asymindex.ax(1,panel).YLabel.Position(1) = hd.asymindex.ax(1,panel).YLabel.Position(1) - 20;
  203. end
  204. hd.asymindex.ax(1,1).XLabel.String = 'stimulus types';
  205. % hd.asymindex.ax(1,2).XLabel.String = 'stimulus types';
  206. [hd.asymindex.explanation.FontSize, ...
  207. hd.asymindex.pvalue.FontSize] = deal(13);
  208. for panel = 1:1
  209. hd.asymindex.ax(1,panel).XLabel.FontSize = 13;
  210. hd.asymindex.ax(1,panel).FontSize = 13;
  211. hd.asymindex.ax(1,panel).LabelFontSizeMultiplier = 1;
  212. hd.asymindex.ax(1,panel).YGrid = 'on';
  213. hd.asymindex.ax(1,panel).TickLength = [0 0];
  214. end
  215. end
  216. function hd = displayPattern(barwidth)
  217. spatialfreq = 180/barwidth;
  218. grid.colour = .3*[1 1 1];
  219. grid.width = 360;
  220. grid.height = 180;
  221. grid.x0 = -180;
  222. grid.y0 = -90;
  223. hd.pattern.fg = figure();
  224. hd.pattern.ax = axes();
  225. hold on
  226. dy = grid.height;
  227. numbar = ceil(grid.width*spatialfreq);
  228. dx = grid.width / spatialfreq / 2;
  229. for barindex = 1:numbar
  230. x0 = grid.x0 + (barindex-1) * dx * 2;
  231. xmax = grid.x0 + grid.width;
  232. y0 = grid.y0;
  233. darkbar(barindex) = patch([x0*[1 1],min(x0+dx,xmax)*[1 1]], ...
  234. [y0,y0+dy,y0+dy,y0], grid.colour);
  235. end
  236. [darkbar(:).EdgeColor] = deal('none');
  237. outline = plot(grid.x0*[1 1 1 1 1] + grid.width*[0 0 1 1 0], ...
  238. grid.y0*[1 1 1 1 1] + grid.height*[0 1 1 0 0], ...
  239. 'Color', grid.colour);
  240. hold off
  241. hd.pattern.ax.PlotBoxAspectRatio = [grid.width, grid.height, grid.height];
  242. % % [hd.pattern.ax.XAxis.Visible, hd.pattern.ax.YAxis.Visible] = deal('off');
  243. hd.pattern.ax.XLim = [-180 180];
  244. hd.pattern.ax.YLim = [-90 90];
  245. hd.pattern.ax.XTick = [-180 180];
  246. hd.pattern.ax.YTick = [-90 0 90];
  247. hd.pattern.ax.XLabel.String = 'azimuth';
  248. hd.pattern.ax.YLabel.String = 'elevation';
  249. end
  250. function hd = displayCosine(maxvelocity)
  251. time = 0:.2:25;
  252. velocity = sin(time*2*pi/10) * maxvelocity;
  253. hd.cosine.fg = figure();
  254. hd.cosine.ax = axes();
  255. hold on
  256. hd.cosine.ob(1) = plot(time([1 end]),[0 0]);
  257. hd.cosine.ob(2) = plot(time,velocity);
  258. hold off
  259. hd.cosine.ob(1).LineStyle = ':';
  260. hd.cosine.ob(1).Color = .0*[1 1 1];
  261. hd.cosine.ob(2).Color = .2*[1 1 1];
  262. hd.cosine.ax.Box = 'off';
  263. hd.cosine.ax.XGrid = 'on';
  264. % hd.cosine.ax.XMinorGrid = 'on';
  265. % hd.cosine.ax.YGrid = 'on';
  266. % hd.cosine.ax.XAxisLocation = 'origin';
  267. hd.cosine.ax.XTick = [0 25];
  268. % hd.cosine.ax.XTick = 25;
  269. % hd.cosine.ax.XTickLabel = hd.cosine.ax.XTick;
  270. hd.cosine.ax.YTick = [-1 1] * maxvelocity;
  271. end
  272. function [handle,data,datafit] = gainAnalysisDisk(varargin) %#ok<FNDEF>
  273. % GAINANALYSIS and all subfunctions were written by Florian Alexander
  274. % Dehmelt in 2018. This is version 20180911, a.k.a. gainAnalysis20180911.m.
  275. % Apologies for the lack of polish (and comments) at this point.
  276. %
  277. % All input arguments are optional:
  278. %
  279. % Resolution - scalar, indicating your choice of resolution for the
  280. % rendering of the spherical surface as part % of the
  281. % figures. Actual data fitting is completely unaffected.
  282. % CamAzimuth - scalar, horizontal angle at which sphere is shown
  283. % CamElevation - scalar, vertical angle at which sphere is shown
  284. % Normalisation - options: nothing, or 'mean gain per hemisphere',
  285. % or 'max gain per hemisphere',
  286. % or 'ratio of mean gains per hemisphere'
  287. %
  288. % Output arguments are:
  289. %
  290. % handle - structure containing handles to all graphics objects
  291. % (figures, axes, plots, decorations)
  292. % data - structure containing pre-processed data, with those fields:
  293. % .left - numerical array of means of OKR gain for the left eye
  294. % .right - numerical array of means of OKR gain for the right eye
  295. % .both - numerical array of mean of OKR gain means across both eyes
  296. % datafit - structure containing the fits to data, with following fields:
  297. % .centre - 2x2 numerical array of azimuths (1st row) and elevation
  298. % angles (2nd row) of the two individial von Mises-Fisher
  299. % functions fit to data (1st column = 1st function, etc.).
  300. % These are NOT the peaks of the actual fit (= sum of both
  301. % functions)! I still have to implement that feature.
  302. % .value - 36x1 numerical array containing the value of the actual fit
  303. % (= sum of both functions) at the locations sampled by data
  304. % .param - 1x9 numerical array containing all best-fit parameters
  305. % (offset, amplitude1, azimuth1, elevation1, concentration1,
  306. % amplitude2, azimuth2, elevation2, concentration2)
  307. %
  308. % The recommended default is calling "hd = gainAnalysis20180911(200);".
  309. %
  310. % To specify camera position before analysis, call the function as follows:
  311. % hd = gainAnalysis20180911(200,'CamAzimuth',-30,'CamElevation',15);
  312. %
  313. % To edit camera position post-hoc, adjust and execute the following lines:
  314. % az = 0; el = 0; % substitute your desired camera coordinates here
  315. % [hd.gain(end).ax(1:6).CameraPosition] = ...
  316. % deal([cosd(az)*cosd(el),sind(az)*cosd(el),sind(el)]/19.0526);
  317. % figure(21)
  318. % MANAGE VARIABLE INPUT
  319. p = inputParser;
  320. valid.resolution = @(x) isnumeric(x) && numel(x)==1 && x>0;
  321. valid.camazimuth = @(x) isnumeric(x) && numel(x)==1;
  322. valid.camelevation = @(x) isnumeric(x) && numel(x)==1;
  323. valid.normalisation = @(x) ischar(x);
  324. % valid.rotation = @(x) ischar(x);
  325. default.resolution = 200;
  326. default.camazimuth = 0;
  327. default.camelevation = 45;
  328. default.normalisation = '';
  329. % default.rotation = 'none';
  330. addOptional(p, 'Resolution', default.resolution, valid.resolution);
  331. addOptional(p, 'CamAzimuth', default.camazimuth, valid.camazimuth);
  332. addOptional(p, 'CamElevation', default.camelevation, valid.camelevation);
  333. addOptional(p, 'Normalisation', default.normalisation, valid.normalisation);
  334. % addOptional(p, 'Rotation', default.rotation, valid.rotation);
  335. parse(p,varargin{:});
  336. resolution = p.Results.Resolution;
  337. cam.azimuth = p.Results.CamAzimuth;
  338. cam.elevation = p.Results.CamElevation;
  339. normalisation = p.Results.Normalisation;
  340. % rotation = p.Results.Rotation;
  341. % GENERAL SETTINGS
  342. warning('off','MATLAB:interp1:UsePCHIP')
  343. % The line above deactivates the following cbrewer warning:
  344. % Warning: INTERP1(...,'CUBIC') will change in a future release. Use INTERP1(...,'PCHIP') instead.
  345. % > In interp1>sanitycheckmethod (line 265)
  346. % In interp1>parseinputs (line 401)
  347. % In interp1 (line 80)
  348. % In interpolate_cbrewer (line 31)
  349. % In cbrewer (line 101)
  350. % In heatmap20180823 (line 21)
  351. % Identify which stimulus entries correspond to the disk-mask stimuli.
  352. xlsrange = 2:37;
  353. % xlsrange = 9:46;
  354. % % xlsrange = 9:44;
  355. % !!! IMPORTANT !!! These are the Excel row numbers, not stimulus types.
  356. % READ DATA
  357. % Query user to manually select the main data regularset. Other files have
  358. % standard names and are expected to be in the same folder.
  359. [mfilefolder,~,~] = fileparts(mfilename('fullpath'));
  360. % % [regularset.result,regularset.folder] = uigetfile([mfilefolder,'\*.mat']);
  361. % regularset.file = 'result_20180110_125537.mat';
  362. % regularset.folder = ['..\..\data\Fig3 default disk data\Data D Series (neue Experimente)\'];
  363. % rotatedset.file = 'result_20181001_114504.mat';
  364. % rotatedset.folder = ['..\..\data\Fig3 default disk data\Control Data Rotation (arena flip)\'];
  365. % upside down embedding, no rotation of arena
  366. % (both identical to bypass correction!)
  367. regularset.file = 'result_20180913_151415.mat';
  368. regularset.folder = ['..\..\data\Fig4 rotated arena data\Control Data Upside Down (Kontrolle ventral)\'];
  369. rotatedset.file = regularset.file;
  370. rotatedset.folder = regularset.folder;
  371. regularset.structure = load([regularset.folder,regularset.file],'result');
  372. rotatedset.structure = load([rotatedset.folder,rotatedset.file],'result');
  373. regularset.result = regularset.structure.result;
  374. rotatedset.result = rotatedset.structure.result;
  375. % Discard unwanted trials, based on .xlsx documenting manual assessment.
  376. regularset.result = discardTrial(regularset.result,regularset.folder);
  377. rotatedset.result = discardTrial(rotatedset.result,rotatedset.folder);
  378. % Pool gain data in various ways (this is a significant subfunction!).
  379. regularset.gain = poolGainData(regularset.result);
  380. rotatedset.gain = poolGainData(rotatedset.result);
  381. % READ STIMULUS INFORMATION
  382. regularset.stimcentre = readStimulus(regularset.folder,xlsrange);
  383. rotatedset.stimcentre = readStimulus(rotatedset.folder,xlsrange);
  384. % reordering = [1;2;3;5;4;7;6]; % hemisphere stimuli, U vs. D and L vs. R flipped; now add disks:
  385. % for regularindex = 1:size(regularset.stimcentre,1)
  386. % % Find rotated-arena stimulus appearing at same position relative to fish:
  387. % rotatedindex = find(sum(regularset.stimcentre(regularindex,:)==rotatedset.stimcentre,2)==2);
  388. % reordering = [reordering; rotatedindex]; %#ok<AGROW>
  389. % end
  390. %
  391. % Correction above is unnecessary if stimulus coordinates were already in
  392. % fish-centred coordinates. In this case, do not reorder data:
  393. % reordering = 1:45;
  394. reordering = 1:43;
  395. % CHOOSE SUBSET OF DATA TO ANALYSE, AND PROCEED TO ANALYSE IT
  396. variantlist = {'regular','rotated','corrected'};
  397. for variant = 1:numel(variantlist)
  398. switch variantlist{variant}
  399. case 'regular'
  400. data(variant).left = regularset.gain.mean.FR{1};
  401. data(variant).right = regularset.gain.mean.FR{2};
  402. case 'rotated'
  403. data(variant).left = rotatedset.gain.mean.FR{1}(reordering);
  404. data(variant).right = rotatedset.gain.mean.FR{1}(reordering);
  405. case 'corrected'
  406. % data(variant).left = (regularset.gain.mean.FR{1} + rotatedset.gain.mean.FR{1}) / 2;
  407. % data(variant).right = (regularset.gain.mean.FR{2} + rotatedset.gain.mean.FR{1}) / 2;
  408. data(variant).left = (regularset.gain.mean.FR{1} + rotatedset.gain.mean.FR{1}(reordering)) / 2;
  409. data(variant).right = (regularset.gain.mean.FR{2} + rotatedset.gain.mean.FR{1}(reordering)) / 2;
  410. for selection = xlsrange-1
  411. gLreg = regularset.gain.mean.FR{1}(selection);
  412. gRreg = regularset.gain.mean.FR{2}(selection);
  413. gLrot = rotatedset.gain.mean.FR{1}(reordering(selection));
  414. gRrot = rotatedset.gain.mean.FR{2}(reordering(selection));
  415. data(variant).asymmetry.A1(selection) = ((gLreg-gRreg)/(gLreg+gRreg) - (gLrot-gRrot)/(gLrot+gRrot)) / 2;
  416. data(variant).asymmetry.A2(selection) = ((gLreg-gRreg)/(gLreg+gRreg) + (gLrot-gRrot)/(gLrot+gRrot)) / 2;
  417. end
  418. end
  419. data(variant).both = [data(variant).left(xlsrange(1:end/2)-1);data(variant).right(xlsrange(end/2+1:end)-1)];
  420. % data(variant).both = mean([data(variant).left,data(variant).right],2); % important change! no more averaging
  421. % Create figure(s).
  422. hd.gain(variant).fg = figure();
  423. hd.gain(variant).fg.Name = 'OKR gain';
  424. hd.gain(variant).fg.Color = [1 1 1];
  425. hd.gain(variant).fg.Position = [50 50 1000 650];
  426. hd.gain(variant).fg.Colormap = flipud(cbrewer('div','RdBu',100));
  427. % hd.gain(variant).fg.Colormap = cbrewer('div','PRGn',100);
  428. choicelist = {'left','both','right'};
  429. for choice = 1:numel(choicelist)
  430. switch choicelist{choice}
  431. case 'right'
  432. response = data(variant).right(xlsrange-1); % -1 is offset by Excel title row
  433. case 'left'
  434. response = data(variant).left(xlsrange-1);
  435. case 'both'
  436. response = data(variant).both;
  437. otherwise
  438. error(['Input argument must be one of the following strings: ' ...
  439. '''left'', ''right'', or ''both''.'])
  440. end
  441. [x,y,z] = sphere(resolution);
  442. [datafit(choice).matrix,datafit(choice).param] = ...
  443. vonMisesFisherFit(regularset.stimcentre,response,x,y,z);
  444. % set 3D fish parameters
  445. scale = .1;
  446. yaw = 180;
  447. pitch = 0;
  448. roll = 0;
  449. % set decorative arc parameters; compute their cartesian coordinates
  450. equator.base = -1:.01:1;
  451. equator.x = [equator.base, fliplr(equator.base)];
  452. equator.y = [sqrt(1-equator.base.^2), -sqrt(1-equator.base.^2)];
  453. equator.z = 0*ones(1,numel(equator.x));
  454. meridian.x = equator.x;
  455. meridian.y = equator.z;
  456. meridian.z = equator.y;
  457. aura.x = equator.z;
  458. aura.y = equator.y;
  459. aura.z = equator.x;
  460. [sample.x,sample.y,sample.z] = ...
  461. geo2car(regularset.stimcentre(:,1), regularset.stimcentre(:,2), ...
  462. ones(size(regularset.stimcentre,1),1),'CCW');
  463. % plot all of the above onto the correct panel of the existing figure
  464. panel = choice;
  465. figure(hd.gain(variant).fg.Number)
  466. hd.gain(variant).ax(panel) = axes;
  467. hd.gain(variant).ax(panel).Position = [.13 .45 .35 .45] + (panel-1)*[.25 0 0 0];
  468. hold on
  469. hd.gain(variant).ob(panel,1) = surf(x,y,z,datafit(panel).matrix);
  470. shading interp
  471. colour.eye = 0*[1 1 1];
  472. colour.body = 1*[1 1 1];
  473. hd.gain(variant).ob(panel,2:4) = plot3dFish(scale,yaw,pitch,roll,colour);
  474. hd.gain(variant).ob(panel,5) = plot3(equator.x,equator.y,equator.z,'k','LineWidth',2);
  475. hd.gain(variant).ob(panel,6) = plot3(meridian.x,meridian.y,meridian.z,'--k');
  476. hd.gain(variant).ob(panel,7) = plot3(aura.x,aura.y,aura.z,':k');
  477. hd.gain(variant).ob(panel,8) = scatter3(1.03*sample.x,1.03*sample.y,1.03*sample.z, ...
  478. 180*ones(size(sample.x)),response,'Marker','o');
  479. hold off
  480. end
  481. % Apply full formatting to the figure(s). Subfunction included below.
  482. hd = fishbowlFigure(hd,cam,choicelist);
  483. % % Compute yoking/lateralisation index per individual stimulus location.
  484. % convention = 'CCW';
  485. % switch convention
  486. % case 'CCW'
  487. % stim.isleft = stim.azimuth > 0;
  488. % stim.isright = stim.azimuth < 0;
  489. % case 'CW'
  490. % stim.isleft = stim.azimuth < 0;
  491. % stim.isright = stim.azimuth > 0;
  492. % otherwise
  493. % error('Sign convention must either be ''CCW'' or ''CW''.')
  494. % end
  495. end
  496. % Collect output arguments, unless already done (e.g., "datafit").
  497. handle = hd;
  498. end
  499. function stimcentre = readStimulus(datafolder,xlsrange)
  500. stimulus = 'Stimulus.xlsx';
  501. [~,tableconvention,~] = xlsread([datafolder,stimulus],'C1:C1');
  502. azimuth = xlsread([datafolder,stimulus],['C',num2str(xlsrange(1)),':C',num2str(xlsrange(end))]);
  503. elevation = xlsread([datafolder,stimulus],['D',num2str(xlsrange(1)),':D',num2str(xlsrange(end))]);
  504. % Stimulus tables may use CW convention; default code uses CCW.
  505. switch tableconvention{:}
  506. case 'azimuth (CCW)'
  507. case 'azimuth (CW)'
  508. azimuth = -azimuth;
  509. otherwise
  510. error(['Field C1 of Stimulus.xlsx should contain either ' ...
  511. 'string ''azimuth (CCW)'' or string ''azimuth (CW)''.'])
  512. end
  513. stimcentre = [azimuth,elevation];
  514. end
  515. function gain = poolGainData(result)
  516. %POOLGAINDATA converts a raw result structure into plottable data by
  517. %pooling OKR gain data in various ways, and storing those results in a new
  518. %structure called "gain".
  519. % Pool data in various ways (e.g., .FR = pooled across all [F]ish and
  520. % all [R]epetitions, but with a separate array for each [E]ye).
  521. for stimtype = 1:size(result,3)
  522. gainpool(stimtype).FER = [result(:,:,stimtype,:).gain];
  523. for eye = 1:size(result,2)
  524. gainpool(stimtype).FR{eye} = [result(:,eye,stimtype,:).gain];
  525. end
  526. for fish = 1:size(result,1)
  527. gainpool(stimtype).ER{fish} = [result(fish,:,stimtype,:).gain];
  528. end
  529. for repetition = 1:size(result,4)
  530. gainpool(stimtype).FE{repetition} = [result(:,:,stimtype,repetition).gain];
  531. end
  532. for fish = 1:size(result,1)
  533. for eye = 1:size(result,2)
  534. gainpool(stimtype).R{fish,eye} = [result(fish,eye,stimtype,:).gain];
  535. end
  536. end
  537. end
  538. % Make sure that in following section, if a stimulus type is not present,
  539. % the array contain a NaN at that position, instead of a zero.
  540. nantemplate = NaN(numel(gainpool),1);
  541. gain.mean .FER = nantemplate;
  542. gain.median.FER = nantemplate;
  543. gain.std .FER = nantemplate;
  544. gain.sem .FER = nantemplate;
  545. for eye = 1:size(result,2)
  546. gain.mean .FR{eye} = nantemplate;
  547. gain.median.FR{eye} = nantemplate;
  548. gain.std .FR{eye} = nantemplate;
  549. gain.sem .FR{eye} = nantemplate;
  550. end
  551. for fish = 1:size(result,1)
  552. gain.mean .ER{fish} = nantemplate;
  553. gain.median.ER{fish} = nantemplate;
  554. gain.std .ER{fish} = nantemplate;
  555. gain.sem .ER{fish} = nantemplate;
  556. end
  557. for fish = 1:size(result,1)
  558. for eye = 1:size(result,2)
  559. gain.mean .R{fish,eye} = nantemplate;
  560. gain.median.R{fish,eye} = nantemplate;
  561. gain.std .R{fish,eye} = nantemplate;
  562. gain.sem .R{fish,eye} = nantemplate;
  563. end
  564. end
  565. % mean, median, stdev, sem across all pooled data, one per stimulus type
  566. for stimtype = 1:numel(gainpool)
  567. selection = gainpool(stimtype).FER;
  568. gain.mean .FER(stimtype) = mean(selection);
  569. gain.median.FER(stimtype) = median(selection);
  570. gain.std .FER(stimtype) = std(selection);
  571. gain.sem .FER(stimtype) = std(selection)/sqrt(numel(selection));
  572. for eye = 1:size(result,2)
  573. selection = gainpool(stimtype).FR{eye};
  574. gain.mean .FR{eye}(stimtype) = mean(selection);
  575. gain.median.FR{eye}(stimtype) = median(selection);
  576. gain.std .FR{eye}(stimtype) = std(selection);
  577. gain.sem .FR{eye}(stimtype) = std(selection)/sqrt(numel(selection));
  578. end
  579. for fish = 1:size(result,1)
  580. selection = gainpool(stimtype).ER{fish};
  581. gain.mean .ER{fish}(stimtype) = mean(selection);
  582. gain.median.ER{fish}(stimtype) = median(selection);
  583. gain.std .ER{fish}(stimtype) = std(selection);
  584. gain.sem .ER{fish}(stimtype) = std(selection)/sqrt(numel(selection));
  585. end
  586. for fish = 1:size(result,1)
  587. for eye = 1:size(result,2)
  588. selection = gainpool(stimtype).R{fish,eye};
  589. gain.mean .R{fish,eye}(stimtype) = mean(selection);
  590. gain.median.R{fish,eye}(stimtype) = median(selection);
  591. gain.std .R{fish,eye}(stimtype) = std(selection);
  592. gain.sem .R{fish,eye}(stimtype) = std(selection)/sqrt(numel(selection));
  593. end
  594. end
  595. end
  596. end
  597. function [fitresult,fitparam] = vonMisesFisherFit(stimcentre,response,x,y,z)
  598. predictor = stimcentre;
  599. % Initialise fit parameters.
  600. offset = 0;
  601. amplitude(1) = 1;
  602. amplitude(2) = 1;
  603. meanazimuth(1) = 70;
  604. meanelevation(1) = 20; % azimuth, elevation
  605. meanazimuth(2) = -70;
  606. meanelevation(2) = 20; % azimuth, elevation
  607. concentration(1) = 2.5;
  608. concentration(2) = 2.5;
  609. initialparam = [offset, ...
  610. amplitude(1), ...
  611. meanazimuth(1), ...
  612. meanelevation(1), ...
  613. concentration(1), ...
  614. amplitude(2), ...
  615. meanazimuth(2), ...
  616. meanelevation(2), ...
  617. concentration(2)];
  618. % Fit.
  619. option.MaxIter = 1e4;
  620. datafit.param = nlinfit(predictor,response,@vonmisesfisher9param,initialparam,option);
  621. datafit.value = vonmisesfisher9param(datafit.param,predictor);
  622. % Identify centres.
  623. azimuth(1) = datafit.param(3);
  624. elevation(1) = datafit.param(4);
  625. azimuth(2) = datafit.param(7);
  626. elevation(2) = datafit.param(8);
  627. for k = 1:2
  628. % azimuth(k) = datafit.param(3); % This was incorrect - same value read twice.
  629. % elevation(k) = datafit.param(4);
  630. uniqueazimuth(k) = mod(azimuth(k)+180,360) - 180;
  631. uniqueelevation(k) = mod(elevation(k)+90,180) - 90;
  632. datafit.centre(:,k) = [uniqueazimuth(k), uniqueelevation(k)];
  633. end
  634. % compute geographic coordinates of sphere, and its colour data values
  635. [azimuth,elevation,radius] = car2geo(x,y,z,'CCW');
  636. predictor = [azimuth(:),elevation(:),radius(:)];
  637. fitresult = reshape(vonmisesfisher9param(datafit.param,predictor),size(radius));
  638. fitparam = datafit.param;
  639. end
  640. function hd = fishbowlFigure(hd,cam,choicelist)
  641. %FISHBOWLFIGURE configures an existing figure of fishbowl plots, adds decorations
  642. %
  643. % HD = fishbowlFigure sets the parameters of an existing fishbowl figure,
  644. % including axis limits, labels, colours, axes object sizes, etc., and
  645. % adds additional decorations such as colorbars.
  646. cmin = min([min(min(hd.gain(end).ob(1,1).CData)), ...
  647. min(min(hd.gain(end).ob(2,1).CData)), ...
  648. min(min(hd.gain(end).ob(3,1).CData)), ...
  649. min(min(hd.gain(end).ob(1,8).CData)), ...
  650. min(min(hd.gain(end).ob(2,8).CData)), ...
  651. min(min(hd.gain(end).ob(3,8).CData))]);
  652. cmax = max([max(max(hd.gain(end).ob(1,1).CData)), ...
  653. max(max(hd.gain(end).ob(2,1).CData)), ...
  654. max(max(hd.gain(end).ob(3,1).CData)), ...
  655. max(max(hd.gain(end).ob(1,8).CData)), ...
  656. max(max(hd.gain(end).ob(2,8).CData)), ...
  657. max(max(hd.gain(end).ob(3,8).CData))]);
  658. clim = max(abs([cmin cmax]))*[-1 1];
  659. for panel = 1:3
  660. switch choicelist{panel}
  661. case 'left'
  662. string.title = 'left eye gain g_L';
  663. case 'right'
  664. string.title = 'right eye gain g_R';
  665. case 'both'
  666. string.title = 'merge stimulated';
  667. % string.title = 'mean = (g_L+ g_R)/2';
  668. end
  669. hd.gain(end).ax(panel).Title.String = string.title;
  670. % title(string.title,'Interpreter','LaTeX')
  671. hd.gain(end).ob(panel,8).MarkerFaceColor = hd.gain(end).ob(panel,8).MarkerEdgeColor;
  672. hd.gain(end).ob(panel,8).MarkerEdgeColor = [0 0 0];
  673. hd.gain(end).ax(panel).CLim = [0 cmax];
  674. % hd.gain(end).ax(panel).CLim = [0 max(max(datafit(choice).matrix))];
  675. % hd.gain(end).ob(panel,1).FaceAlpha = .85;
  676. hd.gain(end).ob(panel,1).FaceAlpha = .75;
  677. hd.gain(end).ob(panel,1).EdgeColor = 'none';
  678. % hd.gain(end).ob(panel,8).Shading = 'flat';
  679. shading interp
  680. axis square
  681. xlabel('x')
  682. ylabel('y')
  683. zlabel('z')
  684. hd.gain(end).ax(panel).XLim = [-1.1 1.1];
  685. hd.gain(end).ax(panel).YLim = [-1.1 1.1];
  686. hd.gain(end).ax(panel).ZLim = [-1.1 1.1];
  687. % hd.gain(end).ax(panel).CameraPosition = [12.2934, -9.0967,8.1315];
  688. cam.radius = 19.0526; % MATLAB automatically reverts to this values while rotating, anyway. Might as well choose it as default.
  689. [cam.x,cam.y,cam.z] = geo2car(cam.azimuth,cam.elevation,cam.radius,'CCW');
  690. hd.gain(end).ax(panel).CameraPosition = [cam.x cam.y cam.z]; % FD20190507: this hides 3rd fish!!!
  691. hd.gain(end).ax(panel).CameraTarget = [0 0 0];
  692. hd.gain(end).ax(panel).CameraUpVector = [0 0 1];
  693. % hd.gain(end).ax(panel).CameraViewAngle = 10.134;
  694. hd.gain(end).ax(panel).CameraViewAngle = 9;
  695. end
  696. hd.gain(end).ax(4) = axes;
  697. hd.gain(end).ax(4).Position = hd.gain(end).ax(1).Position - [.13 -.1 .2 .2];
  698. hd.gain(end).ax(4).CLim = hd.gain(end).ax(1).CLim;
  699. hd.gain(end).ob(4,1) = colorbar;
  700. hd.gain(end).ob(4,1).Label.String = 'OKR gain';
  701. hd.gain(end).ob(4,1).Label.Units = 'normalized';
  702. hd.gain(end).ob(4,1).Label.Position = [-.8 1 1] .* hd.gain(end).ob(4,1).Label.Position;
  703. [hd.gain(end).ob(4,1).Label.FontSize, hd.gain(end).ob(4,1).FontSize, ...
  704. hd.gain(end).ax.FontSize] = deal(14);
  705. [hd.gain(end).ax(1).Title.FontSize, ...
  706. hd.gain(end).ax(2).Title.FontSize, ...
  707. hd.gain(end).ax(3).Title.FontSize] = deal(14);
  708. [hd.gain(end).ax(1).Title.FontWeight, ...
  709. hd.gain(end).ax(2).Title.FontWeight, ...
  710. hd.gain(end).ax(3).Title.FontWeight] = deal('normal');
  711. [hd.gain(end).ax.Visible] = deal('off');
  712. % [hd.gain(end).ax.Title.Visible] = deal('on');
  713. set(([hd.gain(end).ax.Title]),'Visible','on')
  714. set(([hd.gain(end).ax.Title]),'Position',[1 1 .7] .* hd.gain(end).ax(1).Title.Position)
  715. end
  716. function [retained,original] = discardTrial(original,folderstring)
  717. % DISCARDTRIAL removes data obtained from trials consider "poorly fit" etc.
  718. %
  719. % This function takes a "result" structure as its only input argument, and
  720. % requires an XLSX file containing the indices of trials to be discarded.
  721. % It then removes those bad trials, and returns a reduced structure as its
  722. % primary output argument, "retained". For easy access, it also returns its
  723. % original input.
  724. %
  725. % This function can safely be executed multiple times, as "discarded"
  726. % trials are in fact just set to empty, so their array indices are not
  727. % reassigned to other trials.
  728. %
  729. % Written by Florian Alexander Dehmelt, Tuebingen University, in 2018.
  730. % Identify and load an Excel file containing the indices of all unwanted
  731. % trials to be discarded. This file must contain four columns of numbers
  732. % (from left to right: fish no., eye no., stimulus type, and repetition.
  733. % It may contain as many text comments as desired; these will be ignored.
  734. folder.badtrial = folderstring;
  735. % folder.badtrial = 'C:\Users\fdehmelt\Dropbox\ThinkPad Dropbox\MATLAB\201808 SphereAnalysis\Sample Data\D series\';
  736. regularset.badtrial = 'DiscardTrial.xlsx';
  737. badtrial = xlsread([folder.badtrial,regularset.badtrial]);
  738. % The "retained" structure is the same as the "original" one...
  739. retained = original;
  740. % ...except that unwanted trials are overwritten by empty values.
  741. % They thus appear the same way as non-existent trials already did.
  742. for trial = 1:size(badtrial,1)
  743. % Find the position of the bad trial within the structure.
  744. fish = badtrial(trial,1);
  745. eye = badtrial(trial,2);
  746. stimtype = badtrial(trial,3) + 1; % The +1 converts index >=0 to >=1.
  747. repetition = badtrial(trial,4);
  748. % Overwrite all field entries with "[]".
  749. fieldname = fieldnames(retained);
  750. for field = 1:numel(fieldname)
  751. retained(fish,eye,stimtype,repetition).(fieldname{field}) = [];
  752. end
  753. end
  754. end
  755. function [altered,original] = zeroTrial(original,folderstring)
  756. % ZEROTRIAL sets gains to zero based on visual inspection of trials.
  757. %
  758. % This function takes a "result" structure as its only input argument, and
  759. % requires an XLSX file containing the indices of trials to be set to zero.
  760. % It then adjusts those gains, and returns an altered structure as its
  761. % primary output argument, "retained". For easy access, it also returns its
  762. % original input.
  763. %
  764. % This function can safely be executed multiple times, as "altered"
  765. % trials are in fact just set to zero, so their array indices are not
  766. % reassigned to other trials.
  767. %
  768. % Written by Florian Alexander Dehmelt, Tuebingen University, in 2018.
  769. % Identify and load an Excel file containing the indices of all unwanted
  770. % trials to be discarded. This file must contain four columns of numbers
  771. % (from left to right: fish no., eye no., stimulus type, and repetition.
  772. % It may contain as many text comments as desired; these will be ignored.
  773. folder.badtrial = folderstring;
  774. % folder.badtrial = 'C:\Users\fdehmelt\Dropbox\ThinkPad Dropbox\MATLAB\201808 SphereAnalysis\Sample Data\D series\';
  775. regularset.badtrial = 'ZeroTrial.xlsx';
  776. badtrial = xlsread([folder.badtrial,regularset.badtrial]);
  777. % The "altered" structure is the same as the "original" one...
  778. altered = original;
  779. % ...except that gains of visually identified trials are set to zero.
  780. % They thus appear the same way as non-existent trials already did.
  781. for trial = 1:size(badtrial,1)
  782. % Find the position of the bad trial within the structure.
  783. fish = badtrial(trial,1);
  784. eye = badtrial(trial,2);
  785. stimtype = badtrial(trial,3) + 1; % The +1 converts index >=0 to >=1.
  786. repetition = badtrial(trial,4);
  787. % Overwrite the gain field entries with "0", retain all other values.
  788. altered(fish,eye,stimtype,repetition).gain = 0;
  789. end
  790. end
  791. function combinedvalue = vonmisesfisher9param(param,predictor)
  792. %VONMISESFISHER9PARAM Evaluate two overlapping von Mises-Fisher distributions.
  793. %
  794. % VALUE = VONMISESFISHER9PARAM computes the value of two(!) overlapping,
  795. % spherical (p=3) von Mises-Fisher distributions at the chosen location.
  796. %
  797. % It is formatted to match the input/output structure required by the
  798. % built-in non-linear regression function nlinfit.
  799. convention = 'CCW';
  800. dimension = 3;
  801. radius = 1;
  802. if size(predictor,2) == 2
  803. predictor(:,3) = radius*ones(size(predictor(:,1)));
  804. end
  805. [x,y,z] = geo2car(predictor(:,1),predictor(:,2),predictor(:,3),convention);
  806. direction = [x,y,z];
  807. numberofdistributions = 2;
  808. value = NaN(size(predictor,1),numberofdistributions);
  809. for k = 1:numberofdistributions
  810. offset = param(1);
  811. amplitude = param(4*k-2);
  812. meanazimuth = param(4*k-1);
  813. meanelevation = param(4*k);
  814. concentration = param(4*k+1);
  815. [meanx,meany,meanz] = geo2car(meanazimuth,meanelevation,radius,convention);
  816. meandirection = [meanx,meany,meanz];
  817. bessel = concentration / ...
  818. (2*pi * (exp(concentration) - exp(-concentration)));
  819. normalisation = concentration^(dimension/2 - 1) / ...
  820. ((2*pi)^(dimension/2) * bessel);
  821. value(:,k) = normalisation * exp(concentration*meandirection*direction')';
  822. value(:,k) = amplitude * value(:,k) + offset;
  823. % HACK to constrain values
  824. if abs(meanazimuth) > 180 || ...
  825. abs(meanelevation) > 90 || ...
  826. amplitude < 0 || ...
  827. concentration < 0
  828. value(:,k) = 1e10*ones(size(value(:,k)));
  829. end
  830. end
  831. combinedvalue = sum(value,2);
  832. end
  833. function [x,y,z] = geo2car(azimuth,elevation,radius,convention)
  834. switch convention
  835. case 'CCW'
  836. x = radius .* cosd(elevation).*cosd(azimuth);
  837. y = radius .* cosd(elevation).*sind(azimuth);
  838. z = radius .* sind(elevation);
  839. case 'CW'
  840. x = radius .* cosd(elevation).*cosd(azimuth);
  841. y = radius .* cosd(elevation).*-sind(azimuth);
  842. z = radius .* sind(elevation);
  843. otherwise
  844. error('Convention not supported.')
  845. end
  846. end
  847. function [azimuth,elevation,radius] = car2geo(x,y,z,convention)
  848. azimuth = atand(y./x) + 180*((x<0).*(y>=0) - (x<0).*(y<0));
  849. switch convention
  850. case 'CCW'
  851. case 'CW'
  852. azimuth = -azimuth;
  853. otherwise
  854. error('Convention not supported.')
  855. end
  856. radius = sqrt(x.^2+y.^2+z.^2);
  857. elevation = asind(z./radius);
  858. end
  859. function [lefteyehandle,righteyehandle,bodyhandle] = ...
  860. plot3dFish(scale,yaw,pitch,roll,colour)
  861. % This corresponds to version 20170505a, a.k.a. plot3dFish20170505a.m.
  862. set(gcf,'Color',[1 1 1])
  863. resolution = 10;
  864. % colour.eye = .1*[1 1 1];
  865. % colour.body = .7*[1 1 1];
  866. eye.x = scale * (cos(-pi:pi/resolution:3*pi-pi/resolution)');
  867. eye.y = scale * (0.7*sin(0:pi/resolution:4*pi-pi/resolution)');
  868. eye.z = scale * ([.7*ones(2*resolution,1); ...
  869. 1*ones(2*resolution,1)]);
  870. N = 2*resolution;
  871. lefteye.vertices = [eye.x, eye.y, eye.z];
  872. righteye.vertices = [eye.x, -eye.y, eye.z];
  873. lefteye.faces = [[1:2*resolution 1]; ...
  874. [1+2*resolution:4*resolution 1+2*resolution]];
  875. for k = 1:N
  876. lefteye.faces = [lefteye.faces; ...
  877. [mod([k-1 k],N)+1, ...
  878. mod([k k-1],N)+1+N, ...
  879. mod(k-1,N)+1, ...
  880. NaN(1,N-4)]];
  881. end
  882. lefteye.facevertexcdata = repmat(colour.eye,[size(lefteye.faces,1) 1]);
  883. righteye.faces = lefteye.faces;
  884. righteye.facevertexcdata = lefteye.facevertexcdata;
  885. lefteyehandle = patch(lefteye);
  886. righteyehandle = patch(righteye);
  887. set([lefteyehandle,righteyehandle],'FaceColor','flat','EdgeColor','none')
  888. mainbody.x = scale * (-.1 + [1.0*cos(-pi:pi/resolution:pi-pi/resolution), ...
  889. 0.4*cos(-pi:pi/resolution:pi-pi/resolution)]');
  890. mainbody.y = scale * ([0.7*sin(0:pi/resolution:2*pi-pi/resolution), ...
  891. 0.2*sin(0:pi/resolution:2*pi-pi/resolution)]');
  892. mainbody.z = scale * ([1.1*ones(2*resolution,1); ...
  893. -7*ones(2*resolution,1)]);
  894. N = 2*resolution;
  895. body.vertices = [mainbody.x, mainbody.y, mainbody.z];
  896. body.faces = [[1:2*resolution 1]; ...
  897. [1+2*resolution:4*resolution 1+2*resolution]];
  898. for k = 1:N
  899. body.faces = [body.faces; ...
  900. [mod([k-1 k],N)+1, ...
  901. mod([k k-1],N)+1+N, ...
  902. mod(k-1,N)+1, ...
  903. NaN(1,N-4)]];
  904. end
  905. body.facevertexcdata = repmat(colour.body,[size(body.faces,1) 1]);
  906. bodyhandle = patch(body);
  907. set(bodyhandle,'FaceColor','flat','EdgeColor','none')
  908. rotate(lefteyehandle,[1 0 0],80)
  909. rotate(lefteyehandle,[0 0 1],-10)
  910. rotate(righteyehandle,[1 0 0],-80)
  911. rotate(righteyehandle,[0 0 1],10)
  912. rotate(bodyhandle,[0 1 0],-90)
  913. rotate(lefteyehandle,[0 0 1],yaw)
  914. rotate(righteyehandle,[0 0 1],yaw)
  915. rotate(bodyhandle,[0 0 1],yaw)
  916. rotate(lefteyehandle,[0 1 0],pitch)
  917. rotate(righteyehandle,[0 1 0],pitch)
  918. rotate(bodyhandle,[0 1 0],pitch)
  919. rotate(lefteyehandle,[1 0 0],roll)
  920. rotate(righteyehandle,[1 0 0],roll)
  921. rotate(bodyhandle,[1 0 0],roll)
  922. end
  923. function [hd,stim] = displayDiskStimuli(choice)
  924. warning('off','MATLAB:interp1:UsePCHIP')
  925. % The line above deactivates the following cbrewer warning:
  926. % Warning: INTERP1(...,'CUBIC') will change in a future release. Use INTERP1(...,'PCHIP') instead.
  927. % > In interp1>sanitycheckmethod (line 265)
  928. % In interp1>parseinputs (line 401)
  929. % In interp1 (line 80)
  930. % In interpolate_cbrewer (line 31)
  931. % In cbrewer (line 101)
  932. % In heatmap20180823 (line 21)
  933. % close all
  934. % clf
  935. % regularset.folder = '..\..\data\Fig3 default disk data\Data D Series (neue Experimente)\';
  936. % xlsrange = 9:46;
  937. regularset.folder = ['..\..\data\Fig4 rotated arena data\Control Data Upside Down (Kontrolle ventral)\'];
  938. xlsrange = 2:37;
  939. % regularset.folder = '..\..\data\Fig5 size data\Data A Series (A-Series)\';
  940. % xlsrange = 2:43;
  941. % xlsrange = 2:8;
  942. % regularset.folder = '..\..\data\Fig5 frequency data\Data V Series (V-Serie)\';
  943. % xlsrange = 2:43;
  944. regularset.stimulus = 'Stimulus.xlsx';
  945. [~,stim.tableconvention,~] = xlsread([regularset.folder,regularset.stimulus],'C1:C1');
  946. stim.azimuth = xlsread([regularset.folder,regularset.stimulus],['C',num2str(xlsrange(1)),':C',num2str(xlsrange(end))]);
  947. stim.elevation = xlsread([regularset.folder,regularset.stimulus],['D',num2str(xlsrange(1)),':D',num2str(xlsrange(end))]);
  948. % stim.radius = xlsread([regularset.folder,regularset.stimulus],['E',num2str(xlsrange(1)),':E',num2str(xlsrange(end))]);
  949. % stim.azimuth = stim.azimuth(1:2:end);
  950. % stim.elevation = stim.elevation(1:2:end);
  951. stim.radius = 64*ones(size(stim.elevation));
  952. % % [surface.]backgroundSphere
  953. % % compute background sphere with shading
  954. % [surface.x,surface.y,surface.z] = sphere(100);
  955. % reference.geo = [30;20;1]; % desired azimuth, elevation, radius of maximum colour value
  956. % reference.azim = reference.geo(1);
  957. % reference.elev = reference.geo(2);
  958. % reference.rads = ones(size(reference.geo(1)));
  959. % [reference.x,reference.y,reference.z] = geo2car(reference.azim,reference.elev,reference.rads,'CW');
  960. % scalarproduct = [surface.x(:),surface.y(:),surface.z(:)] * [reference.x;reference.y;reference.z];
  961. % surface.c = reshape(scalarproduct,size(surface.x));
  962. % surface.c = 10.^(surface.c+1);
  963. % % surface.c = 2.^(surface.c+1);
  964. % compute disk boundaries
  965. diskcentre.geo = [stim.azimuth, stim.elevation];
  966. diskcentre.angle = stim.radius;
  967. % diskcentre.geo = [ 45 20; ...
  968. % 30 5; ...
  969. % 60 -45];
  970. % diskcentre.angle = 40 * [ones(size(diskcentre.geo,1),1)];
  971. diskcentre.azim = diskcentre.geo(:,1);
  972. diskcentre.elev = diskcentre.geo(:,2);
  973. diskcentre.rads = ones(size(diskcentre.geo(:,1)));
  974. resolution = .2;
  975. numdisk = size(diskcentre.geo,1);
  976. for disk = 1:numdisk
  977. [boundary.azim{disk}, ...
  978. boundary.elev{disk}] = diskBoundary(diskcentre.azim(disk), ...
  979. diskcentre.elev(disk), diskcentre.angle(disk), resolution);
  980. [boundary.x{disk}, ...
  981. boundary.y{disk}, ...
  982. boundary.z{disk}] = geo2car(boundary.azim{disk}, boundary.elev{disk}, ...
  983. ones(size(boundary.azim{disk})),'CW');
  984. end
  985. [diskcentre.x,diskcentre.y,diskcentre.z] = ...
  986. geo2car(diskcentre.azim,diskcentre.elev,diskcentre.rads,'CW');
  987. [textcentre.x,textcentre.y,textcentre.z] = ...
  988. geo2car(diskcentre.azim,.91*diskcentre.elev,1.10*diskcentre.rads,'CW');
  989. % create figure
  990. hd.default.fg = figure();
  991. hd.default.fg.Name = 'This is a sphere.';
  992. hd.default.fg.Position = [50 50 500 800];
  993. hd.default.fg.Color = [1 1 1];
  994. hd.default.ax(1) = axes;
  995. hd.default.ax(1).Position = [0 0 1 1];
  996. hold on
  997. % plot background sphere with shading
  998. hd.default.globe = displaySphere;
  999. % hd.default.globe = surf(surface.x,surface.y,surface.z,surface.c);
  1000. % shading flat
  1001. % axis square
  1002. % greyscale = (.8:.01:.99)' * [1 1 1];
  1003. % colormap(greyscale)
  1004. fish.scale = .05;
  1005. fish.yaw = 180;
  1006. fish.pitch = 0;
  1007. fish.roll = 0;
  1008. colour.eye = 0*[1 1 1];
  1009. colour.body = .5*[1 1 1];
  1010. plot3dFish(fish.scale,fish.yaw,fish.pitch,fish.roll,colour);
  1011. hd.default.globe.FaceAlpha = .5;
  1012. cropdisk = 8;
  1013. switch choice
  1014. case 'left'
  1015. disklist = 1 : numdisk/2;
  1016. case 'right'
  1017. disklist = numdisk/2+1 : numdisk;
  1018. case 'single'
  1019. disklist = cropdisk;
  1020. end
  1021. % plot disk boundaries
  1022. % diskcolour = cbrewer('seq','Greens',numdisk*2);
  1023. cbrewergreen = cbrewer('seq','Greens',numdisk);
  1024. diskcolour = repmat(cbrewergreen(round(numdisk/1.4),:),[numdisk,1]);
  1025. % diskcolour = flipud(cbrewer('seq','Greys',numdisk*2));
  1026. % diskcolour = diskcolour(randperm(numdisk),:);
  1027. for disk = disklist
  1028. if sum(disk==cropdisk)
  1029. plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',2,'Color',0*[1 1 1])
  1030. disktext(disk) = text(textcentre.x(disk),textcentre.y(disk),textcentre.z(disk), ...
  1031. ['',num2str(disk)], ...
  1032. 'FontSize', 13, 'HorizontalAlignment','center','VerticalAlignment','middle');
  1033. else
  1034. plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',1,'Color',.6*[1 1 1])
  1035. if sum(disk==disklist)
  1036. disktext(disk) = text(textcentre.x(disk),textcentre.y(disk),textcentre.z(disk), ...
  1037. ['',num2str(disk)], ...
  1038. 'FontSize', 13, 'HorizontalAlignment','center','VerticalAlignment','middle');
  1039. end
  1040. % plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',2,'Color',1*[1 1 1])
  1041. % plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',1,'Color',diskcolour(disk,:))
  1042. % plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',1,'Color',.2*[1 1 1])
  1043. end
  1044. end
  1045. hold off
  1046. switch choice
  1047. case 'single'
  1048. disktext(cropdisk).Visible = 'off';
  1049. barwidth = 360/22/2;
  1050. % gratingazimuth = -180:barwidth:(180-barwidth);
  1051. gratingazimuth = -180:barwidth:-80;
  1052. for bar = 1:2:numel(gratingazimuth)-1
  1053. % if bar < numel(gratingazimuth)
  1054. azimlimit = gratingazimuth([bar bar+1]);
  1055. % else
  1056. % azimlimit = gratingazimuth([bar 1]);
  1057. % end
  1058. displaySphereBar(azimlimit(1),azimlimit(2),boundary.azim{cropdisk},boundary.elev{cropdisk})
  1059. end
  1060. case {'left','right'}
  1061. disktext(disklist(ceil(end/2))).Position = ...
  1062. disktext(disklist(ceil(end/2))).Position - [0 0 .16];
  1063. end
  1064. % hd.default.ax(1).CameraPosition = [2.3 17 2.2];
  1065. switch choice
  1066. case {'left','single'}
  1067. hd.default.ax(1).CameraPosition = [0 20 0];
  1068. case 'right'
  1069. hd.default.ax(1).CameraPosition = [0 -20 0];
  1070. end
  1071. hd.default.ax(1).CameraViewAngle = 6;
  1072. [hd.default.ax(1).XAxis.Visible, ...
  1073. hd.default.ax(1).YAxis.Visible, ...
  1074. hd.default.ax(1).ZAxis.Visible] = deal('off');
  1075. end
  1076. function globe = displaySphere
  1077. [surface.x,surface.y,surface.z] = sphere(300);
  1078. reference.geo = [30;20;1]; % desired azimuth, elevation, radius of maximum colour value
  1079. reference.azim = reference.geo(1);
  1080. reference.elev = reference.geo(2);
  1081. reference.rads = ones(size(reference.geo(1)));
  1082. [reference.x,reference.y,reference.z] = geo2car(reference.azim,reference.elev,reference.rads,'CW');
  1083. scalarproduct = [surface.x(:),surface.y(:),surface.z(:)] * [reference.x;reference.y;reference.z];
  1084. surface.c = reshape(scalarproduct,size(surface.x));
  1085. surface.c = 10.^(surface.c);
  1086. globe = surf(surface.x,surface.y,surface.z,surface.c);
  1087. shading interp
  1088. axis square
  1089. greyscale = (.8:.01:.99)' * [1 1 1];
  1090. % greyscale = (.9:.01:.99)' * [1 1 1];
  1091. colormap(greyscale)
  1092. end
  1093. function [azimuthOut, elevationOut] = diskBoundary(azimuthIn, elevationIn, angleIn, resolutionIn)
  1094. % The following equation describes orthodromic (or "great circle") distance as an angle:
  1095. % arccos(sin(centre.elevation)*sin(border.elevation) + ...
  1096. % cos(centre.elevation)*cos(border.elevation)*cos(border.azimuth-centre.azimuth)) = angle;
  1097. % This equation is used to compute border.azimuth below.
  1098. centre.azimuth = azimuthIn;
  1099. centre.elevation = elevationIn;
  1100. border.angle = angleIn / 2; % convert solid angle ("diameter") to "radius"
  1101. border.resolution = resolutionIn;
  1102. intended = -90 : border.resolution : 90;
  1103. % limit = cosd([centre.elevation-border.angle centre.elevation+border.angle]);
  1104. % intended = acosd(min(limit) : border.resolution : max(limit));
  1105. % "auxiliary" elevations are explicitly added to guarantee coverage near top/bottom of circle:
  1106. auxiliary = centre.elevation + border.angle*[-1+1e-9 1-1e-9];
  1107. border.elevation = unique([intended,auxiliary]);
  1108. border.azimuth = centre.azimuth + ...
  1109. acosd((cosd(border.angle) - sind(centre.elevation)*sind(border.elevation)) ./ ...
  1110. (cosd(centre.elevation)*cosd(border.elevation)));
  1111. % Eliminate complex elements...
  1112. border.azimuth(imag(border.azimuth)~=0) = NaN; % This statement sometimes fails horribly.
  1113. % border.azimuth(imag(border.azimuth)>1e-5) = NaN; % This statement is silly, but more stable.
  1114. % ...and compute corresponding values for the second half of the boundary.
  1115. border.azimuth = [border.azimuth, 2*centre.azimuth-border.azimuth];
  1116. border.elevation = [border.elevation, border.elevation];
  1117. border.azimuth = mod(border.azimuth+180,360)-168;
  1118. azimuthOut = border.azimuth';
  1119. elevationOut = border.elevation';
  1120. end
  1121. function hd = displaySphereBar(azimlimit1,azimlimit2,boundaryazim,boundaryelev)
  1122. resolution = .01;
  1123. if azimlimit2 < azimlimit1
  1124. error('sdasdsa')
  1125. end
  1126. crop.index = logical((azimlimit1 <= boundaryazim) .* (boundaryazim <= azimlimit2));
  1127. if sum(crop.index) > 0
  1128. crop.azim = boundaryazim(crop.index)';
  1129. crop.elev = boundaryelev(crop.index)';
  1130. crop.azim1 = crop.azim(diff(crop.azim) > 0);
  1131. crop.azim2 = crop.azim(diff(crop.azim) < 0);
  1132. crop.elev1 = crop.elev(diff(crop.azim) > 0);
  1133. crop.elev2 = crop.elev(diff(crop.azim) < 0);
  1134. sequence = @(a,b) a:(sign(b-a)*resolution):b;
  1135. edge.elev1 = sequence(crop.elev1(1), crop.elev2(end));
  1136. edge.elev2 = sequence(crop.elev2(1), crop.elev1(end));
  1137. % edge.elev1 = sequence(crop.elev1(1), crop.elev2(end));
  1138. % edge.elev2 = sequence(crop.elev2(1), crop.elev1(end));
  1139. edge.azim1 = azimlimit1*ones(size(edge.elev1));
  1140. edge.azim2 = azimlimit2*ones(size(edge.elev2));
  1141. azim = [edge.azim1,fliplr(crop.azim2),edge.azim2,fliplr(crop.azim1)];
  1142. elev = [edge.elev1,fliplr(crop.elev2),edge.elev2,fliplr(crop.elev1)];
  1143. rads = 1.009*ones(size(elev));
  1144. [bar.x,bar.y,bar.z] = geo2car(azim,elev,rads,'CW');
  1145. hold on, p = patch(bar.x,bar.y,bar.z,'k'); hold off
  1146. % p.FaceColor = [.2 .8 .4];
  1147. p.FaceColor = .2*[1 1 1];
  1148. p.EdgeColor = 'none';
  1149. % hold on, plot3(bar.x,bar.y,bar.z,'r'); hold off
  1150. end
  1151. end