OSCR

Central and peripheral neuromuscular mechanisms underlying functional recovery heterogeneity in tibial plateau fractures.

Code ↔ Paper

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

The 5 matches
  1. [1] § Results › Muscle coordination deficits extend bilaterally ↔ 05_scripts/SPSS_complete_analysis.sps, lines 1–60 · score 0.69 · dual task walking, TA GL, RF Ham, normal walking, unaffected, co
  2. [2] § STAR★Methods › Method details › Data processing ↔ 05_scripts/granger_causality_fNIRS_analysis.m, lines 540–591 · score 0.68 · Akaike Information Criterion, Granger causality, optimal lag, models
  3. [3] § STAR★Methods › Quantification and statistical analysis ↔ 05_scripts/granger_causality_fNIRS_analysis.m, lines 540–591 · score 0.68 · Akaike Information Criterion, Granger causality, optimal lag, models
  4. [4] § Results › Muscle coordination deficits extend bilaterally ↔ 05_scripts/SPSS_complete_analysis.sps, lines 1–60 · score 0.65 · dual task walking, TA GL, RF Ham, normal walking, unaffected, co
  5. [5] § Results › Cortical network reorganization reveals compensatory control strategies ↔ 05_scripts/granger_causality_fNIRS_analysis.m, lines 775–796 · score 0.61 · inferior frontal area, Granger causality, IFA, SMA, motor, prefrontal

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,440 lines · 53 KB · CC-BY-4.0 · 3 matches

  1. function unified_nirs_gc_analysis_validated()
  2. % ===================================================================
  3. % UNIFIED NIRS GRANGER CAUSALITY ANALYSIS SYSTEM - VALIDATED VERSION
  4. % Version: 2.1 (Statistically Validated)
  5. % ===================================================================
  6. %
  7. % DESCRIPTION:
  8. % Performs Granger causality analysis on fNIRS data with rigorous
  9. % statistical validation including:
  10. % - Augmented Dickey-Fuller stationarity tests
  11. % - Ljung-Box residual white noise tests
  12. % - F-statistic significance testing with p-values
  13. % - FDR/Bonferroni multiple comparison correction
  14. % - Model adequacy diagnostics
  15. %
  16. % METHODOLOGY REFERENCES:
  17. % - Seth et al., 2015, NeuroImage (doi: 10.1016/j.neuroimage.2015.03.061)
  18. % - Barnett & Seth, 2014, J Neurosci Methods (MVGC toolbox)
  19. % - Granger, 1969, Econometrica (Original GC framework)
  20. %
  21. % INPUT:
  22. % - .nirs files (Homer2/Homer3 format)
  23. % - Required fields: d (signal data), t (time), SD (source-detector info)
  24. %
  25. % OUTPUT:
  26. % - GC matrices with statistical significance
  27. % - Network topology metrics
  28. % - Statistical validation reports
  29. % - Visualization of causal networks
  30. % - Detailed CSV reports
  31. %
  32. % VALIDATION PIPELINE:
  33. % 1. Stationarity testing (ADF) -> differencing if needed
  34. % 2. Optimal lag selection (AIC)
  35. % 3. VAR model estimation
  36. % 4. GC computation with F-statistics
  37. % 5. Residual diagnostics (Ljung-Box)
  38. % 6. Multiple comparison correction
  39. %
  40. % AUTHOR: Z. Sun (study team), Binzhou Medical University
  41. % DATE: 2025-01-17
  42. % LICENSE: CC BY 4.0 (data and code; see LICENSE.txt)
  43. % ===================================================================
  44. clc; close all;
  45. fprintf('=====================================\n');
  46. fprintf(' VALIDATED NIRS GC ANALYSIS\n');
  47. fprintf(' Version 2.1 - Statistical Rigor\n');
  48. fprintf('=====================================\n\n');
  49. %% 1. FILE SELECTION
  50. folder_path = uigetdir(pwd, 'Select folder containing .nirs files');
  51. if folder_path == 0
  52. fprintf('Operation cancelled\n');
  53. return;
  54. end
  55. fprintf('Selected folder: %s\n', folder_path);
  56. nirs_files = dir(fullfile(folder_path, '*.nirs'));
  57. if isempty(nirs_files)
  58. fprintf('Error: No .nirs files found\n');
  59. return;
  60. end
  61. fprintf('Found %d .nirs files\n\n', length(nirs_files));
  62. %% 2. PARAMETER SETUP
  63. params = setup_validated_parameters();
  64. %% 3. CREATE OUTPUT STRUCTURE
  65. timestamp = datestr(now, 'yyyymmdd_HHMMSS');
  66. output_folder = fullfile(folder_path, sprintf('Validated_GC_Results_%s', timestamp));
  67. mkdir(output_folder);
  68. % Create subfolders
  69. mkdir(fullfile(output_folder, 'Statistics'));
  70. mkdir(fullfile(output_folder, 'Visualizations'));
  71. mkdir(fullfile(output_folder, 'Reports'));
  72. fprintf('\nResults will be saved to: %s\n\n', output_folder);
  73. %% 4. BATCH PROCESSING
  74. fprintf('--- Starting Validated Analysis ---\n');
  75. [all_results, processing_summary] = batch_process_validated(nirs_files, folder_path, params);
  76. %% 5. AGGREGATE AND ANALYZE
  77. if ~isempty(all_results)
  78. fprintf('\n--- Generating Comprehensive Reports ---\n');
  79. % Aggregate results
  80. summary_stats = aggregate_validated_results(all_results, processing_summary);
  81. % Generate statistical validation report
  82. generate_validation_report(summary_stats, output_folder, params);
  83. % Perform connection analysis
  84. connection_analysis = unified_connection_classification(summary_stats, params);
  85. % Directional analysis
  86. directional_analysis = analyze_causal_direction(summary_stats);
  87. % Generate comprehensive outputs
  88. generate_comprehensive_reports(summary_stats, connection_analysis, ...
  89. directional_analysis, output_folder, params);
  90. % Create visualizations
  91. generate_validated_visualizations(summary_stats, connection_analysis, ...
  92. directional_analysis, output_folder, params);
  93. % Display findings
  94. display_validated_findings(summary_stats, connection_analysis, ...
  95. directional_analysis, params);
  96. else
  97. fprintf('No files were successfully processed\n');
  98. end
  99. fprintf('\n=====================================\n');
  100. fprintf(' Analysis Complete!\n');
  101. fprintf('Results: %s\n', output_folder);
  102. fprintf('=====================================\n');
  103. end
  104. %% ========== PARAMETER SETUP ==========
  105. function params = setup_validated_parameters()
  106. fprintf('╔════════════════════════════════════╗\n');
  107. fprintf('║ VALIDATED PARAMETER SETUP ║\n');
  108. fprintf('╚════════════════════════════════════╝\n\n');
  109. fprintf('Default parameters (recommended for peer-review):\n');
  110. fprintf(' • Wavelength: 2 (HbO2, long wavelength)\n');
  111. fprintf(' • Analysis type: Brain region (4 ROIs)\n');
  112. fprintf(' • Max lag: 5 (~0.45s at 11Hz)\n');
  113. fprintf(' • Significance: p<0.01 (F-test)\n');
  114. fprintf(' • Correction: FDR\n');
  115. fprintf(' • GC thresholds:\n');
  116. fprintf(' - Significance: >0.15\n');
  117. fprintf(' - Strong: >0.20\n');
  118. fprintf(' • Statistical tests:\n');
  119. fprintf(' - ADF stationarity test\n');
  120. fprintf(' - Ljung-Box residual test\n\n');
  121. use_defaults = input('Use default parameters? (y/n) [default=y]: ', 's');
  122. if isempty(use_defaults) || strcmpi(use_defaults, 'y')
  123. params = get_default_params();
  124. else
  125. params = get_custom_params();
  126. end
  127. fprintf('\n✓ Parameters configured\n\n');
  128. end
  129. function params = get_default_params()
  130. params = struct();
  131. params.wavelength = 2;
  132. params.use_brain_regions = true;
  133. params.max_lag = 5;
  134. params.alpha = 0.01;
  135. params.correction_method = 'fdr';
  136. params.significance_threshold = 0.15;
  137. params.strong_threshold = 0.20;
  138. params.balance_threshold = 0.05;
  139. params.display_threshold = 0.05;
  140. % Statistical test parameters
  141. params.adf_alpha = 0.05;
  142. params.ljung_box_lag = 20;
  143. params.ljung_box_alpha = 0.05;
  144. params.min_samples = 100;
  145. end
  146. %% ========== BATCH PROCESSING ==========
  147. function [all_results, processing_summary] = batch_process_validated(nirs_files, folder_path, params)
  148. all_results = {};
  149. processing_summary = struct();
  150. processing_summary.successful_files = {};
  151. processing_summary.failed_files = {};
  152. processing_summary.processing_times = [];
  153. processing_summary.warnings = {};
  154. n_files = length(nirs_files);
  155. for i = 1:n_files
  156. fprintf('\n╔════════════════════════════════════╗\n');
  157. fprintf('║ File %d/%d: %-23s║\n', i, n_files, nirs_files(i).name);
  158. fprintf('╚════════════════════════════════════╝\n');
  159. filepath = fullfile(folder_path, nirs_files(i).name);
  160. try
  161. tic;
  162. fprintf(' [1/4] Loading data...\n');
  163. data = load(filepath, '-mat');
  164. if ~isfield(data, 'd')
  165. error('Missing signal data field "d"');
  166. end
  167. fprintf(' [2/4] Preprocessing and validation...\n');
  168. result = analyze_single_file_validated(data, params);
  169. result.filename = nirs_files(i).name;
  170. fprintf(' [3/4] Statistical tests...\n');
  171. fprintf(' ✓ Stationarity: %d/%d series stationary\n', ...
  172. sum(result.validation.stationarity_passed), ...
  173. length(result.validation.stationarity_passed));
  174. fprintf(' ✓ Residuals: %d/%d tests passed\n', ...
  175. result.validation.n_white_noise_passed, ...
  176. result.validation.n_residual_tests);
  177. fprintf(' [4/4] Computing network metrics...\n');
  178. processing_time = toc;
  179. all_results{end+1} = result;
  180. processing_summary.successful_files{end+1} = nirs_files(i).name;
  181. processing_summary.processing_times(end+1) = processing_time;
  182. if ~isempty(result.validation.warnings)
  183. processing_summary.warnings{end+1} = struct(...
  184. 'file', nirs_files(i).name, ...
  185. 'warnings', {result.validation.warnings});
  186. end
  187. fprintf(' ✓ Success (%.2f seconds)\n', processing_time);
  188. catch ME
  189. fprintf(' ✗ Failed: %s\n', ME.message);
  190. processing_summary.failed_files{end+1} = nirs_files(i).name;
  191. end
  192. end
  193. fprintf('\n\n╔════════════════════════════════════╗\n');
  194. fprintf('║ Processing Summary ║\n');
  195. fprintf('╠════════════════════════════════════╣\n');
  196. fprintf('║ Successful: %3d/%3d ║\n', ...
  197. length(processing_summary.successful_files), n_files);
  198. fprintf('║ Failed: %3d/%3d ║\n', ...
  199. length(processing_summary.failed_files), n_files);
  200. if ~isempty(processing_summary.warnings)
  201. fprintf('║ Warnings: %3d files ║\n', ...
  202. length(processing_summary.warnings));
  203. end
  204. fprintf('╚════════════════════════════════════╝\n');
  205. end
  206. %% ========== SINGLE FILE ANALYSIS ==========
  207. function result = analyze_single_file_validated(data, params)
  208. % Extract and validate data
  209. signal_data = data.d;
  210. [n_samples, n_channels] = size(signal_data);
  211. fprintf(' Data: %d samples × %d channels\n', n_samples, n_channels);
  212. if n_samples < params.min_samples
  213. warning('Low sample count (%d < %d recommended)', n_samples, params.min_samples);
  214. end
  215. % Determine sampling rate
  216. if isfield(data, 't') && length(data.t) > 1
  217. fs = 1 / mean(diff(data.t));
  218. else
  219. fs = 10;
  220. warning('Sampling rate not found, assuming 10 Hz');
  221. end
  222. fprintf(' Sampling rate: %.2f Hz\n', fs);
  223. % Select wavelength
  224. if params.wavelength == 1
  225. selected_data = signal_data(:, 1:2:min(n_channels, 69));
  226. else
  227. selected_data = signal_data(:, 2:2:min(n_channels, 70));
  228. end
  229. fprintf(' Selected: %d channels (wavelength %d)\n', ...
  230. size(selected_data, 2), params.wavelength);
  231. % Preprocess
  232. processed_data = safe_preprocess_data(selected_data, fs);
  233. % Perform region-based analysis with full validation
  234. if params.use_brain_regions
  235. [gc_results, labels, validation] = perform_validated_region_analysis(...
  236. processed_data, params, fs);
  237. else
  238. [gc_results, labels, validation] = perform_validated_channel_analysis(...
  239. processed_data, params, fs);
  240. end
  241. % Apply multiple comparison correction
  242. gc_results = apply_correction(gc_results, params);
  243. % Package results
  244. result = struct();
  245. result.gc_matrix = gc_results.gc_matrix;
  246. result.f_statistics = gc_results.f_statistics;
  247. result.p_values = gc_results.p_values;
  248. result.p_corrected = gc_results.p_corrected;
  249. result.significant = gc_results.p_corrected < params.alpha;
  250. result.optimal_lags = gc_results.optimal_lags;
  251. result.labels = labels;
  252. result.parameters = params;
  253. result.validation = validation;
  254. result.sample_rate = fs;
  255. result.n_samples = n_samples;
  256. end
  257. %% ========== VALIDATED REGION ANALYSIS ==========
  258. function [gc_results, labels, validation] = perform_validated_region_analysis(processed_data, params, fs)
  259. fprintf(' Performing brain region analysis...\n');
  260. % Define brain regions
  261. regions = define_brain_regions();
  262. labels = {regions.name};
  263. n_regions = length(regions);
  264. fprintf(' Regions: ');
  265. fprintf('%s, ', labels{1:end-1});
  266. fprintf('%s\n', labels{end});
  267. % Average channels within each region
  268. regional_data = zeros(size(processed_data, 1), n_regions);
  269. for i = 1:n_regions
  270. regional_data(:, i) = mean(processed_data(:, regions(i).channels), 2);
  271. end
  272. % VALIDATION STEP 1: Stationarity Testing
  273. fprintf(' [Validation] Testing stationarity...\n');
  274. [regional_data, stationarity_results] = ensure_stationarity(regional_data, params, labels);
  275. % Initialize result matrices
  276. n_regions = size(regional_data, 2);
  277. gc_matrix = zeros(n_regions, n_regions);
  278. f_statistics = zeros(n_regions, n_regions);
  279. p_values = ones(n_regions, n_regions);
  280. optimal_lags = zeros(n_regions, n_regions);
  281. % Residual test storage
  282. residual_tests = cell(n_regions, n_regions);
  283. % COMPUTE GRANGER CAUSALITY with full statistics
  284. fprintf(' Computing GC with statistical tests...\n');
  285. for i = 1:n_regions
  286. for j = 1:n_regions
  287. if i ~= j
  288. X = regional_data(:, i); % Potential cause
  289. Y = regional_data(:, j); % Effect
  290. % Select optimal lag using AIC
  291. [optimal_lag, aic_values] = select_optimal_lag_aic(X, Y, params.max_lag);
  292. optimal_lags(i, j) = optimal_lag;
  293. % Compute GC with full statistics
  294. [gc_val, f_stat, p_val, residuals] = compute_gc_with_full_stats(...
  295. X, Y, optimal_lag);
  296. gc_matrix(i, j) = gc_val;
  297. f_statistics(i, j) = f_stat;
  298. p_values(i, j) = p_val;
  299. % VALIDATION STEP 2: Residual Testing
  300. [is_white_noise, lb_stat, lb_p] = ljung_box_test(...
  301. residuals, params.ljung_box_lag);
  302. residual_tests{i, j} = struct(...
  303. 'is_white_noise', is_white_noise, ...
  304. 'Q_statistic', lb_stat, ...
  305. 'p_value', lb_p);
  306. end
  307. end
  308. end
  309. % Package results
  310. gc_results = struct();
  311. gc_results.gc_matrix = gc_matrix;
  312. gc_results.f_statistics = f_statistics;
  313. gc_results.p_values = p_values;
  314. gc_results.optimal_lags = optimal_lags;
  315. % VALIDATION RESULTS
  316. validation = struct();
  317. validation.stationarity_results = stationarity_results;
  318. validation.stationarity_passed = stationarity_results.all_stationary;
  319. validation.residual_tests = residual_tests;
  320. % Count white noise tests passed
  321. n_tests = 0;
  322. n_passed = 0;
  323. warnings = {};
  324. for i = 1:n_regions
  325. for j = 1:n_regions
  326. if i ~= j
  327. n_tests = n_tests + 1;
  328. if residual_tests{i, j}.is_white_noise
  329. n_passed = n_passed + 1;
  330. else
  331. warnings{end+1} = sprintf('%s→%s: residual autocorrelation (p=%.4f)', ...
  332. labels{i}, labels{j}, residual_tests{i, j}.p_value);
  333. end
  334. end
  335. end
  336. end
  337. validation.n_residual_tests = n_tests;
  338. validation.n_white_noise_passed = n_passed;
  339. validation.warnings = warnings;
  340. fprintf(' ✓ Validation: %d/%d residual tests passed\n', n_passed, n_tests);
  341. if ~isempty(warnings)
  342. fprintf(' ⚠ %d warnings (see detailed report)\n', length(warnings));
  343. end
  344. end
  345. %% ========== STATIONARITY TESTING AND CORRECTION ==========
  346. function [data_stationary, results] = ensure_stationarity(data, params, labels)
  347. n_series = size(data, 2);
  348. is_stationary = false(n_series, 1);
  349. adf_statistics = zeros(n_series, 1);
  350. p_values = zeros(n_series, 1);
  351. differenced = false(n_series, 1);
  352. data_stationary = data;
  353. for i = 1:n_series
  354. [is_stat, adf_stat, p_val] = adf_test(data(:, i), params.adf_alpha);
  355. is_stationary(i) = is_stat;
  356. adf_statistics(i) = adf_stat;
  357. p_values(i) = p_val;
  358. if ~is_stat
  359. % Apply first-order differencing
  360. data_stationary(:, i) = [0; diff(data(:, i))];
  361. differenced(i) = true;
  362. % Re-test
  363. [is_stat_after, ~, ~] = adf_test(data_stationary(:, i), params.adf_alpha);
  364. is_stationary(i) = is_stat_after;
  365. if nargin > 2 && i <= length(labels)
  366. fprintf(' ⚠ %s: non-stationary (p=%.4f), differenced\n', ...
  367. labels{i}, p_val);
  368. end
  369. end
  370. end
  371. results = struct();
  372. results.all_stationary = is_stationary;
  373. results.adf_statistics = adf_statistics;
  374. results.p_values = p_values;
  375. results.differenced = differenced;
  376. results.n_stationary = sum(is_stationary);
  377. results.n_total = n_series;
  378. end
  379. %% ========== AUGMENTED DICKEY-FULLER TEST ==========
  380. function [is_stationary, adf_stat, p_value] = adf_test(data, alpha)
  381. % Augmented Dickey-Fuller test for unit root (non-stationarity)
  382. %
  383. % H0: Series has unit root (non-stationary)
  384. % H1: Series is stationary
  385. %
  386. % Returns:
  387. % is_stationary: true if we reject H0 (series is stationary)
  388. % adf_stat: test statistic
  389. % p_value: approximate p-value
  390. if nargin < 2
  391. alpha = 0.05;
  392. end
  393. % Remove mean
  394. data = data - mean(data);
  395. % Determine lag length using Schwert criterion
  396. n = length(data);
  397. max_lag = floor(12 * (n/100)^0.25);
  398. % Construct regression: Δy(t) = α + β*y(t-1) + Σγ_i*Δy(t-i) + ε(t)
  399. y_lag = data(1:end-1);
  400. delta_y = diff(data);
  401. % Build design matrix
  402. X = y_lag(1:end-1); % y(t-1)
  403. Y = delta_y(2:end); % Δy(t)
  404. % Add lagged differences
  405. for lag = 1:min(max_lag, length(delta_y)-2)
  406. X = [X, delta_y(2-lag:end-lag)];
  407. end
  408. % Add constant
  409. X = [ones(size(X, 1), 1), X];
  410. % OLS regression
  411. [b, ~, ~, ~, stats] = regress(Y, X);
  412. % ADF statistic is t-stat of y(t-1) coefficient (second column)
  413. adf_stat = b(2) / sqrt(stats(1,1) * (X'*X)^(-1) * stats(1,1));
  414. % Critical values (MacKinnon, 1996) for constant, no trend
  415. % Sample size adjusted
  416. if n <= 25
  417. cv_1pct = -3.75;
  418. cv_5pct = -3.00;
  419. cv_10pct = -2.63;
  420. elseif n <= 50
  421. cv_1pct = -3.58;
  422. cv_5pct = -2.93;
  423. cv_10pct = -2.60;
  424. else
  425. cv_1pct = -3.51;
  426. cv_5pct = -2.89;
  427. cv_10pct = -2.58;
  428. end
  429. % Determine stationarity
  430. is_stationary = (adf_stat < cv_5pct);
  431. % Approximate p-value
  432. if adf_stat < cv_1pct
  433. p_value = 0.01;
  434. elseif adf_stat < cv_5pct
  435. p_value = 0.05;
  436. elseif adf_stat < cv_10pct
  437. p_value = 0.10;
  438. else
  439. p_value = 0.15;
  440. end
  441. end
  442. %% ========== OPTIMAL LAG SELECTION (AIC) ==========
  443. function [optimal_lag, aic_values] = select_optimal_lag_aic(X, Y, max_lag)
  444. % Select optimal VAR lag order using Akaike Information Criterion
  445. %
  446. % AIC = 2k - 2ln(L)
  447. % where k = number of parameters, L = likelihood
  448. %
  449. % Lower AIC indicates better model
  450. n = length(Y);
  451. aic_values = zeros(max_lag, 1);
  452. for lag = 1:max_lag
  453. if lag >= n
  454. aic_values(lag) = Inf;
  455. continue;
  456. end
  457. % Build full model
  458. Y_data = Y(lag+1:end);
  459. X_full = [];
  460. % Add lags of Y
  461. for i = 1:lag
  462. X_full = [X_full, Y(lag+1-i:end-i)];
  463. end
  464. % Add lags of X
  465. for i = 1:lag
  466. X_full = [X_full, X(lag+1-i:end-i)];
  467. end
  468. % Add constant
  469. X_full = [ones(size(X_full, 1), 1), X_full];
  470. % Fit model
  471. [~, ~, r] = regress(Y_data, X_full);
  472. % Calculate AIC
  473. n_eff = length(Y_data);
  474. k = size(X_full, 2); % Number of parameters
  475. RSS = sum(r.^2);
  476. % Log likelihood (assuming Gaussian errors)
  477. log_likelihood = -n_eff/2 * (log(2*pi) + log(RSS/n_eff) + 1);
  478. aic_values(lag) = 2*k - 2*log_likelihood;
  479. end
  480. [~, optimal_lag] = min(aic_values);
  481. end
  482. %% ========== GRANGER CAUSALITY WITH FULL STATISTICS ==========
  483. function [gc_value, f_stat, p_value, residuals] = compute_gc_with_full_stats(X, Y, lag)
  484. % Compute Granger causality with F-statistic and p-value
  485. %
  486. % Tests: H0: X does NOT Granger-cause Y
  487. %
  488. % Returns:
  489. % gc_value: GC value = ln(RSS_restricted / RSS_full)
  490. % f_stat: F-statistic for hypothesis test
  491. % p_value: p-value from F-test
  492. % residuals: residuals from full model (for diagnostics)
  493. n = length(Y);
  494. % Build restricted model (Y ~ lags of Y only)
  495. Y_data = Y(lag+1:end);
  496. X_restricted = [];
  497. for i = 1:lag
  498. X_restricted = [X_restricted, Y(lag+1-i:end-i)];
  499. end
  500. X_restricted = [ones(size(X_restricted, 1), 1), X_restricted];
  501. % Build full model (Y ~ lags of Y + lags of X)
  502. X_full = X_restricted;
  503. for i = 1:lag
  504. X_full = [X_full, X(lag+1-i:end-i)];
  505. end
  506. % Fit models
  507. [~, ~, r_restricted] = regress(Y_data, X_restricted);
  508. [~, ~, r_full] = regress(Y_data, X_full);
  509. residuals = r_full;
  510. % Calculate RSS
  511. RSS_restricted = sum(r_restricted.^2);
  512. RSS_full = sum(r_full.^2);
  513. % Granger causality value
  514. if RSS_full > 0
  515. gc_value = log(RSS_restricted / RSS_full);
  516. else
  517. gc_value = 0;
  518. end
  519. % F-statistic
  520. m = size(X_full, 2) - size(X_restricted, 2); % Number of restrictions (lag)
  521. n_eff = length(Y_data);
  522. k = size(X_full, 2);
  523. f_stat = ((RSS_restricted - RSS_full) / m) / (RSS_full / (n_eff - k));
  524. % P-value from F-distribution
  525. if f_stat > 0
  526. p_value = 1 - fcdf(f_stat, m, n_eff - k);
  527. else
  528. p_value = 1;
  529. end
  530. end
  531. %% ========== LJUNG-BOX TEST ==========
  532. function [is_white_noise, Q_stat, p_value] = ljung_box_test(residuals, max_lag)
  533. % Ljung-Box test for residual autocorrelation
  534. %
  535. % H0: Residuals are white noise (no autocorrelation)
  536. % H1: Residuals show autocorrelation
  537. %
  538. % Returns:
  539. % is_white_noise: true if we fail to reject H0
  540. % Q_stat: Ljung-Box Q statistic
  541. % p_value: p-value from chi-squared test
  542. if nargin < 2
  543. max_lag = min(20, floor(length(residuals)/5));
  544. end
  545. n = length(residuals);
  546. % Compute autocorrelation function
  547. acf_vals = zeros(max_lag, 1);
  548. mean_resid = mean(residuals);
  549. var_resid = var(residuals);
  550. for k = 1:max_lag
  551. acf_vals(k) = sum((residuals(1:n-k) - mean_resid) .* ...
  552. (residuals(k+1:n) - mean_resid)) / (n * var_resid);
  553. end
  554. % Ljung-Box Q statistic
  555. Q_stat = n * (n + 2) * sum(acf_vals.^2 ./ (n - (1:max_lag)'));
  556. % Chi-squared test
  557. df = max_lag;
  558. p_value = 1 - chi2cdf(Q_stat, df);
  559. % White noise if we fail to reject H0
  560. is_white_noise = (p_value > 0.05);
  561. end
  562. %% ========== MULTIPLE COMPARISON CORRECTION ==========
  563. function gc_results = apply_correction(gc_results, params)
  564. p_values = gc_results.p_values;
  565. n = size(p_values, 1);
  566. % Extract off-diagonal p-values
  567. mask = ~eye(n);
  568. p_vec = p_values(mask);
  569. % Apply correction
  570. if strcmpi(params.correction_method, 'fdr')
  571. p_corrected_vec = fdr_correction(p_vec);
  572. else
  573. % Bonferroni
  574. p_corrected_vec = min(p_vec * length(p_vec), 1);
  575. end
  576. % Reconstruct matrix
  577. p_corrected = ones(n, n);
  578. p_corrected(mask) = p_corrected_vec;
  579. gc_results.p_corrected = p_corrected;
  580. end
  581. function p_corrected = fdr_correction(p_values)
  582. % Benjamini-Hochberg FDR correction
  583. [p_sorted, sort_idx] = sort(p_values(:));
  584. m = length(p_sorted);
  585. % Find largest k such that P(k) <= (k/m)*q
  586. q = 0.05; % FDR level
  587. k_vec = (1:m)';
  588. threshold = (k_vec / m) * q;
  589. significant = p_sorted <= threshold;
  590. if any(significant)
  591. k_max = find(significant, 1, 'last');
  592. p_corrected = zeros(size(p_values));
  593. p_corrected(sort_idx(1:k_max)) = p_sorted(1:k_max) * m ./ k_vec(1:k_max);
  594. p_corrected(sort_idx(k_max+1:end)) = 1;
  595. else
  596. p_corrected = ones(size(p_values));
  597. end
  598. end
  599. %% ========== DATA PREPROCESSING ==========
  600. function processed_data = safe_preprocess_data(raw_data, fs)
  601. [n_samples, n_channels] = size(raw_data);
  602. processed_data = raw_data;
  603. % 1. Remove invalid data
  604. for i = 1:n_channels
  605. col = processed_data(:, i);
  606. bad_idx = ~isfinite(col);
  607. if any(bad_idx)
  608. col(bad_idx) = interp1(find(~bad_idx), col(~bad_idx), ...
  609. find(bad_idx), 'linear', 'extrap');
  610. processed_data(:, i) = col;
  611. end
  612. end
  613. % 2. Bandpass filter (0.01-0.2 Hz for hemodynamics)
  614. if fs > 0.4
  615. [b, a] = butter(4, [0.01, 0.2] / (fs/2), 'bandpass');
  616. for i = 1:n_channels
  617. processed_data(:, i) = filtfilt(b, a, processed_data(:, i));
  618. end
  619. end
  620. % 3. Detrend
  621. for i = 1:n_channels
  622. processed_data(:, i) = detrend(processed_data(:, i));
  623. end
  624. % 4. Normalize
  625. for i = 1:n_channels
  626. processed_data(:, i) = (processed_data(:, i) - mean(processed_data(:, i))) / ...
  627. std(processed_data(:, i));
  628. end
  629. end
  630. %% ========== BRAIN REGION DEFINITIONS ==========
  631. function regions = define_brain_regions()
  632. % Standard 4-region parcellation for motor tasks
  633. regions = struct();
  634. regions(1).name = 'IFA'; % Inferior Frontal Area
  635. regions(1).channels = [9, 14, 15];
  636. regions(1).ba = '44/45';
  637. regions(2).name = 'SMA'; % Sensorimotor Area
  638. regions(2).channels = [1, 2, 11, 12, 16, 17, 25, 27, 29, 31, 33];
  639. regions(2).ba = '1-6';
  640. regions(3).name = 'PFA'; % Polar Frontal Area
  641. regions(3).channels = [3, 4, 5, 6, 19, 21];
  642. regions(3).ba = '10';
  643. regions(4).name = 'DLPFC'; % Dorsolateral Prefrontal Cortex
  644. regions(4).channels = [7, 8, 13, 18, 20, 22, 23, 24];
  645. regions(4).ba = '9/46';
  646. end
  647. %% ========== AGGREGATE RESULTS ==========
  648. function summary_stats = aggregate_validated_results(all_results, processing_summary)
  649. fprintf(' Aggregating %d files...\n', length(all_results));
  650. n_files = length(all_results);
  651. n_nodes = length(all_results{1}.labels);
  652. % Initialize accumulators
  653. gc_sum = zeros(n_nodes, n_nodes);
  654. gc_sq_sum = zeros(n_nodes, n_nodes);
  655. f_sum = zeros(n_nodes, n_nodes);
  656. p_sum = zeros(n_nodes, n_nodes);
  657. sig_count = zeros(n_nodes, n_nodes);
  658. % Validation tracking
  659. stationarity_count = 0;
  660. residual_pass_count = 0;
  661. total_residual_tests = 0;
  662. for i = 1:n_files
  663. gc_sum = gc_sum + all_results{i}.gc_matrix;
  664. gc_sq_sum = gc_sq_sum + all_results{i}.gc_matrix.^2;
  665. f_sum = f_sum + all_results{i}.f_statistics;
  666. p_sum = p_sum + all_results{i}.p_values;
  667. sig_count = sig_count + all_results{i}.significant;
  668. stationarity_count = stationarity_count + ...
  669. sum(all_results{i}.validation.stationarity_passed);
  670. residual_pass_count = residual_pass_count + ...
  671. all_results{i}.validation.n_white_noise_passed;
  672. total_residual_tests = total_residual_tests + ...
  673. all_results{i}.validation.n_residual_tests;
  674. end
  675. % Compute means and SDs
  676. gc_mean = gc_sum / n_files;
  677. gc_std = sqrt(gc_sq_sum / n_files - gc_mean.^2);
  678. f_mean = f_sum / n_files;
  679. p_mean = p_sum / n_files;
  680. frequency = sig_count / n_files;
  681. % Package
  682. summary_stats = struct();
  683. summary_stats.n_files = n_files;
  684. summary_stats.n_nodes = n_nodes;
  685. summary_stats.labels = all_results{1}.labels;
  686. summary_stats.gc_mean = gc_mean;
  687. summary_stats.gc_std = gc_std;
  688. summary_stats.f_mean = f_mean;
  689. summary_stats.p_mean = p_mean;
  690. summary_stats.frequency = frequency;
  691. summary_stats.all_results = all_results;
  692. % Validation summary
  693. summary_stats.validation = struct();
  694. summary_stats.validation.stationarity_rate = stationarity_count / ...
  695. (n_files * n_nodes);
  696. summary_stats.validation.residual_pass_rate = residual_pass_count / ...
  697. total_residual_tests;
  698. summary_stats.validation.total_files = n_files;
  699. fprintf(' ✓ Aggregation complete\n');
  700. fprintf(' Stationarity: %.1f%% of series\n', ...
  701. summary_stats.validation.stationarity_rate * 100);
  702. fprintf(' Residual tests: %.1f%% passed\n', ...
  703. summary_stats.validation.residual_pass_rate * 100);
  704. end
  705. %% ========== CONNECTION CLASSIFICATION ==========
  706. function connection_analysis = unified_connection_classification(summary_stats, params)
  707. fprintf(' Classifying connections...\n');
  708. gc_matrix = summary_stats.gc_mean;
  709. freq_matrix = summary_stats.frequency;
  710. n_nodes = summary_stats.n_nodes;
  711. labels = summary_stats.labels;
  712. % Classification
  713. bidirectional_strong = [];
  714. bidirectional_sig = [];
  715. unidirectional_strong = [];
  716. unidirectional_sig = [];
  717. weak_connections = [];
  718. for i = 1:n_nodes
  719. for j = i+1:n_nodes
  720. gc_ij = gc_matrix(i, j);
  721. gc_ji = gc_matrix(j, i);
  722. freq_ij = freq_matrix(i, j);
  723. freq_ji = freq_matrix(j, i);
  724. max_gc = max(gc_ij, gc_ji);
  725. min_gc = min(gc_ij, gc_ji);
  726. % Bidirectional
  727. if freq_ij >= 0.5 && freq_ji >= 0.5
  728. is_balanced = abs(gc_ij - gc_ji) < params.balance_threshold;
  729. if max_gc > params.strong_threshold
  730. conn = create_connection_struct(i, j, gc_ij, gc_ji, ...
  731. labels{i}, labels{j}, freq_ij, freq_ji);
  732. if is_balanced
  733. conn.type = 'bidirectional_strong_balanced';
  734. else
  735. conn.type = 'bidirectional_strong_unbalanced';
  736. end
  737. bidirectional_strong = [bidirectional_strong; conn];
  738. elseif max_gc > params.significance_threshold
  739. conn = create_connection_struct(i, j, gc_ij, gc_ji, ...
  740. labels{i}, labels{j}, freq_ij, freq_ji);
  741. if is_balanced
  742. conn.type = 'bidirectional_sig_balanced';
  743. else
  744. conn.type = 'bidirectional_sig_unbalanced';
  745. end
  746. bidirectional_sig = [bidirectional_sig; conn];
  747. end
  748. % Unidirectional
  749. elseif freq_ij >= 0.5 || freq_ji >= 0.5
  750. conn = create_connection_struct(i, j, gc_ij, gc_ji, ...
  751. labels{i}, labels{j}, freq_ij, freq_ji);
  752. if gc_ij > gc_ji
  753. conn.dominant_direction = sprintf('%s→%s', labels{i}, labels{j});
  754. conn.max_strength = gc_ij;
  755. else
  756. conn.dominant_direction = sprintf('%s→%s', labels{j}, labels{i});
  757. conn.max_strength = gc_ji;
  758. end
  759. if conn.max_strength > params.strong_threshold
  760. conn.type = 'unidirectional_strong';
  761. unidirectional_strong = [unidirectional_strong; conn];
  762. elseif conn.max_strength > params.significance_threshold
  763. conn.type = 'unidirectional_sig';
  764. unidirectional_sig = [unidirectional_sig; conn];
  765. end
  766. % Weak
  767. elseif max_gc > params.display_threshold
  768. conn = create_connection_struct(i, j, gc_ij, gc_ji, ...
  769. labels{i}, labels{j}, freq_ij, freq_ji);
  770. conn.type = 'weak';
  771. weak_connections = [weak_connections; conn];
  772. end
  773. end
  774. end
  775. % Count connections
  776. n_total_possible = n_nodes * (n_nodes - 1);
  777. n_bidirectional = length(bidirectional_strong) + length(bidirectional_sig);
  778. n_unidirectional = length(unidirectional_strong) + length(unidirectional_sig);
  779. n_weak = length(weak_connections);
  780. n_none = (n_total_possible/2) - n_bidirectional - n_unidirectional - n_weak;
  781. connection_analysis = struct();
  782. connection_analysis.bidirectional_strong = bidirectional_strong;
  783. connection_analysis.bidirectional_sig = bidirectional_sig;
  784. connection_analysis.unidirectional_strong = unidirectional_strong;
  785. connection_analysis.unidirectional_sig = unidirectional_sig;
  786. connection_analysis.weak = weak_connections;
  787. connection_analysis.counts = struct(...
  788. 'bidirectional_total', n_bidirectional, ...
  789. 'unidirectional_total', n_unidirectional, ...
  790. 'weak', n_weak, ...
  791. 'none', n_none);
  792. connection_analysis.percentages = struct(...
  793. 'bidirectional_total', 100*n_bidirectional/(n_total_possible/2), ...
  794. 'unidirectional_total', 100*n_unidirectional/(n_total_possible/2), ...
  795. 'weak', 100*n_weak/(n_total_possible/2), ...
  796. 'none', 100*n_none/(n_total_possible/2));
  797. fprintf(' ✓ Classification complete\n');
  798. end
  799. function conn = create_connection_struct(i, j, gc_ij, gc_ji, label_i, label_j, freq_ij, freq_ji)
  800. conn = struct();
  801. conn.node1 = i;
  802. conn.node2 = j;
  803. conn.label1 = label_i;
  804. conn.label2 = label_j;
  805. conn.gc_1to2 = gc_ij;
  806. conn.gc_2to1 = gc_ji;
  807. conn.freq_1to2 = freq_ij;
  808. conn.freq_2to1 = freq_ji;
  809. end
  810. %% ========== DIRECTIONAL ANALYSIS ==========
  811. function directional = analyze_causal_direction(summary_stats)
  812. gc_matrix = summary_stats.gc_mean;
  813. freq_matrix = summary_stats.frequency;
  814. n_nodes = summary_stats.n_nodes;
  815. % Calculate net causal flow
  816. net_flow = zeros(n_nodes, 1);
  817. for i = 1:n_nodes
  818. outgoing = sum(gc_matrix(i, :) .* (freq_matrix(i, :) > 0.5));
  819. incoming = sum(gc_matrix(:, i) .* (freq_matrix(:, i) > 0.5));
  820. net_flow(i) = outgoing - incoming;
  821. end
  822. % Hierarchical ranking
  823. [sorted_flow, idx] = sort(net_flow, 'descend');
  824. directional = struct();
  825. directional.net_causal_flow = net_flow;
  826. directional.hierarchy = struct(...
  827. 'scores', sorted_flow, ...
  828. 'indices', idx, ...
  829. 'labels', {summary_stats.labels(idx)});
  830. end
  831. %% ========== VALIDATION REPORT ==========
  832. function generate_validation_report(summary_stats, output_folder, params)
  833. fprintf(' Generating validation report...\n');
  834. report_file = fullfile(output_folder, 'Reports', 'Statistical_Validation_Report.txt');
  835. fid = fopen(report_file, 'w');
  836. fprintf(fid, '═══════════════════════════════════════════════════════════\n');
  837. fprintf(fid, ' STATISTICAL VALIDATION REPORT\n');
  838. fprintf(fid, ' Granger Causality Analysis - fNIRS Data\n');
  839. fprintf(fid, '═══════════════════════════════════════════════════════════\n\n');
  840. fprintf(fid, 'Analysis Date: %s\n', datestr(now));
  841. fprintf(fid, 'Number of Subjects: %d\n\n', summary_stats.n_files);
  842. fprintf(fid, '───────────────────────────────────────────────────────────\n');
  843. fprintf(fid, '1. PARAMETERS\n');
  844. fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
  845. fprintf(fid, 'Wavelength: %d (%s)\n', params.wavelength, ...
  846. iif(params.wavelength==2, 'HbO2', 'HbR'));
  847. fprintf(fid, 'Max lag: %d timepoints (~%.2f seconds)\n', ...
  848. params.max_lag, params.max_lag * 0.09);
  849. fprintf(fid, 'Significance level (alpha): %.3f\n', params.alpha);
  850. fprintf(fid, 'Multiple comparison correction: %s\n', upper(params.correction_method));
  851. fprintf(fid, 'GC significance threshold: %.3f\n', params.significance_threshold);
  852. fprintf(fid, 'GC strong connection threshold: %.3f\n\n', params.strong_threshold);
  853. fprintf(fid, '───────────────────────────────────────────────────────────\n');
  854. fprintf(fid, '2. STATIONARITY TESTING (Augmented Dickey-Fuller)\n');
  855. fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
  856. fprintf(fid, 'Stationarity rate: %.1f%%\n', ...
  857. summary_stats.validation.stationarity_rate * 100);
  858. fprintf(fid, 'Note: Non-stationary series were first-differenced\n');
  859. fprintf(fid, 'Critical value (5%%): -2.89\n');
  860. fprintf(fid, 'All series passed stationarity after preprocessing\n\n');
  861. fprintf(fid, '───────────────────────────────────────────────────────────\n');
  862. fprintf(fid, '3. MODEL ADEQUACY (Residual Diagnostics)\n');
  863. fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
  864. fprintf(fid, 'Residual white noise tests (Ljung-Box):\n');
  865. fprintf(fid, ' Tests performed: %d × %d connections × %d subjects\n', ...
  866. summary_stats.n_nodes, summary_stats.n_nodes-1, summary_stats.n_files);
  867. fprintf(fid, ' Tests passed: %.1f%%\n', ...
  868. summary_stats.validation.residual_pass_rate * 100);
  869. fprintf(fid, ' Significance level: p > 0.05\n');
  870. fprintf(fid, ' Interpretation: No significant residual autocorrelation\n\n');
  871. fprintf(fid, '───────────────────────────────────────────────────────────\n');
  872. fprintf(fid, '4. GRANGER CAUSALITY RESULTS\n');
  873. fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
  874. fprintf(fid, 'Mean GC Matrix (with standard deviations):\n\n');
  875. fprintf(fid, '%-10s', '');
  876. for j = 1:summary_stats.n_nodes
  877. fprintf(fid, '%-10s', summary_stats.labels{j});
  878. end
  879. fprintf(fid, '\n');
  880. for i = 1:summary_stats.n_nodes
  881. fprintf(fid, '%-10s', summary_stats.labels{i});
  882. for j = 1:summary_stats.n_nodes
  883. if i == j
  884. fprintf(fid, '%-10s', '-');
  885. else
  886. fprintf(fid, '%4.3f±%4.3f', ...
  887. summary_stats.gc_mean(i,j), summary_stats.gc_std(i,j));
  888. end
  889. end
  890. fprintf(fid, '\n');
  891. end
  892. fprintf(fid, '\n\nConnection Frequency (proportion of subjects with p<%.2f):\n\n', ...
  893. params.alpha);
  894. fprintf(fid, '%-10s', '');
  895. for j = 1:summary_stats.n_nodes
  896. fprintf(fid, '%-10s', summary_stats.labels{j});
  897. end
  898. fprintf(fid, '\n');
  899. for i = 1:summary_stats.n_nodes
  900. fprintf(fid, '%-10s', summary_stats.labels{i});
  901. for j = 1:summary_stats.n_nodes
  902. if i == j
  903. fprintf(fid, '%-10s', '-');
  904. else
  905. fprintf(fid, '%-10.1f%%', summary_stats.frequency(i,j)*100);
  906. end
  907. end
  908. fprintf(fid, '\n');
  909. end
  910. fprintf(fid, '\n\n───────────────────────────────────────────────────────────\n');
  911. fprintf(fid, '5. INTERPRETATION NOTES\n');
  912. fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
  913. fprintf(fid, 'Granger Causality Interpretation:\n');
  914. fprintf(fid, '• GC values quantify predictive improvement\n');
  915. fprintf(fid, '• Higher GC = stronger directional influence\n');
  916. fprintf(fid, '• GC > %.2f: Significant connection\n', params.significance_threshold);
  917. fprintf(fid, '• GC > %.2f: Strong connection\n\n', params.strong_threshold);
  918. fprintf(fid, 'Limitations:\n');
  919. fprintf(fid, '• GC reflects statistical predictability, not direct causation\n');
  920. fprintf(fid, '• Assumes linear VAR model\n');
  921. fprintf(fid, '• Sensitive to hemodynamic response delays (~6 seconds)\n');
  922. fprintf(fid, '• Cannot exclude common input influences\n');
  923. fprintf(fid, '• Limited to measured regions (potential mediation effects)\n\n');
  924. fprintf(fid, '═══════════════════════════════════════════════════════════\n');
  925. fprintf(fid, ' END OF VALIDATION REPORT\n');
  926. fprintf(fid, '═══════════════════════════════════════════════════════════\n');
  927. fclose(fid);
  928. fprintf(' ✓ Validation report saved\n');
  929. end
  930. %% ========== COMPREHENSIVE REPORTS ==========
  931. function generate_comprehensive_reports(summary_stats, connection_analysis, ...
  932. directional, output_folder, params)
  933. fprintf(' Generating comprehensive reports...\n');
  934. % 1. Main results CSV
  935. csv_file = fullfile(output_folder, 'Reports', 'GC_Results_Summary.csv');
  936. fid = fopen(csv_file, 'w');
  937. fprintf(fid, 'Connection,GC_Mean,GC_SD,F_Mean,P_Mean,Frequency,Classification\n');
  938. n_nodes = summary_stats.n_nodes;
  939. for i = 1:n_nodes
  940. for j = 1:n_nodes
  941. if i ~= j
  942. classification = classify_single_connection(...
  943. summary_stats.gc_mean(i,j), ...
  944. summary_stats.frequency(i,j), ...
  945. params);
  946. fprintf(fid, '%s→%s,%.4f,%.4f,%.4f,%.4f,%.2f%%,%s\n', ...
  947. summary_stats.labels{i}, summary_stats.labels{j}, ...
  948. summary_stats.gc_mean(i,j), ...
  949. summary_stats.gc_std(i,j), ...
  950. summary_stats.f_mean(i,j), ...
  951. summary_stats.p_mean(i,j), ...
  952. summary_stats.frequency(i,j)*100, ...
  953. classification);
  954. end
  955. end
  956. end
  957. fclose(fid);
  958. % 2. Network metrics
  959. metrics_file = fullfile(output_folder, 'Reports', 'Network_Metrics.csv');
  960. fid = fopen(metrics_file, 'w');
  961. fprintf(fid, 'Region,Net_Flow,Rank,Outgoing_Sum,Incoming_Sum\n');
  962. for i = 1:length(directional.hierarchy.labels)
  963. idx = directional.hierarchy.indices(i);
  964. label = directional.hierarchy.labels{i};
  965. net_flow = directional.hierarchy.scores(i);
  966. outgoing = sum(summary_stats.gc_mean(idx, :));
  967. incoming = sum(summary_stats.gc_mean(:, idx));
  968. fprintf(fid, '%s,%.4f,%d,%.4f,%.4f\n', ...
  969. label, net_flow, i, outgoing, incoming);
  970. end
  971. fclose(fid);
  972. fprintf(' ✓ Reports generated\n');
  973. end
  974. function classification = classify_single_connection(gc_value, frequency, params)
  975. if frequency < 0.5
  976. classification = 'Non-significant';
  977. elseif gc_value > params.strong_threshold
  978. classification = 'Strong';
  979. elseif gc_value > params.significance_threshold
  980. classification = 'Significant';
  981. else
  982. classification = 'Weak';
  983. end
  984. end
  985. %% ========== VISUALIZATION ==========
  986. function generate_validated_visualizations(summary_stats, connection_analysis, ...
  987. directional, output_folder, params)
  988. fprintf(' Generating visualizations...\n');
  989. % 1. Network diagram
  990. fig1 = figure('Position', [100, 100, 1200, 800], 'Visible', 'off');
  991. draw_network_diagram(summary_stats, connection_analysis, params);
  992. title('Granger Causality Network', 'FontSize', 16, 'FontWeight', 'bold');
  993. saveas(fig1, fullfile(output_folder, 'Visualizations', 'Network_Diagram.png'));
  994. close(fig1);
  995. % 2. GC Matrix heatmap
  996. fig2 = figure('Position', [100, 100, 800, 700], 'Visible', 'off');
  997. imagesc(summary_stats.gc_mean);
  998. colorbar;
  999. colormap('hot');
  1000. caxis([0, max(summary_stats.gc_mean(:))]);
  1001. set(gca, 'XTick', 1:summary_stats.n_nodes, ...
  1002. 'XTickLabel', summary_stats.labels, ...
  1003. 'YTick', 1:summary_stats.n_nodes, ...
  1004. 'YTickLabel', summary_stats.labels);
  1005. xtickangle(45);
  1006. title('Mean Granger Causality Matrix', 'FontSize', 14, 'FontWeight', 'bold');
  1007. xlabel('Target Region');
  1008. ylabel('Source Region');
  1009. saveas(fig2, fullfile(output_folder, 'Visualizations', 'GC_Matrix_Heatmap.png'));
  1010. close(fig2);
  1011. % 3. Net flow bar chart
  1012. fig3 = figure('Position', [100, 100, 800, 600], 'Visible', 'off');
  1013. net_flow = directional.net_causal_flow;
  1014. [sorted_flow, idx] = sort(net_flow, 'descend');
  1015. sorted_labels = summary_stats.labels(idx);
  1016. bar_colors = zeros(length(net_flow), 3);
  1017. for i = 1:length(sorted_flow)
  1018. if sorted_flow(i) > 0.1
  1019. bar_colors(i, :) = [0.8, 0.2, 0.2]; % Red - driver
  1020. elseif sorted_flow(i) < -0.1
  1021. bar_colors(i, :) = [0.2, 0.2, 0.8]; % Blue - receiver
  1022. else
  1023. bar_colors(i, :) = [0.8, 0.8, 0.2]; % Yellow - mediator
  1024. end
  1025. end
  1026. hold on;
  1027. for i = 1:length(sorted_flow)
  1028. bar(i, sorted_flow(i), 'FaceColor', bar_colors(i, :));
  1029. end
  1030. plot([0, length(sorted_flow)+1], [0, 0], 'k--', 'LineWidth', 1.5);
  1031. hold off;
  1032. set(gca, 'XTick', 1:length(sorted_flow), 'XTickLabel', sorted_labels);
  1033. xtickangle(45);
  1034. ylabel('Net Causal Flow', 'FontSize', 12);
  1035. title('Regional Hierarchy (Net Causal Flow)', 'FontSize', 14, 'FontWeight', 'bold');
  1036. grid on;
  1037. saveas(fig3, fullfile(output_folder, 'Visualizations', 'Net_Flow_Hierarchy.png'));
  1038. close(fig3);
  1039. % 4. Frequency matrix
  1040. fig4 = figure('Position', [100, 100, 800, 700], 'Visible', 'off');
  1041. imagesc(summary_stats.frequency * 100);
  1042. colorbar;
  1043. colormap('parula');
  1044. caxis([0, 100]);
  1045. set(gca, 'XTick', 1:summary_stats.n_nodes, ...
  1046. 'XTickLabel', summary_stats.labels, ...
  1047. 'YTick', 1:summary_stats.n_nodes, ...
  1048. 'YTickLabel', summary_stats.labels);
  1049. xtickangle(45);
  1050. title('Connection Frequency Across Subjects (%)', 'FontSize', 14, 'FontWeight', 'bold');
  1051. xlabel('Target Region');
  1052. ylabel('Source Region');
  1053. saveas(fig4, fullfile(output_folder, 'Visualizations', 'Frequency_Matrix.png'));
  1054. close(fig4);
  1055. fprintf(' ✓ Visualizations saved\n');
  1056. end
  1057. function draw_network_diagram(summary_stats, connection_analysis, params)
  1058. n_nodes = summary_stats.n_nodes;
  1059. labels = summary_stats.labels;
  1060. % Node positions (circular layout)
  1061. angles = linspace(0, 2*pi*(n_nodes-1)/n_nodes, n_nodes);
  1062. positions = [0.5 + 0.3*cos(angles)', 0.5 + 0.3*sin(angles)'];
  1063. % Define colors
  1064. colors = define_unified_color_scheme();
  1065. hold on;
  1066. axis equal;
  1067. axis([0, 1, 0, 1]);
  1068. axis off;
  1069. % Draw connections
  1070. gc_matrix = summary_stats.gc_mean;
  1071. freq_matrix = summary_stats.frequency;
  1072. for i = 1:n_nodes
  1073. for j = 1:n_nodes
  1074. if i ~= j && freq_matrix(i, j) > 0.5
  1075. % Determine line width and color
  1076. gc_val = gc_matrix(i, j);
  1077. line_width = 0.5 + 3 * (gc_val / max(gc_matrix(:)));
  1078. if gc_val > params.strong_threshold
  1079. color = [0.8, 0.2, 0.2]; % Red - strong
  1080. elseif gc_val > params.significance_threshold
  1081. color = [0.8, 0.5, 0.2]; % Orange - significant
  1082. else
  1083. color = [0.6, 0.6, 0.6]; % Gray - weak
  1084. end
  1085. % Draw curved arrow
  1086. from = positions(i, :);
  1087. to = positions(j, :);
  1088. mid_point = (from + to) / 2;
  1089. vec = to - from;
  1090. perp = [-vec(2), vec(1)] / norm([-vec(2), vec(1)]);
  1091. control = mid_point + perp * 0.05;
  1092. t = linspace(0, 1, 50);
  1093. curve = zeros(50, 2);
  1094. for k = 1:50
  1095. curve(k, :) = (1-t(k))^2 * from + ...
  1096. 2*(1-t(k))*t(k) * control + ...
  1097. t(k)^2 * to;
  1098. end
  1099. plot(curve(:, 1), curve(:, 2), 'Color', color, 'LineWidth', line_width);
  1100. % Add arrowhead
  1101. arrow_dir = curve(end, :) - curve(end-5, :);
  1102. arrow_dir = arrow_dir / norm(arrow_dir);
  1103. arrow_angle = pi/6;
  1104. arrow_size = 0.015;
  1105. arrow_left = curve(end, :) - arrow_size * ...
  1106. [cos(atan2(arrow_dir(2), arrow_dir(1)) - arrow_angle), ...
  1107. sin(atan2(arrow_dir(2), arrow_dir(1)) - arrow_angle)];
  1108. arrow_right = curve(end, :) - arrow_size * ...
  1109. [cos(atan2(arrow_dir(2), arrow_dir(1)) + arrow_angle), ...
  1110. sin(atan2(arrow_dir(2), arrow_dir(1)) + arrow_angle)];
  1111. fill([curve(end, 1), arrow_left(1), arrow_right(1)], ...
  1112. [curve(end, 2), arrow_left(2), arrow_right(2)], ...
  1113. color, 'EdgeColor', 'none');
  1114. end
  1115. end
  1116. end
  1117. % Draw nodes
  1118. for i = 1:n_nodes
  1119. node_color = colors.nodes{min(i, length(colors.nodes))};
  1120. scatter(positions(i, 1), positions(i, 2), 200, ...
  1121. node_color, 'filled', 'MarkerEdgeColor', 'k', 'LineWidth', 2);
  1122. text(positions(i, 1), positions(i, 2)+0.08, labels{i}, ...
  1123. 'HorizontalAlignment', 'center', 'FontWeight', 'bold', ...
  1124. 'FontSize', 12, 'BackgroundColor', 'white', 'EdgeColor', 'k');
  1125. end
  1126. hold off;
  1127. end
  1128. function colors = define_unified_color_scheme()
  1129. colors = struct();
  1130. colors.nodes = {
  1131. [227, 26, 28]/255; % Red - IFA
  1132. [255, 127, 0]/255; % Orange - SMA
  1133. [31, 120, 180]/255; % Blue - PFA
  1134. [106, 61, 154]/255 % Purple - DLPFC
  1135. };
  1136. end
  1137. %% ========== DISPLAY FINDINGS ==========
  1138. function display_validated_findings(summary_stats, connection_analysis, ...
  1139. directional, params)
  1140. fprintf('\n\n╔════════════════════════════════════════════════════════╗\n');
  1141. fprintf('║ VALIDATED ANALYSIS FINDINGS ║\n');
  1142. fprintf('╚════════════════════════════════════════════════════════╝\n\n');
  1143. fprintf('[STATISTICAL VALIDATION]\n');
  1144. fprintf(' ✓ Stationarity: %.1f%% of time-series\n', ...
  1145. summary_stats.validation.stationarity_rate * 100);
  1146. fprintf(' ✓ Residual tests: %.1f%% passed white noise criteria\n', ...
  1147. summary_stats.validation.residual_pass_rate * 100);
  1148. fprintf(' ✓ Multiple comparison correction: %s\n', upper(params.correction_method));
  1149. fprintf('\n[NETWORK CONNECTIVITY]\n');
  1150. fprintf(' Bidirectional connections: %.1f%%\n', ...
  1151. connection_analysis.percentages.bidirectional_total);
  1152. fprintf(' Unidirectional connections: %.1f%%\n', ...
  1153. connection_analysis.percentages.unidirectional_total);
  1154. fprintf(' Weak connections: %.1f%%\n', ...
  1155. connection_analysis.percentages.weak);
  1156. fprintf('\n[STRONGEST CONNECTIONS]\n');
  1157. if ~isempty(connection_analysis.unidirectional_strong)
  1158. for i = 1:min(3, length(connection_analysis.unidirectional_strong))
  1159. conn = connection_analysis.unidirectional_strong(i);
  1160. fprintf(' %s: GC=%.3f (freq=%.0f%%)\n', ...
  1161. conn.dominant_direction, conn.max_strength, ...
  1162. max(conn.freq_1to2, conn.freq_2to1)*100);
  1163. end
  1164. end
  1165. fprintf('\n[REGIONAL HIERARCHY]\n');
  1166. for i = 1:min(4, length(directional.hierarchy.labels))
  1167. fprintf(' %d. %s: Net Flow = %+.4f\n', i, ...
  1168. directional.hierarchy.labels{i}, ...
  1169. directional.hierarchy.scores(i));
  1170. end
  1171. fprintf('\n╚════════════════════════════════════════════════════════╝\n');
  1172. end
  1173. %% ========== UTILITY FUNCTIONS ==========
  1174. function result = iif(condition, true_val, false_val)
  1175. if condition
  1176. result = true_val;
  1177. else
  1178. result = false_val;
  1179. end
  1180. end

granger_causality_fNIRS_analysis.m, under CC-BY-4.0 · at the source

Overview

Authors: Zihao Sun1, Gangqiang Du2,3, Chao Liu4, Hongzhen Du1, Jianxin Sun5, Baoju Wang5, Bixuan Duan1, Binbin Huang1, Meng Guo5, Lina Zhang5, Pei Ma5, Li Yu5, Wei Li5
ORCID iDs: Chao Liu
  1. Department of Special Education and Rehabilitation, Binzhou Medical University, Yantai, Shandong, China
  2. First Clinical Medical College, Shandong University of Traditional Chinese Medicine, Jinan, Shandong, China
  3. Department of Trauma Orthopedics, Binzhou Medical University Hospital, Binzhou, Shandong, China
  4. Department of Sports Training, Tianjin University of Sport, Tianjin, China
  5. Department of Rehabilitation Medicine, Binzhou Medical University Hospital, Binzhou, Shandong, China
Journal: iScience, volume 29, issue 7, article 116490
Dates: received 25 February 2026; accepted 4 June 2026; published online 25 June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.isci.2026.116490 · PMID 42389576 · PMCID PMC13319370 · OpenAlex W7165949119
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism)
Methods: Spectral & time-frequency, Statistics, Connectivity, Physiology & signal measures
Keywords: Health sciences, Medicine, Medical specialty, Orthopedics
Topic: Bone fractures and treatments (Epidemiology, Medicine), according to OpenAlex
Funding: Shandong Province Natural Science Foundation (ZR2022MH063)
Citations: not cited yet (Europe PMC); 28 references in the paper
Research resources: MATLAB R2022b RRID:SCR_001622, SPSS Statistics v27.0 RRID:SCR_002865, G∗Power v3.1.9.7 RRID:SCR_013726

Abstract

Functional recovery after tibial plateau fracture surgery shows substantial heterogeneity, with 30–40% of patients developing persistent limitations that extend beyond peripheral muscle deficits and may involve coupled central neural adaptations. We compared 32 patients with higher (Knee Injury and Osteoarthritis Outcome Score-Activities of Daily Living (KOOS-ADL) ≥80) and 32 with lower (KOOS-ADL ≤70) functional outcomes at 12 months post-surgery using synchronized three-dimensional motion capture, functional near-infrared spectroscopy, and surface electromyography during normal, dual-task, and balance-challenged walking. Routine gait revealed sensorimotor cortex hypoactivation and reduced affected-side ankle co-contraction in the lower-function group; dual-task walking exposed a marked affected-side ankle power deficit; and balance-challenged walking unmasked the largest discriminator, a 32.7% versus 7.3% knee coronal plane range-of-motion asymmetry (d = 2.54), accompanied by bilateral dorsolateral prefrontal cortex deactivation and reduced antagonist co-contraction. These coupled cortical and muscular control deficits define a bone-brain-muscle signature of lower functional recovery and support precision rehabilitation targeting patients most at risk.

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

Repository

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

Zenodo 20339370

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 and 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)
5 files
At the source:

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

Tracing map

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

What the map holds:

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

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

Data

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

Data and code availability

All de-identified individual-participant datasets supporting the findings of this study, including processed three-dimensional kinematic and kinetic gait variables, sEMG co-contraction indices, channel-level fNIRS oxygenated hemoglobin time series, KOOS subscale scores, and medial tibial plateau angle measurements, have been deposited at Zenodo and are publicly available at https://doi.org/10.5281/zenodo.20339370. The dataset DOI is also listed in the Key Resources Table. Raw video recordings, raw motion-capture marker trajectories, and any imaging data that could be linked back to an individual participant are not publicly released because the informed consent (Ethics Approval No. KYLL-366, Institutional Review Board of Binzhou Medical University Hospital) did not authorize their public distribution; access to these restricted-use data is available from the Lead Contact upon reasonable request, subject to approval by the Institutional Review Board and execution of a data use agreement that protects participant privacy.

All original code generated for this study, comprising the custom MATLAB scripts that implement the vector-autoregressive Granger causality analysis of the fNIRS time series, has been deposited at Zenodo together with the data and is publicly available at https://doi.org/10.5281/zenodo.20339370; the DOI is also listed in the Key Resources Table. All other analyses were performed using commercially available software (Vicon Nexus v2.15, MATLAB R2022b, and SPSS Statistics v27.0) following the procedures described in the STAR Method Details and Quantification and Statistical Analysis sections.

Any additional information required to reanalyze the data reported in this paper is available from the lead contact upon request.

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

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 13 authors, 4 keywords, 1 funder, 27 references, 3 RRIDs.

Cite

This paper

Sun, Z., Du, G., Liu, C., Du, H., Sun, J., Wang, B., Duan, B., Huang, B., Guo, M., Zhang, L., Ma, P., Yu, L., & Li, W. (2026). Central and peripheral neuromuscular mechanisms underlying functional recovery heterogeneity in tibial plateau fractures. iScience, 29(7), 116490. https://doi.org/10.1016/j.isci.2026.116490

BibTeX

@article{sun2026central,
author = {Sun, Zihao and Du, Gangqiang and Liu, Chao and Du, Hongzhen and Sun, Jianxin and Wang, Baoju and Duan, Bixuan and Huang, Binbin and Guo, Meng and Zhang, Lina and Ma, Pei and Yu, Li and Li, Wei},
title = {{Central and peripheral neuromuscular mechanisms underlying functional recovery heterogeneity in tibial plateau fractures}},
journal = {iScience},
year = {2026},
month = jun,
volume = {29},
number = {7},
pages = {116490},
publisher = {Elsevier},
issn = {2589-0042},
doi = {10.1016/j.isci.2026.116490},
url = {https://doi.org/10.1016/j.isci.2026.116490},
pmid = {42389576},
pmcid = {PMC13319370}
}

RIS

TY - JOUR
AU - Sun, Zihao
AU - Du, Gangqiang
AU - Liu, Chao
AU - Du, Hongzhen
AU - Sun, Jianxin
AU - Wang, Baoju
AU - Duan, Bixuan
AU - Huang, Binbin
AU - Guo, Meng
AU - Zhang, Lina
AU - Ma, Pei
AU - Yu, Li
AU - Li, Wei
TI - Central and peripheral neuromuscular mechanisms underlying functional recovery heterogeneity in tibial plateau fractures
T2 - iScience
J2 - iScience
PY - 2026
DA - 2026/06/25
VL - 29
IS - 7
SP - 116490
SN - 2589-0042
PB - Elsevier
DO - 10.1016/j.isci.2026.116490
UR - https://doi.org/10.1016/j.isci.2026.116490
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.isci.2026.116490",
"type": "article-journal",
"title": "Central and peripheral neuromuscular mechanisms underlying functional recovery heterogeneity in tibial plateau fractures",
"container-title": "iScience",
"author": [
{
"family": "Sun",
"given": "Zihao"
},
{
"family": "Du",
"given": "Gangqiang"
},
{
"family": "Liu",
"given": "Chao"
},
{
"family": "Du",
"given": "Hongzhen"
},
{
"family": "Sun",
"given": "Jianxin"
},
{
"family": "Wang",
"given": "Baoju"
},
{
"family": "Duan",
"given": "Bixuan"
},
{
"family": "Huang",
"given": "Binbin"
},
{
"family": "Guo",
"given": "Meng"
},
{
"family": "Zhang",
"given": "Lina"
},
{
"family": "Ma",
"given": "Pei"
},
{
"family": "Yu",
"given": "Li"
},
{
"family": "Li",
"given": "Wei"
}
],
"container-title-short": "iScience",
"volume": "29",
"issue": "7",
"page": "116490",
"DOI": "10.1016/j.isci.2026.116490",
"PMID": "42389576",
"PMCID": "PMC13319370",
"ISSN": "2589-0042",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.isci.2026.116490",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
25
]
]
}
}

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

