OSCR

Stabilizing stiffness is the most limiting factor in human force exertion.

Code ↔ Paper

1 match between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 1 match
  1. [1] § Results › The effect of pushing condition and arm configuration on maximum force ↔ main.m, lines 777–842 · score 0.53 · pairwise comparisons, post hoc, Bonferroni, FEX, ULCK, FLAT

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

MATLAB · 1,062 lines · 41 KB · MIT · 1 match

  1. %% ========================================================================
  2. % Code generated by Federico Tessari, PhD
  3. % Mechanical Engineering Department, MIT
  4. % for any question reach out to [email hidden]
  5. % Latest update May 7, 2026
  6. % ========================================================================
  7. % Press "Run" to execute the whole script and obtain all the figures and
  8. % results of our work.
  9. clear; clc; close all
  10. %% ---------- Figure/graphics defaults ----------
  11. set(0, 'DefaultLineLineWidth', 1);
  12. set(groot,'defaultAxesFontSize',10);
  13. set(0,'defaultfigurecolor',[1 1 1]); % white figure background
  14. set(groot, 'defaultAxesTickLabelInterpreter','latex');
  15. set(groot, 'defaultLegendInterpreter','latex');
  16. set(groot,'defaultTextInterpreter','latex');
  17. % Extra prints path if present (harmless otherwise)
  18. addpath(genpath('Data'));
  19. % % Source Data workbook (Nature Communications) - Commented Out as
  20. % Source_Data.xlsx is available in GitHub
  21. % srcFile = 'Source_Data.xlsx';
  22. % if exist(srcFile,'file'), delete(srcFile); end
  23. %% ---------- Labels & constants ----------
  24. condition = {'CN','UCN'}; % Arm configurations: 1=CN (Undesired), 2=UCN (Preferred)
  25. task = {'FLAT','LCK','FEX','FEXURD','ULCK'};
  26. Nsubj = 9; % Number of subjects
  27. % Scaling Factors - The collected force and moments frome the ATI force
  28. % sensor are in 'lbf' and 'lbf*inch', respectively.
  29. sN = 4.44822; %[N/lbf]
  30. sNm = 0.112985; %[Nm/lbf*inch]
  31. %% ---------- Sampling & trimming ----------
  32. fs = 50; % [Hz] (sampling frequency)
  33. N = 150; % samples to save per trial
  34. t_tot = (1/fs)*N; % total time per trial (not used later, retained for clarity)
  35. % Cropping indices: keep the segment [end-idx_min : end-idx_max]
  36. idx_min = 199;
  37. idx_max = 50;
  38. %% ========================================================================
  39. % DATA LOADING
  40. % ========================================================================
  41. % Containers
  42. MVF_i_data = cell(Nsubj,1);
  43. MVF_f_data = cell(Nsubj,1);
  44. MVF_tot_i = nan(Nsubj,3); % [subj, mean|F|, std|F|]
  45. MVF_tot_f = nan(Nsubj,3);
  46. dataset = cell(Nsubj,2,5,5); % {subj,cond,task,trial}
  47. f_tot_med = cell(Nsubj,2,5,5); % per-trial median |F|
  48. for subj = 1:Nsubj
  49. % ---- MVF files (Pre/Post) ----
  50. MVF_i = importdata(sprintf('S%d_MVF_i.txt', subj));
  51. MVF_f = importdata(sprintf('S%d_MVF_f.txt', subj));
  52. MVF_i_data{subj}.data = [MVF_i.data(:,1:3)*sN, MVF_i.data(:,4:6)*sNm];
  53. MVF_f_data{subj}.data = [MVF_f.data(:,1:3)*sN, MVF_f.data(:,4:6)*sNm];
  54. % MVF_i_data{subj}.data = MVF_i.data;
  55. % MVF_f_data{subj}.data = MVF_f.data;
  56. % Safe cropping (same indices, clamped to data length)
  57. MVF_i_data{subj}.data_trim = MVF_i_data{subj}.data( ...
  58. max(1,end-idx_min) : max(1,end-idx_max), :);
  59. MVF_f_data{subj}.data_trim = MVF_f_data{subj}.data( ...
  60. max(1,end-idx_min) : max(1,end-idx_max), :);
  61. % Subject id
  62. MVF_tot_i(subj,1) = subj;
  63. MVF_tot_f(subj,1) = subj;
  64. % mean/std of |F|
  65. MVF_tot_i(subj,2) = mean(sqrt(sum(MVF_i_data{subj}.data_trim(:,1:3).^2,2)));
  66. MVF_tot_i(subj,3) = std( sqrt(sum(MVF_i_data{subj}.data_trim(:,1:3).^2,2)));
  67. MVF_tot_f(subj,2) = mean(sqrt(sum(MVF_f_data{subj}.data_trim(:,1:3).^2,2)));
  68. MVF_tot_f(subj,3) = std( sqrt(sum(MVF_f_data{subj}.data_trim(:,1:3).^2,2)));
  69. % ---- Trials ----
  70. for cond = 1:2
  71. for t = 1:5
  72. for trial = 1:5
  73. data_name = sprintf('S%d_%s_%s_T%d.txt', subj, condition{cond}, task{t}, trial);
  74. data_file = importdata(data_name);
  75. dataset{subj,cond,t,trial}.data = [data_file.data(:,1:3)*sN, data_file.data(:,4:6)*sNm];
  76. % dataset{subj,cond,t,trial}.data = data_file.data;
  77. dataset{subj,cond,t,trial}.name = data_name;
  78. % 3 s window from trial end: [end-idx_min : end-idx_max]
  79. data_trim = dataset{subj,cond,t,trial}.data( ...
  80. max(1,end-idx_min) : max(1,end-idx_max), :);
  81. dataset{subj,cond,t,trial}.data_trim = data_trim;
  82. % Total force magnitude |F|
  83. fmag = sqrt(sum(data_trim(:,1:3).^2,2));
  84. f_tot_med{subj,cond,t,trial} = median(fmag);
  85. end
  86. end
  87. end
  88. end
  89. %% ========================================================================
  90. % Median Difference between Arm Configurations (per trial)
  91. % ========================================================================
  92. medianCondDiff = []; % CN - UCN
  93. medianCondDiff_per = []; % percent relative to UCN
  94. for subj = 1:Nsubj
  95. for t = 1:5
  96. for trial = 1:5
  97. if (subj ~= 9 && t ~= 2)
  98. mCN = f_tot_med{subj,1,t,trial};
  99. mUCN = f_tot_med{subj,2,t,trial};
  100. medianCondDiff(end+1) = mCN - mUCN;
  101. medianCondDiff_per(end+1) = 100*(mCN - mUCN)/mUCN;
  102. end
  103. end
  104. end
  105. end
  106. %% ========================================================================
  107. % Pooled trials per subject/cond/task (concatenate all 5 trials)
  108. % ========================================================================
  109. f_tot_tr_pl = cell(Nsubj,2,5); % (subj, cond, task)
  110. for subj = 1:Nsubj
  111. for cond = 1:2
  112. for t = 1:5
  113. pooled_trials = [];
  114. for trial = 1:5
  115. fmag = sqrt(sum(dataset{subj,cond,t,trial}.data_trim(:,1:3).^2,2));
  116. pooled_trials = [pooled_trials; fmag];
  117. end
  118. f_tot_tr_pl{subj,cond,t} = pooled_trials;
  119. end
  120. end
  121. end
  122. % Assumes: f_tot_tr_pl is Nsubj x 2 x 5 cell array
  123. % Tasks: 1=FLAT, 2=LCK, 3=FEX, 4=URD, 5=ULCK
  124. numTasks = 5;
  125. numConds = 2;
  126. rows = Nsubj * numConds;
  127. subj_col = zeros(rows,1);
  128. cond_col = strings(rows,1);
  129. medians = nan(rows, numTasks);
  130. row = 0;
  131. for subj = 1:Nsubj
  132. for cond = 1:numConds
  133. row = row + 1;
  134. subj_col(row) = subj;
  135. cond_col(row) = ternary(cond==1, "undesired", "preferred");
  136. for t = 1:numTasks
  137. % Exclude specific missing data (subject 9, cond=1, task=2)
  138. if subj == 9 && cond == 1 && t == 2
  139. medians(row,t) = NaN; % mark as not available
  140. continue;
  141. end
  142. v = f_tot_tr_pl{subj,cond,t};
  143. if ~isempty(v)
  144. medians(row,t) = median(v, 'omitnan');
  145. else
  146. medians(row,t) = NaN;
  147. end
  148. end
  149. end
  150. end
  151. % Row-wise range (max - min across available tasks)
  152. rowMax = max(medians, [], 2, 'omitnan');
  153. rowMin = min(medians, [], 2, 'omitnan');
  154. rangeCol = rowMax - rowMin;
  155. % Percentage difference = range / max * 100
  156. pctCol = nan(rows,1);
  157. nonzero = rowMax > 0 & ~isnan(rowMax);
  158. pctCol(nonzero) = (rangeCol(nonzero) ./ rowMax(nonzero)) * 100;
  159. % Build the table
  160. Tmed = table( ...
  161. subj_col, ...
  162. cond_col, ...
  163. medians(:,1), medians(:,2), medians(:,3), medians(:,4), medians(:,5), ...
  164. rangeCol, pctCol, ...
  165. 'VariableNames', {'Subject','ArmCond','FLAT','LCK','FEX','URD','ULCK','Range','PctDiff'});
  166. disp(Tmed);
  167. % ------------------------------------------------------------------------
  168. % Helper inline function for ternary logic
  169. function out = ternary(cond, valTrue, valFalse)
  170. if cond
  171. out = valTrue;
  172. else
  173. out = valFalse;
  174. end
  175. end
  176. clearvars fmag data_trim data_file data_name
  177. %% ========================================================================
  178. % Prepare data for boxplot/ANOVA (Pre vs Post MVF)
  179. % ========================================================================
  180. all_data = []; % concatenated magnitudes
  181. group = []; % subject indices
  182. condIPostLabels = {}; % 'Pre' or 'Post'
  183. median_pre = NaN(Nsubj,1);
  184. median_post = NaN(Nsubj,1);
  185. for subj = 1:Nsubj
  186. norm_i = sqrt(sum(MVF_i_data{subj}.data_trim(:,1:3).^2,2));
  187. norm_f = sqrt(sum(MVF_f_data{subj}.data_trim(:,1:3).^2,2));
  188. median_pre(subj) = median(norm_i,'omitnan');
  189. median_post(subj) = median(norm_f,'omitnan');
  190. all_data = [all_data; norm_i; norm_f]; %#ok<AGROW>
  191. nThis = numel(norm_i) + numel(norm_f);
  192. group = [group; repmat(subj, nThis, 1)]; %#ok<AGROW>
  193. condIPostLabels = [condIPostLabels; ...
  194. [repmat({'Pre'}, numel(norm_i), 1); ...
  195. repmat({'Post'}, numel(norm_f), 1)]]; %#ok<AGROW>
  196. end
  197. %% ========================================================================
  198. % Figure 2 (and Supplementary Figures 13..21): SINGLE SUBJECT ANALYSIS
  199. % ========================================================================
  200. % Task colors & legend
  201. task_color = zeros(5,3);
  202. task_color(1,:) = [0 0.4470 0.7410]; % FLAT
  203. task_color(2,:) = [0.3010 0.7450 0.9330]; % LCK
  204. task_color(3,:) = [0.4660 0.6740 0.1880]; % FEX
  205. task_color(4,:) = [0.9290 0.6940 0.1250]; % FEXURD
  206. task_color(5,:) = [0.8500 0.3250 0.0980]; % ULCK
  207. task_legend = {'FLAT','LCK','FEX','URD','ULCK'}; % 'URD' label for FEXURD
  208. for subj = 1:Nsubj
  209. figure('Color','white', 'Units','inches', 'Position',[1 1 7 5],'Renderer', 'painters')
  210. tt = tiledlayout(3,4, 'TileSpacing', 'compact', 'Padding', 'compact');
  211. title(tt, sprintf('Subject: %d', subj), 'FontName', 'Times New Roman', 'FontSize', 14);
  212. hLegend = gobjects(1, numel(task_legend));
  213. for cond = 1:2
  214. % --- Total Force panel (left col for CN, right col for UCN) ---
  215. if cond == 1
  216. axCol1 = nexttile(1,[3 1]);
  217. else
  218. axCol3 = nexttile(3,[3 1]);
  219. end
  220. hold on
  221. for t = 1:5
  222. for trial = 1:5
  223. f_tot = sqrt(sum(dataset{subj,cond,t,trial}.data(:,1:3).^2,2));
  224. h = plot(linspace(0, 7, length(f_tot)), f_tot, ...
  225. 'LineWidth', 1.5, 'Color', task_color(t,:));
  226. if trial == 1 && cond == 1
  227. hLegend(t) = h;
  228. end
  229. end
  230. end
  231. ylim([0 45*sN]);
  232. yl = ylim;
  233. % Shade [3,6] s window
  234. fill([3 6 6 3], [yl(1) yl(1) yl(2) yl(2)], [0.5 0.5 0.5], ...
  235. 'FaceAlpha',0.2, 'EdgeColor','none');
  236. plot([3 3],[yl(1) yl(2)],'k--');
  237. plot([6 6],[yl(1) yl(2)],'k--');
  238. xlabel('Time [s]'); ylabel('$F$ [N]'); box off
  239. % --- Fz ---
  240. if cond == 1
  241. axCol2Top = nexttile(2);
  242. else
  243. axCol4Top = nexttile(4);
  244. end
  245. hold on
  246. for t = 1:5
  247. for trial = 1:5
  248. fz = -dataset{subj,cond,t,trial}.data(:,3);
  249. plot(linspace(0,7,length(fz)), fz, 'LineWidth', 1.5, 'Color', task_color(t,:));
  250. end
  251. end
  252. xlabel('Time [s]'); ylabel('$F_z$ [N]'); ylim([0*sN 45*sN]); box off
  253. % --- Fx ---
  254. if cond == 1
  255. nexttile(6);
  256. else
  257. nexttile(8);
  258. end
  259. hold on
  260. for t = 1:5
  261. for trial = 1:5
  262. fx = dataset{subj,cond,t,trial}.data(:,1);
  263. plot(linspace(0,7,length(fx)), fx, 'LineWidth', 1.5, 'Color', task_color(t,:));
  264. end
  265. end
  266. xlabel('Time [s]'); ylabel('$F_x$ [N]'); ylim([-10*sN 10*sN]); box off
  267. % --- Fy ---
  268. if cond == 1
  269. nexttile(10);
  270. else
  271. nexttile(12);
  272. end
  273. hold on
  274. for t = 1:5
  275. for trial = 1:5
  276. fy = dataset{subj,cond,t,trial}.data(:,2);
  277. plot(linspace(0,7,length(fy)), fy, 'LineWidth', 1.5, 'Color', task_color(t,:));
  278. end
  279. end
  280. xlabel('Time [s]'); ylabel('$F_y$ [N]'); ylim([-10*sN 10*sN]); box off
  281. end
  282. % Legend under layout
  283. lgd = legend(hLegend, task_legend, 'Box','off', 'Orientation','horizontal', 'FontName','Times New Roman');
  284. lgd.Layout.Tile = 'south'; lgd.NumColumns = numel(task_legend);
  285. drawnow;
  286. % Column headers "(A) Undesired" / "(B) Preferred"
  287. p1 = axCol1.Position; p2 = axCol2Top.Position;
  288. p3 = axCol3.Position; p4 = axCol4Top.Position;
  289. xUndes = (p1(1)+p1(3)+p2(1))/2;
  290. xPref = (p3(1)+p3(3)+p4(1))/2;
  291. yTop = min(max([p1(2)+p1(4), p2(2)+p2(4), p3(2)+p3(4), p4(2)+p4(4)]), 0.98);
  292. annotation('textbox', [xUndes-0.095, yTop, 0.25, 0.03], 'String', '(A) Undesired', ...
  293. 'HorizontalAlignment','center','VerticalAlignment','bottom', ...
  294. 'EdgeColor','none','FontName','Times New Roman','FontSize',12);
  295. annotation('textbox', [xPref-0.095, yTop, 0.25, 0.03], 'String', '(B) Preferred', ...
  296. 'HorizontalAlignment','center','VerticalAlignment','bottom', ...
  297. 'EdgeColor','none','FontName','Times New Roman','FontSize',12);
  298. % Save as true vectorial graphics with painters renderer
  299. % savefig(gcf, sprintf('ForceTrend_Subject%d.fig', subj));
  300. % exportgraphics(gcf, sprintf('ForceTrend_Subject%d.pdf', subj), 'ContentType','vector');
  301. % print(gcf, sprintf('ForceTrend_Subject%d.svg', subj), '-dsvg', '-painters');
  302. end
  303. %% ========================================================================
  304. % Figure 3 – Boxplots: 3x3 subjects, each with 1x2 (Undesired | Preferred)
  305. % ========================================================================
  306. figure('Color','white', 'Units','inches', 'Position',[0.05 0.05 7 7],'Renderer', 'painters')
  307. tOuter = tiledlayout(3,3, 'TileSpacing','loose', 'Padding','compact');
  308. % (Optional) store medians per subject x cond x task
  309. medians = nan(9, 2, 5);
  310. for subj = 1:9
  311. tSub = tiledlayout(tOuter, 1, 2, 'TileSpacing','compact', 'Padding','compact');
  312. tSub.Layout.Tile = subj;
  313. title(tSub, sprintf('Subject: %d', subj), 'FontName','Times New Roman');
  314. for cond = 1:2
  315. ax = nexttile(tSub, cond);
  316. hold(ax,'on')
  317. % Pooled |F| across trials for each task
  318. allData = cell(1,5);
  319. for t = 1:5
  320. pooledData = [];
  321. for trial = 1:5
  322. f_tot = sqrt(sum(dataset{subj,cond,t,trial}.data_trim(:,1:3).^2,2));
  323. pooledData = [pooledData; f_tot];
  324. end
  325. allData{t} = pooledData;
  326. medians(subj,cond,t) = median(pooledData,'omitnan');
  327. end
  328. boxplot(ax, cell2mat(allData), ...
  329. repelem(1:5, cellfun(@numel, allData)), ...
  330. 'Colors', task_color, 'Symbol','.');
  331. grid(ax,'on'); box(ax,'off');
  332. ax.FontName = 'Times New Roman'; ax.FontSize = 10;
  333. xticks(ax, 1:5); xticklabels(ax, task_legend);
  334. ylim(ax, [0 45*sN]);
  335. if cond == 1
  336. title(ax, '(A) Undesired', 'FontName','Times New Roman');
  337. else
  338. title(ax, '(B) Preferred', 'FontName','Times New Roman');
  339. end
  340. end
  341. [resultsTable{subj}, anovaTable{subj}, descriptivesTable{subj}] = perform_anova_analysis(dataset, subj, 1);
  342. end
  343. xlabel(tOuter, 'Task Conditions', 'FontName','Times New Roman');
  344. ylabel(tOuter, '$F$ [N]', 'Interpreter','latex', 'FontName','Times New Roman');
  345. %% ========================================================================
  346. % Figure 4 – Histogram of Median Differences (CN − UCN)
  347. % ========================================================================
  348. figure('Color','white','Units','inches','Position',[1 1 5 3.5]);
  349. tiledlayout(2,1)
  350. nexttile()
  351. histogram(medianCondDiff,'EdgeColor','none','BinWidth',1*sN); hold on
  352. plot(median(medianCondDiff,'omitnan')*ones(1,10), linspace(0,40,10),'r--','LineWidth',2);
  353. xlabel('$\Delta F_{med}$ [N]','FontSize',11);
  354. ylabel('Frequency','FontSize',11);
  355. box off
  356. nexttile()
  357. histogram(medianCondDiff_per,'EdgeColor','none','BinWidth',4.5); hold on
  358. plot(median(medianCondDiff_per,'omitnan')*ones(1,10), linspace(0,35,10),'r--','LineWidth',2);
  359. xlabel('$\Delta e_{med}$ [\%]','FontSize',11);
  360. ylabel('Frequency','FontSize',11);
  361. box off
  362. %% ========================================================================
  363. % Figure 5 – Pre vs Post (MVF) boxplots aligned by subject
  364. % ========================================================================
  365. figure('Color','white','Units','inches','Position',[1 1 7 3.5],'Renderer', 'painters');
  366. % Ensure order Pre -> Post (avoid name collision with `condition` above)
  367. condIPostCat = categorical(condIPostLabels, {'Pre','Post'});
  368. % Positions: for each subject, place Pre at s-0.18, Post at s+0.18
  369. nSubj = numel(unique(group));
  370. pos = [ (1:nSubj)-0.18 ; (1:nSubj)+0.18 ];
  371. pos = pos(:)';
  372. blankLabels = repmat({''}, 1, numel(pos));
  373. boxplot(all_data, {group, condIPostCat}, ...
  374. 'positions', pos, ...
  375. 'labels', blankLabels, ... % suppress boxplot's own labels
  376. 'colors', ['k','b'], ... % black=Pre, blue=Post
  377. 'symbol', 'o', ...
  378. 'plotstyle','traditional');
  379. ax = gca;
  380. ax.FontSize = 12;
  381. ax.FontName = 'Times New Roman';
  382. ax.XTick = 1:nSubj;
  383. ax.XTickLabel = string(1:nSubj);
  384. xlabel('Subject','FontName','Times New Roman');
  385. ylabel('F [N]','FontName','Times New Roman');
  386. box off
  387. % Single legend (markers only)
  388. hold on
  389. h1 = plot(NaN,NaN,'sk','MarkerFaceColor','k');
  390. h2 = plot(NaN,NaN,'sb','MarkerFaceColor','b');
  391. legend([h1 h2], {'Pre','Post'}, 'Location','northoutside', ...
  392. 'Orientation','horizontal', 'FontName','Times New Roman', 'Box','off');
  393. %% ========================================================================
  394. % Figure 5 stats: 2-way ANOVA (Subject x Time) + post-hoc Pre vs Post
  395. % ========================================================================
  396. timeCat = categorical(condIPostLabels, {'Pre','Post'});
  397. timeNum = double(timeCat); % 1=Pre, 2=Post
  398. subjNum = group(:);
  399. yPP = all_data(:);
  400. % ---- 2-way ANOVA with interaction ----
  401. [pPP, tblPP, ~] = anovan(yPP, {subjNum, timeNum}, ...
  402. 'model','interaction', ...
  403. 'varnames', {'Subject','Time'}, ...
  404. 'display','off');
  405. % Parse ANOVA table
  406. hdr = string(tblPP(1,:));
  407. rows = strtrim(string(tblPP(2:end,1)));
  408. colSS = find(contains(hdr,'Sum Sq','IgnoreCase',true),1);
  409. colDF = find(contains(hdr,'d.f','IgnoreCase',true) | strcmpi(strtrim(hdr),'DF'),1);
  410. colMS = find(contains(hdr,'Mean Sq','IgnoreCase',true),1);
  411. colF = find(strcmpi(strtrim(hdr),'F'),1);
  412. colP = find(contains(hdr,'Prob>F','IgnoreCase',true),1);
  413. idxSubj = find(strcmpi(rows,'Subject'),1);
  414. idxTime = find(strcmpi(rows,'Time'),1);
  415. idxInt = find(contains(lower(rows),'subject') & contains(lower(rows),'time') & ...
  416. ~strcmpi(rows,'Subject') & ~strcmpi(rows,'Time'), 1);
  417. idxErr = find(strcmpi(rows,'Error'),1);
  418. factors = ["Subject"; "Time"; "Subject*Time"];
  419. idxRows = [idxSubj; idxTime; idxInt];
  420. SS = nan(3,1); DF = nan(3,1); MS = nan(3,1); Fv = nan(3,1); Pv = nan(3,1); etaP = nan(3,1);
  421. SSerr = NaN;
  422. if ~isempty(idxErr) && ~isempty(colSS)
  423. SSerr = local_cell2num(tblPP{idxErr+1,colSS});
  424. end
  425. for k = 1:3
  426. if isempty(idxRows(k)), continue; end
  427. rr = idxRows(k)+1;
  428. if ~isempty(colSS), SS(k) = local_cell2num(tblPP{rr,colSS}); end
  429. if ~isempty(colDF), DF(k) = local_cell2num(tblPP{rr,colDF}); end
  430. if ~isempty(colMS), MS(k) = local_cell2num(tblPP{rr,colMS}); end
  431. if ~isempty(colF), Fv(k) = local_cell2num(tblPP{rr,colF}); end
  432. if ~isempty(colP), Pv(k) = local_cell2num(tblPP{rr,colP}); else, Pv(k) = pPP(k); end
  433. if ~isnan(SS(k)) && ~isnan(SSerr) && (SS(k)+SSerr)>0
  434. etaP(k) = SS(k)/(SS(k)+SSerr); % partial eta^2
  435. end
  436. end
  437. ANOVA_PrePost = table(factors, SS, DF, MS, Fv, Pv, etaP, ...
  438. 'VariableNames', {'Factor','SS','DF','MS','F','P_Value','PartialEtaSq'});
  439. disp('=== Figure 5 ANOVA (Subject x Time) ===');
  440. disp(ANOVA_PrePost);
  441. % ---- Post-hoc: Pre vs Post within each subject (Bonferroni corrected) ----
  442. alpha = 0.05;
  443. m = Nsubj; % 9 comparisons
  444. Subject = (1:Nsubj).';
  445. N1_Pre = nan(Nsubj,1); N2_Post = nan(Nsubj,1);
  446. Mean1_Pre = nan(Nsubj,1); Mean2_Post = nan(Nsubj,1);
  447. MeanDiff_PreMinusPost = nan(Nsubj,1);
  448. T_Statistic = nan(Nsubj,1); DF_t = nan(Nsubj,1);
  449. CI95Bonf_Lower = nan(Nsubj,1); CI95Bonf_Upper = nan(Nsubj,1);
  450. P_Raw = nan(Nsubj,1); P_Bonferroni = nan(Nsubj,1);
  451. Significant = false(Nsubj,1); Cohens_d = nan(Nsubj,1);
  452. for subj = 1:Nsubj
  453. xPre = sqrt(sum(MVF_i_data{subj}.data_trim(:,1:3).^2,2));
  454. xPost = sqrt(sum(MVF_f_data{subj}.data_trim(:,1:3).^2,2));
  455. N1_Pre(subj) = numel(xPre);
  456. N2_Post(subj) = numel(xPost);
  457. Mean1_Pre(subj) = mean(xPre,'omitnan');
  458. Mean2_Post(subj) = mean(xPost,'omitnan');
  459. MeanDiff_PreMinusPost(subj) = Mean1_Pre(subj) - Mean2_Post(subj);
  460. [~, p0, ~, st] = ttest2(xPre, xPost, 'Tail','both', 'Vartype','equal', 'Alpha',alpha);
  461. [~, ~, ciB] = ttest2(xPre, xPost, 'Tail','both', 'Vartype','equal', 'Alpha',alpha/m);
  462. P_Raw(subj) = p0;
  463. P_Bonferroni(subj) = min(p0*m, 1);
  464. Significant(subj) = P_Bonferroni(subj) < alpha;
  465. T_Statistic(subj) = st.tstat;
  466. DF_t(subj) = st.df;
  467. CI95Bonf_Lower(subj) = ciB(1);
  468. CI95Bonf_Upper(subj) = ciB(2);
  469. Cohens_d(subj) = local_cohens_d(xPre, xPost);
  470. end
  471. PostHoc_PrePost = table(Subject, N1_Pre, N2_Post, Mean1_Pre, Mean2_Post, MeanDiff_PreMinusPost, ...
  472. T_Statistic, DF_t, CI95Bonf_Lower, CI95Bonf_Upper, P_Raw, P_Bonferroni, Significant, Cohens_d, ...
  473. 'VariableNames', {'Subject','N1_Pre','N2_Post','Mean1_Pre','Mean2_Post','MeanDiff_PreMinusPost', ...
  474. 'T_Statistic','DF','CI95Bonf_Lower','CI95Bonf_Upper','P_Raw','P_Bonferroni','Significant','Cohens_d'});
  475. disp('=== Figure 5 Post-hoc (Pre vs Post within subject) ===');
  476. disp(PostHoc_PrePost);
  477. % Optional export
  478. % writetable(ANOVA_PrePost, 'Figure5_PrePost_Stats.xlsx', 'Sheet', 'ANOVA');
  479. % writetable(PostHoc_PrePost, 'Figure5_PrePost_Stats.xlsx', 'Sheet', 'PostHoc');
  480. %% ========================================================================
  481. % Figure 22 – Boxplots: 3x3 subjects, each with 1x2 (Undesired | Preferred)
  482. % using only Fz
  483. % ========================================================================
  484. figure('Color','white', 'Units','inches', 'Position',[0.05 0.05 7 7],'Renderer', 'painters')
  485. tOuter = tiledlayout(3,3, 'TileSpacing','loose', 'Padding','compact');
  486. % (Optional) store medians per subject x cond x task
  487. medians = nan(9, 2, 5);
  488. for subj = 1:9
  489. tSub = tiledlayout(tOuter, 1, 2, 'TileSpacing','compact', 'Padding','compact');
  490. tSub.Layout.Tile = subj;
  491. title(tSub, sprintf('Subject: %d', subj), 'FontName','Times New Roman');
  492. for cond = 1:2
  493. ax = nexttile(tSub, cond);
  494. hold(ax,'on')
  495. % Pooled |F| across trials for each task
  496. allData = cell(1,5);
  497. for t = 1:5
  498. pooledData = [];
  499. for trial = 1:5
  500. f_tot = -dataset{subj,cond,t,trial}.data_trim(:,3);
  501. pooledData = [pooledData; f_tot];
  502. end
  503. allData{t} = pooledData;
  504. medians(subj,cond,t) = median(pooledData,'omitnan');
  505. end
  506. boxplot(ax, cell2mat(allData), ...
  507. repelem(1:5, cellfun(@numel, allData)), ...
  508. 'Colors', task_color, 'Symbol','.');
  509. grid(ax,'on'); box(ax,'off');
  510. ax.FontName = 'Times New Roman'; ax.FontSize = 10;
  511. xticks(ax, 1:5); xticklabels(ax, task_legend);
  512. ylim(ax, [0 45*sN]);
  513. if cond == 1
  514. title(ax, '(A) Undesired', 'FontName','Times New Roman');
  515. else
  516. title(ax, '(B) Preferred', 'FontName','Times New Roman');
  517. end
  518. end
  519. resultsTable_Fz{subj} = perform_anova_analysis(dataset, subj, 2); %This table contains the results of the statistical analysis for each subject
  520. end
  521. xlabel(tOuter, 'Task Conditions', 'FontName','Times New Roman');
  522. ylabel(tOuter, '$F$ [N]', 'Interpreter','latex', 'FontName','Times New Roman');
  523. %% ========================================================================
  524. % Figure EXTRA – Boxplots: 3x3 subjects, each with 1x2 (Undesired |
  525. % Preferred) using only the means of each time window rather all the
  526. % samples
  527. % ========================================================================
  528. figure('Color','white', 'Units','inches', 'Position',[0.05 0.05 7 7],'Renderer', 'painters')
  529. tOuter = tiledlayout(3,3, 'TileSpacing','loose', 'Padding','compact');
  530. % (Optional) store medians per subject x cond x task
  531. medians = nan(9, 2, 5);
  532. for subj = 1:9
  533. tSub = tiledlayout(tOuter, 1, 2, 'TileSpacing','compact', 'Padding','compact');
  534. tSub.Layout.Tile = subj;
  535. title(tSub, sprintf('Subject: %d', subj), 'FontName','Times New Roman');
  536. for cond = 1:2
  537. ax = nexttile(tSub, cond);
  538. hold(ax,'on')
  539. % Pooled |F| across trials for each task
  540. allData = cell(1,5);
  541. for t = 1:5
  542. pooledData = [];
  543. for trial = 1:5
  544. f_tot = mean(sqrt(sum(dataset{subj,cond,t,trial}.data_trim(:,1:3).^2,2)));
  545. pooledData = [pooledData; f_tot];
  546. end
  547. allData{t} = pooledData;
  548. medians(subj,cond,t) = median(pooledData,'omitnan');
  549. end
  550. boxplot(ax, cell2mat(allData), ...
  551. repelem(1:5, cellfun(@numel, allData)), ...
  552. 'Colors', task_color, 'Symbol','.');
  553. grid(ax,'on'); box(ax,'off');
  554. ax.FontName = 'Times New Roman'; ax.FontSize = 10;
  555. xticks(ax, 1:5); xticklabels(ax, task_legend);
  556. ylim(ax, [0 45*sN]);
  557. if cond == 1
  558. title(ax, '(A) Undesired', 'FontName','Times New Roman');
  559. else
  560. title(ax, '(B) Preferred', 'FontName','Times New Roman');
  561. end
  562. end
  563. resultsTable_med{subj} = perform_anova_analysis(dataset, subj, 3); %This table contains the results of the statistical analysis for each subject
  564. end
  565. xlabel(tOuter, 'Task Conditions', 'FontName','Times New Roman');
  566. ylabel(tOuter, '$F$ [N]', 'Interpreter','latex', 'FontName','Times New Roman');
  567. %% ========================================================================
  568. % SOURCE DATA EXPORT (Nature Communications) - Commented Out As
  569. % Source_Data.xlsx is available on GitHub
  570. % ========================================================================
  571. % task_lbl = {'FLAT','LCK','FEX','URD','ULCK'};
  572. %
  573. % % -------- Figure 2 (ONLY subject 6) --------
  574. % SD2 = table;
  575. % subj = 6;
  576. % for cond = 1:2
  577. % for t = 1:5
  578. % for trial = 1:5
  579. % X = dataset{subj,cond,t,trial}.data; % full 7 s trace
  580. % n = size(X,1);
  581. % time_s = linspace(0,7,n).';
  582. % Fmag = sqrt(sum(X(:,1:3).^2,2));
  583. %
  584. % SD2 = [SD2; table( ...
  585. % repmat(subj,n,1), repmat({condition{cond}},n,1), repmat({task{t}},n,1), repmat(trial,n,1), ...
  586. % time_s, X(:,1), X(:,2), X(:,3), Fmag, ...
  587. % 'VariableNames', {'Subject','Condition','Task','Trial','Time_s','Fx_N','Fy_N','Fz_N','Fmag_N'})]; %#ok<AGROW>
  588. % end
  589. % end
  590. % end
  591. % writetable(SD2, srcFile, 'Sheet', 'Figure2');
  592. %
  593. % % -------- Supplementary Figures 13–21 (all subjects except 6) --------
  594. % SDsupp = table;
  595. % for subj = 1:Nsubj
  596. % if subj == 6, continue; end
  597. % suppFigNum = 12 + subj; % S1->13, ..., S9->21 (subject 6 would be 18)
  598. %
  599. % for cond = 1:2
  600. % for t = 1:5
  601. % for trial = 1:5
  602. % X = dataset{subj,cond,t,trial}.data;
  603. % n = size(X,1);
  604. % time_s = linspace(0,7,n).';
  605. % Fmag = sqrt(sum(X(:,1:3).^2,2));
  606. %
  607. % SDsupp = [SDsupp; table( ...
  608. % repmat(suppFigNum,n,1), repmat(subj,n,1), ...
  609. % repmat({condition{cond}},n,1), repmat({task{t}},n,1), repmat(trial,n,1), ...
  610. % time_s, X(:,1), X(:,2), X(:,3), Fmag, ...
  611. % 'VariableNames', {'SuppFigure','Subject','Condition','Task','Trial','Time_s','Fx_N','Fy_N','Fz_N','Fmag_N'})]; %#ok<AGROW>
  612. % end
  613. % end
  614. % end
  615. % end
  616. % writetable(SDsupp, srcFile, 'Sheet', 'SuppFig13_21');
  617. %
  618. % % -------- Figure 3 (boxplot source values: pooled |F|) --------
  619. % SD3 = table;
  620. % for subj = 1:Nsubj
  621. % for cond = 1:2
  622. % for t = 1:5
  623. % v = f_tot_tr_pl{subj,cond,t};
  624. % n = numel(v);
  625. % SD3 = [SD3; table( ...
  626. % repmat(subj,n,1), repmat({condition{cond}},n,1), repmat({task_lbl{t}},n,1), (1:n)', v(:), ...
  627. % 'VariableNames', {'Subject','Condition','Task','Sample','Fmag_N'})]; %#ok<AGROW>
  628. % end
  629. % end
  630. % end
  631. % writetable(SD3, srcFile, 'Sheet', 'Figure3');
  632. %
  633. % % -------- Figure 4 (histogram inputs) --------
  634. % SD4 = table(medianCondDiff(:), medianCondDiff_per(:), ...
  635. % 'VariableNames', {'DeltaFmed_CN_minus_UCN_N','DeltaMedianPercent_CN_minus_UCN'});
  636. % writetable(SD4, srcFile, 'Sheet', 'Figure4');
  637. %
  638. % % -------- Figure 5 (Pre/Post MVF boxplot inputs) --------
  639. % SD5 = table(group(:), condIPostLabels(:), all_data(:), ...
  640. % 'VariableNames', {'Subject','Session','Fmag_N'});
  641. % writetable(SD5, srcFile, 'Sheet', 'Figure5');
  642. %
  643. % % -------- Figure 22 (Fz boxplot source values) --------
  644. % SD22 = table;
  645. % for subj = 1:Nsubj
  646. % for cond = 1:2
  647. % for t = 1:5
  648. % pooledFz = [];
  649. % for trial = 1:5
  650. % pooledFz = [pooledFz; -dataset{subj,cond,t,trial}.data_trim(:,3)]; %#ok<AGROW>
  651. % end
  652. % n = numel(pooledFz);
  653. % SD22 = [SD22; table( ...
  654. % repmat(subj,n,1), repmat({condition{cond}},n,1), repmat({task_lbl{t}},n,1), (1:n)', pooledFz(:), ...
  655. % 'VariableNames', {'Subject','Condition','Task','Sample','Fz_N'})]; %#ok<AGROW>
  656. % end
  657. % end
  658. % end
  659. % writetable(SD22, srcFile, 'Sheet', 'Figure22');
  660. %
  661. % fprintf('\nSource data file created: %s\n', srcFile);
  662. %% ========================================================================
  663. % LOCAL FUNCTIONS – used by perform_anova_analysis
  664. % ========================================================================
  665. function [posthocTable, anovaTable, descriptivesTable] = perform_anova_analysis(dataset, subj, flag)
  666. % Subject-level 2-way ANOVA (Condition x Task) + Bonferroni post hoc.
  667. % Outputs:
  668. % posthocTable -> pairwise comparisons with t, df, CI, p, effect size
  669. % anovaTable -> ANOVA factors with SS, df, MS, F, p, partial eta^2
  670. % descriptivesTable -> per cell summary stats (n, mean, SD, SEM, median, IQR, min, max)
  671. task_labels = {'FLAT', 'LCK', 'FEX', 'FEXURD', 'ULCK'};
  672. cond_labels = {'CN', 'UCN'};
  673. alpha = 0.05;
  674. switch flag
  675. case 1, metricLabel = "Fmag";
  676. case 2, metricLabel = "Fz";
  677. case 3, metricLabel = "TrialMeanFmag";
  678. otherwise, metricLabel = "Unknown";
  679. end
  680. allData = [];
  681. groupCond = [];
  682. groupTask = [];
  683. data_map = containers.Map(); % key: 'CN_FLAT', etc.
  684. % -------- Build pooled data and descriptives --------
  685. desc_subj = [];
  686. desc_metric = strings(0,1);
  687. desc_cond = strings(0,1);
  688. desc_task = strings(0,1);
  689. desc_n = [];
  690. desc_mean = []; desc_sd = []; desc_sem = [];
  691. desc_median = []; desc_iqr = []; desc_min = []; desc_max = [];
  692. numCond = 2; numTask = 5; numTrials = 5;
  693. for cond = 1:numCond
  694. for t = 1:numTask
  695. pooledData = [];
  696. for trial = 1:numTrials
  697. D = dataset{subj,cond,t,trial};
  698. if isempty(D) || ~isfield(D,'data_trim') || isempty(D.data_trim)
  699. continue
  700. end
  701. if flag == 1
  702. f_tot = sqrt(sum(D.data_trim(:,1:3).^2,2));
  703. elseif flag == 2
  704. f_tot = -D.data_trim(:,3);
  705. elseif flag == 3
  706. f_tot = mean(sqrt(sum(D.data_trim(:,1:3).^2,2))); % one value per trial
  707. else
  708. f_tot = [];
  709. end
  710. pooledData = [pooledData; f_tot(:)]; %#ok<AGROW>
  711. end
  712. pooledData = pooledData(~isnan(pooledData));
  713. key = sprintf('%s_%s', cond_labels{cond}, task_labels{t});
  714. data_map(key) = pooledData;
  715. if ~isempty(pooledData)
  716. allData = [allData; pooledData]; %#ok<AGROW>
  717. groupCond = [groupCond; repmat(cond, numel(pooledData), 1)]; %#ok<AGROW>
  718. groupTask = [groupTask; repmat(t, numel(pooledData), 1)]; %#ok<AGROW>
  719. end
  720. % descriptives (always create row)
  721. n = numel(pooledData);
  722. if n > 0
  723. mu = mean(pooledData,'omitnan');
  724. sdv = std(pooledData,0,'omitnan');
  725. semv = sdv / sqrt(n);
  726. medv = median(pooledData,'omitnan');
  727. iqrv = iqr(pooledData);
  728. mnv = min(pooledData);
  729. mxv = max(pooledData);
  730. else
  731. mu = NaN; sdv = NaN; semv = NaN; medv = NaN; iqrv = NaN; mnv = NaN; mxv = NaN;
  732. end
  733. desc_subj(end+1,1) = subj; %#ok<AGROW>
  734. desc_metric(end+1,1) = metricLabel; %#ok<AGROW>
  735. desc_cond(end+1,1) = string(cond_labels{cond}); %#ok<AGROW>
  736. desc_task(end+1,1) = string(task_labels{t}); %#ok<AGROW>
  737. desc_n(end+1,1) = n; %#ok<AGROW>
  738. desc_mean(end+1,1) = mu; %#ok<AGROW>
  739. desc_sd(end+1,1) = sdv; %#ok<AGROW>
  740. desc_sem(end+1,1) = semv; %#ok<AGROW>
  741. desc_median(end+1,1) = medv; %#ok<AGROW>
  742. desc_iqr(end+1,1) = iqrv; %#ok<AGROW>
  743. desc_min(end+1,1) = mnv; %#ok<AGROW>
  744. desc_max(end+1,1) = mxv; %#ok<AGROW>
  745. end
  746. end
  747. descriptivesTable = table(desc_subj, desc_metric, desc_cond, desc_task, ...
  748. desc_n, desc_mean, desc_sd, desc_sem, desc_median, desc_iqr, desc_min, desc_max, ...
  749. 'VariableNames', {'Subject','Metric','Condition','Task','N','Mean','SD','SEM','Median','IQR','Min','Max'});
  750. % -------- ANOVA --------
  751. if isempty(allData) || numel(unique(groupCond)) < 2 || numel(unique(groupTask)) < 2
  752. anovaTable = table;
  753. else
  754. [p, tbl, stats] = anovan(allData, {groupCond, groupTask}, ...
  755. 'model','interaction', 'varnames', {'Condition','Task'}, 'display','off'); %#ok<ASGLU>
  756. headers = string(tbl(1,:));
  757. rowNames = string(tbl(2:end,1)); rowNames = strtrim(rowNames);
  758. colSS = find(contains(headers,'Sum Sq','IgnoreCase',true),1);
  759. colDF = find(contains(headers,'d.f','IgnoreCase',true) | strcmpi(strtrim(headers),'DF'),1);
  760. colMS = find(contains(headers,'Mean Sq','IgnoreCase',true),1);
  761. colF = find(strcmpi(strtrim(headers),'F'),1);
  762. if isempty(colF), colF = find(contains(headers,'F','IgnoreCase',true),1); end
  763. colP = find(contains(headers,'Prob>F','IgnoreCase',true),1);
  764. idxCond = find(strcmpi(rowNames,'Condition'),1);
  765. idxTask = find(strcmpi(rowNames,'Task'),1);
  766. idxInter = find(contains(lower(rowNames),'condition') & contains(lower(rowNames),'task') ...
  767. & ~strcmpi(rowNames,'Condition') & ~strcmpi(rowNames,'Task'),1);
  768. idxErr = find(strcmpi(rowNames,'Error'),1);
  769. SS_error = NaN;
  770. if ~isempty(idxErr) && ~isempty(colSS)
  771. SS_error = local_cell2num(tbl{idxErr+1,colSS});
  772. end
  773. factors = {'Condition'; 'Task'; 'Condition*Task'};
  774. idxRows = [idxCond; idxTask; idxInter];
  775. SS = nan(3,1); DF = nan(3,1); MS = nan(3,1); Fv = nan(3,1); Pv = nan(3,1); etaP = nan(3,1);
  776. for k = 1:3
  777. r = idxRows(k);
  778. if isempty(r), continue; end
  779. rr = r + 1; % tbl has header row
  780. if ~isempty(colSS), SS(k) = local_cell2num(tbl{rr,colSS}); end
  781. if ~isempty(colDF), DF(k) = local_cell2num(tbl{rr,colDF}); end
  782. if ~isempty(colMS), MS(k) = local_cell2num(tbl{rr,colMS}); end
  783. if ~isempty(colF), Fv(k) = local_cell2num(tbl{rr,colF}); end
  784. if ~isempty(colP), Pv(k) = local_cell2num(tbl{rr,colP}); else, Pv(k) = p(k); end
  785. if ~isnan(SS(k)) && ~isnan(SS_error) && (SS(k)+SS_error)>0
  786. etaP(k) = SS(k)/(SS(k)+SS_error); % partial eta^2
  787. end
  788. end
  789. anovaTable = table( ...
  790. repmat(subj,3,1), repmat(metricLabel,3,1), string(factors), ...
  791. SS, DF, MS, Fv, Pv, etaP, ...
  792. 'VariableNames', {'Subject','Metric','Factor','SS','DF','MS','F','P_Value','PartialEtaSq'});
  793. end
  794. % -------- Build list of requested post hoc comparisons --------
  795. % 1) same condition, different task
  796. C = []; % [cond1 task1 cond2 task2]
  797. cmpType = strings(0,1);
  798. for cond = 1:2
  799. for t1 = 1:4
  800. for t2 = t1+1:5
  801. C(end+1,:) = [cond t1 cond t2]; %#ok<AGROW>
  802. cmpType(end+1,1) = "WithinCondition"; %#ok<AGROW>
  803. end
  804. end
  805. end
  806. % 2) same task, different condition (CN vs UCN)
  807. for t = 1:5
  808. C(end+1,:) = [1 t 2 t]; %#ok<AGROW>
  809. cmpType(end+1,1) = "BetweenCondition"; %#ok<AGROW>
  810. end
  811. nCmp = size(C,1);
  812. % determine available comparisons for Bonferroni m
  813. available = false(nCmp,1);
  814. for i = 1:nCmp
  815. c1 = C(i,1); t1 = C(i,2); c2 = C(i,3); t2 = C(i,4);
  816. k1 = sprintf('%s_%s', cond_labels{c1}, task_labels{t1});
  817. k2 = sprintf('%s_%s', cond_labels{c2}, task_labels{t2});
  818. if isKey(data_map,k1) && isKey(data_map,k2)
  819. x = data_map(k1); y = data_map(k2);
  820. available(i) = numel(x)>=2 && numel(y)>=2;
  821. end
  822. end
  823. m = sum(available); % number of tests actually performed
  824. if m == 0, m = 1; end
  825. % -------- Post hoc table --------
  826. Group1 = strings(nCmp,1);
  827. Group2 = strings(nCmp,1);
  828. N1 = nan(nCmp,1); N2 = nan(nCmp,1);
  829. Mean1 = nan(nCmp,1); Mean2 = nan(nCmp,1);
  830. MeanDiff = nan(nCmp,1);
  831. T_stat = nan(nCmp,1); DF_t = nan(nCmp,1);
  832. CI95_L = nan(nCmp,1); CI95_U = nan(nCmp,1);
  833. CI95Bonf_L = nan(nCmp,1); CI95Bonf_U = nan(nCmp,1);
  834. P_raw = nan(nCmp,1); P_bonf = nan(nCmp,1);
  835. Significant = false(nCmp,1);
  836. Cohens_d = nan(nCmp,1);
  837. DataAvailable = false(nCmp,1);
  838. for i = 1:nCmp
  839. c1 = C(i,1); t1 = C(i,2); c2 = C(i,3); t2 = C(i,4);
  840. Group1(i) = sprintf('%s - %s', cond_labels{c1}, task_labels{t1});
  841. Group2(i) = sprintf('%s - %s', cond_labels{c2}, task_labels{t2});
  842. k1 = sprintf('%s_%s', cond_labels{c1}, task_labels{t1});
  843. k2 = sprintf('%s_%s', cond_labels{c2}, task_labels{t2});
  844. if ~(isKey(data_map,k1) && isKey(data_map,k2)), continue; end
  845. x = data_map(k1); y = data_map(k2);
  846. x = x(~isnan(x)); y = y(~isnan(y));
  847. N1(i) = numel(x); N2(i) = numel(y);
  848. Mean1(i) = mean(x,'omitnan');
  849. Mean2(i) = mean(y,'omitnan');
  850. MeanDiff(i) = Mean1(i) - Mean2(i);
  851. if numel(x) < 2 || numel(y) < 2
  852. continue
  853. end
  854. DataAvailable(i) = true;
  855. % two-sided t-test (equal variances, consistent with ANOVA-style pooling)
  856. [~, p0, ci0, st0] = ttest2(x, y, 'Tail','both', 'Vartype','equal', 'Alpha',alpha);
  857. [~, ~, ciB, ~] = ttest2(x, y, 'Tail','both', 'Vartype','equal', 'Alpha',alpha/m);
  858. P_raw(i) = p0;
  859. P_bonf(i) = min(p0*m, 1);
  860. Significant(i) = P_bonf(i) < alpha;
  861. T_stat(i) = st0.tstat;
  862. DF_t(i) = st0.df;
  863. CI95_L(i) = ci0(1); CI95_U(i) = ci0(2);
  864. CI95Bonf_L(i) = ciB(1); CI95Bonf_U(i) = ciB(2);
  865. Cohens_d(i) = local_cohens_d(x, y);
  866. end
  867. posthocTable = table( ...
  868. repmat(subj,nCmp,1), repmat(metricLabel,nCmp,1), cmpType, ...
  869. Group1, Group2, DataAvailable, ...
  870. N1, N2, Mean1, Mean2, MeanDiff, ...
  871. T_stat, DF_t, ...
  872. CI95_L, CI95_U, CI95Bonf_L, CI95Bonf_U, ...
  873. P_raw, P_bonf, Significant, Cohens_d, ...
  874. 'VariableNames', {'Subject','Metric','ComparisonType','Group1','Group2','DataAvailable', ...
  875. 'N1','N2','Mean1','Mean2','MeanDiff', ...
  876. 'T_Statistic','DF','CI95_Lower','CI95_Upper','CI95Bonf_Lower','CI95Bonf_Upper', ...
  877. 'P_Raw','P_Bonferroni','Significant','Cohens_d'});
  878. end
  879. function v = local_cell2num(x)
  880. if isnumeric(x)
  881. v = x;
  882. elseif isstring(x) || ischar(x)
  883. v = str2double(x);
  884. else
  885. v = NaN;
  886. end
  887. end
  888. function d = local_cohens_d(x, y)
  889. % Try robust Cohen's d if meanEffectSize is available; else classic d.
  890. d = NaN;
  891. try
  892. if exist('meanEffectSize','file') == 2
  893. T = meanEffectSize(x, y, Effect="robustcohen");
  894. d = T.Effect;
  895. return;
  896. end
  897. catch
  898. % fall through to classic d
  899. end
  900. % classic pooled SD Cohen's d
  901. x = x(~isnan(x)); y = y(~isnan(y));
  902. nx = numel(x); ny = numel(y);
  903. sx = std(x,0); sy = std(y,0);
  904. sp = sqrt(((nx-1)*sx^2 + (ny-1)*sy^2) / (nx+ny-2));
  905. d = (mean(x) - mean(y)) / sp;
  906. end

