figureFreqArea20190527gin.m 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816
  1. function [hd,area,freq,areastim,freqstim] = figureFreqArea() %#ok<FNDEF>
  2. close all
  3. [freq,~,freqstim] = gainAnalysisFreq;
  4. [area,~,areastim] = gainAnalysisArea;
  5. freqoutline = displayStimuli('freq');
  6. areaoutline = displayStimuli('area');
  7. freqpattern = displayPattern('freq',freqstim.uniquespatialfreq);
  8. areapattern = displayPattern('area',freqstim.uniquespatialfreq(4)*ones(1,7));
  9. hd.collage.fg = figure(7);
  10. hd.collage.fg.Name = 'Sphere manuscript (Fig 7); OKR gain dependence on stimulus size, frequency';
  11. hd.collage.fg.Color = [1 1 1];
  12. hd.collage.fg.Position = [0 40 1070 800];
  13. % hd.collage.ax(2,1) = axes;
  14. % Populate the combined figure from previously generated partial figures...
  15. hd.collage.ax(1,1) = copyobj(freqpattern.pattern.ax(1), hd.collage.fg);
  16. hd.collage.ax(1,2) = copyobj(freqoutline.default.ax(1), hd.collage.fg);
  17. hd.collage.ax(2,1) = copyobj(areapattern.pattern.ax(1), hd.collage.fg);
  18. hd.collage.ax(2,2) = copyobj(areaoutline.default.ax(1), hd.collage.fg);
  19. for subpanel = 1:3
  20. % elements of panel c (location of stimulus centres):
  21. hd.collage.ax(3,subpanel) = copyobj(area.gain.ax(1), hd.collage.fg);
  22. % elements of panel d (frequency dependence of gain):
  23. hd.collage.ax(4,subpanel) = copyobj(freq.gain.ax(1+subpanel), hd.collage.fg);
  24. % elements of panel e (area dependence of gain):
  25. hd.collage.ax(5,subpanel) = copyobj(area.gain.ax(1+subpanel), hd.collage.fg);
  26. end
  27. % close([1 2 3])
  28. % ...then adjust the size, position and other properties of those objects.
  29. % Shared properties of all axis objects anywhere:
  30. [hd.collage.ax(1:2,1:2).Units,hd.collage.ax(3:5,:).Units] = deal('pixels');
  31. [hd.collage.ax(1:2,1:2).FontSize,hd.collage.ax(3:5,:).FontSize] = deal(15);
  32. [hd.collage.ax(1:2,1:2).LabelFontSizeMultiplier, ...
  33. hd.collage.ax(1:2,1:2).TitleFontSizeMultiplier, ...
  34. hd.collage.ax(3:5,:).LabelFontSizeMultiplier, ...
  35. hd.collage.ax(3:5,:).TitleFontSizeMultiplier] = deal(1);
  36. % Positions and sizes within panel a (stimulus frequency):
  37. hd.collage.ax(1,1).Units = 'pixels';
  38. hd.collage.ax(1,1).Position = [ 20 600 260 170];
  39. hd.collage.ax(1,2).Units = 'pixels';
  40. hd.collage.ax(1,2).Position = [300 600 170 170];
  41. % Positions and sizes within panel b (stimulus sizes):
  42. hd.collage.ax(2,1).Units = 'pixels';
  43. hd.collage.ax(2,1).Position = [540 600 270 170];
  44. hd.collage.ax(2,2).Units = 'pixels';
  45. hd.collage.ax(2,2).Position = [820 600 170 170];
  46. % Positions and sizes within panel c (location of stimulus centres):
  47. for subpanel = 1:3
  48. hd.collage.ax(3,subpanel).Position(1) = 30; % x offset
  49. hd.collage.ax(3,subpanel).Position(2) = 335 - (subpanel-1)*180; % y offset
  50. hd.collage.ax(3,subpanel).Position(3) = 200; % x width
  51. hd.collage.ax(3,subpanel).Position(4) = 200; % y height
  52. end
  53. hd.collage.ax(3,2).Position(2) = hd.collage.ax(3,2).Position(2) - 8; % shift single y offset
  54. % Positions and sizes within panel d (frequency dependence of gain):
  55. for subpanel = 1:3
  56. hd.collage.ax(4,subpanel).Position(1) = hd.collage.ax(3,subpanel).Position(1) + ...
  57. hd.collage.ax(3,subpanel).Position(3) + ...
  58. 160 + (subpanel-1)*230; % x offset
  59. hd.collage.ax(4,subpanel).Position(2) = 350; % y offset
  60. hd.collage.ax(4,subpanel).Position(3) = 210; % x width
  61. hd.collage.ax(4,subpanel).Position(4) = 142; % y height
  62. end
  63. % Positions and sizes within panel e (area dependence of gain):
  64. for subpanel = 1:3
  65. hd.collage.ax(5,subpanel).Position(1) = hd.collage.ax(4,subpanel).Position(1); % x offset
  66. hd.collage.ax(5,subpanel).Position(2) = 70; % y offset
  67. hd.collage.ax(5,subpanel).Position(3) = hd.collage.ax(4,subpanel).Position(3); % x width
  68. hd.collage.ax(5,subpanel).Position(4) = hd.collage.ax(4,subpanel).Position(4); % y height
  69. end
  70. % Set camera angles for panels a and b:
  71. [hd.collage.ax(1:2,2).CameraPosition] = deal([-1 -1.5 0]);
  72. [hd.collage.ax(1:2,2).CameraViewAngle] = deal(60);
  73. % Set camera angles for panel c:
  74. hd.collage.ax(3,1).CameraPosition = [0.1 0 20];
  75. hd.collage.ax(3,2).CameraPosition = [20 0 0];
  76. hd.collage.ax(3,3).CameraPosition = [-16 -9 6];
  77. [hd.collage.ax(3,:).CameraViewAngle] = deal(9.5);
  78. % Add azimuth and elevation labels to the plots on panel c:
  79. axes(hd.collage.ax(3,1))
  80. labelx = [hd.collage.ax(3,1).Children(1:6).XData] * 1.15;
  81. labely = [hd.collage.ax(3,1).Children(1:6).YData] * 1.15;
  82. labelz = [hd.collage.ax(3,1).Children(1:6).ZData] * 0;
  83. stringlist = areastim.azimuth(7*(1:6)); % unexpected order here, fix by quick hack
  84. stringlist([1 3 4 6]) = stringlist([3 1 6 4]);
  85. for location = 1:6
  86. hold on
  87. hd.collage.tx(3,1,location) = text(1.1*labelx(location), labely(location), labelz(location), ...
  88. [num2str(stringlist(location),'%+.0f'),'°']);
  89. hold off
  90. end
  91. [hd.collage.tx(3,1,1:3).HorizontalAlignment] = deal('right');
  92. [hd.collage.tx(3,1,4:6).HorizontalAlignment] = deal('left');
  93. [hd.collage.tx(3,1,:).VerticalAlignment] = deal('middle');
  94. axes(hd.collage.ax(3,2))
  95. labelx = [hd.collage.ax(3,2).Children(1:6).XData] * 0;
  96. labely = [hd.collage.ax(3,2).Children(1:6).YData] * 1.22;
  97. labelz = [hd.collage.ax(3,2).Children(1:6).ZData] * 1.22;
  98. for location = [2 3 5 6]
  99. hold on
  100. hd.collage.tx(3,2,location) = text(labelx(location), labely(location), labelz(location), ...
  101. [num2str(areastim.elevation(7*location),'%+.0f'),'°']);
  102. hold off
  103. end
  104. [hd.collage.tx(3,2,2:3).HorizontalAlignment] = deal('right');
  105. [hd.collage.tx(3,2,5:6).HorizontalAlignment] = deal('left');
  106. [hd.collage.tx(3,2,[2 3 5 6]).VerticalAlignment] = deal('middle');
  107. [hd.collage.tx(3,1,:).FontSize, hd.collage.tx(3,2,[2 3 5 6]).FontSize] = ...
  108. deal(hd.collage.ax(3,1).FontSize);
  109. % Recolour the background spheres on panels a and b, and remove visible axes:
  110. greyscale = (.8:.01:.99)' * [1 1 1];
  111. [hd.collage.ax(1:2,2).Colormap] = deal(greyscale);
  112. for panel = [1 2]
  113. for subpanel = [1 2]
  114. [hd.collage.ax(panel,subpanel).XAxis.Visible, ...
  115. hd.collage.ax(panel,subpanel).YAxis.Visible, ...
  116. hd.collage.ax(panel,subpanel).ZAxis.Visible] = deal('off');
  117. end
  118. end
  119. % Add and format titles to panels a and b:
  120. for panel = 1:2
  121. hd.collage.ax(panel,1).Title.String = 'stimulus pattern';
  122. hd.collage.ax(panel,2).Title.String = 'stimulus crop';
  123. for subpanel = 1:2
  124. hd.collage.ax(panel,subpanel).Title.FontWeight = 'normal';
  125. hd.collage.ax(panel,subpanel).Title.Units = 'pixels';
  126. end
  127. hd.collage.ax(panel,1).Title.Position(2) = hd.collage.ax(panel,1).Position(4) - 14 + 14;
  128. hd.collage.ax(panel,2).Title.Position(2) = hd.collage.ax(panel,2).Position(4) + 2;
  129. end
  130. % Reformat the meridians, equators etc. on panel c:
  131. for subpanel = 1:3
  132. [hd.collage.ax(3,subpanel).Children(7:9).LineWidth] = deal(1);
  133. end
  134. % Add and format titles to panel c:
  135. hd.collage.ax(3,1).Title.String = 'centre azimuth';
  136. hd.collage.ax(3,2).Title.String = 'centre elevation';
  137. hd.collage.ax(3,3).Title.String = 'oblique view';
  138. for subpanel = 1:3
  139. hd.collage.ax(3,subpanel).Title.FontWeight = 'normal';
  140. hd.collage.ax(3,subpanel).Title.Units = 'pixels';
  141. end
  142. hd.collage.ax(3,1).Title.Position(2) = hd.collage.ax(3,1).Position(4) - 70;
  143. hd.collage.ax(3,2).Title.Position(2) = hd.collage.ax(3,2).Position(4) - 70;
  144. hd.collage.ax(3,3).Title.Position(2) = hd.collage.ax(3,3).Position(4) - 45;
  145. % % Remove redundant x axis labels on panels d and e:
  146. % for panel = [4 5]
  147. % for subpanel = [1 3]
  148. % hd.collage.ax(panel,subpanel).XLabel.String = [];
  149. % end
  150. % end
  151. % Remove redundant titles on panel e:
  152. for panel = 5
  153. for subpanel = [1 2 3]
  154. hd.collage.ax(panel,subpanel).Title.String = [];
  155. end
  156. end
  157. % Reposition X and Y axis labels on panels d and e:
  158. for panel = [4 5]
  159. for subpanel = 1:3
  160. hd.collage.ax(panel,subpanel).XLabel.Units = 'pixels';
  161. end
  162. hd.collage.ax(panel,1).YLabel.Units = 'pixels';
  163. end
  164. for subpanel = 1:3
  165. hd.collage.ax(4,subpanel).XLabel.Position(2) = -33;
  166. hd.collage.ax(5,subpanel).XLabel.Position(2) = -37;
  167. end
  168. hd.collage.ax(4,1).YLabel.Position(1) = -58;
  169. hd.collage.ax(5,1).YLabel.Position(1) = -46;
  170. % Reformat X axis on panel e:
  171. for subpanel = 1:3
  172. hd.collage.ax(5,subpanel).XLim(2) = 1.35;
  173. hd.collage.ax(5,subpanel).XTick = [.1 1];
  174. end
  175. % Reposition and reformat titles on panel d:
  176. for subpanel = 1:3
  177. hd.collage.ax(4,subpanel).Title.Units = 'pixels';
  178. hd.collage.ax(4,subpanel).Title.Position(2) = hd.collage.ax(4,subpanel).Position(4) + 25;
  179. hd.collage.ax(4,subpanel).Title.FontWeight = 'bold';
  180. end
  181. % Overwrite X axis ticks and tick labels on panel d (frequency dependence of gain):
  182. [hd.collage.ax(4,:).XTick] = deal([.02 .05 .1]);
  183. [hd.collage.ax(4,:).XTickLabel] = deal({'0.02','0.05','0.10'});
  184. % Overwrite Y axis ticks and tick labels on panel d (frequency dependence of gain):
  185. [hd.collage.ax(4,:).YTick] = deal([0 .05 .1]);
  186. [hd.collage.ax(4,:).YTickLabel] = deal({'0','0.05','0.10'});
  187. % Display panel lettering:
  188. hd.collage.ax(6,1) = axes;
  189. hd.collage.ax(6,1).Color = 'none';
  190. hd.collage.ax(6,1).Position = [0 0 1 1];
  191. hd.collage.ax(6,1).Units = 'pixels';
  192. hd.collage.ax(6,1).Visible = 'off';
  193. hold on
  194. hd.collage.lt(1) = text(hd.collage.ax(1,1).Position(1) - 0 , ...
  195. hd.collage.ax(1,1).Position(2) + hd.collage.ax(1,1).Position(4) + 20, ...
  196. 'a', 'Units', 'pixels');
  197. hd.collage.lt(2) = text(hd.collage.ax(2,1).Position(1) - 0, ...
  198. hd.collage.ax(2,1).Position(2) + hd.collage.ax(2,1).Position(4) + 20, ...
  199. 'b', 'Units', 'pixels');
  200. hd.collage.lt(3) = text(hd.collage.ax(3,1).Position(1) - 10, ...
  201. hd.collage.ax(3,1).Position(2) + hd.collage.ax(3,1).Position(4) - 28 + 30, ...
  202. 'c', 'Units', 'pixels');
  203. hd.collage.lt(4) = text(hd.collage.ax(4,1).Position(1) - 85, ...
  204. hd.collage.ax(4,1).Position(2) + hd.collage.ax(4,1).Position(4) + 30 + 15, ...
  205. 'd', 'Units', 'pixels');
  206. hd.collage.lt(5) = text(hd.collage.ax(5,1).Position(1) - 85, ...
  207. hd.collage.ax(5,1).Position(2) + hd.collage.ax(5,1).Position(4) + 30, ...
  208. 'e', 'Units', 'pixels');
  209. hold off
  210. % [hd.collage.lt(2:5).FontSize] = deal(15);
  211. % [hd.collage.lt(2:5).FontWeight] = deal('bold');
  212. [hd.collage.lt(:).FontSize] = deal(15);
  213. [hd.collage.lt(:).FontWeight] = deal('bold');
  214. end
  215. function [handle,data,stim] = gainAnalysisArea(varargin)
  216. % GAINANALYSIS and all subfunctions were written by Florian Alexander
  217. % Dehmelt in 2018. This is version 20180924, gainAnalysisArea20180924.m.
  218. % Apologies for the lack of polish (and comments) at this point.
  219. %
  220. % All input arguments are optional:
  221. %
  222. % Resolution - scalar, indicating your choice of resolution for the
  223. % rendering of the spherical surface as part % of the
  224. % figures. Actual data fitting is completely unaffected.
  225. % CamAzimuth - scalar, horizontal angle at which sphere is shown
  226. % CamElevation - scalar, vertical angle at which sphere is shown
  227. % Normalisation - options: nothing, or 'mean gain per hemisphere',
  228. % or 'max gain per hemisphere',
  229. % or 'ratio of mean gains per hemisphere'
  230. %
  231. % Output arguments are:
  232. %
  233. % handle - structure containing handles to all graphics objects
  234. % (figures, axes, plots, decorations)
  235. % data - structure containing pre-processed data, with those fields:
  236. % .left - numerical array of means of OKR gain for the left eye
  237. % .right - numerical array of means of OKR gain for the right eye
  238. % .both - numerical array of mean of OKR gain means across both eyes
  239. % datafit - structure containing the fits to data, with following fields: % removed!
  240. % .centre - 2x2 numerical array of azimuths (1st row) and elevation
  241. % angles (2nd row) of the two individial von Mises-Fisher
  242. % functions fit to data (1st column = 1st function, etc.).
  243. % These are NOT the peaks of the actual fit (= sum of both
  244. % functions)! I still have to implement that feature.
  245. % .value - 36x1 numerical array containing the value of the actual fit
  246. % (= sum of both functions) at the locations sampled by data
  247. % .param - 1x9 numerical array containing all best-fit parameters
  248. % (offset, amplitude1, azimuth1, elevation1, concentration1,
  249. % amplitude2, azimuth2, elevation2, concentration2)
  250. %
  251. % The recommended default is calling "hd = gainAnalysisArea20181217();".
  252. %
  253. % To specify camera position before analysis, call the function as follows:
  254. % hd = gainAnalysisArea20181217(200,'CamAzimuth',-30,'CamElevation',15);
  255. %
  256. % To edit camera position post-hoc, adjust and execute the following lines:
  257. % az = 0; el = 0; % substitute your desired camera coordinates here
  258. % [hd.gain.ax(1:6).CameraPosition] = ...
  259. % deal([cosd(az)*cosd(el),sind(az)*cosd(el),sind(el)]/19.0526);
  260. % figure(21)
  261. %
  262. % To normalise gains on each hemisphere, call the function as follows:
  263. % hd = gainAnalysisArea20181217('Normalisation','mean gain per hemisphere');
  264. % MANAGE VARIABLE INPUT
  265. p = inputParser;
  266. valid.resolution = @(x) isnumeric(x) && numel(x)==1 && x>0;
  267. valid.camazimuth = @(x) isnumeric(x) && numel(x)==1;
  268. valid.camelevation = @(x) isnumeric(x) && numel(x)==1;
  269. valid.normalisation = @(x) ischar(x);
  270. default.resolution = 200;
  271. default.camazimuth = -0;
  272. default.camelevation = 45;
  273. default.normalisation = '';
  274. addOptional(p, 'Resolution', default.resolution, valid.resolution);
  275. addOptional(p, 'CamAzimuth', default.camazimuth, valid.camazimuth);
  276. addOptional(p, 'CamElevation', default.camelevation, valid.camelevation);
  277. addOptional(p, 'Normalisation', default.normalisation, valid.normalisation);
  278. parse(p,varargin{:});
  279. resolution = p.Results.Resolution;
  280. cam.azimuth = p.Results.CamAzimuth;
  281. cam.elevation = p.Results.CamElevation;
  282. normalisation = p.Results.Normalisation;
  283. % GENERAL SETTINGS
  284. warning('off','MATLAB:interp1:UsePCHIP')
  285. % The line above deactivates the following cbrewer warning:
  286. % Warning: INTERP1(...,'CUBIC') will change in a future release. Use INTERP1(...,'PCHIP') instead.
  287. % > In interp1>sanitycheckmethod (line 265)
  288. % In interp1>parseinputs (line 401)
  289. % In interp1 (line 80)
  290. % In interpolate_cbrewer (line 31)
  291. % In cbrewer (line 101)
  292. % In heatmap20180823 (line 21)
  293. % Identify which stimulus entries correspond to the disk-mask stimuli.
  294. datarange = 2:43;
  295. % !!! IMPORTANT !!! These are the Excel row numbers, not stimulus types.
  296. % close all
  297. % IDENTIFY DATA FOLDER
  298. file.folder = '..\..\data\Fig5 size data\Data A Series (A-Series)\';
  299. % READ STIMULUS INFORMATION
  300. % file.folder = 'C:\Users\fdehmelt\Dropbox\ThinkPad Dropbox\MATLAB\201808 SphereAnalysis\Sample Data\D series\';
  301. file.stimulus = 'Stimulus.xlsx';
  302. [~,stim.tableconvention,~] = xlsread([file.folder,file.stimulus],'C1:C1');
  303. stim.azimuth = xlsread([file.folder,file.stimulus],['C',num2str(datarange(1)),':C',num2str(datarange(end))]);
  304. stim.elevation = xlsread([file.folder,file.stimulus],['D',num2str(datarange(1)),':D',num2str(datarange(end))]);
  305. stim.radius = xlsread([file.folder,file.stimulus],['E',num2str(datarange(1)),':E',num2str(datarange(end))]);
  306. % Stimulus tables may use CW convention; default code uses CCW.
  307. switch stim.tableconvention{:}
  308. case 'azimuth (CCW)'
  309. case 'azimuth (CW)'
  310. stim.azimuth = -stim.azimuth;
  311. otherwise
  312. error(['Field C1 of Stimulus.xlsx should contain either ' ...
  313. 'string ''azimuth (CCW)'' or string ''azimuth (CW)''.'])
  314. end
  315. stimcentre = [stim.azimuth, stim.elevation];
  316. % READ DATA
  317. % Query user to manually select the main data file. Other files have
  318. % standard names and are expected to be in the same folder.
  319. [mfilefolder,~,~] = fileparts(mfilename('fullpath'));
  320. % [file.result,file.folder] = uigetfile([mfilefolder,'\*.mat']);
  321. file.result = 'result_20180228_090646.mat';
  322. load([file.folder,file.result],'result')
  323. % Discard unwanted trials, based on .xlsx documenting manual assessment.
  324. result = discardTrial(result,file.folder); %#ok<NODEF>
  325. % % 20181217: optionally, normalise gain values by mean gain across same hemisphere
  326. % switch normalisation
  327. % case 'mean gain per hemisphere'
  328. % meangainposazimuth = mean([result(:,:,stim.azimuth>0,:).gain]);
  329. % meangainnegazimuth = mean([result(:,:,stim.azimuth<0,:).gain]);
  330. % result(:,:,stim.azimuth>0,:).gain = result(:,:,stim.azimuth>0,:).gain / meangainposazimuth;
  331. % result(:,:,stim.azimuth<0,:).gain = result(:,:,stim.azimuth<0,:).gain / meangainnegazimuth;
  332. % end
  333. % Pool gain data in various ways (this is a significant subfunction!).
  334. gain = poolGainData(result);
  335. % Select the three types of data to be plotted.
  336. data.left = gain.mean.FR{1};
  337. data.right = gain.mean.FR{2};
  338. % both = gain.mean.FER;
  339. data.both = mean([data.left,data.right],2);
  340. % 20181217: optionally, normalise gain values by mean gain across same hemisphere
  341. hemisphere1 = datarange(1:end/2) - 1; % The -1 accounts for the Excel title row.
  342. hemisphere2 = datarange(end/2+1:end) - 1;
  343. switch normalisation
  344. case 'mean gain per hemisphere'
  345. data.left(hemisphere1) = data.left(hemisphere1) / mean(data.left(hemisphere1));
  346. data.left(hemisphere2) = data.left(hemisphere2) / mean(data.left(hemisphere2));
  347. data.right(hemisphere1) = data.right(hemisphere1) / mean(data.right(hemisphere1));
  348. data.right(hemisphere2) = data.right(hemisphere2) / mean(data.right(hemisphere2));
  349. data.both(hemisphere1) = data.both(hemisphere1) / mean(data.both(hemisphere1));
  350. data.both(hemisphere2) = data.both(hemisphere2) / mean(data.both(hemisphere2));
  351. case 'max gain per hemisphere'
  352. data.left(hemisphere1) = data.left(hemisphere1) / max(data.left(hemisphere1));
  353. data.left(hemisphere2) = data.left(hemisphere2) / max(data.left(hemisphere2));
  354. data.right(hemisphere1) = data.right(hemisphere1) / max(data.right(hemisphere1));
  355. data.right(hemisphere2) = data.right(hemisphere2) / max(data.right(hemisphere2));
  356. data.both(hemisphere1) = data.both(hemisphere1) / max(data.both(hemisphere1));
  357. data.both(hemisphere2) = data.both(hemisphere2) / max(data.both(hemisphere2));
  358. case 'ratio of mean gains per hemisphere'
  359. data.left(hemisphere1) = data.left(hemisphere1) / ...
  360. (mean(data.left(hemisphere1)) / mean(data.left(hemisphere2)));
  361. data.left(hemisphere2) = data.left(hemisphere2) / ...
  362. (mean(data.left(hemisphere2) / mean(data.left(hemisphere1))));
  363. data.right(hemisphere1) = data.right(hemisphere1) / ...
  364. (mean(data.right(hemisphere1)) / mean(data.right(hemisphere2)));
  365. data.right(hemisphere2) = data.right(hemisphere2) / ...
  366. (mean(data.right(hemisphere2)) / mean(data.right(hemisphere1)));
  367. data.both(hemisphere1) = data.both(hemisphere1) / ...
  368. (mean(data.both(hemisphere1)) / mean(data.both(hemisphere2)));
  369. data.both(hemisphere2) = data.both(hemisphere2) / ...
  370. (mean(data.both(hemisphere2)) / mean(data.both(hemisphere1)));
  371. end
  372. % CHOOSE SUBSET OF DATA TO ANALYSE, AND PROCEED TO ANALYSE IT
  373. choicelist = {'left','both','right'};
  374. for choice = 1:numel(choicelist)
  375. switch choicelist{choice}
  376. case 'right'
  377. response = data.right(datarange-1); % -1 is offset by Excel title row
  378. case 'left'
  379. response = data.left(datarange-1);
  380. case 'both'
  381. response = data.both(datarange-1);
  382. otherwise
  383. error(['Input argument must be one of the following strings: ' ...
  384. '''left'', ''right'', or ''both''.'])
  385. end
  386. predictor = stimcentre;
  387. % compute cartesian coordinates of sphere, and its colour data values
  388. [x,y,z] = sphere(resolution);
  389. % [x,y,z] = sphere(250);
  390. [azimuth,elevation,radius] = car2geo(x,y,z,'CCW');
  391. % predictor = [azimuth(:),elevation(:),radius(:)];
  392. % datafit.matrix{choice} = reshape(vonmisesfisher9param(datafit.param,predictor),size(radius));
  393. % set 3D fish parameters
  394. scale = .1;
  395. yaw = 180;
  396. pitch = 0;
  397. roll = 0;
  398. % set decorative arc parameters; compute their cartesian coordinates
  399. equator.base = -1:.01:1;
  400. equator.x = [equator.base, fliplr(equator.base)];
  401. equator.y = [sqrt(1-equator.base.^2), -sqrt(1-equator.base.^2)];
  402. equator.z = 0*ones(1,numel(equator.x));
  403. meridian.x = equator.x;
  404. meridian.y = equator.z;
  405. meridian.z = equator.y;
  406. aura.x = equator.z;
  407. aura.y = equator.y;
  408. aura.z = equator.x;
  409. [sample.x,sample.y,sample.z] = ...
  410. geo2car(stimcentre(:,1),stimcentre(:,2),ones(size(stimcentre,1),1),'CCW');
  411. [uniquepair,firstoccurrence,~] = unique([stim.azimuth,stim.elevation],'rows');
  412. % MATLAB reorders unique pairs by azimuth (increasing). This does not
  413. % usually match the true order in which stimuli were presented! The
  414. % following two lines recovers this true order, and restores it.
  415. [~,trueorder] = sort(firstoccurrence);
  416. uniquepair = uniquepair(trueorder,:);
  417. stim.uniquecentre.azimuth = uniquepair(:,1);
  418. stim.uniquecentre.elevation = uniquepair(:,2);
  419. [uniquecentre.x,uniquecentre.y,uniquecentre.z] = ...
  420. geo2car(uniquepair(:,1),uniquepair(:,2),ones(size(uniquepair,1),1),'CCW');
  421. stim.uniqueradius = unique(stim.radius);
  422. stim.uniquesteradian = stim.uniqueradius*(2*pi)/360;
  423. stim.uniquearea = stim.uniquesteradian/(2*pi);
  424. fontsize = 14;
  425. if choice == 1
  426. panel = 1;
  427. % hd.location.fg = figure();
  428. % hd.location.fg.Position = [50 300 1200 300];
  429. % hd.location.fg.Color = [1 1 1];
  430. hd.gain.ax(1) = axes;
  431. hd.gain.ax(1).Position = [.025 .14 .20 .8];
  432. hold on
  433. hd.gain.ob(panel,1) = surf(x,y,z);
  434. hd.gain.ob(panel,1).FaceAlpha = 0*.25;
  435. hd.gain.ob(panel,1).FaceColor = 1*[1 1 1];
  436. hd.gain.ob(panel,1).EdgeColor = 'none';
  437. hd.gain.ob(panel,2:4) = plot3dFish(scale,yaw,pitch,roll);
  438. hd.gain.ob(panel,5) = plot3(equator.x,equator.y,equator.z,'k','LineWidth',2);
  439. hd.gain.ob(panel,6) = plot3(meridian.x,meridian.y,meridian.z,'--k');
  440. hd.gain.ob(panel,7) = plot3(aura.x,aura.y,aura.z,':k');
  441. % colour.centre = [1 0 0; 0 1 0; 0 0 1; 1 1 0; 1 0 1; 0 1 1];
  442. colour.centre = [1 .447 .741; .85 .325 .098; .929 .694 .125; ...
  443. .494 .184 .556; .466 .6740 .188; .301 .745 .933];
  444. colour.purple = cbrewer('seq','Purples',100);
  445. colour.green = cbrewer('seq','Greens',100);
  446. colour.centre = [colour.purple(round(end*.8),:); ...
  447. colour.purple(round(end*.3),:); ...
  448. colour.purple(round(end*.5),:); ...
  449. colour.green(round(end*.8),:); ...
  450. colour.green(round(end*.3),:); ...
  451. colour.green(round(end*.5),:)];
  452. for origin = 1:numel(uniquecentre.x)
  453. hd.gain.ob(panel,7+origin) = scatter3(1.03*uniquecentre.x(origin), ...
  454. 1.03*uniquecentre.y(origin), ...
  455. 1.03*uniquecentre.z(origin), ...
  456. 120,'Marker','o');
  457. hd.gain.ob(panel,7+origin).MarkerFaceColor = colour.centre(origin,:);
  458. hd.gain.ob(panel,7+origin).MarkerEdgeColor = .6*[1 1 1];
  459. end
  460. hold off
  461. % % hd.gain.ob(panel,8).Color = [1 1 1];
  462. % hd.gain.ob(panel,1).FaceAlpha = .5;
  463. % hd.gain.ob(panel,1).FaceColor = hd.gain.ob(panel,1).EdgeColor;
  464. axis square
  465. hd.gain.ax(panel).XAxis.Visible = 'off';
  466. hd.gain.ax(panel).YAxis.Visible = 'off';
  467. hd.gain.ax(panel).ZAxis.Visible = 'off';
  468. % hd.gain.ax(panel).Title.String = 'stimulus centres';
  469. hd.gain.ax(panel).CameraPosition = [11.5866 -8.9228 9.2809];
  470. hd.gain.ax(panel).CameraViewAngle = 9.25;
  471. hd.gain.ax(4) = axes;
  472. hd.gain.ax(4).Position = [0 0 1 1];
  473. hd.gain.ax(4).Color = 'none';
  474. hd.gain.ax(4).Visible = 'off';
  475. hd.gain.ob(4,1) = text(.02,.93,'a','Units','normalized');
  476. hd.gain.ob(4,2) = text(.25,.93,'b','Units','normalized');
  477. [hd.gain.ob(4,1:2).FontWeight] = deal('bold');
  478. [hd.gain.ob(4,1:2).FontSize] = deal(14);
  479. end
  480. input.letter = 'e';
  481. panel = choice+1;
  482. croppeddata(:,1) = response(1:7);
  483. croppeddata(:,2) = response(8:14);
  484. croppeddata(:,3) = response(15:21);
  485. croppeddata(:,4) = response(22:28);
  486. croppeddata(:,5) = response(29:35);
  487. croppeddata(:,6) = response(36:42);
  488. hd.gain.ax(panel) = axes;
  489. hd.gain.ax(panel).Position = [.33+(panel-2)*.22 .26 .20 .55];
  490. hold on
  491. for origin = 1:size(croppeddata,2)
  492. hd.gain.ob(panel,origin) = plot(stim.uniquearea,croppeddata(:,origin));
  493. hd.gain.ob(panel,origin).Color = colour.centre(origin,:);
  494. hd.gain.ob(panel,origin).LineWidth = 2;
  495. end
  496. hold off
  497. hd.gain.ax(panel).TitleFontWeight = 'normal';
  498. hd.gain.ax(panel).TitleFontSizeMultiplier = 1;
  499. hd.gain.ax(panel).XLabel.String = 'stimulus size';
  500. if choice == 1
  501. hd.gain.ax(panel).YLabel.String = 'OKR gain';
  502. hd.gain.ax(panel).YLabel.Units = 'normalized';
  503. hd.gain.ax(2).YLabel.Position = [-.2 .5 0];
  504. else
  505. hd.gain.ax(panel).YAxis.Visible = 'off';
  506. end
  507. switch choicelist{choice}
  508. case 'left'
  509. hd.gain.ax(panel).Title.String = 'left eye';
  510. case 'right'
  511. hd.gain.ax(panel).Title.String = 'right eye';
  512. case 'both'
  513. % hd.gain.ax(panel).Title.String = 'mean of both eyes';
  514. hd.gain.ax(panel).Title.String = 'mean';
  515. end
  516. hd.gain.ax(panel).FontSize = fontsize;
  517. if panel > 1
  518. hd.gain.ax(panel).XScale = 'log';
  519. hd.gain.ax(panel).YScale = 'lin';
  520. hd.gain.ax(panel).XLim = [.9*min(hd.gain.ob(panel,1).XData), ...
  521. max(hd.gain.ob(panel,1).XData)];
  522. switch normalisation
  523. case 'max gain per hemisphere'
  524. hd.gain.ax(panel).YLim = [0, 1];
  525. case 'mean gain per hemisphere'
  526. hd.gain.ax(panel).YLim = [0, max([data.left;data.right;data.both])];
  527. case 'ratio of mean gains per hemisphere'
  528. hd.gain.ax(panel).YLim = [0, max([data.left;data.right;data.both])];
  529. otherwise
  530. hd.gain.ax(panel).YLim = [0, .5];
  531. end
  532. hd.gain.ax(panel).TickLength = [.03 .075];
  533. hd.gain.ax(panel).YGrid = 'on';
  534. hd.gain.ax(panel).Title.Units = 'normalized';
  535. hd.gain.ax(panel).Title.Position = [.5 1.1 0];
  536. end
  537. end
  538. cam.radius = 19.0526; % MATLAB automatically reverts to this values while rotating, anyway. Might as well choose it as default.
  539. [cam.x,cam.y,cam.z] = geo2car(cam.azimuth,cam.elevation,cam.radius,'CCW');
  540. hd.gain.ax(1).CameraPosition = [cam.x cam.y cam.z];
  541. hd.gain.ax(1).CameraTarget = [0 0 0];
  542. hd.gain.ax(1).CameraUpVector = [0 0 1];
  543. % hd.gain.ax(panel).CameraViewAngle = 10.134;
  544. hd.gain.ax(1).CameraViewAngle = 9;
  545. % Collect output arguments, unless already done (e.g., "datafit").
  546. % data.left = left;
  547. % data.right = right;
  548. % data.both = both;
  549. handle = hd;
  550. end
  551. function [handle,data,stim] = gainAnalysisFreq(varargin)
  552. % GAINANALYSIS and all subfunctions were written by Florian Alexander
  553. % Dehmelt in 2018. This is version 20180927, gainAnalysisFrequency20180924.m.
  554. % Apologies for the lack of polish (and comments) at this point.
  555. %
  556. % All input arguments are optional:
  557. %
  558. % Resolution - scalar, indicating your choice of resolution for the
  559. % rendering of the spherical surface as part % of the
  560. % figures. Actual data fitting is completely unaffected.
  561. % CamAzimuth - scalar, horizontal angle at which sphere is shown
  562. % CamElevation - scalar, vertical angle at which sphere is shown
  563. % Normalisation - options: nothing, or 'mean gain per hemisphere',
  564. % or 'max gain per hemisphere',
  565. % or 'ratio of mean gains per hemisphere'
  566. %
  567. % Output arguments are:
  568. %
  569. % handle - structure containing handles to all graphics objects
  570. % (figures, axes, plots, decorations)
  571. % data - structure containing pre-processed data, with those fields:
  572. % .left - numerical array of means of OKR gain for the left eye
  573. % .right - numerical array of means of OKR gain for the right eye
  574. % .both - numerical array of mean of OKR gain means across both eyes
  575. % datafit - structure containing the fits to data, with following fields: % removed!
  576. % .centre - 2x2 numerical array of azimuths (1st row) and elevation
  577. % angles (2nd row) of the two individial von Mises-Fisher
  578. % functions fit to data (1st column = 1st function, etc.).
  579. % These are NOT the peaks of the actual fit (= sum of both
  580. % functions)! I still have to implement that feature.
  581. % .value - 36x1 numerical array containing the value of the actual fit
  582. % (= sum of both functions) at the locations sampled by data
  583. % .param - 1x9 numerical array containing all best-fit parameters
  584. % (offset, amplitude1, azimuth1, elevation1, concentration1,
  585. % amplitude2, azimuth2, elevation2, concentration2)
  586. %
  587. % The recommended default is calling "hd = gainAnalysis20180911(200);".
  588. %
  589. % To specify camera position before analysis, call the function as follows:
  590. % hd = gainAnalysis20180911(200,'CamAzimuth',-30,'CamElevation',15);
  591. %
  592. % To edit camera position post-hoc, adjust and execute the following lines:
  593. % az = 0; el = 0; % substitute your desired camera coordinates here
  594. % [hd.gain.ax(1:6).CameraPosition] = ...
  595. % deal([cosd(az)*cosd(el),sind(az)*cosd(el),sind(el)]/19.0526);
  596. % figure(21)
  597. % MANAGE VARIABLE INPUT
  598. p = inputParser;
  599. valid.resolution = @(x) isnumeric(x) && numel(x)==1 && x>0;
  600. valid.camazimuth = @(x) isnumeric(x) && numel(x)==1;
  601. valid.camelevation = @(x) isnumeric(x) && numel(x)==1;
  602. valid.normalisation = @(x) ischar(x);
  603. default.resolution = 200;
  604. default.camazimuth = -0;
  605. default.camelevation = 45;
  606. default.normalisation = '';
  607. addOptional(p, 'Resolution', default.resolution, valid.resolution);
  608. addOptional(p, 'CamAzimuth', default.camazimuth, valid.camazimuth);
  609. addOptional(p, 'CamElevation', default.camelevation, valid.camelevation);
  610. addOptional(p, 'Normalisation', default.normalisation, valid.normalisation);
  611. parse(p,varargin{:});
  612. resolution = p.Results.Resolution;
  613. cam.azimuth = p.Results.CamAzimuth;
  614. cam.elevation = p.Results.CamElevation;
  615. normalisation = p.Results.Normalisation;
  616. % GENERAL SETTINGS
  617. warning('off','MATLAB:interp1:UsePCHIP')
  618. % The line above deactivates the following cbrewer warning:
  619. % Warning: INTERP1(...,'CUBIC') will change in a future release. Use INTERP1(...,'PCHIP') instead.
  620. % > In interp1>sanitycheckmethod (line 265)
  621. % In interp1>parseinputs (line 401)
  622. % In interp1 (line 80)
  623. % In interpolate_cbrewer (line 31)
  624. % In cbrewer (line 101)
  625. % In heatmap20180823 (line 21)
  626. % Identify which stimulus entries correspond to the disk-mask stimuli.
  627. datarange = 2:43;
  628. % !!! IMPORTANT !!! These are the Excel row numbers, not stimulus types.
  629. % close all
  630. % READ DATA
  631. % Query user to manually select the main data file. Other files have
  632. % standard names and are expected to be in the same folder.
  633. [mfilefolder,~,~] = fileparts(mfilename('fullpath'));
  634. % [file.result,file.folder] = uigetfile([mfilefolder,'\*.mat']);
  635. file.result = 'result_20180305_124441.mat';
  636. file.folder = '..\..\data\Fig5 frequency data\Data V Series (V-Serie)\';
  637. load([file.folder,file.result],'result')
  638. % Discard unwanted trials, based on .xlsx documenting manual assessment.
  639. result = discardTrial(result,file.folder); %#ok<NODEF>
  640. result = zeroTrial(result,file.folder); %#ok<NODEF>
  641. % Pool gain data in various ways (this is a significant subfunction!).
  642. gain = poolGainData(result);
  643. % Select the three types of data to be plotted.
  644. data.left = gain.mean.FR{1};
  645. data.right = gain.mean.FR{2};
  646. % both = gain.mean.FER;
  647. data.both = mean([data.left,data.right],2);
  648. % 20181217: optionally, normalise gain values by mean gain across same hemisphere
  649. hemisphere1 = datarange(1:end/2) - 1; % The -1 accounts for the Excel title row.
  650. hemisphere2 = datarange(end/2+1:end) - 1;
  651. switch normalisation
  652. case 'mean gain per hemisphere'
  653. data.left(hemisphere1) = data.left(hemisphere1) / mean(data.left(hemisphere1));
  654. data.left(hemisphere2) = data.left(hemisphere2) / mean(data.left(hemisphere2));
  655. data.right(hemisphere1) = data.right(hemisphere1) / mean(data.right(hemisphere1));
  656. data.right(hemisphere2) = data.right(hemisphere2) / mean(data.right(hemisphere2));
  657. data.both(hemisphere1) = data.both(hemisphere1) / mean(data.both(hemisphere1));
  658. data.both(hemisphere2) = data.both(hemisphere2) / mean(data.both(hemisphere2));
  659. case 'max gain per hemisphere'
  660. data.left(hemisphere1) = data.left(hemisphere1) / max(data.left(hemisphere1));
  661. data.left(hemisphere2) = data.left(hemisphere2) / max(data.left(hemisphere2));
  662. data.right(hemisphere1) = data.right(hemisphere1) / max(data.right(hemisphere1));
  663. data.right(hemisphere2) = data.right(hemisphere2) / max(data.right(hemisphere2));
  664. data.both(hemisphere1) = data.both(hemisphere1) / max(data.both(hemisphere1));
  665. data.both(hemisphere2) = data.both(hemisphere2) / max(data.both(hemisphere2));
  666. case 'ratio of mean gains per hemisphere'
  667. data.left(hemisphere1) = data.left(hemisphere1) / ...
  668. (mean(data.left(hemisphere1)) / mean(data.left(hemisphere2)));
  669. data.left(hemisphere2) = data.left(hemisphere2) / ...
  670. (mean(data.left(hemisphere2) / mean(data.left(hemisphere1))));
  671. data.right(hemisphere1) = data.right(hemisphere1) / ...
  672. (mean(data.right(hemisphere1)) / mean(data.right(hemisphere2)));
  673. data.right(hemisphere2) = data.right(hemisphere2) / ...
  674. (mean(data.right(hemisphere2)) / mean(data.right(hemisphere1)));
  675. data.both(hemisphere1) = data.both(hemisphere1) / ...
  676. (mean(data.both(hemisphere1)) / mean(data.both(hemisphere2)));
  677. data.both(hemisphere2) = data.both(hemisphere2) / ...
  678. (mean(data.both(hemisphere2)) / mean(data.both(hemisphere1)));
  679. end
  680. % READ STIMULUS INFORMATION
  681. file.stimulus = 'Stimulus.xlsx';
  682. [~,stim.tableconvention,~] = xlsread([file.folder,file.stimulus],'C1:C1');
  683. stim.azimuth = xlsread([file.folder,file.stimulus],['C',num2str(datarange(1)),':C',num2str(datarange(end))]);
  684. stim.elevation = xlsread([file.folder,file.stimulus],['D',num2str(datarange(1)),':D',num2str(datarange(end))]);
  685. stim.solidangle = xlsread([file.folder,file.stimulus],['E',num2str(datarange(1)),':E',num2str(datarange(end))]);
  686. stim.spatialfreq = xlsread([file.folder,file.stimulus],['F',num2str(datarange(1)),':F',num2str(datarange(end))]);
  687. stim.tempfreq = xlsread([file.folder,file.stimulus],['G',num2str(datarange(1)),':G',num2str(datarange(end))]);
  688. % Stimulus tables may use CW convention; default code uses CCW.
  689. switch stim.tableconvention{:}
  690. case 'azimuth (CCW)'
  691. case 'azimuth (CW)'
  692. stim.azimuth = -stim.azimuth;
  693. otherwise
  694. error(['Field C1 of Stimulus.xlsx should contain either ' ...
  695. 'string ''azimuth (CCW)'' or string ''azimuth (CW)''.'])
  696. end
  697. stimcentre = [stim.azimuth, stim.elevation];
  698. % CHOOSE SUBSET OF DATA TO ANALYSE, AND PROCEED TO ANALYSE IT
  699. choicelist = {'left','both','right'};
  700. for choice = 1:numel(choicelist)
  701. switch choicelist{choice}
  702. case 'right'
  703. response = data.right(datarange-1); % -1 is offset by Excel title row
  704. case 'left'
  705. response = data.left(datarange-1);
  706. case 'both'
  707. response = data.both(datarange-1);
  708. otherwise
  709. error(['Input argument must be one of the following strings: ' ...
  710. '''left'', ''right'', or ''both''.'])
  711. end
  712. predictor = stimcentre;
  713. % compute cartesian coordinates of sphere, and its colour data values
  714. [x,y,z] = sphere(resolution);
  715. % [x,y,z] = sphere(250);
  716. [azimuth,elevation,radius] = car2geo(x,y,z,'CCW');
  717. % predictor = [azimuth(:),elevation(:),radius(:)];
  718. % datafit.matrix{choice} = reshape(vonmisesfisher9param(datafit.param,predictor),size(radius));
  719. % set 3D fish parameters
  720. scale = .1;
  721. yaw = 180;
  722. pitch = 0;
  723. roll = 0;
  724. % set decorative arc parameters; compute their cartesian coordinates
  725. equator.base = -1:.01:1;
  726. equator.x = [equator.base, fliplr(equator.base)];
  727. equator.y = [sqrt(1-equator.base.^2), -sqrt(1-equator.base.^2)];
  728. equator.z = 0*ones(1,numel(equator.x));
  729. meridian.x = equator.x;
  730. meridian.y = equator.z;
  731. meridian.z = equator.y;
  732. aura.x = equator.z;
  733. aura.y = equator.y;
  734. aura.z = equator.x;
  735. [sample.x,sample.y,sample.z] = ...
  736. geo2car(stimcentre(:,1),stimcentre(:,2),ones(size(stimcentre,1),1),'CCW');
  737. [uniquepair,firstoccurrence,~] = unique([stim.azimuth,stim.elevation],'rows');
  738. % MATLAB reorders unique pairs by azimuth (increasing). This does not
  739. % usually match the true order in which stimuli were presented! The
  740. % following two lines recovers this true order, and restores it.
  741. [~,trueorder] = sort(firstoccurrence);
  742. uniquepair = uniquepair(trueorder,:);
  743. stim.uniquecentre.azimuth = uniquepair(:,1);
  744. stim.uniquecentre.elevation = uniquepair(:,2);
  745. [uniquecentre.x,uniquecentre.y,uniquecentre.z] = ...
  746. geo2car(uniquepair(:,1),uniquepair(:,2),ones(size(uniquepair,1),1),'CCW');
  747. stim.uniquespatialfreq = unique(stim.spatialfreq);
  748. stim.uniquetempfreq = unique(stim.tempfreq);
  749. stim.uniquesolidangle = unique(stim.solidangle);
  750. stim.uniquesteradian = stim.uniquesolidangle*(2*pi)/360;
  751. stim.uniquearea = stim.uniquesteradian/(2*pi);
  752. fontsize = 14;
  753. if choice == 1
  754. panel = 1;
  755. hd.location.fg = figure();
  756. hd.location.fg.Position = [50 300 1200 300];
  757. hd.location.fg.Color = [1 1 1];
  758. hd.gain.ax(1) = axes;
  759. hd.gain.ax(1).Position = [.025 .14 .20 .8];
  760. hold on
  761. hd.gain.ob(panel,1) = surf(x,y,z);
  762. hd.gain.ob(panel,1).FaceAlpha = 0*.25;
  763. hd.gain.ob(panel,1).FaceColor = 1*[1 1 1];
  764. hd.gain.ob(panel,1).EdgeColor = 'none';
  765. hd.gain.ob(panel,2:4) = plot3dFish(scale,yaw,pitch,roll);
  766. hd.gain.ob(panel,5) = plot3(equator.x,equator.y,equator.z,'k','LineWidth',2);
  767. hd.gain.ob(panel,6) = plot3(meridian.x,meridian.y,meridian.z,'--k');
  768. hd.gain.ob(panel,7) = plot3(aura.x,aura.y,aura.z,':k');
  769. % colour.centre = [1 0 0; 0 1 0; 0 0 1; 1 1 0; 1 0 1; 0 1 1];
  770. colour.centre = [1 .447 .741; .85 .325 .098; .929 .694 .125; ...
  771. .494 .184 .556; .466 .6740 .188; .301 .745 .933];
  772. colour.purple = cbrewer('seq','Purples',100);
  773. colour.green = cbrewer('seq','Greens',100);
  774. colour.centre = [colour.purple(round(end*.8),:); ...
  775. colour.purple(round(end*.3),:); ...
  776. colour.purple(round(end*.5),:); ...
  777. colour.green(round(end*.8),:); ...
  778. colour.green(round(end*.3),:); ...
  779. colour.green(round(end*.5),:)];
  780. for origin = 1:numel(uniquecentre.x)
  781. hd.gain.ob(panel,7+origin) = scatter3(1.03*uniquecentre.x(origin), ...
  782. 1.03*uniquecentre.y(origin), ...
  783. 1.03*uniquecentre.z(origin), ...
  784. 120,'Marker','o');
  785. hd.gain.ob(panel,7+origin).MarkerFaceColor = colour.centre(origin,:);
  786. hd.gain.ob(panel,7+origin).MarkerEdgeColor = .6*[1 1 1];
  787. end
  788. hold off
  789. % % hd.gain.ob(panel,8).Color = [1 1 1];
  790. % hd.gain.ob(panel,1).FaceAlpha = .5;
  791. % hd.gain.ob(panel,1).FaceColor = hd.gain.ob(panel,1).EdgeColor;
  792. axis square
  793. hd.gain.ax(panel).XAxis.Visible = 'off';
  794. hd.gain.ax(panel).YAxis.Visible = 'off';
  795. hd.gain.ax(panel).ZAxis.Visible = 'off';
  796. % hd.gain.ax(panel).Title.String = 'stimulus centres';
  797. hd.gain.ax(panel).CameraPosition = [11.5866 -8.9228 9.2809];
  798. hd.gain.ax(panel).CameraViewAngle = 9.25;
  799. hd.gain.ax(4) = axes;
  800. hd.gain.ax(4).Position = [0 0 1 1];
  801. hd.gain.ax(4).Color = 'none';
  802. hd.gain.ax(4).Visible = 'off';
  803. hd.gain.ob(4,1) = text(.02,.93,'c','Units','normalized');
  804. hd.gain.ob(4,2) = text(.25,.93,'d','Units','normalized');
  805. [hd.gain.ob(4,1:2).FontWeight] = deal('bold');
  806. [hd.gain.ob(4,1:2).FontSize] = deal(14);
  807. end
  808. input.letter = 'e';
  809. panel = choice+1;
  810. croppeddata(:,1) = response(1:7);
  811. croppeddata(:,2) = response(8:14);
  812. croppeddata(:,3) = response(15:21);
  813. croppeddata(:,4) = response(22:28);
  814. croppeddata(:,5) = response(29:35);
  815. croppeddata(:,6) = response(36:42);
  816. hd.gain.ax(panel) = axes;
  817. hd.gain.ax(panel).Position = [.33+(panel-2)*.22 .26 .20 .55];
  818. hold on
  819. for origin = 1:size(croppeddata,2)
  820. % hd.gain.ob(panel,origin) = plot(stim.uniquetempfreq,croppeddata(:,origin));
  821. hd.gain.ob(panel,origin) = plot(stim.uniquespatialfreq,croppeddata(:,origin));
  822. hd.gain.ob(panel,origin).Color = colour.centre(origin,:);
  823. hd.gain.ob(panel,origin).LineWidth = 2;
  824. end
  825. hold off
  826. hd.gain.ax(panel).TitleFontWeight = 'normal';
  827. hd.gain.ax(panel).TitleFontSizeMultiplier = 1;
  828. % hd.gain.ax(panel).XLabel.String = 'temporal frequency';
  829. hd.gain.ax(panel).XLabel.String = 'spatial frequency';
  830. if choice == 1
  831. hd.gain.ax(panel).YLabel.String = 'OKR gain';
  832. hd.gain.ax(panel).YLabel.Units = 'normalized';
  833. hd.gain.ax(2).YLabel.Position = [-.2 .5 0];
  834. else
  835. hd.gain.ax(panel).YAxis.Visible = 'off';
  836. end
  837. switch choicelist{choice}
  838. case 'left'
  839. hd.gain.ax(panel).Title.String = 'left eye';
  840. case 'right'
  841. hd.gain.ax(panel).Title.String = 'right eye';
  842. case 'both'
  843. % hd.gain.ax(panel).Title.String = 'mean of both eyes';
  844. hd.gain.ax(panel).Title.String = 'mean';
  845. end
  846. hd.gain.ax(panel).FontSize = fontsize;
  847. if panel > 1
  848. hd.gain.ax(panel).XScale = 'log';
  849. hd.gain.ax(panel).YScale = 'lin';
  850. hd.gain.ax(panel).XLim = [.9*min(hd.gain.ob(panel,1).XData), ...
  851. max(hd.gain.ob(panel,1).XData)];
  852. switch normalisation
  853. case 'max gain per hemisphere'
  854. hd.gain.ax(panel).YLim = [0, 1];
  855. case 'mean gain per hemisphere'
  856. hd.gain.ax(panel).YLim = [0, max([data.left;data.right;data.both])];
  857. case 'ratio of mean gains per hemisphere'
  858. hd.gain.ax(panel).YLim = [0, max([data.left;data.right;data.both])];
  859. otherwise
  860. hd.gain.ax(panel).YLim = [0, max([data.left;data.right;data.both])];
  861. end
  862. hd.gain.ax(panel).TickLength = [.03 .075];
  863. hd.gain.ax(panel).YGrid = 'on';
  864. hd.gain.ax(panel).Title.Units = 'normalized';
  865. hd.gain.ax(panel).Title.Position = [.5 1.1 0];
  866. end
  867. end
  868. cam.radius = 19.0526; % MATLAB automatically reverts to this values while rotating, anyway. Might as well choose it as default.
  869. [cam.x,cam.y,cam.z] = geo2car(cam.azimuth,cam.elevation,cam.radius,'CCW');
  870. hd.gain.ax(1).CameraPosition = [cam.x cam.y cam.z];
  871. hd.gain.ax(1).CameraTarget = [0 0 0];
  872. hd.gain.ax(1).CameraUpVector = [0 0 1];
  873. % hd.gain.ax(panel).CameraViewAngle = 10.134;
  874. hd.gain.ax(1).CameraViewAngle = 9;
  875. % Collect output arguments, unless already done (e.g., "datafit").
  876. % data.left = left;
  877. % data.right = right;
  878. % data.both = both;
  879. handle = hd;
  880. end
  881. function gain = poolGainData(result)
  882. %POOLGAINDATA converts a raw result structure into plottable data by
  883. %pooling OKR gain data in various ways, and storing those results in a new
  884. %structure called "gain".
  885. % Pool data in various ways (e.g., .FR = pooled across all [F]ish and
  886. % all [R]epetitions, but with a separate array for each [E]ye).
  887. for stimtype = 1:size(result,3)
  888. gainpool(stimtype).FER = [result(:,:,stimtype,:).gain];
  889. for eye = 1:size(result,2)
  890. gainpool(stimtype).FR{eye} = [result(:,eye,stimtype,:).gain];
  891. end
  892. for fish = 1:size(result,1)
  893. gainpool(stimtype).ER{fish} = [result(fish,:,stimtype,:).gain];
  894. end
  895. for repetition = 1:size(result,4)
  896. gainpool(stimtype).FE{repetition} = [result(:,:,stimtype,repetition).gain];
  897. end
  898. for fish = 1:size(result,1)
  899. for eye = 1:size(result,2)
  900. gainpool(stimtype).R{fish,eye} = [result(fish,eye,stimtype,:).gain];
  901. end
  902. end
  903. end
  904. % Make sure that in following section, if a stimulus type is not present,
  905. % the array contain a NaN at that position, instead of a zero.
  906. nantemplate = NaN(numel(gainpool),1);
  907. gain.mean .FER = nantemplate;
  908. gain.median.FER = nantemplate;
  909. gain.std .FER = nantemplate;
  910. gain.sem .FER = nantemplate;
  911. for eye = 1:size(result,2)
  912. gain.mean .FR{eye} = nantemplate;
  913. gain.median.FR{eye} = nantemplate;
  914. gain.std .FR{eye} = nantemplate;
  915. gain.sem .FR{eye} = nantemplate;
  916. end
  917. for fish = 1:size(result,1)
  918. gain.mean .ER{fish} = nantemplate;
  919. gain.median.ER{fish} = nantemplate;
  920. gain.std .ER{fish} = nantemplate;
  921. gain.sem .ER{fish} = nantemplate;
  922. end
  923. for fish = 1:size(result,1)
  924. for eye = 1:size(result,2)
  925. gain.mean .R{fish,eye} = nantemplate;
  926. gain.median.R{fish,eye} = nantemplate;
  927. gain.std .R{fish,eye} = nantemplate;
  928. gain.sem .R{fish,eye} = nantemplate;
  929. end
  930. end
  931. % mean, median, stdev, sem across all pooled data, one per stimulus type
  932. for stimtype = 1:numel(gainpool)
  933. selection = gainpool(stimtype).FER;
  934. gain.mean .FER(stimtype) = mean(selection);
  935. gain.median.FER(stimtype) = median(selection);
  936. gain.std .FER(stimtype) = std(selection);
  937. gain.sem .FER(stimtype) = std(selection)/sqrt(numel(selection));
  938. for eye = 1:size(result,2)
  939. selection = gainpool(stimtype).FR{eye};
  940. gain.mean .FR{eye}(stimtype) = mean(selection);
  941. gain.median.FR{eye}(stimtype) = median(selection);
  942. gain.std .FR{eye}(stimtype) = std(selection);
  943. gain.sem .FR{eye}(stimtype) = std(selection)/sqrt(numel(selection));
  944. end
  945. for fish = 1:size(result,1)
  946. selection = gainpool(stimtype).ER{fish};
  947. gain.mean .ER{fish}(stimtype) = mean(selection);
  948. gain.median.ER{fish}(stimtype) = median(selection);
  949. gain.std .ER{fish}(stimtype) = std(selection);
  950. gain.sem .ER{fish}(stimtype) = std(selection)/sqrt(numel(selection));
  951. end
  952. for fish = 1:size(result,1)
  953. for eye = 1:size(result,2)
  954. selection = gainpool(stimtype).R{fish,eye};
  955. gain.mean .R{fish,eye}(stimtype) = mean(selection);
  956. gain.median.R{fish,eye}(stimtype) = median(selection);
  957. gain.std .R{fish,eye}(stimtype) = std(selection);
  958. gain.sem .R{fish,eye}(stimtype) = std(selection)/sqrt(numel(selection));
  959. end
  960. end
  961. end
  962. end
  963. function hd = fishbowlFigure(hd,cam,choicelist)
  964. %FISHBOWLFIGURE configures an existing figure of fishbowl plots, adds decorations
  965. %
  966. % HD = fishbowlFigure sets the parameters of an existing fishbowl figure,
  967. % including axis limits, labels, colours, axes object sizes, etc., and
  968. % adds additional decorations such as colorbars.
  969. cminbottomrow = min([min(min(hd.gain.ob(4,5).CData)), ...
  970. min(min(hd.gain.ob(5,5).CData)), ...
  971. min(min(hd.gain.ob(6,5).CData))]);
  972. cmaxbottomrow = max([max(max(hd.gain.ob(4,5).CData)), ...
  973. max(max(hd.gain.ob(5,5).CData)), ...
  974. max(max(hd.gain.ob(6,5).CData))]);
  975. climbottomrow = max(abs([cminbottomrow cmaxbottomrow]))*[-1 1];
  976. cmintoprow = min([min(min(hd.gain.ob(1,1).CData)), ...
  977. min(min(hd.gain.ob(2,1).CData)), ...
  978. min(min(hd.gain.ob(3,1).CData)), ...
  979. min(min(hd.gain.ob(1,8).CData)), ...
  980. min(min(hd.gain.ob(2,8).CData)), ...
  981. min(min(hd.gain.ob(3,8).CData))]);
  982. cmaxtoprow = max([max(max(hd.gain.ob(1,1).CData)), ...
  983. max(max(hd.gain.ob(2,1).CData)), ...
  984. max(max(hd.gain.ob(3,1).CData)), ...
  985. max(max(hd.gain.ob(1,8).CData)), ...
  986. max(max(hd.gain.ob(2,8).CData)), ...
  987. max(max(hd.gain.ob(3,8).CData))]);
  988. climtoprow = max(abs([cmintoprow cmaxtoprow]))*[-1 1];
  989. for panel = 1:3
  990. switch choicelist{panel}
  991. case 'left'
  992. string.title = 'left eye gain g_L';
  993. case 'right'
  994. string.title = 'right eye gain g_R';
  995. case 'both'
  996. string.title = 'mean = (g_L+ g_R)/2';
  997. end
  998. hd.gain.ax(panel).Title.String = string.title;
  999. % title(string.title,'Interpreter','LaTeX')
  1000. hd.gain.ob(panel,8).MarkerFaceColor = hd.gain.ob(panel,8).MarkerEdgeColor;
  1001. hd.gain.ob(panel,8).MarkerEdgeColor = [0 0 0];
  1002. hd.gain.ax(panel).CLim = [0 cmaxtoprow];
  1003. % hd.gain.ax(panel).CLim = [0 max(max(datafit.matrix{choice}))];
  1004. hd.gain.ob(panel,1).FaceAlpha = .85;
  1005. hd.gain.ob(panel,1).EdgeColor = 'none';
  1006. % hd.gain.ob(panel,8).Shading = 'flat';
  1007. shading flat
  1008. end
  1009. for panel = 4:6
  1010. hd.gain.ax(panel).CLim = climbottomrow;
  1011. hd.gain.ob(panel,5).MarkerFaceColor = hd.gain.ob(panel,5).MarkerEdgeColor;
  1012. hd.gain.ob(panel,1).FaceColor = [1 1 1];
  1013. hd.gain.ob(panel,1).FaceAlpha = .85;
  1014. hd.gain.ob(panel,1).EdgeColor = 'none';
  1015. end
  1016. for panel = 1:6
  1017. axis square
  1018. xlabel('x')
  1019. ylabel('y')
  1020. zlabel('z')
  1021. hd.gain.ax(panel).XLim = [-1.1 1.1];
  1022. hd.gain.ax(panel).YLim = [-1.1 1.1];
  1023. hd.gain.ax(panel).ZLim = [-1.1 1.1];
  1024. % hd.gain.ax(panel).CameraPosition = [12.2934, -9.0967,8.1315];
  1025. cam.radius = 19.0526; % MATLAB automatically reverts to this values while rotating, anyway. Might as well choose it as default.
  1026. [cam.x,cam.y,cam.z] = geo2car(cam.azimuth,cam.elevation,cam.radius,'CCW');
  1027. hd.gain.ax(panel).CameraPosition = [cam.x cam.y cam.z];
  1028. hd.gain.ax(panel).CameraTarget = [0 0 0];
  1029. hd.gain.ax(panel).CameraUpVector = [0 0 1];
  1030. % hd.gain.ax(panel).CameraViewAngle = 10.134;
  1031. hd.gain.ax(panel).CameraViewAngle = 9;
  1032. end
  1033. hd.gain.ax(7) = axes;
  1034. hd.gain.ax(7).Position = hd.gain.ax(1).Position - [.13 -.1 .2 .2];
  1035. hd.gain.ax(7).CLim = hd.gain.ax(1).CLim;
  1036. hd.gain.ob(7,1) = colorbar;
  1037. hd.gain.ob(7,1).Label.String = 'OKR gain';
  1038. hd.gain.ob(7,1).Label.Units = 'normalized';
  1039. hd.gain.ob(7,1).Label.Position = [-.8 1 1] .* hd.gain.ob(7,1).Label.Position;
  1040. hd.gain.ax(8) = axes;
  1041. hd.gain.ax(8).Position = hd.gain.ax(4).Position - [.13 -.1 .2 .2];
  1042. hd.gain.ax(8).CLim = hd.gain.ax(4).CLim;
  1043. hd.gain.ob(8,1) = colorbar;
  1044. hd.gain.ob(8,1).Label.String = 'fit - data';
  1045. hd.gain.ob(8,1).Label.Units = 'normalized';
  1046. hd.gain.ob(8,1).Label.Position = [-.8 1 1] .* hd.gain.ob(8,1).Label.Position;
  1047. [hd.gain.ob(7,1).Label.FontSize, hd.gain.ob(7,1).FontSize, ...
  1048. hd.gain.ob(8,1).Label.FontSize, hd.gain.ob(8,1).FontSize, ...
  1049. hd.gain.ax.FontSize] = deal(14);
  1050. [hd.gain.ax(1).Title.FontSize, ...
  1051. hd.gain.ax(2).Title.FontSize, ...
  1052. hd.gain.ax(3).Title.FontSize] = deal(14);
  1053. [hd.gain.ax(1).Title.FontWeight, ...
  1054. hd.gain.ax(2).Title.FontWeight, ...
  1055. hd.gain.ax(3).Title.FontWeight] = deal('normal');
  1056. [hd.gain.ax.Visible] = deal('off');
  1057. % [hd.gain.ax.Title.Visible] = deal('on');
  1058. set(([hd.gain.ax.Title]),'Visible','on')
  1059. set(([hd.gain.ax.Title]),'Position',[1 1 .7] .* hd.gain.ax(1).Title.Position)
  1060. end
  1061. function [retained,original] = discardTrial(original,folderstring)
  1062. % DISCARDTRIAL removes data obtained from trials consider "poorly fit" etc.
  1063. %
  1064. % This function takes a "result" structure as its only input argument, and
  1065. % requires an XLSX file containing the indices of trials to be discarded.
  1066. % It then removes those bad trials, and returns a reduced structure as its
  1067. % primary output argument, "retained". For easy access, it also returns its
  1068. % original input.
  1069. %
  1070. % This function can safely be executed multiple times, as "discarded"
  1071. % trials are in fact just set to empty, so their array indices are not
  1072. % reassigned to other trials.
  1073. %
  1074. % Written by Florian Alexander Dehmelt, Tuebingen University, in 2018.
  1075. % Identify and load an Excel file containing the indices of all unwanted
  1076. % trials to be discarded. This file must contain four columns of numbers
  1077. % (from left to right: fish no., eye no., stimulus type, and repetition.
  1078. % It may contain as many text comments as desired; these will be ignored.
  1079. folder.badtrial = folderstring;
  1080. % folder.badtrial = 'C:\Users\fdehmelt\Dropbox\ThinkPad Dropbox\MATLAB\201808 SphereAnalysis\Sample Data\D series\';
  1081. file.badtrial = 'DiscardTrial.xlsx';
  1082. badtrial = xlsread([folder.badtrial,file.badtrial]);
  1083. % The "retained" structure is the same as the "original" one...
  1084. retained = original;
  1085. % ...except that unwanted trials are overwritten by empty values.
  1086. % They thus appear the same way as non-existent trials already did.
  1087. for trial = 1:size(badtrial,1)
  1088. % Find the position of the bad trial within the structure.
  1089. fish = badtrial(trial,1);
  1090. eye = badtrial(trial,2);
  1091. stimtype = badtrial(trial,3) + 1; % The +1 converts index >=0 to >=1.
  1092. repetition = badtrial(trial,4);
  1093. % Overwrite all field entries with "[]".
  1094. fieldname = fieldnames(retained);
  1095. for field = 1:numel(fieldname)
  1096. retained(fish,eye,stimtype,repetition).(fieldname{field}) = [];
  1097. end
  1098. end
  1099. end
  1100. function [altered,original] = zeroTrial(original,folderstring)
  1101. % ZEROTRIAL sets gains to zero based on visual inspection of trials.
  1102. %
  1103. % This function takes a "result" structure as its only input argument, and
  1104. % requires an XLSX file containing the indices of trials to be set to zero.
  1105. % It then adjusts those gains, and returns an altered structure as its
  1106. % primary output argument, "retained". For easy access, it also returns its
  1107. % original input.
  1108. %
  1109. % This function can safely be executed multiple times, as "altered"
  1110. % trials are in fact just set to zero, so their array indices are not
  1111. % reassigned to other trials.
  1112. %
  1113. % Written by Florian Alexander Dehmelt, Tuebingen University, in 2018.
  1114. % Identify and load an Excel file containing the indices of all unwanted
  1115. % trials to be discarded. This file must contain four columns of numbers
  1116. % (from left to right: fish no., eye no., stimulus type, and repetition.
  1117. % It may contain as many text comments as desired; these will be ignored.
  1118. folder.badtrial = folderstring;
  1119. % folder.badtrial = 'C:\Users\fdehmelt\Dropbox\ThinkPad Dropbox\MATLAB\201808 SphereAnalysis\Sample Data\D series\';
  1120. file.badtrial = 'ZeroTrial.xlsx';
  1121. badtrial = xlsread([folder.badtrial,file.badtrial]);
  1122. % The "altered" structure is the same as the "original" one...
  1123. altered = original;
  1124. % ...except that gains of visually identified trials are set to zero.
  1125. % They thus appear the same way as non-existent trials already did.
  1126. for trial = 1:size(badtrial,1)
  1127. % Find the position of the bad trial within the structure.
  1128. fish = badtrial(trial,1);
  1129. eye = badtrial(trial,2);
  1130. stimtype = badtrial(trial,3) + 1; % The +1 converts index >=0 to >=1.
  1131. repetition = badtrial(trial,4);
  1132. % Overwrite the gain field entries with "0", retain all other values.
  1133. altered(fish,eye,stimtype,repetition).gain = 0;
  1134. end
  1135. end
  1136. function combinedvalue = vonmisesfisher9param(param,predictor)
  1137. %VONMISESFISHER9PARAM Evaluate two overlapping von Mises-Fisher distributions.
  1138. %
  1139. % VALUE = VONMISESFISHER9PARAM computes the value of two(!) overlapping,
  1140. % spherical (p=3) von Mises-Fisher distributions at the chosen location.
  1141. %
  1142. % It is formatted to match the input/output structure required by the
  1143. % built-in non-linear regression function nlinfit.
  1144. convention = 'CCW';
  1145. dimension = 3;
  1146. radius = 1;
  1147. if size(predictor,2) == 2
  1148. predictor(:,3) = radius*ones(size(predictor(:,1)));
  1149. end
  1150. [x,y,z] = geo2car(predictor(:,1),predictor(:,2),predictor(:,3),convention);
  1151. direction = [x,y,z];
  1152. numberofdistributions = 2;
  1153. value = NaN(size(predictor,1),numberofdistributions);
  1154. for k = 1:numberofdistributions
  1155. offset = param(1);
  1156. amplitude = param(4*k-2);
  1157. meanazimuth = param(4*k-1);
  1158. meanelevation = param(4*k);
  1159. concentration = param(4*k+1);
  1160. [meanx,meany,meanz] = geo2car(meanazimuth,meanelevation,radius,convention);
  1161. meandirection = [meanx,meany,meanz];
  1162. bessel = concentration / ...
  1163. (2*pi * (exp(concentration) - exp(-concentration)));
  1164. normalisation = concentration^(dimension/2 - 1) / ...
  1165. ((2*pi)^(dimension/2) * bessel);
  1166. value(:,k) = normalisation * exp(concentration*meandirection*direction')';
  1167. value(:,k) = amplitude * value(:,k) + offset;
  1168. % HACK to constrain values
  1169. if abs(meanazimuth) > 180 || ...
  1170. abs(meanelevation) > 90 || ...
  1171. amplitude < 0 || ...
  1172. concentration < 0
  1173. value(:,k) = 1e10*ones(size(value(:,k)));
  1174. end
  1175. end
  1176. combinedvalue = sum(value,2);
  1177. end
  1178. function [x,y,z] = geo2car(azimuth,elevation,radius,convention)
  1179. switch convention
  1180. case 'CCW'
  1181. x = radius .* cosd(elevation).*cosd(azimuth);
  1182. y = radius .* cosd(elevation).*sind(azimuth);
  1183. z = radius .* sind(elevation);
  1184. case 'CW'
  1185. x = radius .* cosd(elevation).*cosd(azimuth);
  1186. y = radius .* cosd(elevation).*-sind(azimuth);
  1187. z = radius .* sind(elevation);
  1188. otherwise
  1189. error('Convention not supported.')
  1190. end
  1191. end
  1192. function [azimuth,elevation,radius] = car2geo(x,y,z,convention)
  1193. azimuth = atand(y./x) + 180*((x<0).*(y>=0) - (x<0).*(y<0));
  1194. switch convention
  1195. case 'CCW'
  1196. case 'CW'
  1197. azimuth = -azimuth;
  1198. otherwise
  1199. error('Convention not supported.')
  1200. end
  1201. radius = sqrt(x.^2+y.^2+z.^2);
  1202. elevation = asind(z./radius);
  1203. end
  1204. function [lefteyehandle,righteyehandle,bodyhandle] = ...
  1205. plot3dFish(scale,yaw,pitch,roll)
  1206. % This corresponds to version 20170505a, a.k.a. plot3dFish20170505a.m.
  1207. set(gcf,'Color',[1 1 1])
  1208. resolution = 10;
  1209. eye.colour = .1*[1 1 1];
  1210. mainbody.colour = .7*[1 1 1];
  1211. eye.x = scale * (cos(-pi:pi/resolution:3*pi-pi/resolution)');
  1212. eye.y = scale * (0.7*sin(0:pi/resolution:4*pi-pi/resolution)');
  1213. eye.z = scale * ([.7*ones(2*resolution,1); ...
  1214. 1*ones(2*resolution,1)]);
  1215. N = 2*resolution;
  1216. lefteye.vertices = [eye.x, eye.y, eye.z];
  1217. righteye.vertices = [eye.x, -eye.y, eye.z];
  1218. lefteye.faces = [[1:2*resolution 1]; ...
  1219. [1+2*resolution:4*resolution 1+2*resolution]];
  1220. for k = 1:N
  1221. lefteye.faces = [lefteye.faces; ...
  1222. [mod([k-1 k],N)+1, ...
  1223. mod([k k-1],N)+1+N, ...
  1224. mod(k-1,N)+1, ...
  1225. NaN(1,N-4)]];
  1226. end
  1227. lefteye.facevertexcdata = repmat(eye.colour,[size(lefteye.faces,1) 1]);
  1228. righteye.faces = lefteye.faces;
  1229. righteye.facevertexcdata = lefteye.facevertexcdata;
  1230. lefteyehandle = patch(lefteye);
  1231. righteyehandle = patch(righteye);
  1232. set([lefteyehandle,righteyehandle],'FaceColor','flat','EdgeColor','none')
  1233. mainbody.x = scale * (-.1 + [1.0*cos(-pi:pi/resolution:pi-pi/resolution), ...
  1234. 0.4*cos(-pi:pi/resolution:pi-pi/resolution)]');
  1235. mainbody.y = scale * ([0.7*sin(0:pi/resolution:2*pi-pi/resolution), ...
  1236. 0.2*sin(0:pi/resolution:2*pi-pi/resolution)]');
  1237. mainbody.z = scale * ([1.1*ones(2*resolution,1); ...
  1238. -7*ones(2*resolution,1)]);
  1239. N = 2*resolution;
  1240. body.vertices = [mainbody.x, mainbody.y, mainbody.z];
  1241. body.faces = [[1:2*resolution 1]; ...
  1242. [1+2*resolution:4*resolution 1+2*resolution]];
  1243. for k = 1:N
  1244. body.faces = [body.faces; ...
  1245. [mod([k-1 k],N)+1, ...
  1246. mod([k k-1],N)+1+N, ...
  1247. mod(k-1,N)+1, ...
  1248. NaN(1,N-4)]];
  1249. end
  1250. body.facevertexcdata = repmat(mainbody.colour,[size(body.faces,1) 1]);
  1251. bodyhandle = patch(body);
  1252. set(bodyhandle,'FaceColor','flat','EdgeColor','none')
  1253. rotate(lefteyehandle,[1 0 0],80)
  1254. rotate(lefteyehandle,[0 0 1],-10)
  1255. rotate(righteyehandle,[1 0 0],-80)
  1256. rotate(righteyehandle,[0 0 1],10)
  1257. rotate(bodyhandle,[0 1 0],-90)
  1258. rotate(lefteyehandle,[0 0 1],yaw)
  1259. rotate(righteyehandle,[0 0 1],yaw)
  1260. rotate(bodyhandle,[0 0 1],yaw)
  1261. rotate(lefteyehandle,[0 1 0],pitch)
  1262. rotate(righteyehandle,[0 1 0],pitch)
  1263. rotate(bodyhandle,[0 1 0],pitch)
  1264. rotate(lefteyehandle,[1 0 0],roll)
  1265. rotate(righteyehandle,[1 0 0],roll)
  1266. rotate(bodyhandle,[1 0 0],roll)
  1267. end
  1268. function hd = displayStimuli(typestring)
  1269. warning('off','MATLAB:interp1:UsePCHIP')
  1270. % The line above deactivates the following cbrewer warning:
  1271. % Warning: INTERP1(...,'CUBIC') will change in a future release. Use INTERP1(...,'PCHIP') instead.
  1272. % > In interp1>sanitycheckmethod (line 265)
  1273. % In interp1>parseinputs (line 401)
  1274. % In interp1 (line 80)
  1275. % In interpolate_cbrewer (line 31)
  1276. % In cbrewer (line 101)
  1277. % In heatmap20180823 (line 21)
  1278. % close all
  1279. % clf
  1280. switch typestring
  1281. case 'disk'
  1282. file.folder = '..\..\data\Fig3 default disk data\Data D Series (neue Experimente)\';
  1283. datarange = 9:46;
  1284. case 'area'
  1285. file.folder = '..\..\data\Fig5 size data\Data A Series (A-Series)\';
  1286. datarange = 2:43;
  1287. datarange = 2:8;
  1288. case 'freq'
  1289. file.folder = '..\..\data\Fig5 frequency data\Data V Series (V-Serie)\';
  1290. datarange = 2:43;
  1291. datarange = 2;
  1292. otherwise
  1293. error('Argument of displayStimuli.m must be ''disk'', ''area'', or ''freq''.')
  1294. end
  1295. file.stimulus = 'Stimulus.xlsx';
  1296. [~,stim.tableconvention,~] = xlsread([file.folder,file.stimulus],'C1:C1');
  1297. stim.azimuth = xlsread([file.folder,file.stimulus],['C',num2str(datarange(1)),':C',num2str(datarange(end))]);
  1298. stim.elevation = xlsread([file.folder,file.stimulus],['D',num2str(datarange(1)),':D',num2str(datarange(end))]);
  1299. stim.radius = xlsread([file.folder,file.stimulus],['E',num2str(datarange(1)),':E',num2str(datarange(end))]);
  1300. % compute disk boundaries
  1301. diskcentre.geo = [stim.azimuth, stim.elevation];
  1302. diskcentre.angle = stim.radius;
  1303. % diskcentre.geo = [ 45 20; ...
  1304. % 30 5; ...
  1305. % 60 -45];
  1306. % diskcentre.angle = 40 * [ones(size(diskcentre.geo,1),1)];
  1307. diskcentre.azim = diskcentre.geo(:,1);
  1308. diskcentre.elev = diskcentre.geo(:,2);
  1309. diskcentre.rads = ones(size(diskcentre.geo(:,1)));
  1310. resolution = .001;
  1311. numdisk = size(diskcentre.geo,1);
  1312. for disk = 1:numdisk
  1313. [boundary.azim{disk}, ...
  1314. boundary.elev{disk}] = diskBoundary(diskcentre.azim(disk), ...
  1315. diskcentre.elev(disk), diskcentre.angle(disk), resolution);
  1316. [boundary.x{disk}, ...
  1317. boundary.y{disk}, ...
  1318. boundary.z{disk}] = geo2car(boundary.azim{disk}, boundary.elev{disk}, ...
  1319. ones(size(boundary.azim{disk})),'CW');
  1320. end
  1321. % create figure
  1322. hd.default.fg = figure(1);
  1323. hd.default.fg.Name = 'This is a sphere.';
  1324. hd.default.fg.Position = [50 50 500 800];
  1325. hd.default.fg.Color = [1 1 1];
  1326. hd.default.ax(1) = axes;
  1327. hd.default.ax(1).Position = [0 0 1 1];
  1328. hold on
  1329. % plot background sphere with shading
  1330. hd.default.globe = displaySphere;
  1331. % plot disk boundaries
  1332. % diskcolour = cbrewer('seq','Greens',numdisk*2);
  1333. diskcolour = flipud(cbrewer('seq','Greys',numdisk*2));
  1334. % diskcolour = diskcolour(randperm(numdisk),:);
  1335. croptext.xpos = [1.27 1.32 1.35 1.34 1.25 0.7 -1.58];
  1336. croptext.ypos = [0 0 0 0 0 0 0];
  1337. croptext.zpos = [.365 .18 .0 -.18 -.45 -.95 -.85];
  1338. for disk = 1:numdisk
  1339. % plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',3,'Color',0*[1 1 1])
  1340. % plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',2,'Color',1*[1 1 1])
  1341. plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',1,'Color',diskcolour(disk,:))
  1342. % plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',1,'Color',.2*[1 1 1])
  1343. croptext.label(disk) = text(croptext.xpos(disk),croptext.ypos(disk),croptext.zpos(disk), ...
  1344. [num2str(diskcentre.angle(disk),'%.1f'),'°'],'FontSize',12);
  1345. end
  1346. hold off
  1347. end
  1348. function globe = displaySphere
  1349. [surface.x,surface.y,surface.z] = sphere(100);
  1350. reference.geo = [30;20;1]; % desired azimuth, elevation, radius of maximum colour value
  1351. reference.azim = reference.geo(1);
  1352. reference.elev = reference.geo(2);
  1353. reference.rads = ones(size(reference.geo(1)));
  1354. [reference.x,reference.y,reference.z] = geo2car(reference.azim,reference.elev,reference.rads,'CW');
  1355. scalarproduct = [surface.x(:),surface.y(:),surface.z(:)] * [reference.x;reference.y;reference.z];
  1356. surface.c = reshape(scalarproduct,size(surface.x));
  1357. surface.c = 10.^(surface.c);
  1358. globe = surf(surface.x,surface.y,surface.z,surface.c);
  1359. shading flat
  1360. axis square
  1361. greyscale = (.8:.01:.99)' * [1 1 1];
  1362. colormap(greyscale)
  1363. end
  1364. function [azimuthOut, elevationOut] = diskBoundary(azimuthIn, elevationIn, angleIn, resolutionIn)
  1365. % The following equation describes orthodromic (or "great circle") distance as an angle:
  1366. % arccos(sin(centre.elevation)*sin(border.elevation) + ...
  1367. % cos(centre.elevation)*cos(border.elevation)*cos(border.azimuth-centre.azimuth)) = angle;
  1368. % This equation is used to compute border.azimuth below.
  1369. centre.azimuth = azimuthIn;
  1370. centre.elevation = elevationIn;
  1371. border.angle = angleIn / 2; % convert solid angle ("diameter") to "radius"
  1372. border.resolution = resolutionIn;
  1373. intended = -90 : border.resolution : 90;
  1374. % limit = cosd([centre.elevation-border.angle centre.elevation+border.angle]);
  1375. % intended = acosd(min(limit) : border.resolution : max(limit));
  1376. % "auxiliary" elevations are explicitly added to guarantee coverage near top/bottom of circle:
  1377. auxiliary = centre.elevation + border.angle*[-1 1];
  1378. border.elevation = unique([intended,auxiliary]);
  1379. border.azimuth = centre.azimuth + ...
  1380. acosd((cosd(border.angle) - sind(centre.elevation)*sind(border.elevation)) ./ ...
  1381. (cosd(centre.elevation)*cosd(border.elevation)));
  1382. % Eliminate complex elements...
  1383. border.azimuth(imag(border.azimuth)~=0) = NaN; % This statement sometimes fails horribly.
  1384. % border.azimuth(imag(border.azimuth)>1e-5) = NaN; % This statement is silly, but more stable.
  1385. % ...and compute corresponding values for the second half of the boundary.
  1386. border.azimuth = [border.azimuth, 2*centre.azimuth-border.azimuth];
  1387. border.elevation = [border.elevation, border.elevation];
  1388. border.azimuth = mod(border.azimuth+180,360)-180;
  1389. azimuthOut = border.azimuth';
  1390. elevationOut = border.elevation';
  1391. end
  1392. function hd = displayPattern(typestring,spatialfreq)
  1393. % typestring = 'freq';
  1394. % switch typestring
  1395. %
  1396. % case 'disk'
  1397. % file.folder = '..\..\data\Fig3 default disk data\Data D Series (neue Experimente)\';
  1398. % datarange = 9:46;
  1399. %
  1400. % case 'area'
  1401. % file.folder = '..\..\data\Fig5 size data\Data A Series (A-Series)\';
  1402. % datarange = 2:43;
  1403. % datarange = 2:8;
  1404. %
  1405. % case 'freq'
  1406. % file.folder = '..\..\data\Fig5 frequency data\Data V Series (V-Serie)\';
  1407. % datarange = 2:43;
  1408. % datarange = 2;
  1409. %
  1410. % otherwise
  1411. % error('Argument of displayStimuli.m must be ''disk'', ''area'', or ''freq''.')
  1412. %
  1413. % end
  1414. % switch typestring
  1415. % case 'area'
  1416. % spatialfreq = 5.5 * ones(1,7);
  1417. % case 'freq'
  1418. % % spatialfreq = [1.5 3 5 7 9 11 20];
  1419. % spatialfreq = logspace(0.18,1.2,7);
  1420. % otherwise
  1421. % error('Argument of displayStimuli.m must be ''area'' or ''freq''.')
  1422. % end
  1423. grid.colour = .3*[1 1 1];
  1424. grid.width = 1;
  1425. grid.height = .7;
  1426. grid.xspace = .29;
  1427. grid.yspace = .27 + .25;
  1428. grid.x0 = [-grid.width-grid.xspace/2, +grid.xspace/2, ...
  1429. -grid.width/2*3-grid.xspace, -grid.width/2, +grid.width/2+grid.xspace,...
  1430. -grid.width-grid.xspace/2, +grid.xspace/2];
  1431. grid.y0 = [ [1 1] * (+grid.height/2+grid.yspace), ...
  1432. [1 1 1] * (-grid.height/2), ...
  1433. [1 1] * (-grid.height/2*3-grid.yspace)];
  1434. showazimuthrange = [-180 -135];
  1435. showazimuthfraction = abs(diff(showazimuthrange))/360;
  1436. hd.pattern.fg = figure();
  1437. hd.pattern.ax = axes();
  1438. hold on
  1439. dy = grid.height;
  1440. for gridindex = 1:7
  1441. numbar(gridindex) = ceil(grid.width*360*spatialfreq(gridindex)*showazimuthfraction);
  1442. dx = grid.width / (360*spatialfreq(gridindex)*showazimuthfraction) / 2;
  1443. for barindex = 1:numbar(gridindex)
  1444. x0 = grid.x0(gridindex) + (barindex-1) * dx * 2;
  1445. xmax = grid.x0(gridindex) + grid.width;
  1446. y0 = grid.y0(gridindex);
  1447. darkbar(gridindex,barindex) = patch([x0*[1 1],min(x0+dx,xmax)*[1 1]], ...
  1448. [y0,y0+dy,y0+dy,y0], grid.colour);
  1449. end
  1450. outline(gridindex) = plot(grid.x0(gridindex)*[1 1 1 1 1] + grid.width*[0 0 1 1 0], ...
  1451. grid.y0(gridindex)*[1 1 1 1 1] + grid.height*[0 1 1 0 0], ...
  1452. 'Color', grid.colour);
  1453. textlabel(gridindex) = text(grid.x0(gridindex)+grid.width/2, ...
  1454. grid.y0(gridindex)+grid.height/30, ...
  1455. [num2str(spatialfreq(gridindex),'%.2f'),' cpd']);
  1456. textlabel(gridindex).HorizontalAlignment = 'center';
  1457. textlabel(gridindex).VerticalAlignment = 'top';
  1458. textlabel(gridindex).FontSize = 12;
  1459. end
  1460. hold off
  1461. hd.pattern.ax.PlotBoxAspectRatio = [grid.width, grid.height, grid.height];
  1462. [hd.pattern.ax.XAxis.Visible, hd.pattern.ax.YAxis.Visible] = deal('off');
  1463. hd.pattern.ax.XLim = [-1 1] * 2.2;
  1464. % hd.pattern.ax.XLim = [0 .25] * 2.2;
  1465. hd.pattern.ax.YLim = [-1 1] * 1.8;
  1466. end