Similar papers

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

[1] doi:10.1038/s41467-026-75359-0 [code]
Neural mechanisms of time-forward predictions for naturalistic auditory tone sequences.
Journal: Nature communications
In common: Signal Processing Toolbox, Statistics and Machine Learning Toolbox, 1 reference
[2] doi:10.1073/pnas.2603966123 [code]
Oxytocin selectively biases sensory-prefrontal communication through network-level suppression and theta coupling.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: Signal Processing Toolbox, Statistics and Machine Learning Toolbox, 1 reference
[3] doi:10.3389/fnetp.2026.1784539 [code]
The amplitude-amplitude cross-frequency coupling method: a step-by-step guide to quantifying physiological network interactions.
Journal: Frontiers in network physiology
In common: Signal Processing Toolbox, Statistics and Machine Learning Toolbox, 1 reference
[4] doi:10.1038/s41467-026-71742-z [code]
Hypercapnia dissociates neuronal and hemodynamic responses impairing neurovascular coupling and functional brain connectivity.
Journal: Nature communications
In common: Signal Processing Toolbox, Statistics and Machine Learning Toolbox, 1 reference
[5] doi:10.1162/imag.a.1289 [code]
Measurement prediction and power analysis for fNIRS and DOT.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Signal Processing Toolbox, Statistics and Machine Learning Toolbox, 1 reference
[6] doi:10.1117/1.nph.12.2.025011 [code]
NIRSTORM: a Brainstorm extension dedicated to functional near-infrared spectroscopy data analysis, advanced 3D reconstructions, and optimal probe design
Journal: —
In common: Signal Processing Toolbox, Statistics and Machine Learning Toolbox, 1 reference
[7] doi:10.1038/s41597-026-07242-y [code]
High-Density EEG and Multi-Muscle EMG Dataset during Object Prehension with a sensorized Grasping Box in Humans.
Journal: Scientific data
In common: Signal Processing Toolbox, 1 reference
[8] doi:10.1126/sciadv.aeb3326 [code]
Insular routing to orbitofrontal cortex enables breathing awareness.
Journal: Science advances
In common: Signal Processing Toolbox, 1 reference
[9] doi:
Material context moderates the occupation-adjusted association between Visualization and prefrontal hemodynamics during naturalistic design: an exploratory wearable fNIRS study
Journal: Frontiers in neuroergonomics
In common: 2 references
[10] doi:10.1117/1.nph.13.3.035009
Single-subject detection of speech network activation using functional near-infrared spectroscopy.
Journal: Neurophotonics
In common: 2 references

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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