main.m at commit 1f16c41, under MIT · at the source

Overview

Authors: F. Tessari1, N. Hogan1,2
ORCID iDs: F. Tessari, N. Hogan
  1. Department of Mechanical Engineering, Massachusetts Institute of Technology,Cambridge, MA USA
  2. Department of Brain and Cognitive Sciences, Massachusetts Institute of Technology,Cambridge, MA USA
Institutions: Massachusetts Institute of Technology (United States)
Journal: Nature communications, volume 17, issue 1, article 7648
Dates: received 23 September 2025; accepted 2 June 2026; published online 16 June 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41467-026-74354-9 · PMID 42303617 · PMCID PMC13434010 · OpenAlex W7164902810
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: computational modeling (no new data) (modality), human (organism)
Methods: Statistics
Keywords: Motor control, Dynamical systems
MeSH: Muscle, Skeletal*, Physical Exertion*, Adult, Arm, Biomechanical Phenomena, Female, Humans, Male, Muscle Contraction, Torque, Wrist, Young Adult (* major topic)
Topic: Motor Control and Adaptation (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Eric P. and Evelyn E. Newman Fund
Citations: not cited yet (Europe PMC); 24 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repositories

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

figshare 30135883

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

ftessari23/GimbalForceAnalysis

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 1f16c4146159ecc06c53d46b6236357f3882084e, 15 June 2026
Languages: MATLAB (1)
Size: 490 files, 1 script
Software Heritage: not archived
Found in: “Code availability”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
3 files

Zenodo 20076777

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
3 files
At the source:

Code availability statement

The paper has a code availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1038/s41467-026-74354-9.

Tracing map

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

What the map holds:

  • 3 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 2 scripts, each with its path and the digest of its content;
  • 1 match between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Code and data availability statement

The paper has a code and data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1038/s41467-026-74354-9.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 2 authors, 2 keywords, 12 MeSH terms, 1 funder, 21 references.

Cite

This paper

Tessari, F., & Hogan, N. (2026). Stabilizing stiffness is the most limiting factor in human force exertion. Nature communications, 17(1), 7648. https://doi.org/10.1038/s41467-026-74354-9

BibTeX

@article{tessari2026stabilizing,
author = {Tessari, F. and Hogan, N.},
title = {{Stabilizing stiffness is the most limiting factor in human force exertion}},
journal = {Nature communications},
year = {2026},
month = jun,
volume = {17},
number = {1},
pages = {7648},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-74354-9},
url = {https://doi.org/10.1038/s41467-026-74354-9},
pmid = {42303617},
pmcid = {PMC13434010}
}

RIS

TY - JOUR
AU - Tessari, F.
AU - Hogan, N.
TI - Stabilizing stiffness is the most limiting factor in human force exertion
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/06/16
VL - 17
IS - 1
SP - 7648
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-74354-9
UR - https://doi.org/10.1038/s41467-026-74354-9
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-74354-9",
"type": "article-journal",
"title": "Stabilizing stiffness is the most limiting factor in human force exertion",
"container-title": "Nature communications",
"author": [
{
"family": "Tessari",
"given": "F."
},
{
"family": "Hogan",
"given": "N."
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "7648",
"DOI": "10.1038/s41467-026-74354-9",
"PMID": "42303617",
"PMCID": "PMC13434010",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-74354-9",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
16
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1016/j.isci.2026.115168 [code]
Distinct eccentricity-driven dynamics in foveal and extrafoveal visual crowding.
Journal: iScience
In common: Statistics and Machine Learning Toolbox, 1 reference
[2] doi:10.1371/journal.pone.0355402 [code]
Disentangling sensory contributions to postural control regulation through sample entropy and neural modeling: A preliminary study.
Journal: PloS one
In common: Statistics and Machine Learning Toolbox, computational modeling (no new data)
[3] doi:10.1038/s41467-026-73347-y [code]
A week in the life of the human brain reveals stable states punctuated by chaotic-like transitions.
Journal: Nature communications
In common: Statistics and Machine Learning Toolbox, computational modeling (no new data)
[4] doi:10.1088/1741-2552/ae5fd7 [code]
Metric validation for detection of delayed and directed coupling.
Journal: Journal of neural engineering
In common: Statistics and Machine Learning Toolbox, computational modeling (no new data)
[5] doi:10.1038/s41593-026-02205-3 [code]
Competitive interactions shape mammalian brain network dynamics and computation.
Journal: Nature neuroscience
In common: Statistics and Machine Learning Toolbox, computational modeling (no new data)
[6] doi:10.1523/eneuro.0283-26.2026 [code]
A Cortico-Basal Ganglia-Thalamic Network Model Linking Intermittent Postural Control to Sway-Related Beta-Band Oscillations.
Journal: eNeuro
In common: 1 reference
[7] doi:10.1038/s41598-026-44365-z [code]
Foot-ground force quantifies impaired balance control mechanisms post-stroke.
Journal: Scientific reports
In common: 1 reference
[8] doi:10.1016/j.neuron.2026.07.016 [code]
Inferring brain-wide interactions using data-constrained recurrent neural network models.
Journal: Neuron
In common: Statistics and Machine Learning Toolbox, computational modeling (no new data)
[9] doi:10.7554/elife.109313 [code]
Linear and categorical coding units in the mouse gustatory cortex drive population dynamics and behavior in taste decision-making.
Journal: eLife
In common: Statistics and Machine Learning Toolbox, computational modeling (no new data)
[10] doi:10.1038/s41598-026-55225-1 [code]
Benchmarking criteria to determine latent linear dimensionality in neural data.
Journal: Scientific reports
In common: Statistics and Machine Learning Toolbox, computational modeling (no new data)

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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