Central and peripheral neuromuscular mechanisms underlying functional recovery heterogeneity in tibial plateau fractures.
The 5 matches
- [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] § 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] § 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] § 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] § 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
- function unified_nirs_gc_analysis_validated()
- % ===================================================================
- % UNIFIED NIRS GRANGER CAUSALITY ANALYSIS SYSTEM - VALIDATED VERSION
- % Version: 2.1 (Statistically Validated)
- % ===================================================================
- %
- % DESCRIPTION:
- % Performs Granger causality analysis on fNIRS data with rigorous
- % statistical validation including:
- % - Augmented Dickey-Fuller stationarity tests
- % - Ljung-Box residual white noise tests
- % - F-statistic significance testing with p-values
- % - FDR/Bonferroni multiple comparison correction
- % - Model adequacy diagnostics
- %
- % METHODOLOGY REFERENCES:
- % - Seth et al., 2015, NeuroImage (doi: 10.1016/j.neuroimage.2015.03.061)
- % - Barnett & Seth, 2014, J Neurosci Methods (MVGC toolbox)
- % - Granger, 1969, Econometrica (Original GC framework)
- %
- % INPUT:
- % - .nirs files (Homer2/Homer3 format)
- % - Required fields: d (signal data), t (time), SD (source-detector info)
- %
- % OUTPUT:
- % - GC matrices with statistical significance
- % - Network topology metrics
- % - Statistical validation reports
- % - Visualization of causal networks
- % - Detailed CSV reports
- %
- % VALIDATION PIPELINE:
- % 1. Stationarity testing (ADF) -> differencing if needed
- % 2. Optimal lag selection (AIC)
- % 3. VAR model estimation
- % 4. GC computation with F-statistics
- % 5. Residual diagnostics (Ljung-Box)
- % 6. Multiple comparison correction
- %
- % AUTHOR: Z. Sun (study team), Binzhou Medical University
- % DATE: 2025-01-17
- % LICENSE: CC BY 4.0 (data and code; see LICENSE.txt)
- % ===================================================================
- clc; close all;
- fprintf('=====================================\n');
- fprintf(' VALIDATED NIRS GC ANALYSIS\n');
- fprintf(' Version 2.1 - Statistical Rigor\n');
- fprintf('=====================================\n\n');
- %% 1. FILE SELECTION
- folder_path = uigetdir(pwd, 'Select folder containing .nirs files');
- if folder_path == 0
- fprintf('Operation cancelled\n');
- return;
- end
- fprintf('Selected folder: %s\n', folder_path);
- nirs_files = dir(fullfile(folder_path, '*.nirs'));
- if isempty(nirs_files)
- fprintf('Error: No .nirs files found\n');
- return;
- end
- fprintf('Found %d .nirs files\n\n', length(nirs_files));
- %% 2. PARAMETER SETUP
- params = setup_validated_parameters();
- %% 3. CREATE OUTPUT STRUCTURE
- timestamp = datestr(now, 'yyyymmdd_HHMMSS');
- output_folder = fullfile(folder_path, sprintf('Validated_GC_Results_%s', timestamp));
- mkdir(output_folder);
- % Create subfolders
- mkdir(fullfile(output_folder, 'Statistics'));
- mkdir(fullfile(output_folder, 'Visualizations'));
- mkdir(fullfile(output_folder, 'Reports'));
- fprintf('\nResults will be saved to: %s\n\n', output_folder);
- %% 4. BATCH PROCESSING
- fprintf('--- Starting Validated Analysis ---\n');
- [all_results, processing_summary] = batch_process_validated(nirs_files, folder_path, params);
- %% 5. AGGREGATE AND ANALYZE
- if ~isempty(all_results)
- fprintf('\n--- Generating Comprehensive Reports ---\n');
- % Aggregate results
- summary_stats = aggregate_validated_results(all_results, processing_summary);
- % Generate statistical validation report
- generate_validation_report(summary_stats, output_folder, params);
- % Perform connection analysis
- connection_analysis = unified_connection_classification(summary_stats, params);
- % Directional analysis
- directional_analysis = analyze_causal_direction(summary_stats);
- % Generate comprehensive outputs
- generate_comprehensive_reports(summary_stats, connection_analysis, ...
- directional_analysis, output_folder, params);
- % Create visualizations
- generate_validated_visualizations(summary_stats, connection_analysis, ...
- directional_analysis, output_folder, params);
- % Display findings
- display_validated_findings(summary_stats, connection_analysis, ...
- directional_analysis, params);
- else
- fprintf('No files were successfully processed\n');
- end
- fprintf('\n=====================================\n');
- fprintf(' Analysis Complete!\n');
- fprintf('Results: %s\n', output_folder);
- fprintf('=====================================\n');
- end
- %% ========== PARAMETER SETUP ==========
- function params = setup_validated_parameters()
- fprintf('╔════════════════════════════════════╗\n');
- fprintf('║ VALIDATED PARAMETER SETUP ║\n');
- fprintf('╚════════════════════════════════════╝\n\n');
- fprintf('Default parameters (recommended for peer-review):\n');
- fprintf(' • Wavelength: 2 (HbO2, long wavelength)\n');
- fprintf(' • Analysis type: Brain region (4 ROIs)\n');
- fprintf(' • Max lag: 5 (~0.45s at 11Hz)\n');
- fprintf(' • Significance: p<0.01 (F-test)\n');
- fprintf(' • Correction: FDR\n');
- fprintf(' • GC thresholds:\n');
- fprintf(' - Significance: >0.15\n');
- fprintf(' - Strong: >0.20\n');
- fprintf(' • Statistical tests:\n');
- fprintf(' - ADF stationarity test\n');
- fprintf(' - Ljung-Box residual test\n\n');
- use_defaults = input('Use default parameters? (y/n) [default=y]: ', 's');
- if isempty(use_defaults) || strcmpi(use_defaults, 'y')
- params = get_default_params();
- else
- params = get_custom_params();
- end
- fprintf('\n✓ Parameters configured\n\n');
- end
- function params = get_default_params()
- params = struct();
- params.wavelength = 2;
- params.use_brain_regions = true;
- params.max_lag = 5;
- params.alpha = 0.01;
- params.correction_method = 'fdr';
- params.significance_threshold = 0.15;
- params.strong_threshold = 0.20;
- params.balance_threshold = 0.05;
- params.display_threshold = 0.05;
- % Statistical test parameters
- params.adf_alpha = 0.05;
- params.ljung_box_lag = 20;
- params.ljung_box_alpha = 0.05;
- params.min_samples = 100;
- end
- %% ========== BATCH PROCESSING ==========
- function [all_results, processing_summary] = batch_process_validated(nirs_files, folder_path, params)
- all_results = {};
- processing_summary = struct();
- processing_summary.successful_files = {};
- processing_summary.failed_files = {};
- processing_summary.processing_times = [];
- processing_summary.warnings = {};
- n_files = length(nirs_files);
- for i = 1:n_files
- fprintf('\n╔════════════════════════════════════╗\n');
- fprintf('║ File %d/%d: %-23s║\n', i, n_files, nirs_files(i).name);
- fprintf('╚════════════════════════════════════╝\n');
- filepath = fullfile(folder_path, nirs_files(i).name);
- try
- tic;
- fprintf(' [1/4] Loading data...\n');
- data = load(filepath, '-mat');
- if ~isfield(data, 'd')
- error('Missing signal data field "d"');
- end
- fprintf(' [2/4] Preprocessing and validation...\n');
- result = analyze_single_file_validated(data, params);
- result.filename = nirs_files(i).name;
- fprintf(' [3/4] Statistical tests...\n');
- fprintf(' ✓ Stationarity: %d/%d series stationary\n', ...
- sum(result.validation.stationarity_passed), ...
- length(result.validation.stationarity_passed));
- fprintf(' ✓ Residuals: %d/%d tests passed\n', ...
- result.validation.n_white_noise_passed, ...
- result.validation.n_residual_tests);
- fprintf(' [4/4] Computing network metrics...\n');
- processing_time = toc;
- all_results{end+1} = result;
- processing_summary.successful_files{end+1} = nirs_files(i).name;
- processing_summary.processing_times(end+1) = processing_time;
- if ~isempty(result.validation.warnings)
- processing_summary.warnings{end+1} = struct(...
- 'file', nirs_files(i).name, ...
- 'warnings', {result.validation.warnings});
- end
- fprintf(' ✓ Success (%.2f seconds)\n', processing_time);
- catch ME
- fprintf(' ✗ Failed: %s\n', ME.message);
- processing_summary.failed_files{end+1} = nirs_files(i).name;
- end
- end
- fprintf('\n\n╔════════════════════════════════════╗\n');
- fprintf('║ Processing Summary ║\n');
- fprintf('╠════════════════════════════════════╣\n');
- fprintf('║ Successful: %3d/%3d ║\n', ...
- length(processing_summary.successful_files), n_files);
- fprintf('║ Failed: %3d/%3d ║\n', ...
- length(processing_summary.failed_files), n_files);
- if ~isempty(processing_summary.warnings)
- fprintf('║ Warnings: %3d files ║\n', ...
- length(processing_summary.warnings));
- end
- fprintf('╚════════════════════════════════════╝\n');
- end
- %% ========== SINGLE FILE ANALYSIS ==========
- function result = analyze_single_file_validated(data, params)
- % Extract and validate data
- signal_data = data.d;
- [n_samples, n_channels] = size(signal_data);
- fprintf(' Data: %d samples × %d channels\n', n_samples, n_channels);
- if n_samples < params.min_samples
- warning('Low sample count (%d < %d recommended)', n_samples, params.min_samples);
- end
- % Determine sampling rate
- if isfield(data, 't') && length(data.t) > 1
- fs = 1 / mean(diff(data.t));
- else
- fs = 10;
- warning('Sampling rate not found, assuming 10 Hz');
- end
- fprintf(' Sampling rate: %.2f Hz\n', fs);
- % Select wavelength
- if params.wavelength == 1
- selected_data = signal_data(:, 1:2:min(n_channels, 69));
- else
- selected_data = signal_data(:, 2:2:min(n_channels, 70));
- end
- fprintf(' Selected: %d channels (wavelength %d)\n', ...
- size(selected_data, 2), params.wavelength);
- % Preprocess
- processed_data = safe_preprocess_data(selected_data, fs);
- % Perform region-based analysis with full validation
- if params.use_brain_regions
- [gc_results, labels, validation] = perform_validated_region_analysis(...
- processed_data, params, fs);
- else
- [gc_results, labels, validation] = perform_validated_channel_analysis(...
- processed_data, params, fs);
- end
- % Apply multiple comparison correction
- gc_results = apply_correction(gc_results, params);
- % Package results
- result = struct();
- result.gc_matrix = gc_results.gc_matrix;
- result.f_statistics = gc_results.f_statistics;
- result.p_values = gc_results.p_values;
- result.p_corrected = gc_results.p_corrected;
- result.significant = gc_results.p_corrected < params.alpha;
- result.optimal_lags = gc_results.optimal_lags;
- result.labels = labels;
- result.parameters = params;
- result.validation = validation;
- result.sample_rate = fs;
- result.n_samples = n_samples;
- end
- %% ========== VALIDATED REGION ANALYSIS ==========
- function [gc_results, labels, validation] = perform_validated_region_analysis(processed_data, params, fs)
- fprintf(' Performing brain region analysis...\n');
- % Define brain regions
- regions = define_brain_regions();
- labels = {regions.name};
- n_regions = length(regions);
- fprintf(' Regions: ');
- fprintf('%s, ', labels{1:end-1});
- fprintf('%s\n', labels{end});
- % Average channels within each region
- regional_data = zeros(size(processed_data, 1), n_regions);
- for i = 1:n_regions
- regional_data(:, i) = mean(processed_data(:, regions(i).channels), 2);
- end
- % VALIDATION STEP 1: Stationarity Testing
- fprintf(' [Validation] Testing stationarity...\n');
- [regional_data, stationarity_results] = ensure_stationarity(regional_data, params, labels);
- % Initialize result matrices
- n_regions = size(regional_data, 2);
- gc_matrix = zeros(n_regions, n_regions);
- f_statistics = zeros(n_regions, n_regions);
- p_values = ones(n_regions, n_regions);
- optimal_lags = zeros(n_regions, n_regions);
- % Residual test storage
- residual_tests = cell(n_regions, n_regions);
- % COMPUTE GRANGER CAUSALITY with full statistics
- fprintf(' Computing GC with statistical tests...\n');
- for i = 1:n_regions
- for j = 1:n_regions
- if i ~= j
- X = regional_data(:, i); % Potential cause
- Y = regional_data(:, j); % Effect
- % Select optimal lag using AIC
- [optimal_lag, aic_values] = select_optimal_lag_aic(X, Y, params.max_lag);
- optimal_lags(i, j) = optimal_lag;
- % Compute GC with full statistics
- [gc_val, f_stat, p_val, residuals] = compute_gc_with_full_stats(...
- X, Y, optimal_lag);
- gc_matrix(i, j) = gc_val;
- f_statistics(i, j) = f_stat;
- p_values(i, j) = p_val;
- % VALIDATION STEP 2: Residual Testing
- [is_white_noise, lb_stat, lb_p] = ljung_box_test(...
- residuals, params.ljung_box_lag);
- residual_tests{i, j} = struct(...
- 'is_white_noise', is_white_noise, ...
- 'Q_statistic', lb_stat, ...
- 'p_value', lb_p);
- end
- end
- end
- % Package results
- gc_results = struct();
- gc_results.gc_matrix = gc_matrix;
- gc_results.f_statistics = f_statistics;
- gc_results.p_values = p_values;
- gc_results.optimal_lags = optimal_lags;
- % VALIDATION RESULTS
- validation = struct();
- validation.stationarity_results = stationarity_results;
- validation.stationarity_passed = stationarity_results.all_stationary;
- validation.residual_tests = residual_tests;
- % Count white noise tests passed
- n_tests = 0;
- n_passed = 0;
- warnings = {};
- for i = 1:n_regions
- for j = 1:n_regions
- if i ~= j
- n_tests = n_tests + 1;
- if residual_tests{i, j}.is_white_noise
- n_passed = n_passed + 1;
- else
- warnings{end+1} = sprintf('%s→%s: residual autocorrelation (p=%.4f)', ...
- labels{i}, labels{j}, residual_tests{i, j}.p_value);
- end
- end
- end
- end
- validation.n_residual_tests = n_tests;
- validation.n_white_noise_passed = n_passed;
- validation.warnings = warnings;
- fprintf(' ✓ Validation: %d/%d residual tests passed\n', n_passed, n_tests);
- if ~isempty(warnings)
- fprintf(' ⚠ %d warnings (see detailed report)\n', length(warnings));
- end
- end
- %% ========== STATIONARITY TESTING AND CORRECTION ==========
- function [data_stationary, results] = ensure_stationarity(data, params, labels)
- n_series = size(data, 2);
- is_stationary = false(n_series, 1);
- adf_statistics = zeros(n_series, 1);
- p_values = zeros(n_series, 1);
- differenced = false(n_series, 1);
- data_stationary = data;
- for i = 1:n_series
- [is_stat, adf_stat, p_val] = adf_test(data(:, i), params.adf_alpha);
- is_stationary(i) = is_stat;
- adf_statistics(i) = adf_stat;
- p_values(i) = p_val;
- if ~is_stat
- % Apply first-order differencing
- data_stationary(:, i) = [0; diff(data(:, i))];
- differenced(i) = true;
- % Re-test
- [is_stat_after, ~, ~] = adf_test(data_stationary(:, i), params.adf_alpha);
- is_stationary(i) = is_stat_after;
- if nargin > 2 && i <= length(labels)
- fprintf(' ⚠ %s: non-stationary (p=%.4f), differenced\n', ...
- labels{i}, p_val);
- end
- end
- end
- results = struct();
- results.all_stationary = is_stationary;
- results.adf_statistics = adf_statistics;
- results.p_values = p_values;
- results.differenced = differenced;
- results.n_stationary = sum(is_stationary);
- results.n_total = n_series;
- end
- %% ========== AUGMENTED DICKEY-FULLER TEST ==========
- function [is_stationary, adf_stat, p_value] = adf_test(data, alpha)
- % Augmented Dickey-Fuller test for unit root (non-stationarity)
- %
- % H0: Series has unit root (non-stationary)
- % H1: Series is stationary
- %
- % Returns:
- % is_stationary: true if we reject H0 (series is stationary)
- % adf_stat: test statistic
- % p_value: approximate p-value
- if nargin < 2
- alpha = 0.05;
- end
- % Remove mean
- data = data - mean(data);
- % Determine lag length using Schwert criterion
- n = length(data);
- max_lag = floor(12 * (n/100)^0.25);
- % Construct regression: Δy(t) = α + β*y(t-1) + Σγ_i*Δy(t-i) + ε(t)
- y_lag = data(1:end-1);
- delta_y = diff(data);
- % Build design matrix
- X = y_lag(1:end-1); % y(t-1)
- Y = delta_y(2:end); % Δy(t)
- % Add lagged differences
- for lag = 1:min(max_lag, length(delta_y)-2)
- X = [X, delta_y(2-lag:end-lag)];
- end
- % Add constant
- X = [ones(size(X, 1), 1), X];
- % OLS regression
- [b, ~, ~, ~, stats] = regress(Y, X);
- % ADF statistic is t-stat of y(t-1) coefficient (second column)
- adf_stat = b(2) / sqrt(stats(1,1) * (X'*X)^(-1) * stats(1,1));
- % Critical values (MacKinnon, 1996) for constant, no trend
- % Sample size adjusted
- if n <= 25
- cv_1pct = -3.75;
- cv_5pct = -3.00;
- cv_10pct = -2.63;
- elseif n <= 50
- cv_1pct = -3.58;
- cv_5pct = -2.93;
- cv_10pct = -2.60;
- else
- cv_1pct = -3.51;
- cv_5pct = -2.89;
- cv_10pct = -2.58;
- end
- % Determine stationarity
- is_stationary = (adf_stat < cv_5pct);
- % Approximate p-value
- if adf_stat < cv_1pct
- p_value = 0.01;
- elseif adf_stat < cv_5pct
- p_value = 0.05;
- elseif adf_stat < cv_10pct
- p_value = 0.10;
- else
- p_value = 0.15;
- end
- end
- %% ========== OPTIMAL LAG SELECTION (AIC) ==========
- function [optimal_lag, aic_values] = select_optimal_lag_aic(X, Y, max_lag)
- % Select optimal VAR lag order using Akaike Information Criterion
- %
- % AIC = 2k - 2ln(L)
- % where k = number of parameters, L = likelihood
- %
- % Lower AIC indicates better model
- n = length(Y);
- aic_values = zeros(max_lag, 1);
- for lag = 1:max_lag
- if lag >= n
- aic_values(lag) = Inf;
- continue;
- end
- % Build full model
- Y_data = Y(lag+1:end);
- X_full = [];
- % Add lags of Y
- for i = 1:lag
- X_full = [X_full, Y(lag+1-i:end-i)];
- end
- % Add lags of X
- for i = 1:lag
- X_full = [X_full, X(lag+1-i:end-i)];
- end
- % Add constant
- X_full = [ones(size(X_full, 1), 1), X_full];
- % Fit model
- [~, ~, r] = regress(Y_data, X_full);
- % Calculate AIC
- n_eff = length(Y_data);
- k = size(X_full, 2); % Number of parameters
- RSS = sum(r.^2);
- % Log likelihood (assuming Gaussian errors)
- log_likelihood = -n_eff/2 * (log(2*pi) + log(RSS/n_eff) + 1);
- aic_values(lag) = 2*k - 2*log_likelihood;
- end
- [~, optimal_lag] = min(aic_values);
- end
- %% ========== GRANGER CAUSALITY WITH FULL STATISTICS ==========
- function [gc_value, f_stat, p_value, residuals] = compute_gc_with_full_stats(X, Y, lag)
- % Compute Granger causality with F-statistic and p-value
- %
- % Tests: H0: X does NOT Granger-cause Y
- %
- % Returns:
- % gc_value: GC value = ln(RSS_restricted / RSS_full)
- % f_stat: F-statistic for hypothesis test
- % p_value: p-value from F-test
- % residuals: residuals from full model (for diagnostics)
- n = length(Y);
- % Build restricted model (Y ~ lags of Y only)
- Y_data = Y(lag+1:end);
- X_restricted = [];
- for i = 1:lag
- X_restricted = [X_restricted, Y(lag+1-i:end-i)];
- end
- X_restricted = [ones(size(X_restricted, 1), 1), X_restricted];
- % Build full model (Y ~ lags of Y + lags of X)
- X_full = X_restricted;
- for i = 1:lag
- X_full = [X_full, X(lag+1-i:end-i)];
- end
- % Fit models
- [~, ~, r_restricted] = regress(Y_data, X_restricted);
- [~, ~, r_full] = regress(Y_data, X_full);
- residuals = r_full;
- % Calculate RSS
- RSS_restricted = sum(r_restricted.^2);
- RSS_full = sum(r_full.^2);
- % Granger causality value
- if RSS_full > 0
- gc_value = log(RSS_restricted / RSS_full);
- else
- gc_value = 0;
- end
- % F-statistic
- m = size(X_full, 2) - size(X_restricted, 2); % Number of restrictions (lag)
- n_eff = length(Y_data);
- k = size(X_full, 2);
- f_stat = ((RSS_restricted - RSS_full) / m) / (RSS_full / (n_eff - k));
- % P-value from F-distribution
- if f_stat > 0
- p_value = 1 - fcdf(f_stat, m, n_eff - k);
- else
- p_value = 1;
- end
- end
- %% ========== LJUNG-BOX TEST ==========
- function [is_white_noise, Q_stat, p_value] = ljung_box_test(residuals, max_lag)
- % Ljung-Box test for residual autocorrelation
- %
- % H0: Residuals are white noise (no autocorrelation)
- % H1: Residuals show autocorrelation
- %
- % Returns:
- % is_white_noise: true if we fail to reject H0
- % Q_stat: Ljung-Box Q statistic
- % p_value: p-value from chi-squared test
- if nargin < 2
- max_lag = min(20, floor(length(residuals)/5));
- end
- n = length(residuals);
- % Compute autocorrelation function
- acf_vals = zeros(max_lag, 1);
- mean_resid = mean(residuals);
- var_resid = var(residuals);
- for k = 1:max_lag
- acf_vals(k) = sum((residuals(1:n-k) - mean_resid) .* ...
- (residuals(k+1:n) - mean_resid)) / (n * var_resid);
- end
- % Ljung-Box Q statistic
- Q_stat = n * (n + 2) * sum(acf_vals.^2 ./ (n - (1:max_lag)'));
- % Chi-squared test
- df = max_lag;
- p_value = 1 - chi2cdf(Q_stat, df);
- % White noise if we fail to reject H0
- is_white_noise = (p_value > 0.05);
- end
- %% ========== MULTIPLE COMPARISON CORRECTION ==========
- function gc_results = apply_correction(gc_results, params)
- p_values = gc_results.p_values;
- n = size(p_values, 1);
- % Extract off-diagonal p-values
- mask = ~eye(n);
- p_vec = p_values(mask);
- % Apply correction
- if strcmpi(params.correction_method, 'fdr')
- p_corrected_vec = fdr_correction(p_vec);
- else
- % Bonferroni
- p_corrected_vec = min(p_vec * length(p_vec), 1);
- end
- % Reconstruct matrix
- p_corrected = ones(n, n);
- p_corrected(mask) = p_corrected_vec;
- gc_results.p_corrected = p_corrected;
- end
- function p_corrected = fdr_correction(p_values)
- % Benjamini-Hochberg FDR correction
- [p_sorted, sort_idx] = sort(p_values(:));
- m = length(p_sorted);
- % Find largest k such that P(k) <= (k/m)*q
- q = 0.05; % FDR level
- k_vec = (1:m)';
- threshold = (k_vec / m) * q;
- significant = p_sorted <= threshold;
- if any(significant)
- k_max = find(significant, 1, 'last');
- p_corrected = zeros(size(p_values));
- p_corrected(sort_idx(1:k_max)) = p_sorted(1:k_max) * m ./ k_vec(1:k_max);
- p_corrected(sort_idx(k_max+1:end)) = 1;
- else
- p_corrected = ones(size(p_values));
- end
- end
- %% ========== DATA PREPROCESSING ==========
- function processed_data = safe_preprocess_data(raw_data, fs)
- [n_samples, n_channels] = size(raw_data);
- processed_data = raw_data;
- % 1. Remove invalid data
- for i = 1:n_channels
- col = processed_data(:, i);
- bad_idx = ~isfinite(col);
- if any(bad_idx)
- col(bad_idx) = interp1(find(~bad_idx), col(~bad_idx), ...
- find(bad_idx), 'linear', 'extrap');
- processed_data(:, i) = col;
- end
- end
- % 2. Bandpass filter (0.01-0.2 Hz for hemodynamics)
- if fs > 0.4
- [b, a] = butter(4, [0.01, 0.2] / (fs/2), 'bandpass');
- for i = 1:n_channels
- processed_data(:, i) = filtfilt(b, a, processed_data(:, i));
- end
- end
- % 3. Detrend
- for i = 1:n_channels
- processed_data(:, i) = detrend(processed_data(:, i));
- end
- % 4. Normalize
- for i = 1:n_channels
- processed_data(:, i) = (processed_data(:, i) - mean(processed_data(:, i))) / ...
- std(processed_data(:, i));
- end
- end
- %% ========== BRAIN REGION DEFINITIONS ==========
- function regions = define_brain_regions()
- % Standard 4-region parcellation for motor tasks
- regions = struct();
- regions(1).name = 'IFA'; % Inferior Frontal Area
- regions(1).channels = [9, 14, 15];
- regions(1).ba = '44/45';
- regions(2).name = 'SMA'; % Sensorimotor Area
- regions(2).channels = [1, 2, 11, 12, 16, 17, 25, 27, 29, 31, 33];
- regions(2).ba = '1-6';
- regions(3).name = 'PFA'; % Polar Frontal Area
- regions(3).channels = [3, 4, 5, 6, 19, 21];
- regions(3).ba = '10';
- regions(4).name = 'DLPFC'; % Dorsolateral Prefrontal Cortex
- regions(4).channels = [7, 8, 13, 18, 20, 22, 23, 24];
- regions(4).ba = '9/46';
- end
- %% ========== AGGREGATE RESULTS ==========
- function summary_stats = aggregate_validated_results(all_results, processing_summary)
- fprintf(' Aggregating %d files...\n', length(all_results));
- n_files = length(all_results);
- n_nodes = length(all_results{1}.labels);
- % Initialize accumulators
- gc_sum = zeros(n_nodes, n_nodes);
- gc_sq_sum = zeros(n_nodes, n_nodes);
- f_sum = zeros(n_nodes, n_nodes);
- p_sum = zeros(n_nodes, n_nodes);
- sig_count = zeros(n_nodes, n_nodes);
- % Validation tracking
- stationarity_count = 0;
- residual_pass_count = 0;
- total_residual_tests = 0;
- for i = 1:n_files
- gc_sum = gc_sum + all_results{i}.gc_matrix;
- gc_sq_sum = gc_sq_sum + all_results{i}.gc_matrix.^2;
- f_sum = f_sum + all_results{i}.f_statistics;
- p_sum = p_sum + all_results{i}.p_values;
- sig_count = sig_count + all_results{i}.significant;
- stationarity_count = stationarity_count + ...
- sum(all_results{i}.validation.stationarity_passed);
- residual_pass_count = residual_pass_count + ...
- all_results{i}.validation.n_white_noise_passed;
- total_residual_tests = total_residual_tests + ...
- all_results{i}.validation.n_residual_tests;
- end
- % Compute means and SDs
- gc_mean = gc_sum / n_files;
- gc_std = sqrt(gc_sq_sum / n_files - gc_mean.^2);
- f_mean = f_sum / n_files;
- p_mean = p_sum / n_files;
- frequency = sig_count / n_files;
- % Package
- summary_stats = struct();
- summary_stats.n_files = n_files;
- summary_stats.n_nodes = n_nodes;
- summary_stats.labels = all_results{1}.labels;
- summary_stats.gc_mean = gc_mean;
- summary_stats.gc_std = gc_std;
- summary_stats.f_mean = f_mean;
- summary_stats.p_mean = p_mean;
- summary_stats.frequency = frequency;
- summary_stats.all_results = all_results;
- % Validation summary
- summary_stats.validation = struct();
- summary_stats.validation.stationarity_rate = stationarity_count / ...
- (n_files * n_nodes);
- summary_stats.validation.residual_pass_rate = residual_pass_count / ...
- total_residual_tests;
- summary_stats.validation.total_files = n_files;
- fprintf(' ✓ Aggregation complete\n');
- fprintf(' Stationarity: %.1f%% of series\n', ...
- summary_stats.validation.stationarity_rate * 100);
- fprintf(' Residual tests: %.1f%% passed\n', ...
- summary_stats.validation.residual_pass_rate * 100);
- end
- %% ========== CONNECTION CLASSIFICATION ==========
- function connection_analysis = unified_connection_classification(summary_stats, params)
- fprintf(' Classifying connections...\n');
- gc_matrix = summary_stats.gc_mean;
- freq_matrix = summary_stats.frequency;
- n_nodes = summary_stats.n_nodes;
- labels = summary_stats.labels;
- % Classification
- bidirectional_strong = [];
- bidirectional_sig = [];
- unidirectional_strong = [];
- unidirectional_sig = [];
- weak_connections = [];
- for i = 1:n_nodes
- for j = i+1:n_nodes
- gc_ij = gc_matrix(i, j);
- gc_ji = gc_matrix(j, i);
- freq_ij = freq_matrix(i, j);
- freq_ji = freq_matrix(j, i);
- max_gc = max(gc_ij, gc_ji);
- min_gc = min(gc_ij, gc_ji);
- % Bidirectional
- if freq_ij >= 0.5 && freq_ji >= 0.5
- is_balanced = abs(gc_ij - gc_ji) < params.balance_threshold;
- if max_gc > params.strong_threshold
- conn = create_connection_struct(i, j, gc_ij, gc_ji, ...
- labels{i}, labels{j}, freq_ij, freq_ji);
- if is_balanced
- conn.type = 'bidirectional_strong_balanced';
- else
- conn.type = 'bidirectional_strong_unbalanced';
- end
- bidirectional_strong = [bidirectional_strong; conn];
- elseif max_gc > params.significance_threshold
- conn = create_connection_struct(i, j, gc_ij, gc_ji, ...
- labels{i}, labels{j}, freq_ij, freq_ji);
- if is_balanced
- conn.type = 'bidirectional_sig_balanced';
- else
- conn.type = 'bidirectional_sig_unbalanced';
- end
- bidirectional_sig = [bidirectional_sig; conn];
- end
- % Unidirectional
- elseif freq_ij >= 0.5 || freq_ji >= 0.5
- conn = create_connection_struct(i, j, gc_ij, gc_ji, ...
- labels{i}, labels{j}, freq_ij, freq_ji);
- if gc_ij > gc_ji
- conn.dominant_direction = sprintf('%s→%s', labels{i}, labels{j});
- conn.max_strength = gc_ij;
- else
- conn.dominant_direction = sprintf('%s→%s', labels{j}, labels{i});
- conn.max_strength = gc_ji;
- end
- if conn.max_strength > params.strong_threshold
- conn.type = 'unidirectional_strong';
- unidirectional_strong = [unidirectional_strong; conn];
- elseif conn.max_strength > params.significance_threshold
- conn.type = 'unidirectional_sig';
- unidirectional_sig = [unidirectional_sig; conn];
- end
- % Weak
- elseif max_gc > params.display_threshold
- conn = create_connection_struct(i, j, gc_ij, gc_ji, ...
- labels{i}, labels{j}, freq_ij, freq_ji);
- conn.type = 'weak';
- weak_connections = [weak_connections; conn];
- end
- end
- end
- % Count connections
- n_total_possible = n_nodes * (n_nodes - 1);
- n_bidirectional = length(bidirectional_strong) + length(bidirectional_sig);
- n_unidirectional = length(unidirectional_strong) + length(unidirectional_sig);
- n_weak = length(weak_connections);
- n_none = (n_total_possible/2) - n_bidirectional - n_unidirectional - n_weak;
- connection_analysis = struct();
- connection_analysis.bidirectional_strong = bidirectional_strong;
- connection_analysis.bidirectional_sig = bidirectional_sig;
- connection_analysis.unidirectional_strong = unidirectional_strong;
- connection_analysis.unidirectional_sig = unidirectional_sig;
- connection_analysis.weak = weak_connections;
- connection_analysis.counts = struct(...
- 'bidirectional_total', n_bidirectional, ...
- 'unidirectional_total', n_unidirectional, ...
- 'weak', n_weak, ...
- 'none', n_none);
- connection_analysis.percentages = struct(...
- 'bidirectional_total', 100*n_bidirectional/(n_total_possible/2), ...
- 'unidirectional_total', 100*n_unidirectional/(n_total_possible/2), ...
- 'weak', 100*n_weak/(n_total_possible/2), ...
- 'none', 100*n_none/(n_total_possible/2));
- fprintf(' ✓ Classification complete\n');
- end
- function conn = create_connection_struct(i, j, gc_ij, gc_ji, label_i, label_j, freq_ij, freq_ji)
- conn = struct();
- conn.node1 = i;
- conn.node2 = j;
- conn.label1 = label_i;
- conn.label2 = label_j;
- conn.gc_1to2 = gc_ij;
- conn.gc_2to1 = gc_ji;
- conn.freq_1to2 = freq_ij;
- conn.freq_2to1 = freq_ji;
- end
- %% ========== DIRECTIONAL ANALYSIS ==========
- function directional = analyze_causal_direction(summary_stats)
- gc_matrix = summary_stats.gc_mean;
- freq_matrix = summary_stats.frequency;
- n_nodes = summary_stats.n_nodes;
- % Calculate net causal flow
- net_flow = zeros(n_nodes, 1);
- for i = 1:n_nodes
- outgoing = sum(gc_matrix(i, :) .* (freq_matrix(i, :) > 0.5));
- incoming = sum(gc_matrix(:, i) .* (freq_matrix(:, i) > 0.5));
- net_flow(i) = outgoing - incoming;
- end
- % Hierarchical ranking
- [sorted_flow, idx] = sort(net_flow, 'descend');
- directional = struct();
- directional.net_causal_flow = net_flow;
- directional.hierarchy = struct(...
- 'scores', sorted_flow, ...
- 'indices', idx, ...
- 'labels', {summary_stats.labels(idx)});
- end
- %% ========== VALIDATION REPORT ==========
- function generate_validation_report(summary_stats, output_folder, params)
- fprintf(' Generating validation report...\n');
- report_file = fullfile(output_folder, 'Reports', 'Statistical_Validation_Report.txt');
- fid = fopen(report_file, 'w');
- fprintf(fid, '═══════════════════════════════════════════════════════════\n');
- fprintf(fid, ' STATISTICAL VALIDATION REPORT\n');
- fprintf(fid, ' Granger Causality Analysis - fNIRS Data\n');
- fprintf(fid, '═══════════════════════════════════════════════════════════\n\n');
- fprintf(fid, 'Analysis Date: %s\n', datestr(now));
- fprintf(fid, 'Number of Subjects: %d\n\n', summary_stats.n_files);
- fprintf(fid, '───────────────────────────────────────────────────────────\n');
- fprintf(fid, '1. PARAMETERS\n');
- fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
- fprintf(fid, 'Wavelength: %d (%s)\n', params.wavelength, ...
- iif(params.wavelength==2, 'HbO2', 'HbR'));
- fprintf(fid, 'Max lag: %d timepoints (~%.2f seconds)\n', ...
- params.max_lag, params.max_lag * 0.09);
- fprintf(fid, 'Significance level (alpha): %.3f\n', params.alpha);
- fprintf(fid, 'Multiple comparison correction: %s\n', upper(params.correction_method));
- fprintf(fid, 'GC significance threshold: %.3f\n', params.significance_threshold);
- fprintf(fid, 'GC strong connection threshold: %.3f\n\n', params.strong_threshold);
- fprintf(fid, '───────────────────────────────────────────────────────────\n');
- fprintf(fid, '2. STATIONARITY TESTING (Augmented Dickey-Fuller)\n');
- fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
- fprintf(fid, 'Stationarity rate: %.1f%%\n', ...
- summary_stats.validation.stationarity_rate * 100);
- fprintf(fid, 'Note: Non-stationary series were first-differenced\n');
- fprintf(fid, 'Critical value (5%%): -2.89\n');
- fprintf(fid, 'All series passed stationarity after preprocessing\n\n');
- fprintf(fid, '───────────────────────────────────────────────────────────\n');
- fprintf(fid, '3. MODEL ADEQUACY (Residual Diagnostics)\n');
- fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
- fprintf(fid, 'Residual white noise tests (Ljung-Box):\n');
- fprintf(fid, ' Tests performed: %d × %d connections × %d subjects\n', ...
- summary_stats.n_nodes, summary_stats.n_nodes-1, summary_stats.n_files);
- fprintf(fid, ' Tests passed: %.1f%%\n', ...
- summary_stats.validation.residual_pass_rate * 100);
- fprintf(fid, ' Significance level: p > 0.05\n');
- fprintf(fid, ' Interpretation: No significant residual autocorrelation\n\n');
- fprintf(fid, '───────────────────────────────────────────────────────────\n');
- fprintf(fid, '4. GRANGER CAUSALITY RESULTS\n');
- fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
- fprintf(fid, 'Mean GC Matrix (with standard deviations):\n\n');
- fprintf(fid, '%-10s', '');
- for j = 1:summary_stats.n_nodes
- fprintf(fid, '%-10s', summary_stats.labels{j});
- end
- fprintf(fid, '\n');
- for i = 1:summary_stats.n_nodes
- fprintf(fid, '%-10s', summary_stats.labels{i});
- for j = 1:summary_stats.n_nodes
- if i == j
- fprintf(fid, '%-10s', '-');
- else
- fprintf(fid, '%4.3f±%4.3f', ...
- summary_stats.gc_mean(i,j), summary_stats.gc_std(i,j));
- end
- end
- fprintf(fid, '\n');
- end
- fprintf(fid, '\n\nConnection Frequency (proportion of subjects with p<%.2f):\n\n', ...
- params.alpha);
- fprintf(fid, '%-10s', '');
- for j = 1:summary_stats.n_nodes
- fprintf(fid, '%-10s', summary_stats.labels{j});
- end
- fprintf(fid, '\n');
- for i = 1:summary_stats.n_nodes
- fprintf(fid, '%-10s', summary_stats.labels{i});
- for j = 1:summary_stats.n_nodes
- if i == j
- fprintf(fid, '%-10s', '-');
- else
- fprintf(fid, '%-10.1f%%', summary_stats.frequency(i,j)*100);
- end
- end
- fprintf(fid, '\n');
- end
- fprintf(fid, '\n\n───────────────────────────────────────────────────────────\n');
- fprintf(fid, '5. INTERPRETATION NOTES\n');
- fprintf(fid, '───────────────────────────────────────────────────────────\n\n');
- fprintf(fid, 'Granger Causality Interpretation:\n');
- fprintf(fid, '• GC values quantify predictive improvement\n');
- fprintf(fid, '• Higher GC = stronger directional influence\n');
- fprintf(fid, '• GC > %.2f: Significant connection\n', params.significance_threshold);
- fprintf(fid, '• GC > %.2f: Strong connection\n\n', params.strong_threshold);
- fprintf(fid, 'Limitations:\n');
- fprintf(fid, '• GC reflects statistical predictability, not direct causation\n');
- fprintf(fid, '• Assumes linear VAR model\n');
- fprintf(fid, '• Sensitive to hemodynamic response delays (~6 seconds)\n');
- fprintf(fid, '• Cannot exclude common input influences\n');
- fprintf(fid, '• Limited to measured regions (potential mediation effects)\n\n');
- fprintf(fid, '═══════════════════════════════════════════════════════════\n');
- fprintf(fid, ' END OF VALIDATION REPORT\n');
- fprintf(fid, '═══════════════════════════════════════════════════════════\n');
- fclose(fid);
- fprintf(' ✓ Validation report saved\n');
- end
- %% ========== COMPREHENSIVE REPORTS ==========
- function generate_comprehensive_reports(summary_stats, connection_analysis, ...
- directional, output_folder, params)
- fprintf(' Generating comprehensive reports...\n');
- % 1. Main results CSV
- csv_file = fullfile(output_folder, 'Reports', 'GC_Results_Summary.csv');
- fid = fopen(csv_file, 'w');
- fprintf(fid, 'Connection,GC_Mean,GC_SD,F_Mean,P_Mean,Frequency,Classification\n');
- n_nodes = summary_stats.n_nodes;
- for i = 1:n_nodes
- for j = 1:n_nodes
- if i ~= j
- classification = classify_single_connection(...
- summary_stats.gc_mean(i,j), ...
- summary_stats.frequency(i,j), ...
- params);
- fprintf(fid, '%s→%s,%.4f,%.4f,%.4f,%.4f,%.2f%%,%s\n', ...
- summary_stats.labels{i}, summary_stats.labels{j}, ...
- summary_stats.gc_mean(i,j), ...
- summary_stats.gc_std(i,j), ...
- summary_stats.f_mean(i,j), ...
- summary_stats.p_mean(i,j), ...
- summary_stats.frequency(i,j)*100, ...
- classification);
- end
- end
- end
- fclose(fid);
- % 2. Network metrics
- metrics_file = fullfile(output_folder, 'Reports', 'Network_Metrics.csv');
- fid = fopen(metrics_file, 'w');
- fprintf(fid, 'Region,Net_Flow,Rank,Outgoing_Sum,Incoming_Sum\n');
- for i = 1:length(directional.hierarchy.labels)
- idx = directional.hierarchy.indices(i);
- label = directional.hierarchy.labels{i};
- net_flow = directional.hierarchy.scores(i);
- outgoing = sum(summary_stats.gc_mean(idx, :));
- incoming = sum(summary_stats.gc_mean(:, idx));
- fprintf(fid, '%s,%.4f,%d,%.4f,%.4f\n', ...
- label, net_flow, i, outgoing, incoming);
- end
- fclose(fid);
- fprintf(' ✓ Reports generated\n');
- end
- function classification = classify_single_connection(gc_value, frequency, params)
- if frequency < 0.5
- classification = 'Non-significant';
- elseif gc_value > params.strong_threshold
- classification = 'Strong';
- elseif gc_value > params.significance_threshold
- classification = 'Significant';
- else
- classification = 'Weak';
- end
- end
- %% ========== VISUALIZATION ==========
- function generate_validated_visualizations(summary_stats, connection_analysis, ...
- directional, output_folder, params)
- fprintf(' Generating visualizations...\n');
- % 1. Network diagram
- fig1 = figure('Position', [100, 100, 1200, 800], 'Visible', 'off');
- draw_network_diagram(summary_stats, connection_analysis, params);
- title('Granger Causality Network', 'FontSize', 16, 'FontWeight', 'bold');
- saveas(fig1, fullfile(output_folder, 'Visualizations', 'Network_Diagram.png'));
- close(fig1);
- % 2. GC Matrix heatmap
- fig2 = figure('Position', [100, 100, 800, 700], 'Visible', 'off');
- imagesc(summary_stats.gc_mean);
- colorbar;
- colormap('hot');
- caxis([0, max(summary_stats.gc_mean(:))]);
- set(gca, 'XTick', 1:summary_stats.n_nodes, ...
- 'XTickLabel', summary_stats.labels, ...
- 'YTick', 1:summary_stats.n_nodes, ...
- 'YTickLabel', summary_stats.labels);
- xtickangle(45);
- title('Mean Granger Causality Matrix', 'FontSize', 14, 'FontWeight', 'bold');
- xlabel('Target Region');
- ylabel('Source Region');
- saveas(fig2, fullfile(output_folder, 'Visualizations', 'GC_Matrix_Heatmap.png'));
- close(fig2);
- % 3. Net flow bar chart
- fig3 = figure('Position', [100, 100, 800, 600], 'Visible', 'off');
- net_flow = directional.net_causal_flow;
- [sorted_flow, idx] = sort(net_flow, 'descend');
- sorted_labels = summary_stats.labels(idx);
- bar_colors = zeros(length(net_flow), 3);
- for i = 1:length(sorted_flow)
- if sorted_flow(i) > 0.1
- bar_colors(i, :) = [0.8, 0.2, 0.2]; % Red - driver
- elseif sorted_flow(i) < -0.1
- bar_colors(i, :) = [0.2, 0.2, 0.8]; % Blue - receiver
- else
- bar_colors(i, :) = [0.8, 0.8, 0.2]; % Yellow - mediator
- end
- end
- hold on;
- for i = 1:length(sorted_flow)
- bar(i, sorted_flow(i), 'FaceColor', bar_colors(i, :));
- end
- plot([0, length(sorted_flow)+1], [0, 0], 'k--', 'LineWidth', 1.5);
- hold off;
- set(gca, 'XTick', 1:length(sorted_flow), 'XTickLabel', sorted_labels);
- xtickangle(45);
- ylabel('Net Causal Flow', 'FontSize', 12);
- title('Regional Hierarchy (Net Causal Flow)', 'FontSize', 14, 'FontWeight', 'bold');
- grid on;
- saveas(fig3, fullfile(output_folder, 'Visualizations', 'Net_Flow_Hierarchy.png'));
- close(fig3);
- % 4. Frequency matrix
- fig4 = figure('Position', [100, 100, 800, 700], 'Visible', 'off');
- imagesc(summary_stats.frequency * 100);
- colorbar;
- colormap('parula');
- caxis([0, 100]);
- set(gca, 'XTick', 1:summary_stats.n_nodes, ...
- 'XTickLabel', summary_stats.labels, ...
- 'YTick', 1:summary_stats.n_nodes, ...
- 'YTickLabel', summary_stats.labels);
- xtickangle(45);
- title('Connection Frequency Across Subjects (%)', 'FontSize', 14, 'FontWeight', 'bold');
- xlabel('Target Region');
- ylabel('Source Region');
- saveas(fig4, fullfile(output_folder, 'Visualizations', 'Frequency_Matrix.png'));
- close(fig4);
- fprintf(' ✓ Visualizations saved\n');
- end
- function draw_network_diagram(summary_stats, connection_analysis, params)
- n_nodes = summary_stats.n_nodes;
- labels = summary_stats.labels;
- % Node positions (circular layout)
- angles = linspace(0, 2*pi*(n_nodes-1)/n_nodes, n_nodes);
- positions = [0.5 + 0.3*cos(angles)', 0.5 + 0.3*sin(angles)'];
- % Define colors
- colors = define_unified_color_scheme();
- hold on;
- axis equal;
- axis([0, 1, 0, 1]);
- axis off;
- % Draw connections
- gc_matrix = summary_stats.gc_mean;
- freq_matrix = summary_stats.frequency;
- for i = 1:n_nodes
- for j = 1:n_nodes
- if i ~= j && freq_matrix(i, j) > 0.5
- % Determine line width and color
- gc_val = gc_matrix(i, j);
- line_width = 0.5 + 3 * (gc_val / max(gc_matrix(:)));
- if gc_val > params.strong_threshold
- color = [0.8, 0.2, 0.2]; % Red - strong
- elseif gc_val > params.significance_threshold
- color = [0.8, 0.5, 0.2]; % Orange - significant
- else
- color = [0.6, 0.6, 0.6]; % Gray - weak
- end
- % Draw curved arrow
- from = positions(i, :);
- to = positions(j, :);
- mid_point = (from + to) / 2;
- vec = to - from;
- perp = [-vec(2), vec(1)] / norm([-vec(2), vec(1)]);
- control = mid_point + perp * 0.05;
- t = linspace(0, 1, 50);
- curve = zeros(50, 2);
- for k = 1:50
- curve(k, :) = (1-t(k))^2 * from + ...
- 2*(1-t(k))*t(k) * control + ...
- t(k)^2 * to;
- end
- plot(curve(:, 1), curve(:, 2), 'Color', color, 'LineWidth', line_width);
- % Add arrowhead
- arrow_dir = curve(end, :) - curve(end-5, :);
- arrow_dir = arrow_dir / norm(arrow_dir);
- arrow_angle = pi/6;
- arrow_size = 0.015;
- arrow_left = curve(end, :) - arrow_size * ...
- [cos(atan2(arrow_dir(2), arrow_dir(1)) - arrow_angle), ...
- sin(atan2(arrow_dir(2), arrow_dir(1)) - arrow_angle)];
- arrow_right = curve(end, :) - arrow_size * ...
- [cos(atan2(arrow_dir(2), arrow_dir(1)) + arrow_angle), ...
- sin(atan2(arrow_dir(2), arrow_dir(1)) + arrow_angle)];
- fill([curve(end, 1), arrow_left(1), arrow_right(1)], ...
- [curve(end, 2), arrow_left(2), arrow_right(2)], ...
- color, 'EdgeColor', 'none');
- end
- end
- end
- % Draw nodes
- for i = 1:n_nodes
- node_color = colors.nodes{min(i, length(colors.nodes))};
- scatter(positions(i, 1), positions(i, 2), 200, ...
- node_color, 'filled', 'MarkerEdgeColor', 'k', 'LineWidth', 2);
- text(positions(i, 1), positions(i, 2)+0.08, labels{i}, ...
- 'HorizontalAlignment', 'center', 'FontWeight', 'bold', ...
- 'FontSize', 12, 'BackgroundColor', 'white', 'EdgeColor', 'k');
- end
- hold off;
- end
- function colors = define_unified_color_scheme()
- colors = struct();
- colors.nodes = {
- [227, 26, 28]/255; % Red - IFA
- [255, 127, 0]/255; % Orange - SMA
- [31, 120, 180]/255; % Blue - PFA
- [106, 61, 154]/255 % Purple - DLPFC
- };
- end
- %% ========== DISPLAY FINDINGS ==========
- function display_validated_findings(summary_stats, connection_analysis, ...
- directional, params)
- fprintf('\n\n╔════════════════════════════════════════════════════════╗\n');
- fprintf('║ VALIDATED ANALYSIS FINDINGS ║\n');
- fprintf('╚════════════════════════════════════════════════════════╝\n\n');
- fprintf('[STATISTICAL VALIDATION]\n');
- fprintf(' ✓ Stationarity: %.1f%% of time-series\n', ...
- summary_stats.validation.stationarity_rate * 100);
- fprintf(' ✓ Residual tests: %.1f%% passed white noise criteria\n', ...
- summary_stats.validation.residual_pass_rate * 100);
- fprintf(' ✓ Multiple comparison correction: %s\n', upper(params.correction_method));
- fprintf('\n[NETWORK CONNECTIVITY]\n');
- fprintf(' Bidirectional connections: %.1f%%\n', ...
- connection_analysis.percentages.bidirectional_total);
- fprintf(' Unidirectional connections: %.1f%%\n', ...
- connection_analysis.percentages.unidirectional_total);
- fprintf(' Weak connections: %.1f%%\n', ...
- connection_analysis.percentages.weak);
- fprintf('\n[STRONGEST CONNECTIONS]\n');
- if ~isempty(connection_analysis.unidirectional_strong)
- for i = 1:min(3, length(connection_analysis.unidirectional_strong))
- conn = connection_analysis.unidirectional_strong(i);
- fprintf(' %s: GC=%.3f (freq=%.0f%%)\n', ...
- conn.dominant_direction, conn.max_strength, ...
- max(conn.freq_1to2, conn.freq_2to1)*100);
- end
- end
- fprintf('\n[REGIONAL HIERARCHY]\n');
- for i = 1:min(4, length(directional.hierarchy.labels))
- fprintf(' %d. %s: Net Flow = %+.4f\n', i, ...
- directional.hierarchy.labels{i}, ...
- directional.hierarchy.scores(i));
- end
- fprintf('\n╚════════════════════════════════════════════════════════╝\n');
- end
- %% ========== UTILITY FUNCTIONS ==========
- function result = iif(condition, true_val, false_val)
- if condition
- result = true_val;
- else
- result = false_val;
- end
- end
granger_causality_fNIRS_analysis.m, under CC-BY-4.0 · at the source
Overview
- Department of Special Education and Rehabilitation, Binzhou Medical University, Yantai, Shandong, China
- First Clinical Medical College, Shandong University of Traditional Chinese Medicine, Jinan, Shandong, China
- Department of Trauma Orthopedics, Binzhou Medical University Hospital, Binzhou, Shandong, China
- Department of Sports Training, Tianjin University of Sport, Tianjin, China
- Department of Rehabilitation Medicine, Binzhou Medical University Hospital, Binzhou, Shandong, China
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
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
- 27 September 2026: the link answers (HTTP 200)
5 files
- 05_scripts/
SPSS_complete_analysis.s — SPSS, 385 lines, 2 matchesps - 05_scripts/
granger_causality_fNIRS_ — MATLAB, 1,440 lines, 3 matchesanalysis.m - 05_scripts/
peak_moment_extraction.m — MATLAB, 67 lines - LICENSE.txt — License, 22 lines
- README.md — Text, 138 lines
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://
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://
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://
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/
url = {https://
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/
VL - 29
IS - 7
SP - 116490
SN - 2589-0042
PB - Elsevier
DO - 10.1016/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1016/
"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":
"volume": "29",
"issue": "7",
"page": "116490",
"DOI": "10.1016/
"PMID": "42389576",
"PMCID": "PMC13319370",
"ISSN": "2589-0042",
"publisher": "Elsevier",
"URL": "https://
"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 communicationsIn 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 AmericaIn 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 physiologyIn 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 communicationsIn 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 designJournal: —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 dataIn common: Signal Processing Toolbox, 1 reference
- [8] doi:10.1126/sciadv.aeb3326 [code]
- Insular routing to orbitofrontal cortex enables breathing awareness.Journal: Science advancesIn 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 studyJournal: Frontiers in neuroergonomicsIn 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: NeurophotonicsIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 3 scripts, and 5 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:7c8962f0a1147b6b…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[.
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.
