diff --git a/+nla/+edge/+permutationMethods/+tree/PermutationNode.m b/+nla/+edge/+permutationMethods/+tree/PermutationNode.m new file mode 100644 index 00000000..89c1e168 --- /dev/null +++ b/+nla/+edge/+permutationMethods/+tree/PermutationNode.m @@ -0,0 +1,68 @@ +classdef PermutationNode < handle + %PERMUTATIONNODE Node which defines one grouping of each permutatino tree + % + % node = permutationNode(level, input_data, permutation_groups) + % node = One grouping of data for each permutation group. Each group contains the data (functional connectivity) + % along with the index (column) the data is located and the original column + % level = The level of the tree. 0 is the root, 1 the first column of permutation groups, 2 the next... + % input_data = The input data. Currently, this is the functional connectivity + % permutation_groups = the columns from the csv file that characterizes the permutation groupings + % Each column of the data is 1 permutation grouping. + % Each row is one subject/data entry + % Values should be positive integers. Groups must be independent in each column + + + properties + children = [] % Nodes that descend from the current node. The column to the right of the current level in permutation groups + level = 0 % 0 is root, initial_data + parent = false % if false, this is the root of the tree. If not, this is the node that came before + data_with_indexes = {} % The data. 3 cell object. {data (some big array), current index, original index} + permutation_groups = [] % The groups that descend off of this one + end + + properties (SetAccess = immutable) + original_data % matrix of size input_data.length x 3 with each row [data_value, current_index, original_index] + end + + methods + function obj = PermutationNode(level, input_data, permutation_groups) + % Inputs: + % input_data is the functional connectivity with each subject as a vector. Size: [edges(?) x subjects] + % permutation groups. This will be a matrix with each level of permutation a vector. Size: [subjects x levels of permutations] + + if isequal(level, 0) + obj.permutation_groups = permutation_groups; + obj.original_data = {input_data, [1:size(input_data, 2)], [1:size(input_data, 2)]}; + obj.data_with_indexes = obj.original_data; + else + size_permutation_groups = size(permutation_groups); + if size_permutation_groups(2) > 1 + obj.permutation_groups = permutation_groups(:, 2:end); + end + functional_connectivity = input_data{1}; + index_length = size(input_data{2}, 2); + current_index = [1:index_length]; + original_index = input_data{3}; + obj.original_data = {functional_connectivity, current_index, original_index}; + obj.data_with_indexes = obj.original_data; + end + if ~isempty(obj.permutation_groups) + group_numbers = unique(obj.permutation_groups(:, 1)); + for group_number = 1:numel(group_numbers) + group_indexes = (obj.permutation_groups(:, 1) == group_numbers(group_number)); + temp_permutation_groups = obj.permutation_groups(group_indexes, :); + group_input_data = obj.data_with_indexes{1}; + group_current_indexes = obj.data_with_indexes{2}; + group_original_indexes = obj.data_with_indexes{3}; + group_data = {group_input_data(:, group_indexes), group_current_indexes(group_indexes), group_original_indexes(group_indexes)}; + obj.children = [obj.children obj.createNodes(obj.level + 1, temp_permutation_groups, group_data, obj)]; + end + end + end + + function node = createNodes(obj, level, group_permutations, input_group_data, parent_node) + node = nla.edge.permutationMethods.tree.PermutationNode(level, input_group_data, group_permutations); + node.parent = parent_node; + end + end +end \ No newline at end of file diff --git a/+nla/+edge/+permutationMethods/+tree/PermutationTree.m b/+nla/+edge/+permutationMethods/+tree/PermutationTree.m new file mode 100644 index 00000000..b0925b76 --- /dev/null +++ b/+nla/+edge/+permutationMethods/+tree/PermutationTree.m @@ -0,0 +1,31 @@ +classdef PermutationTree < handle + %PERMUTATIONTREE Base class for permutation groups. This just bundles all the permutation nodes + % together for ease. + % + % tree = PermutationTree(input_data, permutation_groups) + % tree = The tree object. Consists of the raw permutation groups along with the raw data along + % starting indexes + % + % input_data = The data that is permuted. This is usually the functional connectivity (right now, that's all it works for) + % permutation_groups = the columns from the csv file that characterizes the permutation groupings + % Each column of the data is 1 permutation grouping. + % Each row is one subject/data entry + % Values should be positive integers. Groups must be independent in each column + + properties + permutation_groups % Raw from the behavior table to define permutation grouping + root_node % The first "level 0" node of the tree + end + + properties (SetAccess = immutable) + original_data = {} % This is going to be a matrix of input_data.length x 2 [data_value, original_index] + end + + methods + function obj = PermutationTree(input_data, permutation_groups) + obj.original_data = {input_data, [1:size(input_data, 2)]'}; + obj.permutation_groups = permutation_groups; + obj.root_node = nla.edge.permutationMethods.tree.PermutationNode(0, input_data, permutation_groups); + end + end +end \ No newline at end of file diff --git a/+nla/+edge/+permutationMethods/MultiLevel.m b/+nla/+edge/+permutationMethods/MultiLevel.m new file mode 100644 index 00000000..e360703f --- /dev/null +++ b/+nla/+edge/+permutationMethods/MultiLevel.m @@ -0,0 +1,81 @@ +classdef MultiLevel < nla.edge.permutationMethods.Base + %MULTILEVEL Multilevel permutation method + % + % multi-level strategy = multiLevel() + % multi-level strategy = the object which will be doing the permutations the data. The functional + % connectivity is permuted + % + % needed for methods: + % test_options = this is the options and data for the tests being run. Sometimes called 'input_struct' + % + % How this works: THe object controls the permutations. It can be instantiated without any data + % Before permutations can be run, a permutation tree needs to be created. This needs to be done with the + % "createPermutationTree(test_options)" method. This creates a permutation tree (with the nodes). It + % also will find all the "terminal nodes" - the nodes that have no children. These are the last groupings + % and the actual data to be permuted + % + % After the tree is created (this only is done once), the object is ready to do the permutations. + % This is done by taking each terminal node, getting the current indexes of the data, permuting them, + % and then applying these indexes to the data. + + properties + permutation_tree + terminal_nodes = [] + end + + methods + function obj = MultiLevel() + end + + function obj = createPermutationTree(obj, input_struct) + if ~isfield(input_struct, "permutation_groups") + input_struct.permutation_groups = []; + end + obj.permutation_tree = nla.edge.permutationMethods.tree.PermutationTree(... + input_struct.func_conn.v, input_struct.permutation_groups... + ); + tree_root = obj.permutation_tree.root_node; + obj = obj.depthFirstSearch(tree_root); + end + + function permuted_input_struct = permute(obj, orig_input_struct) + permuted_input_struct = orig_input_struct; + permuted_transposed_functional_connectivity = zeros(size(orig_input_struct.func_conn.v')); + + for node_index = 1:numel(obj.terminal_nodes) + node = obj.terminal_nodes(node_index); + current_indexes = node.data_with_indexes{2}; + original_indexes = node.data_with_indexes{3}; + data = node.data_with_indexes{1}'; % transpose to match dimensions of indexes + + permuted_indexes = nla.helpers.permuteVector(current_indexes); + sorted_original_indexes = sort(original_indexes, 2); + original_indexes = original_indexes(permuted_indexes); + data = data(permuted_indexes, :); + node.data_with_indexes = {data', current_indexes, original_indexes}; + permuted_transposed_functional_connectivity(sorted_original_indexes, :) = data; + end + + permuted_input_struct.func_conn.v = permuted_transposed_functional_connectivity'; + end + + function obj = depthFirstSearch(obj, node_input) + node_queue = [node_input]; + while ~isempty(node_queue) + node = node_queue(1); + if isempty(node.children) + obj.terminal_nodes = [obj.terminal_nodes, node]; + else + for child = 1:numel(node.children) + node_queue = [node_queue; node.children(child)]; + end + end + if size(node_queue, 1) > 1 + node_queue = node_queue(2:end, :); + else + node_queue = []; + end + end + end + end +end \ No newline at end of file diff --git a/+nla/+edge/+result/Base.m b/+nla/+edge/+result/Base.m index 0d6a18c1..06062ad7 100755 --- a/+nla/+edge/+result/Base.m +++ b/+nla/+edge/+result/Base.m @@ -44,6 +44,7 @@ function output(obj, net_atlas, flags, prob_label) if ~isfield(flags, 'display_sig') flags.display_sig = true; end + if flags.display_sig if ~exist('prob_label', 'var') prob_label = [sprintf('Edge-level Significance (P < %g)', obj.prob_max), prob_label_appended]; diff --git a/+nla/+edge/+result/SandwichEstimator.m b/+nla/+edge/+result/SandwichEstimator.m new file mode 100755 index 00000000..93aa7e43 --- /dev/null +++ b/+nla/+edge/+result/SandwichEstimator.m @@ -0,0 +1,102 @@ +classdef SandwichEstimator < nla.edge.result.Base + + properties + % test result specific properties go here (things that are + % specific to a particular data input, ie: results of running the sandwich + % estimator on said data, or covariates which are specific to a + % particular data set) + regressCoeffs % numCovariates x numFcElems + + stdError + tVals + pVals + contrasts + contrastCalc + contrastSE + contrastTVal + contrastPVal + end + + methods + function obj = SandwichEstimator(size, prob_max) + import nla.* % required due to matlab package system quirks + + %MATLAB WEIRDNESS WARNING + %In parallel processing, this constructor is somehow called + %with zero arguments, not in any code I've written. Need to be + %able to get through zero argument case without erroring in + %order to run in parallel mode. Does not happen in non-parallel + %mode. + if nargin ~= 0 + %want to call superclass constructor obj@nla.EdgeLevelResult(size, prob_max); + %but can't within any "if block" due to MATLABism + %copying here + obj.coeff = TriMatrix(size); + obj.prob = TriMatrix(size); + obj.prob_sig = TriMatrix(size, 'logical'); + obj.prob_max = prob_max; + obj.coeff_name = 'contrast t-vals'; + + end + + end + + function output(obj, net_atlas, display_sig) + import nla.* % required due to matlab package system quirks + output@nla.edge.result.Base(obj, net_atlas, display_sig); + + end + + % merged is a function which merges 2 results from the same test + function merge(obj, results) + import nla.* % required due to matlab package system quirks + merge@nla.edge.result.Base(obj, results); + + %% TODO Code to merge multiple SandwichEstimatorResults goes here + % This function is called by TestPool, signature should not + % change. Results is a vector of other + % SandwhichEstimatorResults(one per process). This function is + % called to merge the results of said processes. If you are ex: + % calculating an average value, you should probably do it here: + % ex: sum([results.value]) / obj.perm_count would produce the + % average of 'value' + end + + function setContrasts(obj, newContrasts) + %force contrasts to be row vector + if ~any(size(newContrasts)==1) + error('SandwichEstimatorResults: contrasts must be 1D vector'); + else + newContrasts = reshape(newContrasts,1,numel(newContrasts)); + end + + %check that sizes of contrasts matches size of regression + %coefficients if they've already been fit + %if model has already been fit, confirm contrasts is proper + %size of existing results and modify result values to match new + %contrast setting + if ~isempty(obj.regressCoeff) + %confirm contrasts is proper size + if length(newContrasts) ~= size(obj.regressCoeffs,1) + errMsg = sprintf(['SandwichEstimatorResult: ',... + 'size of contrasts does not match number of current fitted regression coefficients',... + '%i contrasts vs %i regression coeffs'],... + length(newContrasts),size(obj.regressCoeffs,1)); + error(errMsg); + end + + %update existing coeff and prob results with new contrasts + obj.coeff.v = (newContrasts * obj.regressCoeffs)'; + obj.prob.v = (newContrasts * obj.betaCovarTvals)'; + end + + obj.contrasts = newContrasts; + + + end + + + + end +end + diff --git a/+nla/+edge/+test/PairedT.m b/+nla/+edge/+test/PairedT.m index b93ba47c..f04f2ef6 100644 --- a/+nla/+edge/+test/PairedT.m +++ b/+nla/+edge/+test/PairedT.m @@ -49,6 +49,7 @@ behavior_handle = nla.helpers.firstInstanceOfClass(inputs, 'nla.inputField.Behavior'); behavior_handle.covariates_enabled = nla.inputField.CovariatesEnabled.ONLY_FC; + inputs{end + 1} = nla.inputField.Label("", ""); inputs{end + 1} = nla.inputField.String('group1_name', 'Group 1 name:', 'Group1'); inputs{end + 1} = nla.inputField.Number('group1_val', 'Group 1 behavior value:', -Inf, 1, Inf); inputs{end + 1} = nla.inputField.String('group2_name', 'Group 2 name:', 'Group2'); diff --git a/+nla/+edge/+test/SandwichEstimator.m b/+nla/+edge/+test/SandwichEstimator.m new file mode 100755 index 00000000..0ecb1fe5 --- /dev/null +++ b/+nla/+edge/+test/SandwichEstimator.m @@ -0,0 +1,232 @@ +classdef SandwichEstimator < nla.edge.BaseTest + %SANDWICHESTIMATOR Summary of this class goes here + % Detailed explanation goes here + properties (Constant) + name = "Sandwich Estimator" + coeff_name = 'SwE Contrast T-value' + end + + properties + % test specific properties go here (things that will persist + % over multiple runs) + aren't specific to a given data set) + end + + methods + function obj = SandwichEstimator() + obj@nla.edge.BaseTest(); + + end + + function result = run(obj, input_struct) + + sweInput = obj.sweInputObjFromStruct(input_struct); + + if ~isfield(input_struct, 'fit_intercept') | input_struct.fit_intercept + sweInput = obj.addInterceptCovariateToInputIfNone(sweInput); %Forces model to include fitting an intercept term. Adds column to covariates and contrasts + end + + numContrasts = size(sweInput.contrasts,1); + if numContrasts == 1 + result = obj.fitModel(sweInput); + else + result = cell(numContrasts,1); + for i = 1:numContrasts + thisInput = sweInput; + thisInput.contrasts = sweInput.contrasts(i,:); + result{i} = obj.fitModel(thisInput); + end + end + + + end + + + end + + + methods (Access = private) + + + function sweInputStruct = sweInputObjFromStruct(obj, input_struct) + + sweInputStruct = struct(); + + % build scanMetadata object from inputs + %scanMetadata = nlaEckDev.swedata.ScanMetadata(); + sweInputStruct.subjId = input_struct.subjId; + + if isfield(input_struct, 'groupId') + sweInputStruct.groupId = input_struct.groupId; + else + sweInputStruct.groupId = ones(length(input_struct.subjId),1); + end + + if isfield(input_struct, 'visitId') + sweInputStruct.visitId = input_struct.visitId; + else + sweInputStruct.visitId = ones(length(input_struct.subjId),1); + end + + sweInputStruct.fcData = input_struct.func_conn.v'; + sweInputStruct.covariates = [input_struct.behavior, input_struct.covariates]; + %sweInput.scanMetadata = scanMetadata; + sweInputStruct.prob_max = input_struct.prob_max; + sweInputStruct.contrasts = input_struct.contrasts; + + if isfield(input_struct, 'stdErrCalcObj') + sweInputStruct.stdErrCalcObj = input_struct.stdErrCalcObj; %How will this really be passed in? + else + sweInputStruct.stdErrCalcObj = nlaEckDev.sweStdError.UnconstrainedBlocks(); + end + + if isfield(input_struct, 'fit_intercept') + sweInputStruct.fit_intercept = input_struct.fit_intercept; + end + + end + + function sweInput = addInterceptCovariateToInputIfNone(obj, sweInput) + + + columnIsAllSameValue = ~any(diff(sweInput.covariates,1),1); + if ~any(columnIsAllSameValue) + %If no column in covariates is currently all same value, + %add a column so that an intercept term is fit by the + %linear model + numObs = size(sweInput.covariates,1); + sweInput.covariates = [sweInput.covariates, ones(numObs,1)]; + sweInput.contrasts = [input_struct.contrasts, 0]; + + fprintf(['NOTICE: Now adding column of 1''s to covariates in order to fit an intercept.\n',... + 'To fit a model with no intercept, set or add ''fit_intercept'' field ',... + 'of input struct passed to edge test''s ''run''function with value false or zero']); + + end + + + + end + + + function sweRes = fitModel(obj, input) + + + numFcEdges = size(input.fcData,2); + + %the data for each scan in fcData (ie each row) represents the + %flattened lower triangle of fc data. Use the number of fc + %edges represented to compute the size of the original + %non-flattened matrix, which is needed to construct a result + %object + fcMatSize = (1 + sqrt(1 + 4*(numFcEdges*2))) / 2; + + sweRes = nla.edge.result.SandwichEstimator(fcMatSize, input.prob_max); + + designMtx = input.covariates; + %designMtx = obj.zeroMeanUnitVarianceByColumn(designMtx); %Make covariates zero mean unit variance + + [regressCoeffs, residual] = obj.fitLinearModel(input.fcData, designMtx); + + + %Build input and pass to one of several methods for calculating + %standard error + %TODO: Should this calculate beta covariance instead? + + stdErrCalcObj = input.stdErrCalcObj; + %stdErrCalcObj = nlaEckDev.sweStdError.Guillaume(); %If you want to hard code the std err calc object + + stdErrInput = struct();% nlaEckDev.sweStdError.SwEStdErrorInput(); + scanMetadata = struct(); + scanMetadata.subjId = input.subjId; % vec [numScans] + scanMetadata.groupId = input.groupId;%groupId; % vec [numScans] + scanMetadata.visitId = input.visitId; % vec [numScans] + + stdErrInput.scanMetadata = scanMetadata; + stdErrInput.residual = residual; + stdErrInput.pinvDesignMtx = pinv(designMtx); + + %sweRes.stdError = stdErrCalcObj.calculate(stdErrInput); + stdError = stdErrCalcObj.calculate(stdErrInput); + + + contrastCalc = input.contrasts * regressCoeffs; + contrastSE = sqrt((input.contrasts.^2) * (stdError.^2)); + + dof = obj.calcDegreesOfFreedom(designMtx); + + %tVals = regressCoeffs ./ stdError; + contrastTVal = contrastCalc ./ contrastSE; + + %pVals = zeros(size(tVals)); + contrastPVal = zeros(size(contrastTVal)); + for fcIdx = 1:numFcEdges + %pVals(:,fcIdx) = 2 * (1 - cdf('T',abs(tVals(:,fcIdx)), dof)); + contrastPVal(fcIdx) = 2 * (1 - cdf('T',abs(contrastTVal(fcIdx)),dof)); + end + + %sweRes.tVals = tVals; + %sweRes.pVals = pVals; + %sweRes.regressCoeffs = regressCoeffs; + sweRes.coeff.v = contrastTVal'; + sweRes.prob.v = contrastPVal'; + sweRes.prob_sig.v = (sweRes.prob.v < input.prob_max); + sweRes.avg_prob_sig = sum(sweRes.prob_sig.v) ./ numel(sweRes.prob_sig.v); + + %Change expected coefficient range to be more accurate to Sandwich + %Estimator ranges + sweRes.coeff_range = [-3 3]; + + end + + + function outResidual = calcResidual(obj, X, pInvX, Y) + + hat = X * pInvX; + + residCorrectFactor = (1 - diag(hat)).^(-1); %Type 3 residual correction defined in Guillaume 2014 + + %compute residuals + outResidual = diag(residCorrectFactor) * (Y - hat*Y); + + end + + function outMtx = zeroMeanUnitVarianceByColumn(obj, inMtx) + + outMtx = zeros(size(inMtx)); + + for colIdx = 1:size(inMtx,2) + + colMean = mean(inMtx(:,colIdx)); + colStd = std(inMtx(:,colIdx)); + if colStd ~=0 + outMtx(:,colIdx) = (inMtx(:,colIdx) - colMean) / colStd; + else + %If all values of column are same, do not change them + %TODO: handle this here??? or somewhere else? + %TODO: set to zeros? ones? orig values? + outMtx(:,colIdx) = inMtx(:,colIdx); + end + + end + + end + + function degOfFree = calcDegreesOfFreedom(obj, designMtx) + + degOfFree = size(designMtx,1) - size(designMtx,2) - 1; + + end + + function [regressCoeffs, residual] = fitLinearModel(obj, fcData, designMtx) + + pinvDesignMtx = pinv(designMtx); + regressCoeffs = pinvDesignMtx * fcData; + residual = obj.calcResidual(designMtx, pinvDesignMtx, fcData); + + end + + + + end % end private methods +end + diff --git a/+nla/+edge/+test/WelchT.m b/+nla/+edge/+test/WelchT.m index 33076e64..370ea30d 100755 --- a/+nla/+edge/+test/WelchT.m +++ b/+nla/+edge/+test/WelchT.m @@ -53,6 +53,7 @@ behavior_handle = nla.helpers.firstInstanceOfClass(inputs, 'nla.inputField.Behavior'); behavior_handle.covariates_enabled = nla.inputField.CovariatesEnabled.ONLY_FC; + inputs{end + 1} = nla.inputField.Label("", ""); inputs{end + 1} = nla.inputField.String('group1_name', 'Group 1 name:', 'Group1'); inputs{end + 1} = nla.inputField.Number('group1_val', 'Group 1 behavior value:', -Inf, 1, Inf); inputs{end + 1} = nla.inputField.String('group2_name', 'Group 2 name:', 'Group2'); diff --git a/+nla/+edge/unittests/EdgeResultsTest.m b/+nla/+edge/unittests/EdgeResultsTest.m new file mode 100644 index 00000000..4f50b6a2 --- /dev/null +++ b/+nla/+edge/unittests/EdgeResultsTest.m @@ -0,0 +1,35 @@ +classdef EdgeResultsTest < matlab.unittest.TestCase + properties + variables + end + + methods (TestClassSetup) + end + + methods (TestClassTeardown) + function clearTestData(testCase) + clear + end + end + + methods (Test) + function precalculatedInitTest(testCase) + import nla.edge.result.Precalculated + result = Precalculated(); + testCase.verifyEqual(result.coeff.size, uint32(2)); + testCase.verifyEqual(result.prob_max, -1); + end + + function welchTInitTest(testCase) + import nla.edge.result.WelchT + result = WelchT(); + testCase.verifyEqual(result.prob_max, -1); + + result = WelchT(15, 1, {'group_1', 'group_2'}); + testCase.verifyEqual(result.coeff.size, uint32(15)); + testCase.verifyEqual(result.dof, nla.TriMatrix(15)); + testCase.verifyEqual(result.behavior_name, "group_1 > group_2"); + testCase.verifyEqual(result.coeff_range, [-3 3]); + end + end +end \ No newline at end of file diff --git a/+nla/+edge/unittests/MultiLevelPermutationTest.m b/+nla/+edge/unittests/MultiLevelPermutationTest.m new file mode 100644 index 00000000..d11f52ca --- /dev/null +++ b/+nla/+edge/unittests/MultiLevelPermutationTest.m @@ -0,0 +1,98 @@ +classdef MultiLevelPermutationTest < matlab.unittest.TestCase + + properties + test_options + end + + methods (TestMethodSetup) + function loadTestData(testCase) + testCase.test_options = struct(); + testCase.test_options.func_conn.v = [1:10].*ones(10,1); + end + end + + methods (TestMethodTeardown) + function clearTestData(testCase) + clear + end + end + + methods (Test) + function testPermutationNode(testCase) + node = nla.edge.permutationMethods.tree.PermutationNode(0, testCase.test_options.func_conn.v, []); + testCase.verifyEqual(node.level, 0); + testCase.verifyEqual(node.children, []); + testCase.verifyEqual(node.parent, false); + testCase.verifyEqual(node.original_data, node.data_with_indexes); + expected_original_data = {[1:10].*ones(10, 1), 1:10, 1:10}; + testCase.verifyEqual(node.original_data, expected_original_data); + end + + function testPermutationTree(testCase) + tree = nla.edge.permutationMethods.tree.PermutationTree(testCase.test_options.func_conn.v, []); + testCase.verifyClass(tree, 'nla.edge.permutationMethods.tree.PermutationTree'); + testCase.verifyClass(tree.root_node, 'nla.edge.permutationMethods.tree.PermutationNode'); + end + + function testTwoPermutationGroups(testCase) + testCase.test_options.permutation_groups = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; + tree = nla.edge.permutationMethods.tree.PermutationTree(testCase.test_options.func_conn.v, testCase.test_options.permutation_groups); + testCase.verifyEqual(size(tree.root_node.children, 2), 2); + testCase.verifyEqual(tree.root_node, tree.root_node.children(1).parent); + testCase.verifyEqual(tree.root_node, tree.root_node.children(2).parent); + testCase.verifyEqual(tree.root_node.children(1).original_data{3}, [1:5]); + testCase.verifyEqual(tree.root_node.children(2).original_data{3}, [6:10]); + end + + function testMultiLevel(testCase) + testCase.test_options.permutation_groups = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; + + multi_level = nla.edge.permutationMethods.MultiLevel(); + multi_level = multi_level.createPermutationTree(testCase.test_options); + tree = multi_level.permutation_tree; + + testCase.verifyClass(tree, 'nla.edge.permutationMethods.tree.PermutationTree'); + testCase.verifyEqual(size(multi_level.terminal_nodes, 2), 2); + end + + function testPermute(testCase) + testCase.test_options.permutation_groups = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; + multi_level = nla.edge.permutationMethods.MultiLevel(); + multi_level = multi_level.createPermutationTree(testCase.test_options); + + original_options = testCase.test_options; + permuted_options = multi_level.permute(testCase.test_options); + testCase.verifyNotEqual(permuted_options.func_conn, original_options.func_conn); + testCase.verifyEqual(sort(permuted_options.func_conn.v(1, 1:5)), 1:5); + testCase.verifyEqual(sort(permuted_options.func_conn.v(1, 6:10)), 6:10); + end + + function testMultiLevelTwoLevels(testCase) + testCase.test_options.permutation_groups = [1, 1; 1, 1; 1, 2; 1, 2; 1, 2; 2, 3; 2, 3; 2, 3; 2, 4; 2, 4]; + multi_level = nla.edge.permutationMethods.MultiLevel(); + multi_level = multi_level.createPermutationTree(testCase.test_options); + + tree = multi_level.permutation_tree; + terminal_nodes = multi_level.terminal_nodes; + root = tree.root_node; + + testCase.verifyEqual(size(terminal_nodes, 2), 4); + testCase.verifyClass(root.children(1).parent, 'nla.edge.permutationMethods.tree.PermutationNode'); + testCase.verifyEqual(size(root.children(1).children, 2), 2); + end + + function testMultiLevelPermute(testCase) + testCase.test_options.permutation_groups = [1, 1; 1, 1; 1, 2; 1, 2; 1, 2; 2, 3; 2, 3; 2, 3; 2, 4; 2, 4]; + multi_level = nla.edge.permutationMethods.MultiLevel(); + multi_level = multi_level.createPermutationTree(testCase.test_options); + + original_options = testCase.test_options; + permuted_options = multi_level.permute(testCase.test_options); + testCase.verifyNotEqual(permuted_options.func_conn, original_options.func_conn); + testCase.verifyEqual(sort(permuted_options.func_conn.v(1, 1:2)), 1:2); + testCase.verifyEqual(sort(permuted_options.func_conn.v(1, 3:5)), 3:5); + testCase.verifyEqual(sort(permuted_options.func_conn.v(1, 6:8)), 6:8); + testCase.verifyEqual(sort(permuted_options.func_conn.v(1, 9:10)), 9:10); + end + end +end \ No newline at end of file diff --git a/+nla/+edge/unittests/PermutationMethodsTest.m b/+nla/+edge/unittests/PermutationMethodsTest.m new file mode 100644 index 00000000..bf3708ba --- /dev/null +++ b/+nla/+edge/unittests/PermutationMethodsTest.m @@ -0,0 +1,31 @@ +classdef PermutationMethodsTest < matlab.unittest.TestCase + properties + variables + end + + methods (TestClassSetup) + function loadTestData(testCase) + testCase.variables = load("edgeTestInputStruct.mat"); + end + end + + methods (TestClassTeardown) + function clearTestData(testCase) + clear + end + end + + methods (Test) + function behaviorVecTest(testCase) + permutation_method = nla.edge.permutationMethods.BehaviorVec(); + permuted_input_struct = permutation_method.permute(testCase.variables.input_struct); + + testCase.verifyNotEqual(testCase.variables.input_struct.behavior, permuted_input_struct.behavior); + + [counts, original_values] = groupcounts(testCase.variables.input_struct.behavior); + [permuted_counts, permuted_values] = groupcounts(permuted_input_struct.behavior); + testCase.verifyEqual(counts, permuted_counts); + testCase.verifyEqual(original_values, permuted_values); + end + end +end \ No newline at end of file diff --git a/+nla/+gfx/+brain/BrainPlot.m b/+nla/+gfx/+brain/BrainPlot.m new file mode 100644 index 00000000..29da4ea1 --- /dev/null +++ b/+nla/+gfx/+brain/BrainPlot.m @@ -0,0 +1,511 @@ +classdef BrainPlot < handle + + properties + plot_figure + edge_test_options + network_test_options + network_atlas + edge_test_result + network1 + network2 + test_name + upper_limit + lower_limit + color_map = cat(1, winter(1000), flip(autumn(1000))); + color_map_axis + color_bar + ROI_values = [] + function_connectivity_values = [] + ROI_radius + surface_parcels + mesh_type + mesh_alpha + all_edges = [] + end + + properties (Constant) + noncorrelation_input_tests = ["chi_squared", "hypergeometric"] % These are tests that do not use correlation coefficients as inputs + default_settings = struct("upper_limit", 0.5, "lower_limit", -0.5) + end + + properties (Dependent) + is_noncorrelation_input + functional_connectivity_exists + color_functional_connectivity + end + + methods + + function obj = BrainPlot(edge_test_result, edge_test_options, network_test_options, network1, network2, network_atlas, varargin) + brain_input_parser = inputParser; + addRequired(brain_input_parser, "edge_test_result"); + addRequired(brain_input_parser, "edge_test_options"); + addRequired(brain_input_parser, "network_test_options"); + addRequired(brain_input_parser, "network1"); + addRequired(brain_input_parser, "network2"); + addRequired(brain_input_parser, "network_atlas"); + + validNumberInput = @(x) isnumeric(x) && isscalar(x); + addParameter(brain_input_parser, "ROI_radius", 3, validNumberInput); + addParameter(brain_input_parser, "surface_parcels", true, @isboolean); + addParameter(brain_input_parser, "mesh_type", nla.gfx.MeshType.STD, @isenum); + addParameter(brain_input_parser, "mesh_alpha", 0.25, validNumberInput); + + parse(brain_input_parser, edge_test_result, edge_test_options, network_test_options, network1, network2, network_atlas, varargin{:}); + properties = ["edge_test_result", "edge_test_options", "network_test_options", "network1", "network2", "network_atlas", "ROI_radius", "surface_parcels", "mesh_type", "mesh_alpha"]; + for property = properties + obj.(property{1}) = brain_input_parser.Results.(property{1}); + end + + %% + % everything below here we'll need, but we need other values set first. and they need to be editable + obj.plot_figure = nla.gfx.createFigure(1550, 750); + + obj.upper_limit = obj.edge_test_result.coeff_range(2); + obj.lower_limit = obj.edge_test_result.coeff_range(1); + + obj.ROI_values = nan(obj.network_atlas.numROIs(), 1); + obj.function_connectivity_values = nan(obj.network_atlas.numROIs(), 1); + %% + end + + function drawBrainPlots(obj) + import nla.gfx.ViewPos nla.gfx.BrainColorMode + + obj.setROIandConnectivity(); + + if obj.surface_parcels && ~islogical(obj.network_atlas.parcels) + edges1 = obj.singlePlot(subplot("Position", [0.45, 0.505, 0.53, 0.45]), ViewPos.LAT, BrainColorMode.COLOR_ROIS, obj.color_map); + edges2 = obj.singlePlot(subplot("Position", [0.45, 0.055, 0.53, 0.45]), ViewPos.MED, BrainColorMode.COLOR_ROIS, obj.color_map); + obj.all_edges = [edges1 edges2]; + else + edges1 = obj.singlePlot(subplot("Position", [0.45, 0.505, 0.26, 0.45]), ViewPos.BACK, BrainColorMode.NONE, obj.color_map); + edges2 = obj.singlePlot(subplot("Position", [0.73, 0.505, 0.26, 0.45]), ViewPos.FRONT, BrainColorMode.NONE, obj.color_map); + edges3 = obj.singlePlot(subplot("Position", [0.45, 0.055, 0.26, 0.45]), ViewPos.LEFT, BrainColorMode.NONE, obj.color_map); + edges4 = obj.singlePlot(subplot("Position", [0.73, 0.055, 0.26, 0.45]), ViewPos.RIGHT, BrainColorMode.NONE, obj.color_map); + obj.all_edges = [edges1 edges2 edges3 edges4]; + end + + if obj.color_functional_connectivity + plot_axis = subplot("Position", [0.075, 0.175, 0.35, 0.75]); + else + plot_axis = subplot("Position", [0.075, 0.025, 0.35, 0.85]); + end + + if obj.surface_parcels && ~islogical(obj.network_atlas.parcels) + edges5 = obj.singlePlot(plot_axis, ViewPos.DORSAL, BrainColorMode.COLOR_ROIS, obj.color_map); + else + edges5 = obj.singlePlot(plot_axis, ViewPos.DORSAL, BrainColorMode.NONE, obj.color_map); + end + obj.all_edges = [obj.all_edges edges5]; + + light("Position", [0, 100, 100], "Style", "local"); + + % Display Legend + hold(plot_axis, "on"); + if obj.network1 == obj.network2 + legend_entry = bar(plot_axis, NaN); + legend_entry.FaceColor = obj.network_atlas.nets(obj.network1).color; + legend_entry.DisplayName = obj.network_atlas.nets(obj.network1).name; + else + for network = [obj.network1, obj.network2] + legend_entry = bar(plot_axis, NaN); + legend_entry.FaceColor = obj.network_atlas.nets(network).color; + legend_entry.DisplayName = obj.network_atlas.nets(network).name; + end + end + hold(plot_axis, "off"); + nla.gfx.hideAxes(plot_axis); + + obj.drawColorMap(plot_axis); + + obj.color_map_axis = plot_axis; + + obj.addTitle(); + end + + function setROIandConnectivity(obj) + + network1_ROI_indexes = obj.network_atlas.nets(obj.network1).indexes; + network2_ROI_indexes = obj.network_atlas.nets(obj.network2).indexes; + + for ROI_index1_iterator = 1:numel(network1_ROI_indexes) + network1_ROI_index = network1_ROI_indexes(ROI_index1_iterator); + [coefficient1, coefficient2, function_connectivity1, function_connectivity2] = obj.getCoefficients(network1_ROI_index, network2_ROI_indexes); + obj.ROI_values(network1_ROI_index) = (sum(coefficient1) + sum(coefficient2)) / (numel(coefficient1) + numel(coefficient2)); + obj.function_connectivity_values(network1_ROI_index) = (sum(function_connectivity1) + sum(function_connectivity2)) / (numel(function_connectivity1) + numel(function_connectivity2)); + end + + for ROI_index2_iterator = 1:numel(network2_ROI_indexes) + network2_ROI_index = network2_ROI_indexes(ROI_index2_iterator); + [coefficient1, coefficient2, function_connectivity1, function_connectivity2] = obj.getCoefficients(network2_ROI_index, network1_ROI_indexes); + ROI_value = (sum(coefficient1) + sum(coefficient2)) / (numel(coefficient1) + numel(coefficient2)); + function_connectivity_value = (sum(function_connectivity1) + sum(function_connectivity2)) / (numel(function_connectivity1) + numel(function_connectivity2)); + if obj.network1 == obj.network2 + obj.ROI_values(network2_ROI_index) = (obj.ROI_values(network2_ROI_index) + ROI_value) ./ 2; + obj.function_connectivity_values(network2_ROI_index) = (obj.function_connectivity_values(network2_ROI_index) + function_connectivity_value) ./ 2; + else + obj.ROI_values(network2_ROI_index) = ROI_value; + obj.function_connectivity_values(network2_ROI_index) = function_connectivity_value; + end + end + end + + function [coefficient1, coefficient2, function_connectivity1, function_connectivity2] =... + getCoefficients(obj, network1_index, network2_indexes) + coefficient1 = obj.edge_test_result.coeff.get(network1_index, network2_indexes); + coefficient2 = obj.edge_test_result.coeff.get(network2_indexes, network1_index); + + function_connectivity1 = false; + function_connectivity2 = false; + if obj.functional_connectivity_exists + function_connectivity1 = mean(obj.edge_test_options.func_conn.get(network1_index, network2_indexes), 2); + function_connectivity2 = mean(obj.edge_test_options.func_conn.get(network2_indexes, network1_index), 2); + end + + if obj.is_noncorrelation_input + probability_significance1 = obj.edge_test_result.prob_sig.get(network1_index, network2_indexes); + probability_significance2 = obj.edge_test_result.prob_sig.get(network2_indexes, network1_index); + + coefficient1 = coefficient1(logical(probability_significance1)); + coefficient2 = coefficient2(logical(probability_significance2)); + function_connectivity1 = function_connectivity1(logical(probability_significance1)); + function_connectivity2 = function_connectivity2(logical(probability_significance2)); + end + end + + function colors = mapColorsToLimits(obj, value, function_connectivity_average, varargin) + import nla.gfx.valToColor + + if isempty(varargin) + scale_min = -0.5; + scale_max = 0.5; + else + scale_min = str2double(varargin{1}); + scale_max = str2double(varargin{2}); + end + + color_rows = size(obj.color_map); + color_map_positive = obj.color_map(1:(color_rows/2), :); + color_map_negative = obj.color_map(((color_rows/2) + 1):end, :); + + if obj.color_functional_connectivity + colors_positive = valToColor(function_connectivity_average, scale_min, scale_max, color_map_positive); + colors_negative = valToColor(function_connectivity_average, scale_min, scale_max, color_map_negative); + colors(value > 0, :) = colors_positive(value > 0, :); + colors(value <= 0, :) = colors_negative(value <= 0, :); + else + colors = valToColor(value, obj.lower_limit, obj.upper_limit, obj.color_map); + end + colors(isnan(value), :) = 0.5; + end + + function edges = drawEdges(obj, ROI_position, plot_axis) + + point1_indexes = obj.network_atlas.nets(obj.network1).indexes; + point2_indexes = obj.network_atlas.nets(obj.network2).indexes; + + edges = []; + for point1_index = 1:numel(point1_indexes) + point1 = point1_indexes(point1_index); + for point2_index = 1:numel(point2_indexes) + point2 = point2_indexes(point2_index); + if point1 < point2 + network_point1 = point2; + network_point2 = point1; + else + network_point1 = point1; + network_point2 = point2; + end + + [coefficient, ~, function_connectivity_vector, ~] = obj.getCoefficients(network_point1, network_point2); + function_connectivity_average = mean(function_connectivity_vector); + + if ~isempty(coefficient) + edge = obj.assignColorToEdge(ROI_position, network_point1, network_point2, plot_axis, coefficient, function_connectivity_average); + % colorbar(plot_axis, 'off'); + % hold(plot_axis, 'on'); + edge.Annotation.LegendInformation.IconDisplayStyle = "off"; + edges = [edges, edge]; + end + end + end + end + + function edge = assignColorToEdge(obj, ROI_position, network_point1, network_point2, plot_axis, coefficient, function_connectivity_average, varargin) + if ~isempty(coefficient) + if ~isempty(varargin) + color_value = obj.mapColorsToLimits(coefficient, function_connectivity_average, varargin{1}, varargin{2}); + else + color_value = obj.mapColorsToLimits(coefficient, function_connectivity_average); + end + color_value = [reshape(color_value, [1, 3]), 0.5]; + edge = plot3(plot_axis, [ROI_position(network_point1, 1), ROI_position(network_point2, 1)],... + [ROI_position(network_point1, 2), ROI_position(network_point2, 2)],... + [ROI_position(network_point1, 3), ROI_position(network_point2, 3)],... + "Color", color_value, "LineWidth", 5); + % Matlab says you can save a structure to the "UserData" field of a line. You cannot. so, we do something dumb + edge_data = {}; + edge_data{1} = {"coefficient", "function_connectivity_average"}; + edge_data{2} = {coefficient, function_connectivity_average}; + set(edge, "UserData", edge_data) + end + end + + function drawCortex(obj, anatomy, plot_axis, view_position, left_color, right_color) + import nla.gfx.ViewPos + + % Set some defaults up + plot_axis.Color = 'w'; + if ~exist("left_color", "var") + left_color = repmat(0.5, [size(anatomy.hemi_l.nodes, 1), 3]); + end + if ~exist("right_color", "var") + right_color = repmat(0.5, [size(anatomy.hemi_r.nodes, 1), 3]); + end + + % Re-position hemisphere meshes to transverse/axial orientation + [left_mesh, right_mesh] = nla.gfx.anatToMesh(anatomy, obj.mesh_type, view_position); + + % Set lighting and view positioning + if view_position == ViewPos.LAT || view_position == ViewPos.MED + view(plot_axis, [-90, 0]); + % local light is akin to a lightbulb at that location in space + % infinite light has light that originates at that point and only goes in one direction + light(plot_axis, "Position", [-100, 200, 0], "Style", "local"); + light(plot_axis, "Position", [-50, -500, 100], "Style", "infinite"); + light(plot_axis, "Position", [-50, 0, 0], "Style", "infinite"); + else + view(plot_axis, [0, 0]); + switch view_position + case ViewPos.DORSAL + view(plot_axis, [0, 90]); + light(plot_axis, "Position", [100, 300, 100], "Style", "infinite"); + case ViewPos.LEFT + view(plot_axis, [-90, 0]); + light(plot_axis, "Position", [-100, 0, 0], "Style", "infinite"); + case ViewPos.RIGHT + view(plot_axis, [90, 0]); + light(plot_axis, "Position", [100, 0, 0], "Style", "infinite"); + case ViewPos.FRONT + view(plot_axis, [180, 0]); + light(plot_axis, "Position", [100, 300, 100], "Style", "infinite"); + case ViewPos.BACK + light(plot_axis, "Position", [0, -200, 0], "Style", "infinite"); + end + + light(plot_axis, "Position", [-500, -20, 0], "Style", "local"); + light(plot_axis, "Position", [500, -20, 0], "Style", "local"); + light(plot_axis, "Position", [0, -200, 50], "Style", "local"); + end + left_hemisphere = obj.drawCortexHemisphere(plot_axis, anatomy.hemi_l, left_mesh, left_color); + right_hemisphere = obj.drawCortexHemisphere(plot_axis, anatomy.hemi_r, right_mesh, right_color); + + hold(plot_axis, "on"); + axis(plot_axis, "image"); + axis(plot_axis, "off"); + end + + function hemisphere = drawCortexHemisphere(obj, plot_axis, hemisphere_anatomy, mesh, color) + hemisphere = patch(plot_axis, "Faces", hemisphere_anatomy.elements(:, 1:3), "Vertices", mesh,... + "EdgeColor", "none", "FaceColor", "interp", "FaceVertexCData", color,... + "FaceLightin", "gouraud", "FaceAlpha", obj.mesh_alpha, "AmbientStrength", 0.25,... + "DiffuseStrength", 0.75, "SpecularStrength", 0.1); + hemisphere.Annotation.LegendInformation.IconDisplayStyle = "off"; + end + + function edges = singlePlot(obj, plot_axis, view_position, color_mode, color_matrix, upper_limit, lower_limit) + + if exist("upper_limit", "var") + obj.upper_limit = upper_limit; + end + if exist("lower_limit", "var") + obj.lower_limit = lower_limit; + end + + connectivity_map = ~isnan(obj.ROI_values); + + edges = []; + if color_mode == nla.gfx.BrainColorMode.NONE + [ROI_final_positions, ~] = obj.getROIPositions(view_position, color_mode, color_matrix); + obj.drawCortex(obj.network_atlas.anat, plot_axis, view_position); + edges = [edges, obj.drawEdges(ROI_final_positions, plot_axis)]; + else + obj.mesh_alpha = 1; + [ROI_final_positions, ROI_colors] = obj.getROIPositions(view_position, nla.gfx.BrainColorMode.COLOR_ROIS); + if ~isequal(color_mode, nla.gfx.BrainColorMode.NONE) && obj.surface_parcels && ~islogical(obj.network_atlas.parcels) && isequal(size(obj.network_atlas.parcels.ctx_l,1), size(obj.network_atlas.anat.hemi_l.nodes, 1)) && isequal(size(obj.network_atlas.parcels.ctx_r, 1), size(obj.network_atlas.anat.hemi_r.nodes, 1)) + ROI_color_map = [0.5 0.5 0.5; ROI_colors]; + obj.drawCortex(obj.network_atlas.anat, plot_axis, view_position, ROI_color_map(obj.network_atlas.parcels.ctx_l + 1, :), ROI_color_map(obj.network_atlas.parcels.ctx_r + 1, :)); + else + drawCortex(ax, net_atlas.anat, ctx, obj.mesh_alpha, view_pos); + if color_mode ~= BrainColorMode.NONE + for i = 1:net_atlas.numROIs() + % render a sphere at each ROI location + nla.gfx.drawSphere(ax, ROI_final_positions(i, :), ROI_colors(i, :), obj.ROI_radius); + end + end + end + end + + if (~isfield(obj.edge_test_options, "show_ROI_centroids")) || (isfield(obj.edge_test_options, "show_ROI_centroids") && isequal(obj.edge_test_options.show_ROI_centroids, true)) + obj.drawROISpheres(ROI_final_positions, plot_axis, connectivity_map); + end + + colorbar(plot_axis, "off"); + hold(plot_axis, "on"); + end + + function drawROISpheres(obj, ROI_position, plot_axis, connectivity_map) + + for network = [obj.network1, obj.network2] + network_indexes = obj.network_atlas.nets(network).indexes; + for index_iterator = 1:numel(network_indexes) + index = network_indexes(index_iterator); + + if connectivity_map(index) + nla.gfx.drawSphere(plot_axis, ROI_position(index, :), obj.network_atlas.nets(network).color, obj.ROI_radius); + end + end + end + end + + function [ROI_final_positions, ROI_colors] = getROIPositions(obj, view_position, color_mode, color_matrix) + import nla.gfx.BrainColorMode + + [left_mesh, right_mesh] = nla.gfx.anatToMesh(obj.network_atlas.anat, obj.mesh_type, view_position); + ROI_positions = [obj.network_atlas.ROIs.pos]'; + + [left_indexes, left_distances] = knnsearch(obj.network_atlas.anat.hemi_l.nodes, ROI_positions); + [right_indexes, right_distances] = knnsearch(obj.network_atlas.anat.hemi_r.nodes, ROI_positions); + + for network = 1:obj.network_atlas.numNets() + for network_indexes = 1:numel(obj.network_atlas.nets(network).indexes) + ROI_index = obj.network_atlas.nets(network).indexes(network_indexes); + offset = [NaN NaN NaN]; + if left_distances(ROI_index) < right_distances(ROI_index) + offset = left_mesh(left_indexes(ROI_index), :) - obj.network_atlas.anat.hemi_l.nodes(left_indexes(ROI_index), :); + else + offset = right_mesh(right_indexes(ROI_index), :) - obj.network_atlas.anat.hemi_r.nodes(right_indexes(ROI_index), :); + end + ROI_final_positions(ROI_index, :) = ROI_positions(ROI_index, :) + offset; + + switch color_mode + case BrainColorMode.DEFAULT_NETS + ROI_colors(ROI_index, :) = obj.network_atlas.nets(network).color; + case BrainColorMode.COLOR_NETS + ROI_colors(ROI_index, :) = color_matrix(network, :); + case BrainColorMode.COLOR_ROIS + ROI_colors(ROI_index, :) = color_matrix(ROI_index, :); + otherwise + ROI_colors = false; + end + end + end + end + + function drawColorMap(obj, plot_axis) + if obj.color_functional_connectivity + colormap(plot_axis, obj.color_map); + obj.color_bar = colorbar(plot_axis); + obj.color_bar.Location = "southoutside"; + set(obj.color_bar, 'ButtonDownFcn', @obj.openModal); + else + colormap(plot_axis, obj.color_map); + obj.color_bar = colorbar(plot_axis); + obj.color_bar.Location = "southoutside"; + set(obj.color_bar, 'ButtonDownFcn', @obj.openModal); + + number_of_ticks = 10; + ticks = 0:number_of_ticks; + obj.color_bar.Ticks = double(ticks) ./ number_of_ticks; + tick_labels = cell(number_of_ticks + 1, 1); + for tick = ticks + tick_labels{tick + 1} = sprintf("%.2g", obj.lower_limit + (tick * ((double(obj.upper_limit - obj.lower_limit) / number_of_ticks)))); + end + obj.color_bar.TickLabels = tick_labels; + caxis(plot_axis, [0, 1]); + end + end + + function addTitle(obj) + figure_title = sprintf("Brain Visualization: Average of edge-level correlations between nets in [%s - %s] Network Pair", obj.network_atlas.nets(obj.network1).name, obj.network_atlas.nets(obj.network2).name); + if obj.is_noncorrelation_input + figure_title = [figure_title sprintf(" (Edge-level P < %.2g)", obj.edge_test_options.prob_max)]; + end + obj.plot_figure.Name = figure_title; + end + + function openModal(obj, ~, ~) + d = figure("WindowStyle", "normal", "Units", "pixels", "Position", [obj.plot_figure.Position(1) + 10, obj.plot_figure.Position(2) + 10, 350, 140]); + + upper_limit_box_position = [120, 90, 100, 30]; + upper_limit_box = uicontrol("Style", "edit", "Units", "pixels", "String", obj.upper_limit, "Position", upper_limit_box_position); + uicontrol("Style", "text", "Units", "pixels", "String", "Upper Limit", "Position", [upper_limit_box_position(1) - 90, upper_limit_box_position(2) - 2, 80, upper_limit_box_position(4) - 5]); + lower_limit_box_position = [120, 50, 100, 30]; + lower_limit_box = uicontrol("Style", "edit", "Units", "pixels", "String", obj.lower_limit, "Position", lower_limit_box_position); + uicontrol("Style", "text", "Units", "pixels", "String", "Lower Limit", "Position", [lower_limit_box_position(1) - 90, lower_limit_box_position(2) - 2, 80, lower_limit_box_position(4) - 5]); + apply_button_position = [10, 10, 100, 30]; + uicontrol("String", "Apply", "Callback", {@obj.applyScale, upper_limit_box, lower_limit_box}, "Units", "pixels", "Position", apply_button_position); % Apply Button + default_button_position = [apply_button_position(1) + apply_button_position(3) + 10, apply_button_position(2), apply_button_position(3), apply_button_position(4)]; + uicontrol("String", "Default", "Callback", {@obj.setDefaults, upper_limit_box, lower_limit_box}, "Units", "pixels", "Position", default_button_position); + close_button_position = [default_button_position(1) + default_button_position(3) + 10, default_button_position(2), default_button_position(3), default_button_position(4)]; + uicontrol("String", "Close", "Callback", @(~, ~)close(d), "Units", "pixels", "Position", close_button_position); + end + + function applyScale(obj, ~, ~, upper_value, lower_value) + wait_popup = waitbar(0.05, "Please wait while scale is changed..."); + colorbar(obj.color_bar, "off"); + total_colors = 2000; + obj.upper_limit = str2double(upper_value.String); + obj.lower_limit = str2double(lower_value.String); + positive_percent = 0; + negative_percent = 0; + if obj.lower_limit >= 0 + positive_percent = 100; + elseif obj.upper_limit <= 0 + negative_percent = 100; + else + total_spread = obj.upper_limit - obj.lower_limit; + positive_percent = obj.upper_limit / total_spread; + negative_percent = -obj.lower_limit / total_spread; + end + obj.color_map = cat(1, winter(round(total_colors * negative_percent)), flip(autumn(round(total_colors * positive_percent)))); + for edge = obj.all_edges + edge_data = edge.UserData; + edge_data_struct = struct(); + edge_data_names = edge_data{1}; + edge_data_values = edge_data{2}; + for idx = 1:numel(edge_data_names) + edge_data_struct.(edge_data_names{idx}) = edge_data_values{idx}; + end + color_value = obj.mapColorsToLimits(edge_data_struct.coefficient, edge_data_struct.function_connectivity_average, obj.lower_limit, obj.upper_limit); + color_value = [reshape(color_value, [1, 3]), 0.5]; + set(edge, "Color", color_value); + end + obj.drawColorMap(obj.color_map_axis); + waitbar(0.90); + close(wait_popup) + end + + function setDefaults(obj, ~, ~, upper_limit_box, lower_limit_box) + set(upper_limit_box, "String", obj.default_settings.upper_limit); + set(lower_limit_box, "String", obj.default_settings.lower_limit); + end + + %% + % GETTERS for dependent properties + function value = get.is_noncorrelation_input(obj) + % Convenience method to determine if inputs were correlation coefficients, or "significance" values + value = any(strcmp(obj.noncorrelation_input_tests, obj.test_name)); + end + + function value = get.functional_connectivity_exists(obj) + value = isfield(obj.edge_test_options, "func_conn"); + end + + function value = get.color_functional_connectivity(obj) + value = false; + end + %% + end +end \ No newline at end of file diff --git a/+nla/+gfx/+chord/ChordPlot.m b/+nla/+gfx/+chord/ChordPlot.m index 14601949..8ec6e12a 100644 --- a/+nla/+gfx/+chord/ChordPlot.m +++ b/+nla/+gfx/+chord/ChordPlot.m @@ -63,9 +63,9 @@ addRequired(chord_input_parser, 'plot_matrix'); validColorMap = @(x) size(x, 2) == 3; - addParameter(chord_input_parser, 'direction', nla.gfx.SigType.INCREASING, @isenum); + addParameter(chord_input_parser, 'direction', "nla.gfx.SigType.INCREASING"); addParameter(chord_input_parser, 'color_map', turbo(256), validColorMap); - addParameter(chord_input_parser, 'chord_type', nla.PlotType.CHORD, @isenum); + addParameter(chord_input_parser, 'chord_type', "nla.PlotType.CHORD"); addParameter(chord_input_parser, 'upper_limit', 1, @isnumeric); addParameter(chord_input_parser, 'lower_limit', 0, @isnumeric); addParameter(chord_input_parser, 'random_z_order', false, @islogical); @@ -99,7 +99,7 @@ function drawChords(obj) function value = get.space_between_networks_and_labels(obj) value = 6; - if obj.chord_type == nla.PlotType.CHORD + if obj.chord_type == "nla.PlotType.CHORD" value = 3; end end @@ -208,7 +208,7 @@ function createNetworkCircle(obj) import nla.TriMatrix nla.TriMatrixDiag for network = 1:obj.number_of_networks - if obj.chord_type == nla.PlotType.CHORD + if obj.chord_type == "nla.PlotType.CHORD" network_start_radian = (network - 1) * obj.network_size_radians + (obj.space_between_networks_radians / 2); network_end_radian = (network * obj.network_size_radians) - (obj.space_between_networks_radians / 2); else @@ -253,7 +253,7 @@ function rotateNetworkNames(obj, display_name, text_angle, text_position, networ % display_name - the display name for the network, usually some 3-4 letter abbreviation % text_angle - the angle the name should be displayed % text_position - where the name is displayed. A list or array of points - if obj.chord_type == nla.PlotType.CHORD_EDGE && (obj.network_size_radians_array(network) < 0.25) &&... + if obj.chord_type == "nla.PlotType.CHORD_EDGE" && (obj.network_size_radians_array(network) < 0.25) &&... (strlength(display_name) > 5) if strlength(display_name) > 8 @@ -285,9 +285,9 @@ function connectNetworks(obj) % Sort the chords if obj.random_z_order plot_network_indexes = randperm(numel(obj.plot_matrix.v)); - elseif obj.direction == SigType.INCREASING + elseif obj.direction == "nla.gfx.SigType.INCREASING" [~, plot_network_indexes] = sort(obj.plot_matrix.v); - elseif obj.direction == SigType.DECREASING + elseif obj.direction == "nla.gfx.SigType.DECREASING" [~, plot_network_indexes] = sort(obj.plot_matrix.v, 'descend'); else [~, plot_network_indexes] = sort(abs(obj.plot_matrix.v)); @@ -296,7 +296,7 @@ function connectNetworks(obj) % boolean array used to determine if networks connected networks_connected = false(obj.number_of_networks, obj.number_of_networks + 1); - if obj.chord_type == nla.PlotType.CHORD + if obj.chord_type == "nla.PlotType.CHORD" % These two arrays are the networks individucally numbered. Taking the same index of both % (in vector, network_array.v(idx)) gives the two networks we're testing network_array = TriMatrix(obj.number_of_networks, 'double', TriMatrixDiag.KEEP_DIAGONAL); @@ -314,7 +314,7 @@ function connectNetworks(obj) end for network = 1:obj.number_of_networks - if obj.chord_type == nla.PlotType.CHORD + if obj.chord_type == "nla.PlotType.CHORD" % These fill in the four networks above. for network2 = network:obj.number_of_networks network_index = find(networks_connected(network, :) == 0, 1, 'last'); @@ -343,9 +343,9 @@ function connectNetworks(obj) for index_iterator = 1:numel(obj.plot_matrix.v) index = plot_network_indexes(index_iterator); if ~isnan(obj.plot_matrix.v(index)) && (... - (obj.direction == SigType.INCREASING && obj.plot_matrix.v(index) > obj.lower_limit) ||... - (obj.direction == SigType.DECREASING && obj.plot_matrix.v(index) < obj.upper_limit) ||... - (obj.direction == SigType.ABS_INCREASING && abs(obj.plot_matrix.v(index)) > 0)) + (obj.direction == "nla.gfx.SigType.INCREASING" && obj.plot_matrix.v(index) > obj.lower_limit) ||... + (obj.direction == "nla.gfx.SigType.DECREASING" && obj.plot_matrix.v(index) < obj.upper_limit) ||... + (obj.direction == "nla.gfx.SigType.ABS_INCREASING" && abs(obj.plot_matrix.v(index)) > 0)) current_network = obj.plot_matrix.v(index); network_color = nla.gfx.valToColor(current_network, obj.lower_limit, obj.upper_limit, obj.color_map); @@ -354,7 +354,7 @@ function connectNetworks(obj) network_alpha = 0.5; end - if obj.chord_type == nla.PlotType.CHORD + if obj.chord_type == "nla.PlotType.CHORD" network = network_array.v(index); network2 = network2_array.v(index); network_index = network_indexes.v(index); @@ -400,6 +400,7 @@ function connectNetworks(obj) chord1_start_cartesian, 50); mesh_vertices = [outer; flip(inner, 1)]; + warning("off", "MATLAB:polyshape:boundary3Points"); mesh = polyshape(mesh_vertices(:, 1), mesh_vertices(:, 2)); plot(obj.axes, mesh, 'FaceAlpha', network_alpha, 'FaceColor', network_color,... @@ -424,7 +425,7 @@ function connectNetworks(obj) end end - if obj.chord_type == nla.PlotType.CHORD_EDGE + if obj.chord_type == "nla.PlotType.CHORD_EDGE" % This is the inner circle of dots for the rois on the edge chord circle for roi = 1:obj.number_of_ROIs plot(obj.axes, ROI_centers(roi, 1), ROI_centers(roi, 2), '.k', 'MarkerSize', 3); diff --git a/+nla/+gfx/+plots/DiagnosticPlot.m b/+nla/+gfx/+plots/DiagnosticPlot.m new file mode 100644 index 00000000..6ae0da10 --- /dev/null +++ b/+nla/+gfx/+plots/DiagnosticPlot.m @@ -0,0 +1,72 @@ +classdef DiagnosticPlot < handle + + properties + edge_test_options + network_test_options + edge_test_result + network_atlas + networkTestResult + end + + methods + function obj = DiagnosticPlot(edge_test_options, network_test_options, edge_test_result, network_atlas, networkTestResult) + diagnostic_plot_parser = inputParser; + addRequired(diagnostic_plot_parser, 'edge_test_options'); + addRequired(diagnostic_plot_parser, 'network_test_options'); + addRequired(diagnostic_plot_parser, 'edge_test_result'); + addRequired(diagnostic_plot_parser, 'network_atlas'); + addRequired(diagnostic_plot_parser, 'networkTestResult'); + + parse(diagnostic_plot_parser, edge_test_options, network_test_options, edge_test_result, network_atlas, networkTestResult); + properties = {'edge_test_options', "network_test_options", "edge_test_result", "network_atlas", "networkTestResult"}; + for property = properties + obj.(property{1}) = diagnostic_plot_parser.Results.(property{1}); + end + end + + function displayPlots(obj, test_method) + + p_value = nla.net.result.NetworkTestResult().getPValueNames(test_method, obj.networkTestResult.test_name); + + plot_parameters = nla.net.result.NetworkResultPlotParameter(... + obj.networkTestResult, obj.network_atlas, obj.network_test_options... + ); + vs_network_size_parameters = plot_parameters.plotProbabilityVsNetworkSize(test_method, p_value); + no_permutations_vs_network_parameters = plot_parameters.plotProbabilityVsNetworkSize(... + "no_permutations", p_value... + ); + + non_permuted_title = sprintf("Non-permuted P-values vs.\nNetwork-Pair Size"); + permuted_title = sprintf("Permuted P-values vs Network-Pair Size"); + + plotter = nla.net.result.plot.PermutationTestPlotter(obj.network_atlas); + if isequal(test_method, "no_permutations") + nla.gfx.createFigure(500, 500); + plotter.plotProbabilityVsNetworkSize(vs_network_size_parameters, subplot(1, 1, 1), non_permuted_title); + return + end + nla.gfx.createFigure(1200, 500); + + p_value_histogram = obj.networkTestResult.createHistogram(p_value); + plotter.plotProbabilityHistogram(... + subplot(1, 3, 1), p_value_histogram, obj.networkTestResult.full_connectome.(strcat("uncorrected_", p_value)).v,... + obj.networkTestResult.permutation_results.(strcat(p_value, "_permutations")).v(:, 1),... + obj.networkTestResult.test_display_name, obj.network_test_options.prob_max... + ); + plotter.plotProbabilityVsNetworkSize(no_permutations_vs_network_parameters, subplot(1, 3, 2), non_permuted_title); + plotter.plotProbabilityVsNetworkSize(vs_network_size_parameters, subplot(1, 3, 3), permuted_title); + end + end + + methods (Access = private) + function p_value = choosePlottingStatistic(obj, test_method) + p_value = "p_value"; + if obj.network_test_options == nla.gfx.ProbPlotMethod.STATISTIC + p_value = strcat("statistic_", p_value); + end + if ~obj.networkTestResult.is_noncorrelation_input && test_method == "within_network_pair" + p_value = strcat("single_sample_", p_value); + end + end + end +end \ No newline at end of file diff --git a/+nla/+gfx/+plots/MatrixPlot.m b/+nla/+gfx/+plots/MatrixPlot.m index 84ad090f..e0c4e779 100644 --- a/+nla/+gfx/+plots/MatrixPlot.m +++ b/+nla/+gfx/+plots/MatrixPlot.m @@ -21,7 +21,7 @@ figure_size % Size to display. Either nla.gfx.FigSize.SMALL or nla.gfx.FigSize.LARGE draw_legend % Legend on/off draw_colorbar % Colorbar on/off - color_map % Colormap to use (enter 'turbo(256)' for default) + color_map % Colormap to use (enter 'jet(1000)' for default) marked_networks % networks to mark with a symbol discrete_colorbar % colorbar as discrete. TRUE == discrete, FALSE == continuous network_clicked_callback % Button function to add to each network. Used for clickable networks @@ -30,8 +30,10 @@ axes % The axes of the plot image_display % The actual displayed values color_bar % The colorbar + plot_title = false plot_scale % The scale and values being plotted (Linear, log, -log10, p-value, statistic p-value) current_settings % the settings for the plot (upper, lower, scale) + display_legend end properties (Dependent) @@ -50,7 +52,10 @@ colorbar_offset = 15; % Offset of the colorbar colorbar_text_w = 50; % Width of label on colorbar legend_offset = 5; % Offset of the Legend - colormap_choices = {"Parula", "Turbo", "HSV", "Hot", "Cool", "Spring", "Summer", "Autumn", "Winter", "Gray",... + end + + properties (Constant) + colormap_choices = {"Jet", "Parula", "Turbo", "HSV", "Hot", "Cool", "Spring", "Summer", "Autumn", "Winter", "Gray",... "Bone", "Copper", "Pink"}; % Colorbar choices end @@ -77,7 +82,7 @@ % figure_margins = nla.gfx.FigMargins.WHITESPACE % draw_legend = true % draw_colorbar = true - % color_map = turbo(256) + % color_map = jet(1000) % lower_limit = -0.3 % upper_limit = 0.3 % x_position = 0 @@ -99,13 +104,13 @@ addParameter(matrix_input_parser, 'figure_margins', nla.gfx.FigMargins.WHITESPACE, @isenum); addParameter(matrix_input_parser, 'draw_legend', true, @islogical); addParameter(matrix_input_parser, 'draw_colorbar', true, @islogical); - addParameter(matrix_input_parser, 'color_map', turbo(256)); + addParameter(matrix_input_parser, 'color_map', jet(1000)); %reverted to jet(1000) by request addParameter(matrix_input_parser, 'lower_limit', -0.3, validNumberInput); addParameter(matrix_input_parser, 'upper_limit', 0.3, validNumberInput); addParameter(matrix_input_parser, 'x_position', 0, validNumberInput); addParameter(matrix_input_parser, 'y_position', 0, validNumberInput); addParameter(matrix_input_parser, 'discrete_colorbar', false, @islogical); - addParameter(matrix_input_parser, 'plot_scale', nla.gfx.ProbPlotMethod.DEFAULT, @isenum); + addParameter(matrix_input_parser, 'plot_scale', nla.gfx.ProbPlotMethod.DEFAULT); parse(matrix_input_parser, figure, name, matrix, networks, figure_size, varargin{:}); properties = {'figure', 'name', 'matrix', 'networks', 'figure_size', 'network_clicked_callback',... @@ -160,12 +165,12 @@ function displayImage(obj) if obj.draw_colorbar obj.createColorbar(); end - + % Title plot and center title if ~isempty(obj.name) - plot_title = title(obj.axes, ' '); - text(obj.axes, dimensions("plot_width") / 2 , dimensions("offset_y") / 2, obj.name,... - 'FontName', plot_title.FontName, 'FontSize', 14, 'FontWeight', plot_title.FontWeight,... + obj.plot_title = title(obj.axes, ''); + obj.plot_title = text(obj.axes, dimensions("plot_width") / 2 , dimensions("offset_y") / 2, obj.name,... + 'FontName', obj.plot_title.FontName, 'FontSize', 14, 'FontWeight', obj.plot_title.FontWeight,... 'HorizontalAlignment', 'center'); end @@ -174,6 +179,98 @@ function displayImage(obj) end + function applyScale(obj, ~, ~, upper_limit_box, lower_limit_box, scale, color_map_select) + % This callback gets the colormap/scale and then applies the new bounds to the data. + % Only works with APPLY button, will not work with only CLOSE + + import nla.net.result.NetworkResultPlotParameter nla.gfx.ProbPlotMethod + + obj.color_bar.Ticks = []; + + color_map = color_map_select; + if ~isstring(color_map_select) && ~ischar(color_map_select) + color_map = get(color_map_select, "Value"); + color_map = obj.colormap_choices{color_map}; + end + if ischar(color_map) + color_map = string(color_map); + end + + if isnumeric(upper_limit_box) + upper_limit = upper_limit_box; + else + upper_limit = get(upper_limit_box, "String"); + end + + if isnumeric(lower_limit_box) + lower_limit = lower_limit_box; + else + lower_limit = get(lower_limit_box, "String"); + end + + if ~isstring(obj.plot_scale) + obj.plot_scale = char(obj.plot_scale); + end + + obj.matrix = obj.original_matrix; + if ismember(obj.plot_scale, ["nla.ProbPlotMethod.NEGATIVE_LOG_10", "nla.ProbPlotMethod.NEGATIVE_LOG_STATISTIC"]) &&... + ismember(scale, ["nla.ProbPlotMethod.DEFAULT", "nla.ProbPlotMethod.LOG"]) + obj.matrix.v = 10.^(-obj.matrix.v); + + elseif ~ismember(obj.plot_scale, ["nla.ProbPlotMethod.NEGATIVE_LOG_10", "nla.ProbPlotMethod.NEGATIVE_LOG_STATISTIC"]) &&... + ~ismember(scale, ["nla.ProbPlotMethod.DEFAULT", "nla.ProbPlotMethod.LOG"]) + obj.matrix.v = -log10(obj.matrix.v); + lower_limit = 0; + upper_limit = 2; + end + + discrete_colors = NetworkResultPlotParameter().default_discrete_colors; + + if scale == "nla.ProbPlotMethod.DEFAULT" + new_color_map = NetworkResultPlotParameter.getColormap(discrete_colors, upper_limit, color_map); + obj.plot_scale = scale; + elseif scale == "nla.ProbPlotMethod.LOG" + new_color_map = NetworkResultPlotParameter.getLogColormap(discrete_colors, obj.matrix, upper_limit, color_map); + obj.plot_scale = scale; + else + color_map_name = str2func(lower(color_map)); + new_color_map = color_map_name(discrete_colors); + obj.plot_scale = "nla.ProbPlotMethod.NEGATIVE_LOG_10"; + end + obj.color_map = new_color_map; + obj.embiggenMatrix(lower_limit, upper_limit); + obj.createColorbar(lower_limit, upper_limit); + end + + function createLegend(obj) + % Creates the Legend + entries = []; + for network = 1:obj.number_networks + entry = bar(obj.axes, NaN); + entry.FaceColor = obj.networks(network).color; + entry.DisplayName = obj.networks(network).name; + entries = [entries entry]; + end + + obj.display_legend = legend(obj.axes, entries); % Legend object + + dimensions = obj.image_dimensions; + obj.display_legend.Units = 'pixels'; + display_legend_width = obj.display_legend.Position(3); + display_legend_height = obj.display_legend.Position(4); + obj.display_legend.Position = [... + obj.x_position + dimensions("plot_width") - display_legend_width - dimensions("offset_x") - obj.legend_offset,... + obj.y_position + dimensions("plot_height") - display_legend_height - dimensions("offset_y"),... + display_legend_width, display_legend_height... + ]; + end + + function removeLegend(obj) + if ~isempty(obj.display_legend) + delete(obj.display_legend); + end + end + % getters for dependent properties function value = get.number_networks(obj) value = numel(obj.networks); @@ -248,6 +345,7 @@ function displayImage(obj) value = MatrixType.TRIMATRIX; end end + %% end methods (Access = protected) @@ -320,8 +418,14 @@ function addCallback(obj, x) lower_value = obj.lower_limit; else initial_render = false; - upper_value = str2double(varargin{2}); - lower_value = str2double(varargin{1}); + upper_value = varargin{2}; + lower_value = varargin{1}; + if isstring(upper_value) + upper_value = str2double(upper_value); + end + if isstring(lower_value) + lower_value = str2double(lower_value); + end end number_of_networks = obj.number_networks; @@ -481,29 +585,6 @@ function fixRendering(obj) end end - function createLegend(obj) - % Creates the Legend - entries = []; - for network = 1:obj.number_networks - entry = bar(obj.axes, NaN); - entry.FaceColor = obj.networks(network).color; - entry.DisplayName = obj.networks(network).name; - entries = [entries entry]; - end - - display_legend = legend(obj.axes, entries); % Legend object - - dimensions = obj.image_dimensions; - display_legend.Units = 'pixels'; - display_legend_width = display_legend.Position(3); - display_legend_height = display_legend.Position(4); - display_legend.Position = [... - obj.x_position + dimensions("plot_width") - display_legend_width - dimensions("offset_x") - obj.legend_offset,... - obj.y_position + dimensions("plot_height") - display_legend_height - dimensions("offset_y"),... - display_legend_width, display_legend_height... - ]; - end - function createColorbar(obj, varargin) % Creates the colorbar % Annoyance: obj.color_map is a property, colormap is a command. Same with color_bar and colorbar @@ -515,8 +596,14 @@ function createColorbar(obj, varargin) else obj.color_bar.TickLabels = {}; obj.color_bar.Ticks = []; - upper_value = str2double(varargin{2}); - lower_value = str2double(varargin{1}); + upper_value = varargin{2}; + lower_value = varargin{1}; + if isstring(upper_value) + upper_value = str2double(upper_value); + end + if isstring(lower_value) + lower_value = str2double(lower_value); + end end if obj.discrete_colorbar @@ -553,140 +640,11 @@ function createColorbar(obj, varargin) obj.color_bar.Position(2) + dimensions("offset_y"), obj.colorbar_width,... dimensions("image_height") - (dimensions("offset_y") * 2) - 20]; obj.color_bar.Title.Position(2) = 0 - dimensions("offset_y") * 2 / 3; - obj.color_bar.Title.String = sprintf("Click to\nchange scale\n"); obj.color_bar.Title.FontSize = 7; - % Enables callback for clicking on colorbar to scale data - set(obj.color_bar, 'ButtonDownFcn', @obj.openModal) - caxis(obj.axes, [0, 1]); end - function openModal(obj, source, ~) - % Callback for clicking on the colorbar. - % This opens a modal with the upper and lower bounds along with a radio selector between linear and - % log. - import nla.gfx.ProbPlotMethod - - % source is the colorbar, not the figure - d = figure('WindowStyle', 'normal', "Units", "pixels", 'Position', [source.Position(1), source.Position(2),... - source.Position(3) * 16, source.Position(4)/ 1.75]); - % These are the boxes that are the upper and lower end of the scale - upper_limit_box = uicontrol('Style', 'edit', "Units", "pixels",... - 'Position', [90, d.Position(4) - 30, 100, 30], "String", obj.current_settings.upper_limit); - upper_limit_box.Position(4) = upper_limit_box.FontSize * 2; - lower_limit_box = uicontrol('Style', 'edit', "Units", "pixels",... - 'Position', [90, upper_limit_box.Position(2) - 30, 100, 30], "String", obj.current_settings.lower_limit); - lower_limit_box.Position(4) = lower_limit_box.FontSize * 2; - uicontrol('Style', 'text', 'String', 'Upper Limit', "Units", "pixels", 'Position',... - [upper_limit_box.Position(1) - 80, upper_limit_box.Position(2) - 2, 80, upper_limit_box.Position(4)]); - uicontrol('Style', 'text', 'String', 'Lower Limit', "Units", "pixels", 'Position',... - [lower_limit_box.Position(1) - 80, lower_limit_box.Position(2) - 2, 80, lower_limit_box.Position(4)]); - - % These are the buttons that make the scale log or linear - scaleBaseButtons = uibuttongroup(d, "Units", "pixels", "Position", [10, lower_limit_box.Position(2) - 40, 210, 30]); - linear_button = uicontrol(scaleBaseButtons, "Style", "radiobutton", "String", "Linear", "Units", "pixels",... - "Position", [10, 5, 60, 20]); - log_button = uicontrol(scaleBaseButtons, "Style", "radiobutton", "String", "Log", "Units", "pixels",... - "Position", [80, 5, 60, 20]); - neg_log_button = uicontrol(scaleBaseButtons, "Style", "radiobutton", "String", "-Log10", "Units", "pixels",... - "Position", [130, 5, 80, 20]); - % Here we're setting the initial setting for the linear or log button - if obj.current_settings.plot_scale == ProbPlotMethod.DEFAULT || obj.current_settings.plot_scale == ProbPlotMethod.STATISTIC - selected_value = linear_button; - elseif obj.current_settings.plot_scale == ProbPlotMethod.LOG || obj.current_settings.plot_scale == ProbPlotMethod.LOG_STATISTIC - selected_value = log_button; - else - selected_value = neg_log_button; - end - scaleBaseButtons.SelectedObject = selected_value; - - % Color Map selector - % Adapted from colormap-dropdown: https://www.mathworks.com/matlabcentral/fileexchange/43659-colormap-dropdown-menu - uicontrol("Style", "text", "string", "Colormaps", "Units", "pixels",... - "Position", [10, scaleBaseButtons.Position(2) - 45, 80, 25]); - color_map_select = uicontrol('Style', 'popupmenu',... - 'Position', [90, scaleBaseButtons.Position(2) - 45, 280, 30], "Units", "pixels",... - 'FontName', 'Courier'); - initial_colors = 16; - colormap_html = {}; - for colors = 1:numel(obj.colormap_choices) - colormap_function = str2func(strcat(strcat("@(x) ",lower(obj.colormap_choices{colors})), "(x)")); - CData = colormap_function(initial_colors); - new_html_start = ' '; - new_html = ''; - for color_iterator = initial_colors:-1:1 - hex_code = nla.gfx.rgb2hex([CData(color_iterator, 1), CData(color_iterator, 2),... - CData(color_iterator, 3)]); - new_html = [new_html '__']; - end - new_html_end = [new_html ' ']; - new_html = [new_html_start new_html_end]; - colormap_html = [colormap_html; new_html]; - end - - set(color_map_select, "Value", obj.current_settings.color_map, "String", colormap_html); - - apply_button_position = [10, 10, 100, 30]; - apply_button = uicontrol('String', 'Apply',... - 'Callback', {@obj.applyScale, upper_limit_box, lower_limit_box, scaleBaseButtons, color_map_select},... - "Units", "pixels",... - 'Position', apply_button_position); - default_button_position = [apply_button.Position(1) + apply_button.Position(3) + 10,... - apply_button.Position(2), apply_button.Position(3), apply_button.Position(4)]; - uicontrol('String', 'Default',... - 'Callback', {@obj.setDefault, upper_limit_box, lower_limit_box, scaleBaseButtons, color_map_select},... - "Units", "pixels", 'Position',... - default_button_position); - close_button_position = [default_button_position(1) + default_button_position(3) + 10,... - apply_button.Position(2), apply_button.Position(3), apply_button.Position(4)]; - uicontrol("String", "Close", "Callback", @(~, ~)close(d), "Units", "pixels", "Position", close_button_position); - end - - function setDefault(obj, ~, ~, upper_limit_box, lower_limit_box, button_group, color_map_select) - - button_group.Children(end).Value = true; - for child = 1:numel(button_group.Children) - 1 - button_group.Children(child).Value = false; - end - set(upper_limit_box, "String", obj.default_settings.upper_limit); - set(lower_limit_box, "String", obj.default_settings.lower_limit); - set(color_map_select, "Value", obj.default_settings.color_map); - end - - function applyScale(obj, ~, ~, upper_limit_box, lower_limit_box, button_group, color_map_select) - % This callback gets the colormap/scale and then applies the new bounds to the data. - % Only works with APPLY button, will not work with only CLOSE - - import nla.net.result.NetworkResultPlotParameter nla.gfx.ProbPlotMethod - - obj.matrix = copy(obj.original_matrix); - - button_group_value = get(get(button_group, "SelectedObject"), "String"); - - discrete_colors = NetworkResultPlotParameter().default_discrete_colors; - color_map = get(color_map_select, "Value"); - if button_group_value == "Linear" - new_color_map = NetworkResultPlotParameter.getColormap(discrete_colors, get(upper_limit_box, "String"),... - obj.colormap_choices{color_map}); - obj.plot_scale = ProbPlotMethod.DEFAULT; - elseif button_group_value == "Log" - new_color_map = NetworkResultPlotParameter.getLogColormap(discrete_colors, obj.matrix, get(upper_limit_box, "String"), obj.colormap_choices{color_map}); - obj.plot_scale = ProbPlotMethod.LOG; - else - color_map_name = str2func(lower(obj.colormap_choices{color_map})); - new_color_map = color_map_name(discrete_colors); - obj.plot_scale = ProbPlotMethod.NEG_LOG_10; - end - obj.color_map = new_color_map; - obj.embiggenMatrix(get(lower_limit_box, "String"), get(upper_limit_box, "String")); - obj.createColorbar(get(lower_limit_box, "String"), get(upper_limit_box, "String")); - obj.current_settings.upper_limit = get(upper_limit_box, "String"); - obj.current_settings.lower_limit = get(lower_limit_box, "String"); - obj.current_settings.plot_scale = obj.plot_scale; - obj.current_settings.color_map = get(color_map_select, "Value"); - end - function chunk_color = getChunkColor(obj, chunk_raw, upper_value, lower_value) % Get color for the chunk (square) diff --git a/+nla/+gfx/ProbPlotMethod.m b/+nla/+gfx/ProbPlotMethod.m index dd675d32..6f867990 100755 --- a/+nla/+gfx/ProbPlotMethod.m +++ b/+nla/+gfx/ProbPlotMethod.m @@ -1,5 +1,5 @@ classdef ProbPlotMethod enumeration - DEFAULT, LOG, NEG_LOG_10, STATISTIC, LOG_STATISTIC, NEG_LOG_STATISTIC + DEFAULT, LOG, NEGATIVE_LOG_10, STATISTIC, LOG_STATISTIC, NEGATIVE_LOG_STATISTIC end end \ No newline at end of file diff --git a/+nla/+gfx/anatToMesh.m b/+nla/+gfx/anatToMesh.m index 9e78ff06..1aec8029 100755 --- a/+nla/+gfx/anatToMesh.m +++ b/+nla/+gfx/anatToMesh.m @@ -30,7 +30,7 @@ % rotate right hemi around and move to position for visualization cmL = mean(mesh_l, 1); cmR = mean(mesh_r, 1); - rm = nla.helpers.rotationMatrix(nla.Dir.Z, pi); + rm = nla.helpers.rotationMatrix(nla.Direction.Z, pi); % Rotate switch view_pos diff --git a/+nla/+gfx/drawBrainVis.m b/+nla/+gfx/drawBrainVis.m index b9e845bd..aa6a7311 100755 --- a/+nla/+gfx/drawBrainVis.m +++ b/+nla/+gfx/drawBrainVis.m @@ -157,7 +157,20 @@ function drawEdges(ROI_pos, ax, net_atlas, net1, net2, color_map, color_map_p, c end end - function onePlot(ax, pos, color_mode, color_mat) + function onePlot(varargin) + if nargin >= 3 + ax = varargin{1}; + pos = varargin{2}; + color_mode = varargin{3}; + end + if nargin >= 4 + color_mat = varargin{4}; + end + if nargin > 4 + ulimit = str2double(varargin{5}); + llimit = str2double(varargin{6}); + end + if color_mode == nla.gfx.BrainColorMode.NONE ROI_final_pos = nla.gfx.drawROIsOnCortex(ax, net_atlas, ctx, mesh_alpha, ROI_radius, pos, surface_parcels,... nla.gfx.BrainColorMode.NONE); @@ -213,6 +226,37 @@ function onePlot(ax, pos, color_mode, color_mat) hold(ax, 'off'); nla.gfx.hideAxes(ax); + function openModal(source, ~) + d = figure("WindowStyle", "normal", "Units", "pixels", "Position", [source.Parent.Position(1), source.Parent.Position(2), 250, 150]); + upper_limit_box = uicontrol("Style", "edit", "Units", "pixels", "Position", [d.Position(3) / 2, d.Position(4) / 2 + 5, 50, 25], "String", ulimit); + lower_limit_box = uicontrol("Style", "edit", "Units", "pixels", "Position", [d.Position(3) / 2, d.Position(4) / 2 - upper_limit_box.Position(4) - 5, 50, 25], "String", llimit); + uicontrol("Style", "text", "String", "Upper Limit", "Units", "pixels", "Position", [upper_limit_box.Position(1) - 80, upper_limit_box.Position(2) - 5, 80, upper_limit_box.Position(4)]); + uicontrol("Style", "text", "String", "Lower Limit", "Units", "pixels", "Position", [lower_limit_box.Position(1) - 80, lower_limit_box.Position(2) - 5, 80, lower_limit_box.Position(4)]); + + apply_button_position = [d.Position(3) / 2 - 85, 10, 80, 25]; + close_button_position = [apply_button_position(1) + 85, apply_button_position(2), apply_button_position(3), apply_button_position(4)]; + uicontrol("String", "Apply", "Units", "pixels", "Position", apply_button_position, "Callback", {@applyScale, upper_limit_box, lower_limit_box}); + uicontrol("String", "Close", "Units", "pixels", "Position", close_button_position, "Callback", @(~, ~)close(d)); + end + + function applyScale(~, ~, upper_limit_box, lower_limit_box) + upper_limit = get(upper_limit_box, "String"); + lower_limit = get(lower_limit_box, "String"); + figure(fig) + if surface_parcels && ~islogical(net_atlas.parcels) + onePlot(subplot('Position',[.45,0.505,.53,.45]), nla.gfx.ViewPos.LAT, nla.gfx.BrainColorMode.COLOR_ROIS, color_mat, upper_limit, lower_limit); + onePlot(subplot('Position',[.45,0.055,.53,.45]), nla.gfx.ViewPos.MED, nla.gfx.BrainColorMode.COLOR_ROIS, color_mat, upper_limit, lower_limit); + else + onePlot(subplot('Position',[.45,0.505,.26,.45]), nla.gfx.ViewPos.BACK, nla.gfx.BrainColorMode.NONE, false, upper_limit, lower_limit); + onePlot(subplot('Position',[.73,0.505,.26,.45]), nla.gfx.ViewPos.FRONT, nla.gfx.BrainColorMode.NONE, false, upper_limit, lower_limit); + onePlot(subplot('Position',[.45,0.055,.26,.45]), nla.gfx.ViewPos.LEFT, nla.gfx.BrainColorMode.NONE, false, upper_limit, lower_limit); + onePlot(subplot('Position',[.73,0.055,.26,.45]), nla.gfx.ViewPos.RIGHT, nla.gfx.BrainColorMode.NONE, false, upper_limit, lower_limit); + end + % ROI_final_pos = nla.gfx.drawROIsOnCortex(ax, net_atlas, ctx, mesh_alpha, ROI_radius, pos, surface_parcels,... + % nla.gfx.BrainColorMode.NONE); + % drawEdges(ROI_final_pos, ax, net_atlas, net1, net2, color_map, color_map_p, color_map_n, color_fx, fc_exists, lower_limit, upper_limit); + end + %% Display colormap if color_fc % legend(ax, 'Location', 'best'); @@ -232,6 +276,7 @@ function onePlot(ax, pos, color_mode, color_mat) cb = colorbar(ax); cb.Location = 'southoutside'; cb.Label.String = 'Coefficient Magnitude'; + cb.ButtonDownFcn = @openModal; ticks = [0:num_ticks]; cb.Ticks = double(ticks) ./ num_ticks; diff --git a/+nla/+gfx/drawCortexHemi.m b/+nla/+gfx/drawCortexHemi.m index b6fec47c..eebba827 100755 --- a/+nla/+gfx/drawCortexHemi.m +++ b/+nla/+gfx/drawCortexHemi.m @@ -6,7 +6,7 @@ % color: 3x1 vector, cortex mesh color % mesh_alpha: transparency of cortex mesh - obj = patch(ax, 'Faces',anat_hemi.elements(:,1:3),'Vertices', mesh,... + obj = patch(ax, 'Faces', anat_hemi.elements(:,1:3),'Vertices', mesh,... 'EdgeColor','none','FaceColor','interp','FaceVertexCData', color,... 'FaceLighting','gouraud','FaceAlpha',mesh_alpha,... 'AmbientStrength',0.25,'DiffuseStrength',0.75,'SpecularStrength',0.1); diff --git a/+nla/+gfx/unittest/ChordPlotTest.m b/+nla/+gfx/unittest/ChordPlotTest.m new file mode 100644 index 00000000..9fd5f3a4 --- /dev/null +++ b/+nla/+gfx/unittest/ChordPlotTest.m @@ -0,0 +1,139 @@ +classdef ChordPlotTest < matlab.unittest.TestCase + properties + network_atlas + edge_test_options + chord_plot + plot_axes + axis_width + plot_matrix + direction + color_map + chord_type + upper_limit + lower_limit + z_order + end + + methods (TestClassSetup) + function loadTestData(testCase) + import nla.TriMatrix + import nla.gfx.chord.ChordPlot + + root_path = nla.findRootPath(); + + % Load up a network atlas + network_atlas_path = strcat(root_path, fullfile('support_files',... + 'Wheelock_2020_CerebralCortex_15nets_288ROI_on_MNI.mat')); + testCase.network_atlas = nla.NetworkAtlas(network_atlas_path); + + precalculated_path = strcat(root_path, fullfile('examples', 'precalculated/')); + observed_p_file = load(strcat(precalculated_path, 'SIM_obs_p.mat')); + testCase.edge_test_options.precalc_obs_p = TriMatrix(testCase.network_atlas.numROIs); + testCase.edge_test_options.precalc_obs_p.v = observed_p_file.SIM_obs_p; + + testCase.plot_axes = axes(); + testCase.axis_width = 500; + testCase.plot_matrix = testCase.edge_test_options.precalc_obs_p; + testCase.direction = nla.gfx.SigType.INCREASING; + testCase.color_map = parula(256); + testCase.chord_type = nla.PlotType.CHORD_EDGE; + testCase.upper_limit = 1; + testCase.lower_limit = 0; + testCase.z_order = true; + + testCase.chord_plot = ChordPlot(testCase.network_atlas, testCase.plot_axes, testCase.axis_width,... + testCase.plot_matrix, 'direction', testCase.direction, 'color_map', testCase.color_map,... + 'chord_type', testCase.chord_type, 'upper_limit', testCase.upper_limit, 'lower_limit', testCase.lower_limit,... + 'random_z_order', testCase.z_order); + end + end + + methods (TestClassTeardown) + function clearTestData(testCase) + close all + clear + end + end + + methods (Test) + function initailizeChordPlotTest(testCase) + + testCase.verifyEqual(testCase.plot_axes, testCase.chord_plot.axes); + testCase.verifyEqual(testCase.axis_width, testCase.chord_plot.axis_width); + testCase.verifyEqual(testCase.plot_matrix, testCase.chord_plot.plot_matrix); + testCase.verifyEqual(testCase.direction, testCase.chord_plot.direction); + testCase.verifyEqual(testCase.color_map, testCase.chord_plot.color_map); + testCase.verifyEqual(testCase.chord_type, testCase.chord_plot.chord_type); + testCase.verifyEqual([testCase.upper_limit testCase.lower_limit],... + [testCase.chord_plot.upper_limit testCase.chord_plot.lower_limit]); + testCase.verifyEqual(testCase.z_order, testCase.chord_plot.random_z_order); + end + + function getCircleRadiusTest(testCase) + testCase.verifyEqual(testCase.chord_plot.circle_radius, 200); + end + + function getTextRadiusTest(testCase) + testCase.verifyEqual(testCase.chord_plot.text_radius, (... + testCase.chord_plot.circle_radius + (testCase.chord_plot.text_width / 4))); + end + + function getSpaceBetweenNetworksAndLabelsTest(testCase) + testCase.verifyEqual(testCase.chord_plot.space_between_networks_and_labels, 6); + testCase.chord_plot.chord_type = nla.PlotType.CHORD; + testCase.verifyEqual(testCase.chord_plot.space_between_networks_and_labels, 3); + end + + function getSpaceBetweenNetworksAndLabelsRadiansTest(testCase) + testCase.verifyEqual(testCase.chord_plot.space_between_networks_radians,... + atan(testCase.chord_plot.space_between_networks / testCase.chord_plot.circle_radius)); + end + + function getInnerCircleRadiusTest(testCase) + testCase.verifyEqual(testCase.chord_plot.inner_circle_radius,... + testCase.chord_plot.circle_radius - testCase.chord_plot.circle_thickness); + end + + function getChordRadiusTest(testCase) + testCase.verifyEqual(testCase.chord_plot.chord_radius,... + testCase.chord_plot.inner_circle_radius - testCase.chord_plot.space_between_networks_and_labels); + end + + function getNetworkSizeRadiansTest(testCase) + testCase.verifyEqual(testCase.chord_plot.network_size_radians, (2 * pi / testCase.network_atlas.numNets())); + end + + function getNetworkPairSizeRadiansTest(testCase) + expected_value = (testCase.chord_plot.network_size_radians - testCase.chord_plot.space_between_networks_radians) /... + (testCase.network_atlas.numNets() + 1); + testCase.verifyEqual(testCase.chord_plot.network_pair_size_radians, expected_value); + end + + function getROISizeRadiansTest(testCase) + expected_value = ((2 * pi) - (testCase.chord_plot.space_between_networks_radians * testCase.network_atlas.numNets())) ./... + testCase.network_atlas.numROIs(); + testCase.verifyEqual(testCase.chord_plot.ROI_size_radians, expected_value); + end + + function getNumberOfNetworksTest(testCase) + testCase.verifyEqual(testCase.chord_plot.number_of_networks, testCase.network_atlas.numNets()); + end + + function getNumberOfROIsTest(testCase) + testCase.verifyEqual(testCase.chord_plot.number_of_ROIs, testCase.network_atlas.numROIs()); + end + + function getNetworkSizeRadiansArrayTest(testCase) + network_size = []; + for network = 1:testCase.network_atlas.numNets() + network_size(network) = testCase.network_atlas.nets(network).numROIs(); + end + expected_value = network_size .* testCase.chord_plot.ROI_size_radians + testCase.chord_plot.space_between_networks_radians; + testCase.verifyEqual(testCase.chord_plot.network_size_radians_array, expected_value); + end + + function getCumulativeNetworkSizeTest(testCase) + testCase.verifyEqual(testCase.chord_plot.cumulative_network_size, cumsum(testCase.chord_plot.network_size_radians_array)); + end + end +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/AbstractSwEStdErrStrategy.m b/+nla/+helpers/+stdError/AbstractSwEStdErrStrategy.m new file mode 100755 index 00000000..774b5498 --- /dev/null +++ b/+nla/+helpers/+stdError/AbstractSwEStdErrStrategy.m @@ -0,0 +1,10 @@ +classdef (Abstract) AbstractSwEStdErrStrategy < handle + + methods + + stdError = calculate(obj, SwEStdErrInput) %input is SwEStdErrorInput object, output is 2D matrix (numCovariates x numFcEdges) + + end + +end + \ No newline at end of file diff --git a/+nla/+helpers/+stdError/GroupHeteroskedastic_FAST.m b/+nla/+helpers/+stdError/GroupHeteroskedastic_FAST.m new file mode 100755 index 00000000..b55a4eda --- /dev/null +++ b/+nla/+helpers/+stdError/GroupHeteroskedastic_FAST.m @@ -0,0 +1,70 @@ +classdef GroupHeteroskedastic_FAST < nla.helpers.stdError.AbstractSwEStdErrStrategy + + methods + + function stdErr = calculate(obj, sweStdErrInput) + %Computes Standard Error, but accelerated using assumption of + %heteroskeadisticity between groups for quicker computation + % + %To understand what is meant by 'heteroskedasticity' here in computation of covariance of betas, + %refer to https://lukesonnet.com/teaching/inference/200d_standard_errors.pdf + % + %Our covariance of betas is of form M * V * M', and with + %heteroskedasticity assumption, V is diagonal. With this + %assumption, can greatly accelerate computation. + %Efficient algo adapted from + %https://www.mathworks.com/matlabcentral/answers/87629-efficiently-multiplying-diagonal-and-general-matrices + %(And in case that page goes away, copying text in file + %/data/wheelock/data1/people/ecka/fastDiagMatrixMultAlgo.txt) + + + %rename variables for readability + pinvDesignMtx = sweStdErrInput.pinvDesignMtx; + residual = sweStdErrInput.residual; + + %Calculation of standard error assuming heteroskedascticity + %consistent errors + [numCovariates, numObs] = size(pinvDesignMtx); + + + invDesMtxSelfMultPreCompute = zeros(numCovariates,numCovariates,numObs); + + for i = 1:numObs + invDesMtxSelfMultPreCompute(:,:,i) = pinvDesignMtx(:,i) * pinvDesignMtx(:,i)'; + end + invDesMtxSelfMultPreCompute = reshape(invDesMtxSelfMultPreCompute,numCovariates^2, numObs); + + %Use pregenerated matrix to compute covariance of our estimates + %of the regressors. + % + %TODO: correction factor here is 'HC1' (default used in stata), but + %HC2 or HC3 might be preferable? (per + %lukesonnet.com/teaching/inference/200d_standard_errors.pdf) + correctionFactor = (numObs / (numObs - numCovariates)); + + %compute covariance of each group + groupedVariance = zeros(size(residual)); + unqGrps = unique(sweStdErrInput.scanMetadata.groupId); + + for grpIdx = 1:length(unqGrps) + thisGrpId = unqGrps(grpIdx); + rowsThisGrp = sweStdErrInput.scanMetadata.groupId == thisGrpId; + obsThisGrp = sum(rowsThisGrp); + + pooledVarianceThisGrp = correctionFactor * ones(obsThisGrp,1) * sum(residual(rowsThisGrp,:).^2,1)/obsThisGrp; + groupedVariance(rowsThisGrp,:) = pooledVarianceThisGrp; + end + + betaCovarianceFlat = (invDesMtxSelfMultPreCompute * groupedVariance); + + %Get square root of diagonal elements to compute std error + diagElemIdxsInFlatArr = 1:(numCovariates+1):numCovariates^2; + stdErr = sqrt(betaCovarianceFlat(diagElemIdxsInFlatArr,:)); + + end + + end + + + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/Guillaume.m b/+nla/+helpers/+stdError/Guillaume.m new file mode 100755 index 00000000..f2f65cc3 --- /dev/null +++ b/+nla/+helpers/+stdError/Guillaume.m @@ -0,0 +1,295 @@ +classdef Guillaume < nla.helpers.stdError.AbstractSwEStdErrStrategy + + %some code here is adapted from Guillaume implementation of Sandwich + %Estimator available at http://www.nisox.org/Software/SwE/ + %(specifically, swe_cp.m, or swe_contrasts.m where noted) + %adapted blocks of code are marked with a comment noting where + %in the original source file they were referenced from + + properties + totalSpectralCorrections; + end + + methods + + function stdError = calculate(obj, sweStdErrInput) + + + + [numCovariates, numObservations] = size(sweStdErrInput.pinvDesignMtx); + [~, numBetaVectors] = size(sweStdErrInput.residual); + + betaCovar = nla.TriMatrix(zeros(numCovariates,numCovariates,numBetaVectors), nla.TriMatrixDiag.KEEP_DIAGONAL); + obj.totalSpectralCorrections = 0; + + grpIdUnq = unique(sweStdErrInput.scanMetadata.groupId); + + + for grpIdx = 1:length(grpIdUnq) + + %Filter data to just the scans that fall in this group + thisGrpId = grpIdUnq(grpIdx); + scansInGrp = sweStdErrInput.scanMetadata.groupId == thisGrpId; + + + scanMetadataThisGrp = nlaEckDev.swedata.ScanMetadata(sweStdErrInput.scanMetadata); + scanMetadataThisGrp.filterByGroupId(thisGrpId); + + pinvDesignMtxThisGrp = sweStdErrInput.pinvDesignMtx(:,scansInGrp); + residThisGrp = sweStdErrInput.residual(scansInGrp,:); + + %If this group does not span multiple visits, need + %temporary special behavior since TriMatrix objects cannot + %represent 1x1 matrices + unqVisThisGrp = unique(scanMetadataThisGrp.visitId); + + if length(unqVisThisGrp) == 1 + betaCovarThisGroup = obj.calcCovarOnlyOneVisit(residThisGrp, pinvDesignMtxThisGrp); + else + %If there are multiple visits in this group, calculate + %using Guillaume algo from NiSox tutorial + + visitCovarThisGroup = obj.calcVisitCovarMtx(scanMetadataThisGrp, residThisGrp); + betaCovarThisGroup = obj.calcDesignMtxCovarOneGroup(scanMetadataThisGrp, visitCovarThisGroup, pinvDesignMtxThisGrp); + + end + + betaCovar.v = betaCovar.v + betaCovarThisGroup.v; + + end + + stdError = sqrt(betaCovar.v(betaCovar.getDiagElemIdxs,:)); + + + end + + end + + methods (Access = private) + + function betaCovariance = calcCovarOnlyOneVisit(obj, residual, pInvX) + + baseVar = mean(residual.^2,1); + + %ADAPT lines 377-391 + numCovariates = size(pInvX,1); + numBetaCovarElems = numCovariates * (numCovariates + 1) / 2; + + weights = zeros(numBetaCovarElems, 1); + + betaElemIdx = 0; + for betaRowIdx = 1:numCovariates + for betaColIdx = betaRowIdx:numCovariates + betaElemIdx = betaElemIdx + 1; + + thisWeightElem = pInvX(betaRowIdx, :) * pInvX(betaColIdx, :)'; + + weights(betaElemIdx, 1) = thisWeightElem; + + end + end + + numFcEdges = size(residual,2); + betaCovariance = nla.TriMatrix(zeros(numCovariates, numCovariates, numFcEdges), nla.TriMatrixDiag.KEEP_DIAGONAL); + betaCovariance.v = weights * baseVar; + + + end + + function visitCovariance = calcVisitCovarMtx(obj, scanMetadata, residual) + + + [numScans, numFcEdges] = size(residual); + unqVis = unique(scanMetadata.visitId); + numUnqVis = length(unqVis); + + %Initialize covariance object to hold results + initZeroData = zeros(numUnqVis,numUnqVis,numFcEdges); + visitCovariance = nla.TriMatrix(initZeroData, nla.TriMatrixDiag.KEEP_DIAGONAL); + %visitCovariance = swedata.ManyFlatCovarianceMtx(numUnqVis, numFcEdges); + + %First calculate diag elems of visit covar mtx + visitCovariance = obj.calcDiagCovarElems(visitCovariance, scanMetadata, residual); + + %Here Guillaume (2014) would remove pixels that show zero + %variance from residual, beta, and covariance matrices. We have + %decided instead to retain all data + + %calculate off diag elems of visit covar mtx + visitCovariance = obj.calcOffDiagCovarElems(visitCovariance, scanMetadata, residual); + + %NaN may be produced in cov. estimation when one + %corresponding variance is 0 (Per Guillaume). Set NaN's to 0. + visitCovariance.v(isnan(visitCovariance.v)) = 0; + + %Find if any eigenvalues of the covariance matrices for each + %voxel are < 0. If they are, set them to 0 and regenerate the + %covariance matrix for that voxel (Per Guillaume) + allCovLowerDiagMtx = visitCovariance.asMatrix(); + allCovLowerDiagMtx(isnan(allCovLowerDiagMtx))=0; + + + for fcIdx = 1:numFcEdges + thisLowerDiagMtx = allCovLowerDiagMtx(:,:,fcIdx); + thisCovMtx = thisLowerDiagMtx + thisLowerDiagMtx' - diag(diag(thisLowerDiagMtx)); + + + [V, D] = eig(thisCovMtx); + if any(diag(D)<0) + D(D<0) = 0; + thisCovMtx = V * D * V'; + visitCovariance.v(:,fcIdx) = thisCovMtx(tril(ones(size(thisCovMtx)))==1); + obj.totalSpectralCorrections = obj.totalSpectralCorrections + 1; + end + end + + + end + + function covar = calcDiagCovarElems(obj, covar, scanMetadata, residual) + + %ADAPT swe_cp.m lines 740-743 + + unqVisits = unique(scanMetadata.visitId); + + for elemIdx = covar.getDiagElemIdxs() + + [thisVisRowInCovarMtx, ~] = covar.getRowAndColOfElem(elemIdx); + thisVisId = unqVisits(thisVisRowInCovarMtx); + scanFlagThisVis = scanMetadata.visitId == thisVisId; + + covar.v(elemIdx, :) = mean(residual(scanFlagThisVis,:).^2,1); + + end + end + + + + function covar = calcOffDiagCovarElems(obj, covar, scanMetadata, residual) + + %ADAPT lines 755-762 + + for elemIdx = covar.getOffDiagElemIdxs() + + [visId1,visId2] = covar.getRowAndColOfElem(elemIdx); + + [flagVis1, flagVis2] = obj.getFlagsOfSubjScansWithPairOfVisits(scanMetadata, visId1, visId2); + + if any(flagVis1) + covar.v(elemIdx,:) = ... + sum(residual(flagVis1,:).*residual(flagVis2,:)).* ... + sqrt(... + mean(residual(flagVis1,:).^2,1) .* mean(residual(flagVis2,:).^2,1) ./ ... + ( sum(residual(flagVis1,:).^2,1) .* sum(residual(flagVis2,:).^2,1) )... + ); + end + + end + end + + + + function [flagVis1, flagVis2] = getFlagsOfSubjScansWithPairOfVisits(obj, scanMetadata, visId1, visId2) + %Finds scans corresponding to subjects that have data for both of a pair of + %visit IDs in a given group. + %For the pair of visits, returns indices of scans that fall in + %visId1 and visId2 in the given group + %Inputs: + % scanIdInfo - instance of swedata.ScanIdInfo. This class contains subjId, groupId, and visitId; + % grpId - groupId to filter on + % visId1, visId2 - pair of visit id's to search for + % + %Outputs: + % flagVis1 - logical array of whether a scan is vis1 and has + % a corresponding scan from the same subj and group for visId2 + % flagVis2 - same idea for vis2 + + flagVis1 = false(size(scanMetadata.visitId)); + flagVis2 = false(size(scanMetadata.visitId)); + unqSubjs = unique(scanMetadata.subjId); + + %NOTE: yes, the transpose of unqSubjs below is needed. + %To loop over elements of a vector one by one, MATLAB requires + %it to be a row vector. Turns out if instead a column vector is + %used, the loop will run just once using the whole vector. + for subj = unqSubjs' + + hasVisitPair = ... + (scanMetadata.subjId == subj) & ... + ( (scanMetadata.visitId == visId1) | (scanMetadata.visitId == visId2) ); + + if any(hasVisitPair) + hasVisitPairAndIsVis1 = hasVisitPair & (scanMetadata.visitId == visId1); + hasVisitPairAndIsVis2 = hasVisitPair & (scanMetadata.visitId == visId2); + flagVis1(hasVisitPairAndIsVis1) = true; + flagVis2(hasVisitPairAndIsVis2) = true; + end + + end + + end + + + function betaCovariance = calcDesignMtxCovarOneGroup(obj, scanMetadata, visitCovar, pinvX) + + %ADAPT line 797 + weights = obj.calcWeightsForVisitCovarToDesignMtxCovarTform(scanMetadata, visitCovar, pinvX); + + %Initialize empty TriMatrix of proper size, then calculate data field + numCovariates = size(pinvX,1); + numFcEdges = size(visitCovar.v,2); + betaCovariance = nla.TriMatrix(zeros(numCovariates, numCovariates, numFcEdges), nla.TriMatrixDiag.KEEP_DIAGONAL); + betaCovariance.v = weights * visitCovar.v; + + + end + + + function weights = calcWeightsForVisitCovarToDesignMtxCovarTform(obj, scanMetadata, visitCovar, pInvX) + + %ADAPT lines 377-391 + numCovariates = size(pInvX,1); + numBetaCovarElems = numCovariates * (numCovariates + 1) / 2; + + numVisCovarElems = size(visitCovar.v,1); + + weights = zeros(numBetaCovarElems, numVisCovarElems); + + unqVisits = unique(scanMetadata.visitId); + + betaElemIdx = 0; + for betaRowIdx = 1:numCovariates + for betaColIdx = betaRowIdx:numCovariates + betaElemIdx = betaElemIdx + 1; + + for visitCovarElem = 1:numVisCovarElems + [visitRow, visitCol] = visitCovar.getRowAndColOfElem(visitCovarElem); + if any(visitCovarElem == visitCovar.getDiagElemIdxs()) + matchingVisitIdxs = scanMetadata.visitId == unqVisits(visitRow); + thisWeightElem = pInvX(betaRowIdx, matchingVisitIdxs) * pInvX(betaColIdx, matchingVisitIdxs)'; + + weights(betaElemIdx, visitCovarElem) = thisWeightElem; + + else + visitRowId = unqVisits(visitRow); + visitColId = unqVisits(visitCol); + + [flagRowVis, flagColVis] = obj.getFlagsOfSubjScansWithPairOfVisits(scanMetadata, visitRowId, visitColId); + + thisWeightElem = pInvX(betaRowIdx, flagRowVis) * pInvX(betaColIdx, flagColVis)' + ... + pInvX(betaRowIdx, flagColVis) * pInvX(betaColIdx, flagRowVis)'; + + weights(betaElemIdx, visitCovarElem) = thisWeightElem; + + end + + end + + end + end + + end + + end + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/Heteroskedastic.m b/+nla/+helpers/+stdError/Heteroskedastic.m new file mode 100755 index 00000000..0305632a --- /dev/null +++ b/+nla/+helpers/+stdError/Heteroskedastic.m @@ -0,0 +1,35 @@ +classdef Heteroskedastic < nla.helpers.stdError.AbstractSwEStdErrStrategy + + methods + + function stdErr = calculate(obj, sweStdErrInput) + + validateattributes(sweStdErrInput, 'nlaEckDev.sweStdError.SwEStdErrorInput', {}); + %Calculation of standard error assuming heteroskedascticity + %consistent errors + [numCovariates, numObs] = size(sweStdErrInput.pinvDesignMtx); + + %give variables shorter names for readability + pinvDesignMtx = sweStdErrInput.pinvDesignMtx; + residual = sweStdErrInput.residual; + + + numFcEdges = size(residual,2); + stdErr = zeros(numCovariates, numFcEdges); + residSqr = residual.^2; + correctionFactor = (numObs / (numObs - numCovariates)); + + for fcEdgeIdx = 1:numFcEdges + + thisFcEdgeResidSqr = residSqr(:,fcEdgeIdx); + thisV = diag(thisFcEdgeResidSqr); + betaCovariance = pinvDesignMtx * thisV * pinvDesignMtx'; + stdErr(:,fcEdgeIdx) = sqrt(correctionFactor * diag(betaCovariance)); + + end + + end + + end + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/Heteroskedastic_FAST.m b/+nla/+helpers/+stdError/Heteroskedastic_FAST.m new file mode 100755 index 00000000..5b240894 --- /dev/null +++ b/+nla/+helpers/+stdError/Heteroskedastic_FAST.m @@ -0,0 +1,56 @@ +classdef Heteroskedastic_FAST < nla.helpers.stdError.AbstractSwEStdErrStrategy + + methods + + function stdErr = calculate(obj, sweStdErrInput) + %Computes Standard Error, but accelerated using assumption of + %heteroskeadisticity for quicker computation + % + %To understand what is meant by 'heteroskedasticity' here in computation of covariance of betas, + %refer to https://lukesonnet.com/teaching/inference/200d_standard_errors.pdf + % + %Our covariance of betas is of form M * V * M', and with + %heteroskedasticity assumption, V is diagonal. With this + %assumption, can greatly accelerate computation. + %Efficient algo adapted from + %https://www.mathworks.com/matlabcentral/answers/87629-efficiently-multiplying-diagonal-and-general-matrices + %(And in case that page goes away, copying text in file + %/data/wheelock/data1/people/ecka/fastDiagMatrixMultAlgo.txt) + + + %rename variables for readability + pinvDesignMtx = sweStdErrInput.pinvDesignMtx; + residual = sweStdErrInput.residual; + + %Calculation of standard error assuming heteroskedascticity + %consistent errors + [numCovariates, numObs] = size(pinvDesignMtx); + + + invDesMtxSelfMultPreCompute = zeros(numCovariates,numCovariates,numObs); + + for i = 1:numObs + invDesMtxSelfMultPreCompute(:,:,i) = pinvDesignMtx(:,i) * pinvDesignMtx(:,i)'; + end + invDesMtxSelfMultPreCompute = reshape(invDesMtxSelfMultPreCompute,numCovariates^2, numObs); + + %Use pregenerated matrix to compute covariance of our estimates + %of the regressors. + % + %TODO: correction factor here is 'HC1' (default used in stata), but + %HC2 or HC3 might be preferable? (per + %lukesonnet.com/teaching/inference/200d_standard_errors.pdf) + correctionFactor = (numObs / (numObs - numCovariates)); + betaCovarianceFlat = correctionFactor * (invDesMtxSelfMultPreCompute * (residual.^2)); + + %Get square root of diagonal elements to compute std error + diagElemIdxsInFlatArr = 1:(numCovariates+1):numCovariates^2; + stdErr = sqrt(betaCovarianceFlat(diagElemIdxsInFlatArr,:)); + + end + + end + + + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/Homoskedastic.m b/+nla/+helpers/+stdError/Homoskedastic.m new file mode 100755 index 00000000..fce89f6b --- /dev/null +++ b/+nla/+helpers/+stdError/Homoskedastic.m @@ -0,0 +1,26 @@ +classdef Homoskedastic < nla.helpers.stdError.AbstractSwEStdErrStrategy + + methods + + function stdErr = calculate(obj, sweStdErrInput) + + %Calculation of standard error assuming homoskedasticity + %(errors are independent and identically distributed iid) + %This is solution of standard error for Ordinary Least Squares + + pinvDesignMtx = sweStdErrInput.pinvDesignMtx; + residual = sweStdErrInput.residual; + + + degOfFree = size(pinvDesignMtx,2) - size(pinvDesignMtx,1) - 1; + meanSqErr = sum(residual.^2) ./ degOfFree; %In regression, divide by dof instead of number of data points (per wikipedia) + + + stdErr = sqrt(diag(pinvDesignMtx * pinvDesignMtx')*meanSqErr); + + + end + + end + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/SwEStdErrorInput.m b/+nla/+helpers/+stdError/SwEStdErrorInput.m new file mode 100755 index 00000000..96fdf4fe --- /dev/null +++ b/+nla/+helpers/+stdError/SwEStdErrorInput.m @@ -0,0 +1,11 @@ +classdef SwEStdErrorInput + + properties + + scanMetadata nlaEckDev.swedata.ScanMetadata + residual % [numObservations x numOutputVectors] + pinvDesignMtx % pseudoinverse of design matrix [numCovariates x numObservations] + + end + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/UnconstrainedBlocks.m b/+nla/+helpers/+stdError/UnconstrainedBlocks.m new file mode 100755 index 00000000..18944e06 --- /dev/null +++ b/+nla/+helpers/+stdError/UnconstrainedBlocks.m @@ -0,0 +1,102 @@ +classdef UnconstrainedBlocks < nla.helpers.stdError.AbstractSwEStdErrStrategy + + properties + + SPARSITY_THRESHOLD = 0.2; + + end + + methods + + function stdErr = calculate(obj, sweStdErrInput) + %Computes Standard Error assuming unconstrained blocks + %Uses standard matrix multiplication to compute standard error, + %since it does not make assumption that V is sparse. + + groupIds = sweStdErrInput.scanMetadata.groupId; + unqGrps = unique(groupIds); + + obj.throwErrorIfVEntirelyFull(unqGrps); + + vSparsity = obj.computeVSparsity(groupIds); + + %Ben Kay 'Half Sandwich' algorithm seems to be at least as good + %or better than any of the other clever approaches so far. + %Might be able to beat it by using the clever approach and only + %computing the diagonal of covBat + FORCE_HALF_SW_ALGO = true; + + if FORCE_HALF_SW_ALGO + stdErrStrategy = nlaEckDev.sweStdError.UnconstrainedBlocks_BenKay(); + elseif vSparsity <= obj.SPARSITY_THRESHOLD + stdErrStrategy = nlaEckDev.sweStdError.UnconstrainedBlocks_Sparse(); + else + stdErrStrategy = nlaEckDev.sweStdError.UnconstrainedBlocks_Dense(); + end + + stdErr = stdErrStrategy.calculate(sweStdErrInput); + + + end + + + + end + + methods (Access = protected) + + function fractionNonZero = computeVSparsity(obj, groupIds) + %Do quick check of how many elements of V we expect to be + %nonzero given the group Ids of our observations. + %This calculation will only be fast if V is sparse, so we + %should determine how full V will be and warn user if this + %method will be slow. + unqGrps = unique(groupIds); + countInGrps = histcounts(groupIds,[unqGrps;Inf]); + + numNonzeroElems = sum(countInGrps.^2); + totalElems = length(groupIds)*length(groupIds); + + fractionNonZero = numNonzeroElems / totalElems; + + end + + function throwErrorIfVEntirelyFull(obj, uniqueGroupIds) + %If V is entirely full, throw an error + if length(uniqueGroupIds) == 1 + + error(['Standard Error Calculation must include some contraints on error covariance.\n',... + 'If only one group is in data passed to UnconstrainedBlock calculator, covariance of beta reduces to zero.',... + ' Fix by either separating data into different group IDs, or using a different standard error calculator strategy',... + ' (Homoskedastic, Heteroskedastic, etc)\n\n']); + + end + + end + + function [groupedPinv, groupedResidual, groupIds] = reorderDataByGroup(obj, origPinvDesignMtx, origResidual, origGrps) + + [numCovariates, ~] = size(origPinvDesignMtx); + + %Group all data in one matrix and sort by group + %NOTE: need to use transpose of pinvDesignMatrix!!! + allData = [origGrps, origPinvDesignMtx', origResidual]; + sortedData= sortrows(allData,1); + + pinvColRange = 2:(numCovariates+1); + residualColRange = (numCovariates+2):(size(allData,2)); + + groupedPinvTpose = sortedData(:,pinvColRange); + groupedPinv = groupedPinvTpose'; + groupedResidual = sortedData(:,residualColRange); + groupIds = sortedData(:,1); + + end + + + + end + + + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/UnconstrainedBlocks_BenKay.m b/+nla/+helpers/+stdError/UnconstrainedBlocks_BenKay.m new file mode 100755 index 00000000..d743e76b --- /dev/null +++ b/+nla/+helpers/+stdError/UnconstrainedBlocks_BenKay.m @@ -0,0 +1,71 @@ +classdef UnconstrainedBlocks_BenKay < nla.helpers.stdError.UnconstrainedBlocks + + methods + + function stdErr = calculate(obj, sweStdErrInput) + + + %rename variables for readability + pinvDesignMtx = sweStdErrInput.pinvDesignMtx; + residual = sweStdErrInput.residual; + groupIds = sweStdErrInput.scanMetadata.groupId; + unqGrps = unique(groupIds); + + obj.throwErrorIfVEntirelyFull(unqGrps); + + [numCovariates, ~] = size(pinvDesignMtx); + [numObs, numFcEdges] = size(residual); + + stdErr = zeros(numCovariates, numFcEdges); + + WALD_TEST = false; + + if ~WALD_TEST + covB = zeros(numCovariates,numFcEdges); + else + covB = zeros(numCovariates,numCovariates,numFcEdges); + end + + %NOTE, optimized from swe_block.m from Benjamin Kay + %if NOT WALD_TEST, can just compute diagonals of cov(B), which + %makes the original halfsandwich of + %pinvDesignMtx(:,subjThisGrp) * residual(subjThisGrp, fcIdx) + %into a [numCovars x 1] matrix, and then squaring for the + + + for grpIdx = 1:length(unqGrps) + + thisGrpId = unqGrps(grpIdx); + subjThisGrp = groupIds == thisGrpId; + halfSandwich = pinvDesignMtx(:, subjThisGrp) * residual(subjThisGrp,:); + + if WALD_TEST + + for fcEdgeIdx = 1:numFcEdges + covB(:,:,fcEdgeIdx) = covB(:,:,fcEdgeIdx) + ... + (halfSandwich(:,fcEdgeIdx) * halfSandwich(:,fcEdgeIdx)'); + end + + else + + covB = covB + halfSandwich .* halfSandwich; + + end + + end + + + if WALD_TEST + waldRunTime = toc + stdErr = rand(numCovariates, numFcEdges); + error('Not Implemented!'); + else + stdErr(:) = sqrt(covB); + end + + + end + + end + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/UnconstrainedBlocks_Dense.m b/+nla/+helpers/+stdError/UnconstrainedBlocks_Dense.m new file mode 100755 index 00000000..bf0474a5 --- /dev/null +++ b/+nla/+helpers/+stdError/UnconstrainedBlocks_Dense.m @@ -0,0 +1,62 @@ +classdef UnconstrainedBlocks_Dense < nla.helpers.stdError.UnconstrainedBlocks + + methods + + function stdErr = calculate(obj, sweStdErrInput) + %Computes Standard Error assuming unconstrained blocks + %Uses standard matrix multiplication to compute standard error, + %since it does not make assumption that V is sparse. + + + %rename variables for readability + pinvDesignMtx = sweStdErrInput.pinvDesignMtx; + residual = sweStdErrInput.residual; + groupIds = sweStdErrInput.scanMetadata.groupId; + unqGrps = unique(groupIds); + numGrps = length(unqGrps); + + obj.throwErrorIfVEntirelyFull(unqGrps); + + [numCovariates, numObs] = size(pinvDesignMtx); + [~,numFcEdges] = size(residual); + + %Preallocate size of stdErr output + stdErr = zeros(numCovariates, numFcEdges); + + %Reorder data so that grouped observations are together + [pinvDesignMtx_grp, residual_grp, groupIds_grp] = obj.reorderDataByGroup(pinvDesignMtx, residual, groupIds); + + %Determine which observations fit in which group once, outside + %of loop for fc edges + inGroupFlags = zeros(numObs, numGrps); + for grpIdx = 1:numGrps + thisGrpId = unqGrps(grpIdx); + inGroupFlags(:,grpIdx) = (groupIds_grp == thisGrpId); + end + + %For each fc edge, build the V and multiply it by + %pinvDesignMatrix on both sides + thisV = zeros(numObs, numObs); + + for fcIdx = 1:numFcEdges + + thisV(:,:) = 0; + for grpIdx = 1:numGrps + thisGrpFlags = logical(inGroupFlags(:,grpIdx)); + residThisGrp = residual_grp(thisGrpFlags,fcIdx); + thisGrpVBlock = residThisGrp * residThisGrp'; + thisV(thisGrpFlags,thisGrpFlags) = thisGrpVBlock; + end + + thisBetaCovar = pinvDesignMtx_grp * thisV * pinvDesignMtx_grp'; + stdErr(:,fcIdx) = sqrt(diag(thisBetaCovar)); + + end + + + end + + end + + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/UnconstrainedBlocks_MatlabSparse.m b/+nla/+helpers/+stdError/UnconstrainedBlocks_MatlabSparse.m new file mode 100755 index 00000000..483d2936 --- /dev/null +++ b/+nla/+helpers/+stdError/UnconstrainedBlocks_MatlabSparse.m @@ -0,0 +1,83 @@ +classdef UnconstrainedBlocks_MatlabSparse < nla.helpers.stdError.UnconstrainedBlocks + + methods + + function stdErr = calculate(obj, sweStdErrInput) + %Computes Standard Error assuming unconstrained blocks + %Uses standard matrix multiplication to compute standard error, + %since it does not make assumption that V is sparse. + + + %rename variables for readability + pinvDesignMtx = sweStdErrInput.pinvDesignMtx; + residual = sweStdErrInput.residual; + groupIds = sweStdErrInput.scanMetadata.groupId; + unqGrps = unique(groupIds); + numGrps = length(unqGrps); + + obj.throwErrorIfVEntirelyFull(unqGrps); + + [numCovariates, numObs] = size(pinvDesignMtx); + [~,numFcEdges] = size(residual); + + %Preallocate size of stdErr output + stdErr = zeros(numCovariates, numFcEdges); + + %Reorder data so that grouped observations are together + [pinvDesignMtx_grp, residual_grp, groupIds_grp] = obj.reorderDataByGroup(pinvDesignMtx, residual, groupIds); + + %Determine which observations fit in which group once, outside + %of loop for fc edges + inGroupFlags = zeros(numObs, numGrps); + for grpIdx = 1:numGrps + thisGrpId = unqGrps(grpIdx); + inGroupFlags(:,grpIdx) = (groupIds_grp == thisGrpId); + end + + %To prepare sparse matrix, find number of entries for each group + [subjPerGrp,~] = groupcounts(groupIds_grp); + totalNonzerosInV = sum(subjPerGrp.*subjPerGrp); + thisRowInds = zeros(totalNonzerosInV,1); + thisColInds = zeros(totalNonzerosInV,1); + + %precompute row and col inds of nonzero idxs in V + idxOfNonzero = 1; + for grpIdx = 1:numGrps + thisGrpInds = find(inGroupFlags(:,grpIdx)); + + for sparseRow = 1:length(thisGrpInds) + for sparseCol = 1:length(thisGrpInds) + thisRowInds(idxOfNonzero) = thisGrpInds(sparseRow); + thisColInds(idxOfNonzero) = thisGrpInds(sparseCol); + idxOfNonzero = idxOfNonzero+1; + end + end + + end + + + %For each fc edge, build the V and multiply it by + %pinvDesignMatrix on both sides + pinvDesignMtx_grp_tpose = pinvDesignMtx_grp'; + for fcIdx = 1:numFcEdges + + + thisSparseVal = residual_grp(thisRowInds, fcIdx).*residual_grp(thisColInds, fcIdx); + + sparseV = sparse(thisRowInds, thisColInds, thisSparseVal); + + thisBetaCovar = pinvDesignMtx_grp * ... + sparseV * ... + pinvDesignMtx_grp_tpose; + + stdErr(:,fcIdx) = sqrt(diag(thisBetaCovar)); + + end + + + end + + end + + +end \ No newline at end of file diff --git a/+nla/+helpers/+stdError/UnconstrainedBlocks_Sparse.m b/+nla/+helpers/+stdError/UnconstrainedBlocks_Sparse.m new file mode 100755 index 00000000..477cb756 --- /dev/null +++ b/+nla/+helpers/+stdError/UnconstrainedBlocks_Sparse.m @@ -0,0 +1,212 @@ +classdef UnconstrainedBlocks_Sparse < nla.helpers.stdError.UnconstrainedBlocks + + properties + APPROX_MAX_MEMORY_GB = 20; %Limit how much data this algorithm should have in memory at a time + end + + methods + + function stdErr = calculate(obj, sweStdErrInput) + %Computes Standard Error, but uses assumption that V matrix is + %sparse to speed up calculation. If V is not sparse, will + %probably take longer than normal matrix multiplication + %V matrix will be calculated in blocks, where each block along + %the diagonal corresponds to a group. Each block will be the + %residual error of all observations in the group multiplied by + %the transpose of the error, ie residual * residual'; + % + %Efficient algo adapted from + %https://www.mathworks.com/matlabcentral/answers/87629-efficiently-multiplying-diagonal-and-general-matrices + %(And in case that page goes away, copying text in file + %/data/wheelock/data1/people/ecka/fastDiagMatrixMultAlgo.txt) + + %Wrinkle here is that when off diagonal elements of V can be + %non-zero, memory considerations must be applied. + + + %rename variables for readability + pinvDesignMtx = sweStdErrInput.pinvDesignMtx; + residual = sweStdErrInput.residual; + groupIds = sweStdErrInput.scanMetadata.groupId; + unqGrps = unique(groupIds); + + obj.throwErrorIfVEntirelyFull(unqGrps); + + + [numCovariates, ~] = size(pinvDesignMtx); + [numObs, numFcEdges] = size(residual); + + %determine which elements of V will be non-zero based on + %grouping. + %First reorganize data so that observations within groups are + %next to eachother + [pinvDesignMtx_grp, residual_grp, groupIds_grp] = obj.reorderDataByGroup(pinvDesignMtx, residual, groupIds); + + + %Find row and column locations where non-zero values in V will + %be + [rowIdxs, colIdxs] = obj.getCoordsOfNonzerosInV(groupIds); + numNonzeroElems = length(rowIdxs); + + %Split data into blocks so that memory used by this algorithm + %will attempt to be kept below APPROX_MAX_MEMORY_GB + edgesPerMemBlock = obj.calcNumEdgesPerMemoryBlock(numNonzeroElems, numCovariates, obj.APPROX_MAX_MEMORY_GB); + + if edgesPerMemBlock < 1 + error(['%s: Number of nonzero V elements will exceed memory ceiling allocated ',... + 'to this class (APPROX_MAX_MEMORY_GB property currently set to %i GB'],class(obj), obj.APPROX_MAX_MEMORY_GB) + end + + %Make precomputed matrix that will multiply all + invDesMtxSelfMultPreCompute = zeros(numCovariates,numCovariates,numNonzeroElems); + + for i = 1:numNonzeroElems + invDesMtxSelfMultPreCompute(:,:,i) = pinvDesignMtx_grp(:,colIdxs(i)) * pinvDesignMtx_grp(:,rowIdxs(i))'; + end + invDesMtxSelfMultPreCompute = reshape(invDesMtxSelfMultPreCompute,numCovariates^2, numNonzeroElems); + + %Use pregenerated matrix to compute covariance of our estimates + %of the regressors. + + + %Precompute which elems of flattened beta covariance matrix + %correspond to diagonal elements if matrix were square + diagElemIdxsInFlatArr = 1:(numCovariates+1):numCovariates^2; + + %Compute stdError one memory block at a time + flatVTheseFcEdges = zeros(numNonzeroElems, edgesPerMemBlock); + stdErr = zeros(numCovariates, numFcEdges); + fcEdgeBlockStart = 1; + + while fcEdgeBlockStart <= numFcEdges + fcEdgeBlockEnd = min(fcEdgeBlockStart + edgesPerMemBlock - 1, numFcEdges); + + %If this is the last block, it may not have as many fcEdges + %as the other blocks and therefore won't fill up the + %preallocated flatVTheseFcEdges matrix. + %Clear the old one and create a new, smaller one. + %This is because I have not found a way to efficiently + %subindex a large matrix in MATLAB. See this MATLAB forum + %post for the weirdness: + % https://www.mathworks.com/matlabcentral/answers/54522-why-is-indexing-vectors-matrices-in-matlab-very-inefficient + %As part of their test, they showed for a large vector a, + %that "a(1:end) = 1" is 5 times slower than "a(:) = 1" + %Best actual speed solution is probably to incorporate + %these algos into a C MEX file. ADE 20220526 + fcEdgesThisBlock = fcEdgeBlockEnd - fcEdgeBlockStart + 1; + if fcEdgesThisBlock < edgesPerMemBlock + clear flatVTheseFcEdges + flatVTheseFcEdges = zeros(numNonzeroElems, fcEdgesThisBlock); + end + + + flatVTheseFcEdges = obj.makeNonZeroFlatV(... + residual_grp(:,fcEdgeBlockStart:fcEdgeBlockEnd),... + groupIds_grp, flatVTheseFcEdges ); + + betaCovarianceFlat = (invDesMtxSelfMultPreCompute * flatVTheseFcEdges); + + + stdErr(:,fcEdgeBlockStart:fcEdgeBlockEnd) = sqrt(betaCovarianceFlat(diagElemIdxsInFlatArr,:)); + + fcEdgeBlockStart = fcEdgeBlockEnd + 1; + end + + end + + end + + methods (Access = private) + + + function [rowIdxs, colIdxs] = getCoordsOfNonzerosInV(obj, groupIdVec) + %Find the row and column indices that will have non-zero values + %if we were to build the whole V matrix for correlated blocks + %based on the groupIds of the elements + + numObs = length(groupIdVec); + vTemplate = zeros(numObs, numObs); + rowIndexMtx = (1:numObs)' * ones(1,numObs); + colIndexMtx = ones(numObs,1) * (1:numObs); + + unqGrps = unique(groupIdVec); + countInGrps = histcounts(groupIdVec,[unqGrps;Inf]); + + tmpRowIdx = 1; + for grpIdx = 1:length(unqGrps) + startIdx = tmpRowIdx; + endIdx = tmpRowIdx + countInGrps(grpIdx)-1; + + vTemplate([startIdx:endIdx],[startIdx:endIdx]) = 1; + tmpRowIdx = endIdx + 1; + end + + vTemplateFlat = reshape(vTemplate,numel(vTemplate),1); + rowMtxFlat = reshape(rowIndexMtx,numel(vTemplate),1); + colMtxFlat = reshape(colIndexMtx,numel(vTemplate),1); + + rowIdxs = rowMtxFlat(vTemplateFlat==1); + colIdxs = colMtxFlat(vTemplateFlat==1); + + end + + function flatV = makeNonZeroFlatV(obj, residual, groupIds, flatV) + %assumes residuals have already been sorted by groupId + %computes the nonzero elements of V given correlated errors + %within groups + % + %Accepts a previously allocated block of flatV as input and + %overwrites values during algo for speed / memory concerns + % + %If flatV is larger than needed for residual, write only to the + %first columns needed + + + unqGrps = unique(groupIds); + countInGrps = histcounts(groupIds,[unqGrps;Inf]); + + [~, numFcEdgesThisBlock] = size(residual); + [~, numFcEdgesInFlatV] = size(flatV); + + if numFcEdgesInFlatV > numFcEdgesThisBlock + flatV(:,(numFcEdgesThisBlock+1):end) = []; + end + + rowIdx = 1; + + for grpIdx = 1:length(unqGrps) + + thisGrpId = unqGrps(grpIdx); + numObsThisGrp = countInGrps(grpIdx); + thisGrpFlag = groupIds == thisGrpId; + residThisGrp = residual(thisGrpFlag,:); + + + for i = 1:numObsThisGrp + for j = 1:numObsThisGrp + flatVAllEdgesThisRow = residThisGrp(i,:) .* residThisGrp(j,:); + flatV(rowIdx,:) = flatVAllEdgesThisRow; + rowIdx = rowIdx + 1; + end + end + + end + + end + + function edgesPerMemBlock = calcNumEdgesPerMemoryBlock(obj, numNonzeroElemsInV, numCovariates, memBlockSize_GB) + + GBPerMtxElem = 8 / (1024*1024*1024); + + memPrecomputedMtx_GB = numNonzeroElemsInV * (numCovariates^2) * GBPerMtxElem; + memForFcEdgeBlocks_GB = memBlockSize_GB - memPrecomputedMtx_GB; + + memPerEdge_GB = numNonzeroElemsInV * GBPerMtxElem; + + edgesPerMemBlock = floor(memForFcEdgeBlocks_GB / memPerEdge_GB); + + end + + end + +end \ No newline at end of file diff --git a/+nla/+helpers/rotationMatrix.m b/+nla/+helpers/rotationMatrix.m index e3536034..4787b5cb 100755 --- a/+nla/+helpers/rotationMatrix.m +++ b/+nla/+helpers/rotationMatrix.m @@ -1,20 +1,20 @@ function mat = rotationMatrix(dir, theta) % Generate a rotation matrix for the direction given % an angle (in radians). - import nla.Dir + import nla.Direction mat = zeros(3); switch dir - case Dir.X + case Direction.X mat = [1 0 0;... 0 cos(theta) -sin(theta);... 0 sin(theta) cos(theta)]; - case Dir.Y + case Direction.Y mat = [cos(theta) 0 sin(theta);... 0 1 0;... -sin(theta) 0 cos(theta)]; - case Dir.Z + case Direction.Z mat = [cos(theta) -sin(theta) 0;... sin(theta) cos(theta) 0;... 0 0 1]; diff --git a/+nla/+inputField/Behavior.m b/+nla/+inputField/Behavior.m index ccce97a5..f971bed6 100755 --- a/+nla/+inputField/Behavior.m +++ b/+nla/+inputField/Behavior.m @@ -12,6 +12,8 @@ covariates = false covariates_idx = false cols_selected = false + permutation_groups = false + permutation_group_idx = false covariates_enabled end @@ -23,6 +25,8 @@ button_add_cov = false button_sub_cov = false button_view_design_mtx = false + button_add_permutation_level = false + button_remove_permutation_level = false select_partial_variance_label = false select_partial_variance = false end @@ -41,15 +45,14 @@ function resetSelectedCol(obj) end function [w, h] = draw(obj, x, y, parent, fig) - import nla.inputField.LABEL_H + import nla.inputField.LABEL_H nla.inputField.LABEL_GAP obj.fig = fig; - table_w = max(parent.Position(3) - (nla.inputField.LABEL_GAP * 4), 500); + table_w = max(parent.Position(3) - (LABEL_GAP * 4), 500); table_h = 300; - label_gap = nla.inputField.LABEL_GAP; - h = LABEL_H + label_gap + table_h + label_gap + LABEL_H + label_gap + LABEL_H; + h = LABEL_H + LABEL_GAP + table_h + LABEL_GAP + LABEL_H + LABEL_GAP + LABEL_H; %% Create label if ~isgraphics(obj.label) @@ -58,16 +61,16 @@ function resetSelectedCol(obj) obj.label.Text = 'Behavior:'; label_w = nla.inputField.widthOfString(obj.label.Text, LABEL_H); obj.label.HorizontalAlignment = 'left'; - obj.label.Position = [x, y - LABEL_H, label_w + label_gap, LABEL_H]; + obj.label.Position = [x, y - LABEL_H, label_w + LABEL_GAP, LABEL_H]; %% Create button if ~isgraphics(obj.button) obj.button = uibutton(parent, 'push', 'ButtonPushedFcn', @(h,e)obj.buttonClickedCallback()); end button_w = 100; - obj.button.Position = [x + label_w + label_gap, y - LABEL_H, button_w, LABEL_H]; + obj.button.Position = [x + label_w + LABEL_GAP, y - LABEL_H, button_w, LABEL_H]; - w = label_w + label_gap + button_w; + w = label_w + LABEL_GAP + button_w; %% Create table if ~isgraphics(obj.table) @@ -76,48 +79,62 @@ function resetSelectedCol(obj) obj.table.SelectionType = 'column'; obj.table.ColumnName = {'None'}; obj.table.RowName = {}; - obj.table.Position = [x, y - (table_h + label_gap + LABEL_H), table_w, table_h]; + obj.table.Position = [x, y - (table_h + LABEL_GAP + LABEL_H), table_w, table_h]; end w2 = table_w; %% 'Set Behavior' button [obj.button_set_bx, w3] = obj.createButton(obj.button_set_bx, 'Set Behavior', parent, x,... - y - h + LABEL_H + label_gap + LABEL_H, @(h,e)obj.button_set_bxClickedCallback()); + y - h + LABEL_H + LABEL_GAP + LABEL_H, @(h,e)obj.button_set_bxClickedCallback()); obj.button_set_bx.BackgroundColor = '#E3FDD8'; %% 'Add Covariate' button - [obj.button_add_cov, w4] = obj.createButton(obj.button_add_cov, 'Add Covariate', parent, x + w3 + label_gap,... - y - h + LABEL_H + label_gap + LABEL_H, @(h,e)obj.button_add_covClickedCallback()); + [obj.button_add_cov, w4] = obj.createButton(obj.button_add_cov, 'Add Covariate', parent, x + w3 + LABEL_GAP,... + y - h + LABEL_H + LABEL_GAP + LABEL_H, @(h,e)obj.button_add_covClickedCallback()); obj.button_add_cov.BackgroundColor = '#FADADD'; %% 'Remove Covariate' button [obj.button_sub_cov, w5] = obj.createButton(obj.button_sub_cov, 'Remove Covariate', parent,... - x + w3 + label_gap + w4 + label_gap, y - h + LABEL_H + label_gap + LABEL_H,... + x + w3 + LABEL_GAP + w4 + LABEL_GAP, y - h + LABEL_H + LABEL_GAP + LABEL_H,... @(h,e)obj.button_sub_covClickedCallback()); obj.button_sub_cov.BackgroundColor = '#FADADD'; %% 'View Design Matrix' button [obj.button_view_design_mtx, w6] = obj.createButton(obj.button_view_design_mtx, 'View Design Matrix', parent,... - x + w3 + label_gap + w4 + label_gap + w5 + label_gap, y - h + LABEL_H + label_gap + LABEL_H,... + x + w3 + LABEL_GAP + w4 + LABEL_GAP + w5 + LABEL_GAP, y - h + LABEL_H + LABEL_GAP + LABEL_H,... @(h,e)obj.button_view_design_mtxClickedCallback()); + %% Add Permutation button + [obj.button_add_permutation_level, permutation_button_width] = obj.createButton(... + obj.button_add_permutation_level, "Add Permutation Group Level", parent, x, y - h + LABEL_H,... + @(h,e)obj.addPermutationGroup()... + ); + obj.button_add_permutation_level.BackgroundColor = "#8CABFB"; + + %% Remove Permutation button + [obj.button_remove_permutation_level, remove_permutation_button_width] = obj.createButton(... + obj.button_remove_permutation_level, "Remove Last Permutation Group", parent,... + x + permutation_button_width + LABEL_GAP, y - h + LABEL_H, @(h,e)obj.removePermutationGroup()... + ); + obj.button_remove_permutation_level.BackgroundColor = "#8CABFB"; + %% 'Partial Variance' options obj.select_partial_variance_label = uilabel(parent); obj.select_partial_variance_label.HorizontalAlignment = 'left'; obj.select_partial_variance_label.Text = 'Remove shared variance from covariates:'; select_partial_variance_label_w = nla.inputField.widthOfString(obj.select_partial_variance_label.Text, LABEL_H); - obj.select_partial_variance_label.Position = [x, y - h, select_partial_variance_label_w, LABEL_H]; + obj.select_partial_variance_label.Position = [x, y - h - LABEL_H - LABEL_GAP, select_partial_variance_label_w, LABEL_H]; select_partial_variance_w = 100; obj.select_partial_variance = uidropdown(parent); obj.genPartialVarianceOpts(); - obj.select_partial_variance.Position = [x + select_partial_variance_label_w + label_gap, y - h,... + obj.select_partial_variance.Position = [x + select_partial_variance_label_w + LABEL_GAP, y - h - LABEL_H - LABEL_GAP,... select_partial_variance_w, LABEL_H]; obj.select_partial_variance.Value = nla.PartialVarianceType.NONE; - w7 = x + select_partial_variance_label_w + label_gap + select_partial_variance_w; + w7 = x + select_partial_variance_label_w + LABEL_GAP + select_partial_variance_w; - w = max([w, w2, w3 + label_gap + w4 + label_gap + w5 + label_gap + w6, w7]); + w = max([w, w2, w3 + LABEL_GAP + w4 + LABEL_GAP + w5 + LABEL_GAP + w6, w7]); end function undraw(obj) @@ -149,6 +166,12 @@ function undraw(obj) if isgraphics(obj.select_partial_variance) delete(obj.select_partial_variance) end + if isgraphics(obj.button_add_permutation_level) + delete(obj.button_add_permutation_level) + end + if isgraphics(obj.button_remove_permutation_level) + delete(obj.button_remove_permutation_level) + end end function read(obj, input_struct) @@ -192,6 +215,7 @@ function read(obj, input_struct) input_struct.covariates = obj.covariates; input_struct.covariates_idx = obj.covariates_idx; input_struct.partial_variance = obj.select_partial_variance.Value; + input_struct.permutation_groups = obj.permutation_groups; error = false; end end @@ -364,6 +388,42 @@ function button_view_design_mtxClickedCallback(obj) end end + function addPermutationGroup(obj, ~) + if islogical(obj.permutation_group_idx) + obj.permutation_group_idx = []; + end + if islogical(obj.permutation_groups) + obj.permutation_groups = []; + end + if obj.cols_selected + obj.permutation_group_idx = union(obj.permutation_group_idx, obj.cols_selected); + obj.permutation_groups = table2array(obj.table.Data(:, obj.permutation_group_idx)); + end + if isempty(obj.permutation_group_idx) + obj.permutation_group_idx = false; + obj.permutation_groups = false; + end + obj.update(); + end + + function removePermutationGroup(obj, ~) + if islogical(obj.permutation_group_idx) + obj.permutation_group_idx = []; + end + if islogical(obj.permutation_groups) + obj.permutation_groups = []; + end + if obj.cols_selected + obj.permutation_group_idx = setdiff(obj.permutation_group_idx, obj.cols_selected); + obj.permutation_groups = table2array(obj.table.Data(:, obj.permutation_group_idx)); + end + if isempty(obj.permutation_group_idx) + obj.permutation_group_idx = false; + obj.permutation_groups = false; + end + obj.update(); + end + function update(obj) import nla.inputField.widthOfString nla.inputField.LABEL_H @@ -385,6 +445,8 @@ function update(obj) obj.button_view_design_mtx.Enable = false; obj.select_partial_variance.Enable = false; obj.select_partial_variance_label.Enable = false; + obj.button_add_permutation_level.Enable = false; + obj.button_remove_permutation_level.Enable = false; else obj.table.Data = obj.behavior_full; obj.table.ColumnName = obj.behavior_full.Properties.VariableNames; @@ -402,14 +464,22 @@ function update(obj) addStyle(obj.table, cov_s, 'column', obj.covariates_idx) end + if ~islogical(obj.permutation_group_idx) + permutation_groups_style = uistyle("BackgroundColor", "#8CABFB"); + addStyle(obj.table, permutation_groups_style, "column", obj.permutation_group_idx); + end + % Enable buttons obj.table.Enable = 'on'; obj.button_set_bx.Enable = true; - + obj.button_add_permutation_level.Enable = true; + obj.button_remove_permutation_level.Enable = true; + enable_cov = (obj.covariates_enabled ~= nla.inputField.CovariatesEnabled.NONE); obj.button_add_cov.Enable = enable_cov; obj.button_sub_cov.Enable = enable_cov; obj.button_view_design_mtx.Enable = enable_cov; + obj.genPartialVarianceOpts(); diff --git a/+nla/+inputField/Button.m b/+nla/+inputField/Button.m new file mode 100644 index 00000000..2166b8ec --- /dev/null +++ b/+nla/+inputField/Button.m @@ -0,0 +1,56 @@ +classdef Button < nla.inputField.InputField + + properties + name + display_name + callback = false + plot_figure = false + label = false + field = false + end + + properties (Constant) + padding = 12 + end + + methods + + function obj = Button(name, display_name, callback) + obj.name = name; + obj.display_name = display_name; + if nargin == 3 + obj.callback = callback; + end + end + + function [width, height] = draw(obj, x_offset, y_offset, parent, plot_figure) + + obj.plot_figure = plot_figure; + + height = nla.inputField.LABEL_H; + label_width = nla.inputField.widthOfString(obj.display_name, height); + width = label_width + obj.padding + nla.inputField.LABEL_GAP; % add buffer on each side of text + + if ~isgraphics(obj.field) + obj.field = uibutton(parent, "Text", obj.display_name); + end + + if ~isequal(obj.callback, false) + obj.field.ButtonPushedFcn = obj.callback; + end + obj.field.Position = [x_offset, y_offset - height, label_width + obj.padding, height]; + end + + function undraw(obj) + if isgraphics(obj.field) + delete(obj.field); + end + end + + function read(obj, input_struct) + end + + function store(obj, input_struct) + end + end +end \ No newline at end of file diff --git a/+nla/+inputField/CheckBox.m b/+nla/+inputField/CheckBox.m new file mode 100644 index 00000000..66390f61 --- /dev/null +++ b/+nla/+inputField/CheckBox.m @@ -0,0 +1,59 @@ +classdef CheckBox < nla.inputField.InputField + + properties + name + display_name + default_value = false + plot_figure = false + label = false + field = false + end + + properties (Constant) + BOX_WIDTH = 12 + end + + methods + + function obj = CheckBox(name, display_name, default_value) + if nargin >= 2 + obj.name = name; + obj.display_name = display_name; + end + if nargin == 3 + obj.default_value = default_value; + end + end + + function [width, height] = draw(obj, x_offset, y_offset, parent, plot_figure) + import nla.inputField.widthOfString + + obj.plot_figure = plot_figure; + + height = nla.inputField.LABEL_H; + label_width = widthOfString(obj.display_name, height); + + % Checkbox + if ~isgraphics(obj.field) + obj.field = uicheckbox(parent, "Text", obj.display_name, "Position", [x_offset, y_offset - height, label_width + obj.BOX_WIDTH, height]); + end + if obj.default_value + obj.field.Value = true; + end + + width = label_width + nla.inputField.LABEL_GAP + obj.BOX_WIDTH; + end + + function undraw(obj) + if isgraphics(obj.field) + delete(obj.field) + end + end + + function read(obj, test_options) + end + + function store(obj, test_options) + end + end +end \ No newline at end of file diff --git a/+nla/+inputField/Label.m b/+nla/+inputField/Label.m new file mode 100644 index 00000000..92841cfe --- /dev/null +++ b/+nla/+inputField/Label.m @@ -0,0 +1,46 @@ +classdef Label < nla.inputField.InputField + properties + name + display_name + end + + properties (Access = protected) + field = false + end + + methods + function obj = Label(name, display_name) + obj.name = name; + obj.display_name = display_name; + obj.satisfied = true; + end + + function [w, h] = draw(obj, offset_x, offset_y, parent, figure) + import nla.inputField.LABEL_H nla.inputField.LABEL_GAP + + obj.fig = figure; + + if ~isgraphics(obj.field) + obj.field = uilabel(parent); + end + field_width = nla.inputField.widthOfString(obj.display_name, LABEL_H); + obj.field.Position = [offset_x, offset_y - LABEL_H, field_width, LABEL_H]; + + h = LABEL_H; + w = field_width; + end + + function undraw(obj) + if isgraphics(obj.field) + delete(obj.field); + end + end + + function read(obj, ~) + end + + function [input_struct, error] = store(obj, input_struct) + error = false; + end + end +end \ No newline at end of file diff --git a/+nla/+inputField/Number.m b/+nla/+inputField/Number.m index 2e496609..061ebabf 100755 --- a/+nla/+inputField/Number.m +++ b/+nla/+inputField/Number.m @@ -5,9 +5,6 @@ min default max - end - - properties (Access = protected) label = false field = false end @@ -45,6 +42,7 @@ field_w = 50; obj.field.Position = [x + label_w + label_gap, y - h, field_w, h]; obj.field.Limits = [obj.min obj.max]; + obj.field.Value = obj.default; w = label_w + label_gap + field_w; end diff --git a/+nla/+inputField/PullDown.m b/+nla/+inputField/PullDown.m new file mode 100644 index 00000000..86ac133e --- /dev/null +++ b/+nla/+inputField/PullDown.m @@ -0,0 +1,88 @@ +classdef PullDown < nla.inputField.InputField + + properties + display_name + name + options + items_data + plot_figure = false + label = false + field = false + value = false + end + + properties (Constant) + ARROW_SIZE = 15 % Size of pulldown arrow + end + + methods + function obj = PullDown(name, display_name, options, items_data, value) + obj.name = name; + obj.display_name = display_name; + obj.options = options; + if nargin >= 4 + obj.items_data = items_data; + else + obj.items_data = []; + end + if nargin >= 5 + obj.value = value; + else + if isempty(obj.items_data) + obj.value = obj.options(1); + else + obj.value = obj.items_data(1); + end + end + end + + function [width, height] = draw(obj, x_offset, y_offset, parent, plot_figure) + import nla.inputField.widthOfString + + obj.plot_figure = plot_figure; + + height = nla.inputField.LABEL_H; + label_gap = nla.inputField.LABEL_GAP; + + % Label + if ~isgraphics(obj.label) + obj.label = uilabel(parent); + end + obj.label.Text= obj.display_name; + label_width = widthOfString(obj.label.Text, height); + obj.label.HorizontalAlignment = 'left'; + obj.label.Position = [x_offset, y_offset - height, label_width + label_gap, height]; + % pulldown + if ~isgraphics(obj.field) + obj.field = uidropdown(parent, "Items", obj.options, "ItemsData", obj.items_data, "Value", obj.value); + end + max_string_length = 0; + max_string = ""; + for option = obj.options + if widthOfString(option, height) >= max_string_length + max_string = option; + max_string_length = widthOfString(option, height); + end + end + pulldown_width = widthOfString(max_string, height) + obj.ARROW_SIZE; + obj.field.Position = [x_offset + label_width + label_gap, y_offset - height, pulldown_width + obj.ARROW_SIZE, height]; + + width = label_width + label_gap + pulldown_width + obj.ARROW_SIZE; + end + + function undraw(obj) + if isgraphics(obj.label) + delete(obj.label) + end + if isgraphics(obj.field) + delete(obj.field) + end + end + + function read(obj, input_struct) + end + + function store(obj, input_struct) + end + end +end \ No newline at end of file diff --git a/+nla/+net/+mcc/BenjaminiHochberg.m b/+nla/+net/+mcc/BenjaminiHochberg.m index f3786937..a879373f 100755 --- a/+nla/+net/+mcc/BenjaminiHochberg.m +++ b/+nla/+net/+mcc/BenjaminiHochberg.m @@ -12,7 +12,11 @@ if p_max == 0 correction_label = sprintf('FDR_{BH} produced no significant nets'); else - correction_label = sprintf('FDR_{BH}(%g/%d tests)', input_struct.prob_max * input_struct.behavior_count,... + format_specs = "%g/%d tests"; + if isequal(input_struct.behavior_count, 1) + format_specs = "%g/%d test"; + end + correction_label = sprintf(strcat("FDR_{BH}(", format_specs, ")"), input_struct.prob_max * input_struct.behavior_count,... input_struct.behavior_count); end end diff --git a/+nla/+net/+mcc/BenjaminiYekutieli.m b/+nla/+net/+mcc/BenjaminiYekutieli.m index 0c53e1f1..5c8fb6f9 100755 --- a/+nla/+net/+mcc/BenjaminiYekutieli.m +++ b/+nla/+net/+mcc/BenjaminiYekutieli.m @@ -12,7 +12,11 @@ if p_max == 0 correction_label = sprintf('FDR_{BY} produced no significant nets'); else - correction_label = sprintf('FDR_{BY}(%g/%d tests)', input_struct.prob_max * input_struct.behavior_count,... + format_specs = "%g/%d tests"; + if isequal(input_struct.behavior_count, 1) + format_specs = "%g/%d test"; + end + correction_label = sprintf(strcat("FDR_{BY}(", format_specs, ")"), input_struct.prob_max * input_struct.behavior_count,... input_struct.behavior_count); end end diff --git a/+nla/+net/+mcc/Bonferroni.m b/+nla/+net/+mcc/Bonferroni.m index 98cad3f6..de306236 100755 --- a/+nla/+net/+mcc/Bonferroni.m +++ b/+nla/+net/+mcc/Bonferroni.m @@ -8,7 +8,11 @@ p_max = input_struct.prob_max / net_atlas.numNetPairs(); end function correction_label = createLabel(obj, net_atlas, input_struct, prob) - correction_label = sprintf('%g/%d net-pairs/%d tests', input_struct.prob_max * input_struct.behavior_count,... + format_specs_tests = "%d tests"; + if isequal(input_struct.behavior_count, 1) + format_specs_tests = "%d test"; + end + correction_label = sprintf(strcat("%g/%d net-pairs/", format_specs_tests), input_struct.prob_max * input_struct.behavior_count,... net_atlas.numNetPairs(), input_struct.behavior_count); end end diff --git a/+nla/+net/+mcc/None.m b/+nla/+net/+mcc/None.m index 59721829..8ef9dd66 100755 --- a/+nla/+net/+mcc/None.m +++ b/+nla/+net/+mcc/None.m @@ -8,7 +8,11 @@ p_max = input_struct.prob_max; end function correction_label = createLabel(obj, net_atlas, input_struct, prob) - correction_label = sprintf('%g/%d tests', input_struct.prob_max * input_struct.behavior_count,... + format_specs = "%g/%d tests"; + if isequal(input_struct.behavior_count, 1) + format_specs = "%g/%d test"; + end + correction_label = sprintf(format_specs, input_struct.prob_max * input_struct.behavior_count,... input_struct.behavior_count); end end diff --git a/+nla/+net/+result/+chord/ChordPlotter.m b/+nla/+net/+result/+chord/ChordPlotter.m index b8a6918d..c2a056f8 100644 --- a/+nla/+net/+result/+chord/ChordPlotter.m +++ b/+nla/+net/+result/+chord/ChordPlotter.m @@ -15,7 +15,7 @@ network_atlas % Network Atlas for the data edge_test_result % Edge test results split_plot = false % This is an option that is set automatically during operation. - edge_plot_type = nla.gfx.EdgeChordPlotMethod.PROB % Default chord type for edges + edge_plot_type = "nla.gfx.EdgeChordPlotMethod.PROB" % Default chord type for edges end methods @@ -32,7 +32,7 @@ function generateChordFigure(obj, parameters, chord_type) % generateChordFigure plots chords for a network test - import nla.gfx.SigType nla.net.result.plot.PermutationTestPlotter nla.gfx.EdgeChordPlotMethod + import nla.gfx.SigType nla.net.result.plot.PermutationTestPlotter nla.gfx.EdgeChordPlotMethod nla.gfx.setTitle coefficient_bounds = [0, parameters.p_value_plot_max]; if parameters.significance_type == SigType.INCREASING && parameters.p_value_plot_max < 1 @@ -42,26 +42,26 @@ function generateChordFigure(obj, parameters, chord_type) % Check if it's an edge chord plot, and if so, do we plot positive and negative separately if isfield(parameters, 'edge_chord_plot_method') obj.edge_plot_type = parameters.edge_chord_plot_method; - if obj.edge_plot_type == EdgeChordPlotMethod.COEFF_SPLIT || obj.edge_plot_type == EdgeChordPlotMethod.COEFF_BASE_SPLIT + if obj.edge_plot_type == "nla.gfx/EdgeChordPlotMethod.COEFF_SPLIT" || obj.edge_plot_type == "nla.gfx.EdgeChordPlotMethod.COEFF_BASE_SPLIT" obj.split_plot = true; end end % Create the figure windows that all the plots will go in - if obj.split_plot && chord_type == nla.PlotType.CHORD_EDGE - plot_figure = nla.gfx.createFigure((obj.axis_width * 2) + obj.trimatrix_width - 100, obj.axis_width); + if obj.split_plot && chord_type == "nla.PlotType.CHORD_EDGE" + plot_figure = nla.gfx.createFigure((obj.axis_width * 2), obj.axis_width); else - plot_figure = nla.gfx.createFigure(obj.axis_width + obj.trimatrix_width, obj.axis_width); + plot_figure = nla.gfx.createFigure(obj.axis_width , obj.axis_width); end % Plot a standard chord plot - if chord_type == nla.PlotType.CHORD - figure_axis = axes(plot_figure, 'Units', 'pixels', 'Position', [obj.trimatrix_width, 0, obj.axis_width,... + if chord_type == "nla.PlotType.CHORD" + figure_axis = axes(plot_figure, 'Units', 'pixels', 'Position', [0, 0, obj.axis_width,... obj.axis_width]); nla.gfx.hideAxes(figure_axis); insignificance = coefficient_bounds(2); - if parameters.significance_type == SigType.INCREASING + if parameters.significance_type == "nla.gfx.SigType.INCREASING" insignificance = coefficient_bounds(1); end @@ -73,16 +73,13 @@ function generateChordFigure(obj, parameters, chord_type) 'color_map', parameters.color_map, 'direction', parameters.significance_type, 'upper_limit',... coefficient_bounds(2), 'lower_limit', coefficient_bounds(1), 'chord_type', chord_type); chord_plotter.drawChords(); + setTitle(figure_axis, parameters.name_label) else % Plot edge chord obj.generateEdgeChordFigure(plot_figure, parameters, chord_type) end - % Plot Trimatrix with the chord plots - plotter = PermutationTestPlotter(obj.network_atlas); - plotter.plotProbability(plot_figure, parameters, 25, obj.bottom_text_height); - obj.generatePlotText(plot_figure, chord_type); end end @@ -103,7 +100,7 @@ function generateEdgeChordFigure(obj, plot_figure, parameters, chord_type) clipped_values_positive.v(obj.edge_test_result.coeff.v < 0) = 0; color_map = turbo(1000); - significance_type = nla.gfx.SigType.ABS_INCREASING; + significance_type = "nla.gfx.SigType.ABS_INCREASING"; insignificance = 0; % This is basically the background for the plot % This is the title for the positive (or non-split) chord plot positive_title = sprintf("Edge-level correlation (P < %g) (Within Significant Net-Pair)",... @@ -114,21 +111,21 @@ function generateEdgeChordFigure(obj, plot_figure, parameters, chord_type) % There are some settings that need to be changed depending on the specific type of edge plot switch obj.edge_plot_type - case EdgeChordPlotMethod.COEFF + case "nla.gfx.EdgeChordPlotMethod.COEFF" main_title = positive_title; - case EdgeChordPlotMethod.COEFF_SPLIT + case "nla.gfx.EdgeChordPlotMethod.COEFF_SPLIT" main_title = negative_title; positive_main_title = positive_title; - case EdgeChordPlotMethod.COEFF_BASE_SPLIT + case "nla.gfx.EdgeChordPlotMethod.COEFF_BASE_SPLIT" coefficient_min = obj.edge_test_result.coeff_range(1); coefficient_max = obj.edge_test_result.coeff_range(2); main_title = negative_title; positive_main_title = positive_title; - case EdgeChordPlotMethod.COEFF_BASE + case "nla.gfx.edgeChordPlotMethod.COEFF_BASE" coefficient_min = obj.edge_test_result.coeff_range(1); coefficient_max = obj.edge_test_result.coeff_range(2); @@ -139,7 +136,7 @@ function generateEdgeChordFigure(obj, plot_figure, parameters, chord_type) color_map = flip(color_map_base(ceil(logspace(-3, 0, 1000) .* 1000), :)); clipped_values.v = obj.edge_test_result.prob.v; - significance_type = nla.gfx.SigType.DECREASING; + significance_type = "nla.gfx.SigType.DECREASING"; coefficient_min = 0; coefficient_max = obj.edge_test_result.prob_max; @@ -163,13 +160,13 @@ function generateEdgeChordFigure(obj, plot_figure, parameters, chord_type) end end - plot_axis = axes(plot_figure, 'Units', 'pixels', 'Position', [obj.trimatrix_width, 0, obj.axis_width - 50,... + plot_axis = axes(plot_figure, 'Units', 'pixels', 'Position', [0, 0, obj.axis_width - 50,... obj.axis_width - 50]); nla.gfx.hideAxes(plot_axis); plot_axis.Visible = true; - if obj.split_plot && chord_type == nla.PlotType.CHORD_EDGE && (... - obj.edge_plot_type == EdgeChordPlotMethod.COEFF_SPLIT || obj.edge_plot_type == EdgeChordPlotMethod.COEFF_BASE_SPLIT... + if obj.split_plot && chord_type == "nla.PlotType.CHORD_EDGE" && (... + obj.edge_plot_type == "nla.gfx.EdgeChordPlotMethod.COEFF_SPLIT" || obj.edge_plot_type == "nla.gfx.EdgeChordPlotMethod.COEFF_BASE_SPLIT"... ) positive_chord_plotter = nla.gfx.chord.ChordPlot(obj.network_atlas, plot_axis, 450, clipped_values_positive,... 'direction', significance_type, 'chord_type', chord_type, 'color_map', color_map, 'lower_limit',... @@ -179,7 +176,7 @@ function generateEdgeChordFigure(obj, plot_figure, parameters, chord_type) % create another axis, I hate this naming but we can overwrite the old one plot_axis = axes(plot_figure, 'Units', 'pixels', 'Position',... - [obj.trimatrix_width + obj.axis_width - 100, 0 , obj.axis_width - 50, obj.axis_width - 50]); + [obj.axis_width - 100, 0 , obj.axis_width - 50, obj.axis_width - 50]); nla.gfx.hideAxes(plot_axis); plot_axis.Visible = true; end @@ -206,15 +203,5 @@ function generateEdgeChordFigure(obj, plot_figure, parameters, chord_type) end color_bar.TickLabels = labels; end - - function generatePlotText(obj, plot_figure, chord_type) - text_axis = axes(plot_figure, 'Units', 'pixels', 'Position', [55, obj.bottom_text_height + 15, 450, 75]); - nla.gfx.hideAxes(text_axis); - info_text = "Click any net-pair in the above plot to view its edge-level correlations."; - if chord_type == nla.PlotType.CHORD_EDGE - info_text = sprintf("%s\n\nChord plot:\nEach ROI is marked by a dot next to its corresponding network.\nROIs are placed in increasing order counter-clockwise, the first ROI in\na network being the most clockwise, the last being the most counter-\nclockwise.", info_text); - end - text(text_axis, 0, 0, info_text, 'HorizontalAlignment', 'left', 'VerticalAlignment', 'top'); - end end end \ No newline at end of file diff --git a/+nla/+net/+result/+plot/NetworkTestPlot.m b/+nla/+net/+result/+plot/NetworkTestPlot.m new file mode 100644 index 00000000..6fc7a365 --- /dev/null +++ b/+nla/+net/+result/+plot/NetworkTestPlot.m @@ -0,0 +1,395 @@ +classdef NetworkTestPlot < handle + + properties + network_atlas + network_test_result + edge_test_result + test_method + edge_test_options + network_test_options + x_position + y_position + plot_figure = false + options_panel = false + matrix_plot = false + height = 600 + panel_height = 250 + current_settings = struct() + settings + parameters + title = "" + end + + properties (Dependent) + is_noncorrelation_input + end + + properties (Constant) + WIDTH = 500 + colormap_choices = ["Parula", "Turbo", "HSV", "Hot", "Cool", "Spring", "Summer", "Autumn", "Winter", "Gray",... + "Bone", "Copper", "Pink"] % Colorbar choices + legend_visible = ["On", "Off"] + COLORMAP_SAMPLE_COLORS = 16 + changes_to_functions = struct(... + "upper_limit", "scale",... + "lower_limit", "scale",... + "colormap_choice", "scale",... + "plot_scale", "scale",... + "ranking", "ranking",... + "cohens_d", "parameters",... + "centroids", "centroids",... + "mcc", "parameters",... + "convergence_color", "convergence",... + "p_threshold", "parameters",... + "d_threshold", "parameters",... + "legend_visible", "legend"... + ) + end + + methods + + function obj = NetworkTestPlot(network_test_result, edge_test_result, network_atlas, test_method,... + edge_test_options, network_test_options, varargin) + + test_plot_parser = inputParser; + addRequired(test_plot_parser, "network_test_result"); + addRequired(test_plot_parser, "edge_test_result"); + addRequired(test_plot_parser, "network_atlas"); + addRequired(test_plot_parser, "test_method"); + addRequired(test_plot_parser, "edge_test_options"); + addRequired(test_plot_parser, "network_test_options"); + + validNumberInput = @(x) isnumeric(x) && isscalar(x); + addParameter(test_plot_parser, "x_position", 300, validNumberInput); + addParameter(test_plot_parser, "y_position", 0, validNumberInput); + + parse(test_plot_parser, network_test_result, edge_test_result, network_atlas, test_method, edge_test_options,... + network_test_options, varargin{:}); + properties = ["network_test_result", "edge_test_result", "network_atlas", "test_method", "edge_test_options",... + "network_test_options", "x_position", "y_position"]; + for property = properties + obj.(property{1}) = test_plot_parser.Results.(property{1}); + end + end + + function getPlotTitle(obj) + import nla.NetworkLevelMethod + + obj.title = ""; + % Building the plot title by going through options + switch obj.test_method + case "no_permutations" + obj.title = "Non-permuted Method\nNon-permuted Significance"; + case "full_connectome" + obj.title = "Full Connectome Method\nNetwork vs. Connectome Significance"; + case "within_network_pair" + obj.title = "Within Network Pair Method\nNetwork Pair vs. Permuted Network Pair"; + end + if isequal(obj.current_settings.cohens_d, true) + obj.title = sprintf("%s (D > %g)", obj.title, obj.network_test_options.d_max); + end + if ~isequal(obj.test_method, "no_permutations") + if isequal(obj.current_settings.ranking, nla.RankingMethod.WINKLER) + obj.title = strcat(obj.title, "\nRanking by Winkler Method"); + elseif isequal(obj.current_settings.ranking, nla.RankingMethod.WESTFALL_YOUNG) + obj.title = strcat(obj.title, "\nRanking by Westfall-Young Method"); + else + obj.title = strcat(obj.title, "\nUncorrected data"); + end + end + end + + function drawFigure(obj, plot_type) + import nla.inputField.LABEL_GAP + + obj.plot_figure = uifigure("Color", "w"); + obj.plot_figure.Position = [obj.plot_figure.Position(1), obj.plot_figure.Position(2), obj.WIDTH,... + obj.panel_height + (4 * LABEL_GAP)]; + obj.options_panel = uipanel(obj.plot_figure, "Units", "pixels", "Position", [10, 10, 480, obj.panel_height],... + "BackgroundColor", "w"); + obj.drawOptions(); + % obj.resizeOptions(); + + obj.parameters = nla.net.result.NetworkResultPlotParameter(obj.network_test_result, obj.network_atlas,... + obj.network_test_options); + + [width, plot_height] = obj.drawTriMatrixPlot(); + if ~isequal(plot_type, nla.PlotType.FIGURE) + obj.drawChord(plot_type); + end + + + obj.resizeFigure(width, plot_height); + end + + function [width, height] = drawTriMatrixPlot(obj) + import nla.net.result.NetworkTestResult + + if ~isequal(obj.matrix_plot, false) + obj.matrix_plot.plot_title.String = {}; + obj.parameters.updated_test_options.prob_max = obj.current_settings.p_threshold; + end + obj.getPlotTitle(); + + switch obj.current_settings.mcc + case "Benjamini-Hochberg" + mcc = "BenjaminiHochberg"; + case "Benjamini-Yekutieli" + mcc = "BenjaminiYekutieli"; + otherwise + mcc = obj.current_settings.mcc; + end + + probability = NetworkTestResult().getPValueNames(obj.test_method, obj.network_test_result.test_name); + + probability_parameters = obj.parameters.plotProbabilityParameters(obj.edge_test_options, obj.edge_test_result,... + obj.test_method, probability, sprintf(obj.title), mcc, obj.createSignificanceFilter(),... + obj.current_settings.ranking); + + if obj.current_settings.upper_limit ~= 0.3 && obj.current_settings.lower_limit ~= -0.3 + probability_parameters.p_value_plot_max = obj.current_settings.upper_limit; + end + + plotter = nla.net.result.plot.PermutationTestPlotter(obj.network_atlas); + [width, height, obj.matrix_plot] = plotter.plotProbability(obj.plot_figure, probability_parameters,... + nla.inputField.LABEL_GAP, obj.y_position + obj.panel_height); + + if probability_parameters.p_value_plot_max > 0 + obj.settings{7}.field.Value = str2double(obj.matrix_plot.color_bar.TickLabels{end}); + obj.settings{8}.field.Value = str2double(obj.matrix_plot.color_bar.TickLabels{1}); + obj.current_settings.upper_limit = str2double(obj.matrix_plot.color_bar.TickLabels{end}); + obj.current_settings.lower_limit = str2double(obj.matrix_plot.color_bar.TickLabels{1}); + else + obj.settings{7}.field.Value = 0; + obj.settings{8}.field.Value = 0; + obj.current_settings.upper_limit = 0; + obj.current_settings.lower_limit = 0; + end + end + + function drawChord(obj, ~, ~, plot_type) + import nla.gfx.EdgeChordPlotMethod + + obj.getPlotTitle(); + + probability = NetworkTestResult().getPValueNames(obj.test_method, obj.network_test_result.test_name); + p_value = strcat("uncorrected_", probability); + + probability_parameters = obj.parameters.plotProbabilityParameters(obj.edge_test_options, obj.edge_test_result,... + obj.test_method, p_value, sprintf(obj.title), obj.current_settings.mcc, obj.createSignificanceFilter(),... + obj.current_settings.ranking); + + chord_plotter = nla.net.result.chord.ChordPlotter(obj.network_atlas, obj.edge_test_result); + + for setting = obj.settings + if setting{1}.name == "edge_type" + method = setting{1}.field.Value; + probability_parameters.edge_chord_plot_method = method; + break + end + end + chord_plotter.generateChordFigure(probability_parameters, plot_type); + + end + + function resizeFigure(obj, plot_width, plot_height) + % This resizes automatically so that all the elements fit nicely inside. This is not for manually resizing the window + import nla.inputField.LABEL_GAP + + current_width = obj.plot_figure.Position(3); + current_height = obj.plot_figure.Position(4); + + if ~isequal(current_width, plot_width + (2 * LABEL_GAP)) + obj.plot_figure.Position(3) = plot_width + (2 * LABEL_GAP); + obj.options_panel.Position(1) = ((obj.plot_figure.Position(3) - obj.options_panel.Position(3)) / 2); + end + + if ~isequal(current_height, (2 * LABEL_GAP) + obj.plot_figure.Position(4) + plot_height) + obj.plot_figure.Position(4) = (2 * LABEL_GAP) + current_height + plot_height; + end + end + + function cohens_d_filter = createSignificanceFilter(obj) + import nla.NetworkLevelMethod + + % This is for using Cohen's D + cohens_d_filter = nla.TriMatrix(obj.network_atlas.numNets, "logical", nla.TriMatrixDiag.KEEP_DIAGONAL); + if isequal(obj.current_settings.cohens_d, true) + if isequal(obj.test_method, "no_permutations") && ~isequal(obj.network_test_result.no_permutations, false) + cohens_d_filter.v = (obj.network_test_result.no_permutations.d.v >= obj.network_test_options.d_max); + elseif isequal(obj.test_method, "full_connectome") && ~isequal(obj.network_test_result.full_connectome, false) + cohens_d_filter.v = (obj.network_test_result.full_connectome.d.v >= obj.network_test_options.d_max); + elseif isequal(obj.test_method,"within_network_pair") && ~isequal(obj.network_test_result.within_network_pair, false) + cohens_d_filter.v = (obj.network_test_result.within_network_pair.d.v >= obj.network_test_options.d_max); + end + else + cohens_d_filter.v = true(numel(cohens_d_filter.v), 1); + end + end + + function [width, height] = drawOptions(obj) + import nla.inputField.LABEL_GAP nla.inputField.LABEL_H nla.inputField.PullDown nla.inputField.CheckBox + import nla.inputField.Button nla.inputField.Number nla.NetworkLevelMethod nla.RankingMethod + import nla.gfx.EdgeChordPlotMethod nla.gfx.ProbPlotMethod + + % All the options (buttons, pulldowns, checkboxes) + scale_option = PullDown("plot_scale", "Plot Scale", ["Linear", "Log", "Negative Log10"],... + [ProbPlotMethod.DEFAULT, ProbPlotMethod.LOG, ProbPlotMethod.NEGATIVE_LOG_10]); + ranking_method = PullDown("ranking", "Ranking", ["Uncorrected", "Winkler", "Westfall-Young"],... + [RankingMethod.UNCORRECTED, RankingMethod.WINKLER, RankingMethod.WESTFALL_YOUNG]); + cohens_d = CheckBox("cohens_d", "Cohen's D Threshold", true); + centroids = CheckBox("centroids", "ROI Centroids in brain plots", false); + multiple_comparison_correction = PullDown("mcc", "Multiple Comparison Correction",... + ["None", "Bonferroni", "Benjamini-Hochberg", "Benjamini-Yekutieli"]); + network_chord_plot = Button("network_chord", "View Chord Plots", {@obj.drawChord, nla.PlotType.CHORD}); + edge_chord_type = PullDown(... + "edge_type", "Edge-level Chord Type",... + ["p-value", "Coefficient", "Coefficient (Split)", "Coefficient (Basic)", "Coefficient (Basic, Split)"],... + [EdgeChordPlotMethod.PROB, EdgeChordPlotMethod.COEFF, EdgeChordPlotMethod.COEFF_SPLIT, EdgeChordPlotMethod.COEFF_BASE, EdgeChordPlotMethod.COEFF_BASE_SPLIT]... + ); + edge_chord_plot = Button("edge_chord", "View Edge Chord Plots", {@obj.drawChord, nla.PlotType.CHORD_EDGE}); + convergence_plot = Button("convergence", "View Convergence Map", @obj.openConvergencePlot); + convergence_color = PullDown("convergence_color", "Convergence Plot Color",... + ["Bone", "Winter", "Autumn", "Copper"]); + apply = Button("apply", "Apply"); + upper_limit_box = Number("upper_limit", "Upper Limit", -Inf, 0.3, Inf); + lower_limit_box = Number("lower_limit", "Lower Limit", -Inf, -0.3, Inf); + colormap_choice = PullDown("colormap_choice", "Colormap", obj.colormap_choices); + legend_visible = PullDown("legend_visible", "Legend Visible", obj.legend_visible); + p_value_threshold = Number("p_threshold", "p-value Threshold", -Inf, 0.05, Inf); + cohens_d_threshold = Number("d_threshold", "Cohen's D Threshold", -Inf, 0.5, Inf); + + % Draw the options + options = {... + {apply},... + {convergence_plot, convergence_color},... + {edge_chord_type},... + {network_chord_plot, edge_chord_plot},... + {cohens_d, centroids},... + {multiple_comparison_correction},... + {colormap_choice, legend_visible},... + {p_value_threshold, cohens_d_threshold},... + {upper_limit_box, lower_limit_box},... + {scale_option, ranking_method}... + }; + + obj.settings = {scale_option, ranking_method, cohens_d, centroids, multiple_comparison_correction,... + convergence_color, upper_limit_box, lower_limit_box, colormap_choice, legend_visible,... + edge_chord_type, p_value_threshold, cohens_d_threshold}; + + y = LABEL_GAP; + x = LABEL_GAP; + for row = options + for column = row{1} + [x_component, ~] = column{1}.draw(x, y, obj.options_panel, obj.plot_figure); + x = x + LABEL_GAP + x_component; + if ~isequal(column{1}.name, "apply") + if isa(column{1}.field, "matlab.ui.control.Button") + obj.current_settings.(column{1}.name) = column{1}.field.Enable; + else + obj.current_settings.(column{1}.name) = column{1}.field.Value; + end + end + end + y = y + LABEL_GAP + LABEL_H; + x = LABEL_GAP; + end + + if y > obj.panel_height + obj.options_panel.Position(4) = y + LABEL_GAP; + height_difference = y - obj.panel_height; + for row = options + for column = row{1} + column{1}.field.Position(2) = column{1}.field.Position(2) + height_difference; + if isa(column{1}.label, "matlab.ui.control.Label") + column{1}.label.Position(2) = column{1}.label.Position(2) + height_difference; + end + end + end + end + apply.field.ButtonPushedFcn = {@obj.applyChanges, obj.settings}; + end + + %% getters for dependent props + function value = get.is_noncorrelation_input(obj) + value = obj.network_test_result.is_noncorrelation_input; + end + %% + end + + methods (Access = protected) + function applyChanges(obj, ~, ~, values) + % Choosing what is being updated. If we don't have to update the Trimatrix, we'll skip that + progress_bar = uiprogressdlg(obj.plot_figure, "Title", "Please Wait", "Message", "Applying Changes...",... + "Indeterminate", true); + + changes = {}; + for value = values + if isa(value{1}.field, "matlab.ui.control.Button") + setting_value = value{1}.field.Enable; + else + setting_value = value{1}.field.Value; + end + + if ~isequal(setting_value, obj.current_settings.(value{1}.name)) + changes = [changes, obj.changes_to_functions.(value{1}.name)]; + obj.current_settings.(value{1}.name) = setting_value; + end + end + + if any(strcmp("parameters", changes)) || any(strcmp("ranking", changes)) + if isobject(obj.matrix_plot) + obj.matrix_plot.removeLegend(); + delete(obj.matrix_plot.image_display); + delete(obj.matrix_plot.color_bar); + end + progress_bar.Message = "Redrawing TriMatrix..."; + obj.drawTriMatrixPlot(); + elseif any(strcmp("scale", changes)) + progress_bar.Message = "Changing scale of existing TriMatrix..."; + obj.matrix_plot.applyScale(false, false, obj.current_settings.upper_limit,... + obj.current_settings.lower_limit, obj.current_settings.plot_scale,... + obj.current_settings.colormap_choice); + obj.settings{7}.field.Value = str2double(obj.matrix_plot.color_bar.TickLabels{end}); + obj.settings{8}.field.Value = str2double(obj.matrix_plot.color_bar.TickLabels{1}); + end + if any(strcmp("legend", changes)) + if isobject(obj.matrix_plot.display_legend) && isequal(obj.current_settings.legend_visible, "Off") + obj.matrix_plot.display_legend.Visible = "off"; + else + obj.matrix_plot.display_legend.Visible = "on"; + end + end + close(progress_bar); + end + + function openConvergencePlot(obj, ~, ~) + import nla.NetworkLevelMethod + + flags = struct(); + flags.show_full_conn = false; + flags.show_within_net_pair = false; + flags.show_nonpermuted = false; + switch obj.test_method + case "no_permutations" + flags.show_nonpermuted = true; + case "full_connectome" + flags.show_full_conn = true; + case "within_network_pair" + flags.show_within_net_pair = true; + end + [test_number, significance_count_matrix, names] = obj.network_test_result.getSigMat(obj.network_test_options,... + obj.network_atlas, flags); + + colors = str2func(lower(obj.current_settings.convergence_color)); + if isequal(obj.current_settings.convergence_color, "Bone") + color_map = flip(colors()); + else + color_map = [[1, 1, 1]; flip(colors())]; + end + + nla.gfx.drawConvergenceMap(obj.edge_test_options, obj.network_test_options, obj.network_atlas, significance_count_matrix,... + test_number, names, obj.edge_test_result, color_map); + end + end +end \ No newline at end of file diff --git a/+nla/+net/+result/+plot/NetworkTestPlotApp.mlapp b/+nla/+net/+result/+plot/NetworkTestPlotApp.mlapp new file mode 100644 index 00000000..c3ec3caa Binary files /dev/null and b/+nla/+net/+result/+plot/NetworkTestPlotApp.mlapp differ diff --git a/+nla/+net/+result/+plot/NetworkTestPlotApp_exported.m b/+nla/+net/+result/+plot/NetworkTestPlotApp_exported.m new file mode 100644 index 00000000..576c4efb --- /dev/null +++ b/+nla/+net/+result/+plot/NetworkTestPlotApp_exported.m @@ -0,0 +1,617 @@ +classdef NetworkTestPlotApp < matlab.apps.AppBase + + % Properties that correspond to app components + properties (Access = public) + UIFigure matlab.ui.Figure + Menu matlab.ui.container.Menu + SaveasMenu matlab.ui.container.Menu + Panel matlab.ui.container.Panel + PlotScaleDropDownLabel matlab.ui.control.Label + PlotScaleDropDown matlab.ui.control.DropDown + RankingDropDownLabel matlab.ui.control.Label + RankingDropDown matlab.ui.control.DropDown + UpperLimitEditFieldLabel matlab.ui.control.Label + UpperLimitEditField matlab.ui.control.NumericEditField + LowerLimitEditFieldLabel matlab.ui.control.Label + LowerLimitEditField matlab.ui.control.NumericEditField + pvalueThresholdEditFieldLabel matlab.ui.control.Label + pvalueThresholdEditField matlab.ui.control.NumericEditField + CohensDThresholdEditFieldLabel matlab.ui.control.Label + CohensDThresholdEditField matlab.ui.control.NumericEditField + ColormapDropDownLabel matlab.ui.control.Label + ColormapDropDown matlab.ui.control.DropDown + LegendVisibleDropDownLabel matlab.ui.control.Label + LegendVisibleDropDown matlab.ui.control.DropDown + MultipleComparisonCorrectionDropDownLabel matlab.ui.control.Label + MultipleComparisonCorrectionDropDown matlab.ui.control.DropDown + CohensDThresholdCheckBox matlab.ui.control.CheckBox + ROIcentroidsonbrainplotsCheckBox matlab.ui.control.CheckBox + ViewChordPlotsButton matlab.ui.control.Button + ViewEdgeChordPlotsButton matlab.ui.control.Button + EdgeChordPlotTypeDropDownLabel matlab.ui.control.Label + EdgeChordPlotTypeDropDown matlab.ui.control.DropDown + ViewConvergenceMapButton matlab.ui.control.Button + ConvergencePlotColorDropDownLabel matlab.ui.control.Label + ConvergencePlotColorDropDown matlab.ui.control.DropDown + ApplyButton matlab.ui.control.Button + Panel_2 matlab.ui.container.Panel + end + + + properties (Access = public) + network_test_result + edge_test_result + test_method + edge_test_options + network_test_options + x_position + y_position + matrix_plot = false + parameters + title = "" + chord_type = "nla.PlotType.CHORD" + settings = false + old_data = false + end + + properties (Dependent) + is_noncorrelation_input + end + + properties (Constant) + colormap_choices = ["Parula", "Turbo", "HSV", "Hot", "Cool", "Spring", "Summer", "Autumn", "Winter", "Gray",... + "Bone", "Copper", "Pink"] % Colorbar choices + COLORMAP_SAMPLE_COLORS = 16 + end + + methods + %% getters for dependent props + function value = get.is_noncorrelation_input(app) + value = app.network_test_result.is_noncorrelation_input; + end + %% + end + + methods (Access = public) + function getPlotTitle(app) + app.title = ""; + % Building the plot title by going through options + switch app.test_method + case "no_permutations" + app.title = "Non-permuted Method\nNon-permuted Significance"; + case "full_connectome" + app.title = "Full Connectome Method\nNetwork vs. Connectome Significance"; + case "within_network_pair" + app.title = "Within Network Pair Method\nNetwork Pair vs. Permuted Network Pair"; + end + if isequal(app.CohensDThresholdCheckBox.Value, true) + app.title = sprintf("%s (D > %g)", app.title, app.CohensDThresholdEditField.Value); + end + if ~isequal(app.test_method, "no_permutations") + if isequal(app.RankingDropDown.Value, "nla.RankingMethod.WINKLER") % Look at me, I'm MATLAB. I have no idea why enums are beneficial or how to use them + app.title = strcat(app.title, "\nRanking by Winkler Method"); + elseif isequal(app.RankingDropDown.Value, "nla.RankingMethod.WESTFALL_YOUNG") + app.title = strcat(app.title, "\nRanking by Westfall-Young Method"); + else + app.title = strcat(app.title, "\nUncorrected data"); + end + end + end + + function [width, height] = drawTriMatrixPlot(app) + import nla.net.result.NetworkTestResult + + if ~isequal(app.matrix_plot, false) + app.settings = struct(); + app.settings.upperLimit = app.UpperLimitEditField.Value; + app.settings.lowerLimit = app.LowerLimitEditField.Value; + app.settings.plotScale = app.PlotScaleDropDown.Value; + app.settings.pValueThreshold = app.pvalueThresholdEditField.Value; + app.settings.cohensD = app.CohensDThresholdCheckBox.Value; + app.settings.cohensDValue = app.CohensDThresholdEditField.Value; + app.matrix_plot.display_legend.Visible = app.LegendVisibleDropDown.Value; + app.matrix_plot.plot_title.String = {}; + app.network_test_options.prob_max = app.pvalueThresholdEditField.Value; + app.settings.ranking = app.RankingDropDown.Value; + end + + if isfield(app.settings, "ranking") + app.network_test_options.ranking_method = app.settings.ranking; + if isobject(app.matrix_plot) + app.matrix_plot.removeLegend(); + delete(app.matrix_plot.image_display); + delete(app.matrix_plot.color_bar); + end + end + + switch app.MultipleComparisonCorrectionDropDown.Value + case "Benjamini-Hochberg" + mcc = "BenjaminiHochberg"; + case "Benjamini-Yekutieli" + mcc = "BenjaminiYekutieli"; + otherwise + mcc = app.MultipleComparisonCorrectionDropDown.Value; + end + app.getPlotTitle(); + + if isequal(app.old_data, true) + app.RankingDropDown.Enable = false; + if ~isfield(app.network_test_result.full_connectome, 'd') + app.CohensDThresholdCheckBox.Enable = false; + app.CohensDThresholdEditField.Enable = false; + end + end + + probability = NetworkTestResult().getPValueNames(app.test_method, app.network_test_result.test_name); + + app.network_test_options.show_ROI_centroids = app.ROIcentroidsonbrainplotsCheckBox.Value; + + app.parameters = nla.net.result.NetworkResultPlotParameter(app.network_test_result, app.edge_test_options.net_atlas,... + app.network_test_options); + + probability_parameters = app.parameters.plotProbabilityParameters(app.edge_test_options, app.edge_test_result,... + app.test_method, probability, sprintf(app.title), mcc, app.createSignificanceFilter(),... + app.RankingDropDown.Value); + + if ~isequal(app.UpperLimitEditField.Value, 0.3) && ~isequal(app.LowerLimitEditField.Value, 0.3) + probability_parameters.p_value_plot_max = app.UpperLimitEditField.Value; + end + + plotter = nla.net.result.plot.PermutationTestPlotter(app.edge_test_options.net_atlas); + [width, height, app.matrix_plot] = plotter.plotProbability(app.Panel_2, probability_parameters, nla.inputField.LABEL_GAP, -50); + if ~isequal(app.settings, false) + app.UpperLimitEditField.Value = app.settings.upperLimit; + app.LowerLimitEditField.Value = app.settings.lowerLimit; + app.PlotScaleDropDown.Value = app.settings.plotScale; + app.pvalueThresholdEditField.Value = app.settings.pValueThreshold; + app.CohensDThresholdCheckBox.Value = app.settings.cohensD; + app.CohensDThresholdEditField.Value = app.settings.cohensDValue; + app.matrix_plot.display_legend.Visible = app.LegendVisibleDropDown.Value; + app.applyScaleChange(); + end + end + end + + methods (Access = private) + + function cohens_d_filter = createSignificanceFilter(app) + %REMOVE COHENS D FILTERING UNTIL WE DETERMINE CORRECT CALCULATION FOR IT - ADE 2025MAR24 + num_nets = app.edge_test_options.net_atlas.numNets; + cohens_d_filter = nla.TriMatrix(num_nets, "logical", nla.TriMatrixDiag.KEEP_DIAGONAL); + cohens_d_filter.v = true(numel(cohens_d_filter.v), 1); + return; + +% cohens_d_filter = nla.TriMatrix(app.edge_test_options.net_atlas.numNets, "logical", nla.TriMatrixDiag.KEEP_DIAGONAL); +% if isequal(app.CohensDThresholdCheckBox.Enable, true) && isequal(app.CohensDThresholdCheckBox.Value, true) +% if isequal(app.test_method, "no_permutations") && ~isequal(app.network_test_result.no_permutations, false) +% +% end +% if isequal(app.test_method, "full_connectome") && ~isequal(app.network_test_result.full_connectome, false) +% cohens_d_filter.v = (app.network_test_result.full_connectome.d.v >= app.network_test_options.d_max); +% end +% if ~isequal(app.network_test_result.within_network_pair, false) && isfield(app.network_test_result.within_network_pair, "d")... +% && ~isequal(app.test_method, "full_connectome") +% cohens_d_filter.v = (app.network_test_result.within_network_pair.d.v >= app.network_test_options.d_max); +% end +% else +% cohens_d_filter.v = true(numel(cohens_d_filter.v), 1); +% end + end + + function applyScaleChange(app) + progress_bar = uiprogressdlg(app.UIFigure, "Title", "Please Wait", "Message", "Applying Changes...", "Indeterminate", true); + progress_bar.Message = "Chaning scale of existing TriMatrix..."; + app.matrix_plot.applyScale(false, false, app.UpperLimitEditField.Value, app.LowerLimitEditField.Value, app.PlotScaleDropDown.Value, app.ColormapDropDown.Value) + end + + function hideCohensDControls(app) + app.CohensDThresholdEditField.Visible = false; + app.CohensDThresholdCheckBox.Visible = false; + app.CohensDThresholdEditFieldLabel.Visible = false; + + end + end + + + % Callbacks that handle component events + methods (Access = private) + + % Code that executes after component creation + function startupFcn(app, network_test_result, edge_test_result, flags, edge_test_options, network_test_options, old_data, varargin) + if isfield(flags, "show_nonpermuted") && flags.show_nonpermuted + test_method = "no_permutations"; + elseif isfield(flags, "show_full_conn") && flags.show_full_conn + test_method = "full_connectome"; + elseif isfield(flags, "show_within_net_pair") && flags.show_within_net_pair + test_method = "within_network_pair"; + end + + app.hideCohensDControls(); %keep cohens d controls in code, but hide from user until we get right calcluations - ADE2025MAR24 + + app.network_test_result = network_test_result; + app.edge_test_result = edge_test_result; + app.test_method = test_method; + app.edge_test_options = edge_test_options; + app.network_test_options = network_test_options; + app.old_data = old_data; + + % For some reason, MATLAB wouldn't accept this information in the gui editor. *&$*#( + app.EdgeChordPlotTypeDropDown.Items = ["p-value", "Coefficient", "Coefficient (Split)", "Coefficient (Basic)", "Coefficient (Split, Basic)"]; + app.EdgeChordPlotTypeDropDown.ItemsData = ["nla.gfx.EdgeChordPlotMethod.PROB","nla.gfx.EdgeChordPlotMethod.COEFF","nla.gfx.EdgeChordPlotMethod.COEFF_SPLIT","nla.gfx.EdgeChordPlotMethod.COEFF_BASE","nla.gfx.EdgeChordPlotMethod.COEFF_BASE_SPLIT"]; + app.EdgeChordPlotTypeDropDown.Value = "nla.gfx.EdgeChordPlotMethod.PROB"; + + app.edge_test_options.prob_max = app.pvalueThresholdEditField.Value; + app.network_test_options.prob_max = app.pvalueThresholdEditField.Value; + app.network_test_options.prob_plot_method = app.PlotScaleDropDown.Value; + app.pvalueThresholdEditField.Value = app.network_test_options.prob_max; + app.ColormapDropDown.Items = app.colormap_choices; + app.ColormapDropDown.Value = app.colormap_choices{1}; + + app.drawTriMatrixPlot(); + end + + % Callback function + function drawChords(app, event) + import nla.gfx.EdgeChordPlotMethod nla.net.result.NetworkTestResult + + app.getPlotTitle(); + + plot_type = app.chord_type; + + probability = NetworkTestResult().getPValueNames(app.test_method, app.network_test_result.test_name); + p_value = strcat("uncorrected_", probability); + probability_parameters = app.parameters.plotProbabilityParameters(app.edge_test_options, app.edge_test_result,... + app.test_method, p_value, sprintf(app.title), app.MultipleComparisonCorrectionDropDown.Value, app.createSignificanceFilter(),... + app.RankingDropDown.Value); + + chord_plotter = nla.net.result.chord.ChordPlotter(app.edge_test_options.net_atlas, app.edge_test_result); + + probability_parameters.edge_chord_plot_method = app.EdgeChordPlotTypeDropDown.Value; + chord_plotter.generateChordFigure(probability_parameters, plot_type) + end + + % Value changed function: ColormapDropDown, + % LegendVisibleDropDown, LowerLimitEditField, + % MultipleComparisonCorrectionDropDown, PlotScaleDropDown, + % RankingDropDown, UpperLimitEditField + function PlotScaleValueChanged(app, event) + if isequal(app.settings, false) + app.settings = struct(); + end + app.settings.upperLimit = app.UpperLimitEditField.Value; + app.settings.lowerLimit = app.LowerLimitEditField.Value; + app.settings.plotScale = app.PlotScaleDropDown.Value; + app.settings.pValueThreshold = app.pvalueThresholdEditField.Value; + app.settings.cohensD = app.CohensDThresholdCheckBox.Value; + app.settings.cohensDValue = app.CohensDThresholdEditField.Value; + app.settings.legend = app.LegendVisibleDropDown.Value; + app.settings.ranking = app.RankingDropDown.Value; + end + + % Value changed function: ROIcentroidsonbrainplotsCheckBox + function ROIcentroidsonbrainplotsCheckBoxValueChanged(app, event) + % This is used in brain plotting + value = app.ROIcentroidsonbrainplotsCheckBox.Value; + app.network_test_options.show_ROI_centroids = value; + end + + % Value changed function: pvalueThresholdEditField + function pvalueThresholdEditFieldValueChanged(app, event) + % This is used in the trimatrix. It's applied during MCC, even if mcc='None' + value = app.pvalueThresholdEditField.Value; + app.network_test_options.prob_max = value; + end + + % Button pushed function: ViewEdgeChordPlotsButton + function ViewEdgeChordPlotsButtonPushed(app, event) + app.chord_type = "nla.PlotType.CHORD_EDGE"; + app.drawChords(event); + end + + % Value changed function: EdgeChordPlotTypeDropDown + function EdgeChordPlotTypeDropDownValueChanged(app, event) + value = app.EdgeChordPlotTypeDropDown.Value; + app.network_test_options.edge_chord_plot_method = value; + end + + % Button pushed function: ViewChordPlotsButton + function ViewChordPlotsButtonPushed(app, event) + app.chord_type = "nla.PlotType.CHORD"; + app.drawChords(event); + end + + % Button pushed function: ApplyButton + function ApplyButtonPushed(app, event) + app.matrix_plot.color_bar.Ticks = []; + app.drawTriMatrixPlot(); + end + + % Button pushed function: ViewConvergenceMapButton + function ViewConvergenceMapButtonPushed(app, event) + import nla.NetworkLevelMethod + + flags = struct(); + flags.show_full_conn = false; + flags.show_within_net_pair = false; + flags.show_nonpermuted = false; + switch app.test_method + case "no_permutations" + flags.show_nonpermuted = true; + case "full_connectome" + flags.show_full_conn = true; + case "within_network_pair" + flags.show_within_net_pair = true; + end + switch app.MultipleComparisonCorrectionDropDown.Value + case "Benjamini-Hochberg" + app.network_test_options.fdr_correction = "BenjaminiHochberg"; + case "Benjamini-Yekutieli" + app.network_test_options.fdr_correction = "BenjaminiYekutieli"; + otherwise + app.network_test_options.fdr_correction = app.MultipleComparisonCorrectionDropDown.Value; + end + + [test_number, significance_count_matrix, names] = app.network_test_result.getSigMat(app.network_test_options, app.edge_test_options.net_atlas, flags); + + colors = str2func(lower(app.ConvergencePlotColorDropDown.Value)); + if isequal(app.ConvergencePlotColorDropDown.Value, "Bone") + color_map = flip(colors()); + else + color_map = [[1, 1, 1]; flip(colors())]; + end + + nla.gfx.drawConvergenceMap(app.edge_test_options, app.network_test_options, app.edge_test_options.net_atlas, significance_count_matrix,... + test_number, names, app.edge_test_result, color_map); + end + + % Menu selected function: SaveasMenu + function SaveasMenuSelected(app, event) + trimatrix_default_name = strcat("nla_trimatrix_", datestr(datetime("now"), "yyyy_MM_dd")); + [file, path] = uiputfile({'*.png', 'Image (*.png)'; '*.svg', 'Scalable Vector Graphic (*.svg)'}, "Save TriMatrix plot", trimatrix_default_name); + if file ~= 0 + exportgraphics(app.matrix_plot.axes, fullfile(path, file)); + end + end + end + + % Component initialization + methods (Access = private) + + % Create UIFigure and components + function createComponents(app) + + % Create UIFigure and hide until all components are created + app.UIFigure = uifigure('Visible', 'off'); + app.UIFigure.Color = [1 1 1]; + app.UIFigure.Position = [100 100 674 835]; + app.UIFigure.Name = 'MATLAB App'; + + % Create Menu + app.Menu = uimenu(app.UIFigure); + app.Menu.Text = 'File'; + + % Create SaveasMenu + app.SaveasMenu = uimenu(app.Menu); + app.SaveasMenu.MenuSelectedFcn = createCallbackFcn(app, @SaveasMenuSelected, true); + app.SaveasMenu.Text = 'Save as...'; + + % Create Panel + app.Panel = uipanel(app.UIFigure); + app.Panel.BackgroundColor = [1 1 1]; + app.Panel.Position = [141 6 400 330]; + + % Create PlotScaleDropDownLabel + app.PlotScaleDropDownLabel = uilabel(app.Panel); + app.PlotScaleDropDownLabel.HorizontalAlignment = 'right'; + app.PlotScaleDropDownLabel.Position = [3 297 60 22]; + app.PlotScaleDropDownLabel.Text = 'Plot Scale'; + + % Create PlotScaleDropDown + app.PlotScaleDropDown = uidropdown(app.Panel); + app.PlotScaleDropDown.Items = {'Linear', 'Log', 'Negative Log10'}; + app.PlotScaleDropDown.ItemsData = {'nla.ProbPlotMethod.DEFAULT', 'nla.ProbPlotMethod.LOG', 'nla.ProbPlotMethod.NEGATIVE_LOG_10'}; + app.PlotScaleDropDown.ValueChangedFcn = createCallbackFcn(app, @PlotScaleValueChanged, true); + app.PlotScaleDropDown.Position = [78 297 100 22]; + app.PlotScaleDropDown.Value = 'nla.ProbPlotMethod.DEFAULT'; + + % Create RankingDropDownLabel + app.RankingDropDownLabel = uilabel(app.Panel); + app.RankingDropDownLabel.HorizontalAlignment = 'right'; + app.RankingDropDownLabel.Position = [226 297 50 22]; + app.RankingDropDownLabel.Text = 'Ranking'; + + % Create RankingDropDown + app.RankingDropDown = uidropdown(app.Panel); + app.RankingDropDown.Items = {'Uncorrected', 'Winkler', 'Westfall-Young'}; + app.RankingDropDown.ItemsData = {'nla.RankingMethod.UNCORRECTED', 'nla.RankingMethod.WINKLER', 'nla.RankingMethod.WESTFALL_YOUNG'}; + app.RankingDropDown.ValueChangedFcn = createCallbackFcn(app, @PlotScaleValueChanged, true); + app.RankingDropDown.Position = [291 297 100 22]; + app.RankingDropDown.Value = 'nla.RankingMethod.UNCORRECTED'; + + % Create UpperLimitEditFieldLabel + app.UpperLimitEditFieldLabel = uilabel(app.Panel); + app.UpperLimitEditFieldLabel.HorizontalAlignment = 'right'; + app.UpperLimitEditFieldLabel.Position = [46 267 70 22]; + app.UpperLimitEditFieldLabel.Text = 'Upper Limit'; + + % Create UpperLimitEditField + app.UpperLimitEditField = uieditfield(app.Panel, 'numeric'); + app.UpperLimitEditField.ValueChangedFcn = createCallbackFcn(app, @PlotScaleValueChanged, true); + app.UpperLimitEditField.Position = [126 267 52 22]; + app.UpperLimitEditField.Value = 0.05; + + % Create LowerLimitEditFieldLabel + app.LowerLimitEditFieldLabel = uilabel(app.Panel); + app.LowerLimitEditFieldLabel.HorizontalAlignment = 'right'; + app.LowerLimitEditFieldLabel.Position = [259 267 70 22]; + app.LowerLimitEditFieldLabel.Text = 'Lower Limit'; + + % Create LowerLimitEditField + app.LowerLimitEditField = uieditfield(app.Panel, 'numeric'); + app.LowerLimitEditField.ValueChangedFcn = createCallbackFcn(app, @PlotScaleValueChanged, true); + app.LowerLimitEditField.Position = [339 267 52 22]; + + % Create pvalueThresholdEditFieldLabel + app.pvalueThresholdEditFieldLabel = uilabel(app.Panel); + app.pvalueThresholdEditFieldLabel.HorizontalAlignment = 'right'; + app.pvalueThresholdEditFieldLabel.Position = [14 237 102 22]; + app.pvalueThresholdEditFieldLabel.Text = 'p-value Threshold'; + + % Create pvalueThresholdEditField + app.pvalueThresholdEditField = uieditfield(app.Panel, 'numeric'); + app.pvalueThresholdEditField.ValueChangedFcn = createCallbackFcn(app, @pvalueThresholdEditFieldValueChanged, true); + app.pvalueThresholdEditField.Position = [126 237 52 22]; + app.pvalueThresholdEditField.Value = 0.05; + + % Create CohensDThresholdEditFieldLabel + app.CohensDThresholdEditFieldLabel = uilabel(app.Panel); + app.CohensDThresholdEditFieldLabel.HorizontalAlignment = 'right'; + app.CohensDThresholdEditFieldLabel.Enable = 'off'; + app.CohensDThresholdEditFieldLabel.Position = [195 237 118 22]; + app.CohensDThresholdEditFieldLabel.Text = 'Cohen''s D Threshold'; + + % Create CohensDThresholdEditField + app.CohensDThresholdEditField = uieditfield(app.Panel, 'numeric'); + app.CohensDThresholdEditField.Enable = 'off'; + app.CohensDThresholdEditField.Position = [339 237 52 22]; + + % Create ColormapDropDownLabel + app.ColormapDropDownLabel = uilabel(app.Panel); + app.ColormapDropDownLabel.HorizontalAlignment = 'right'; + app.ColormapDropDownLabel.Position = [9 177 58 22]; + app.ColormapDropDownLabel.Text = 'Colormap'; + + % Create ColormapDropDown + app.ColormapDropDown = uidropdown(app.Panel); + app.ColormapDropDown.Items = {}; + app.ColormapDropDown.ValueChangedFcn = createCallbackFcn(app, @PlotScaleValueChanged, true); + app.ColormapDropDown.Position = [78 177 100 22]; + app.ColormapDropDown.Value = {}; + + % Create LegendVisibleDropDownLabel + app.LegendVisibleDropDownLabel = uilabel(app.Panel); + app.LegendVisibleDropDownLabel.HorizontalAlignment = 'right'; + app.LegendVisibleDropDownLabel.Position = [233 177 84 22]; + app.LegendVisibleDropDownLabel.Text = 'Legend Visible'; + + % Create LegendVisibleDropDown + app.LegendVisibleDropDown = uidropdown(app.Panel); + app.LegendVisibleDropDown.Items = {'On', 'Off'}; + app.LegendVisibleDropDown.ItemsData = {'on', 'off'}; + app.LegendVisibleDropDown.ValueChangedFcn = createCallbackFcn(app, @PlotScaleValueChanged, true); + app.LegendVisibleDropDown.Position = [332 177 59 22]; + app.LegendVisibleDropDown.Value = 'on'; + + % Create MultipleComparisonCorrectionDropDownLabel + app.MultipleComparisonCorrectionDropDownLabel = uilabel(app.Panel); + app.MultipleComparisonCorrectionDropDownLabel.HorizontalAlignment = 'right'; + app.MultipleComparisonCorrectionDropDownLabel.Position = [11 147 178 22]; + app.MultipleComparisonCorrectionDropDownLabel.Text = 'Multiple Comparison Correction'; + + % Create MultipleComparisonCorrectionDropDown + app.MultipleComparisonCorrectionDropDown = uidropdown(app.Panel); + app.MultipleComparisonCorrectionDropDown.Items = {'None', 'Bonferroni', 'Benjamini-Hochberg', 'Benjamini-Yekutieli'}; + app.MultipleComparisonCorrectionDropDown.ValueChangedFcn = createCallbackFcn(app, @PlotScaleValueChanged, true); + app.MultipleComparisonCorrectionDropDown.Position = [226 147 165 22]; + app.MultipleComparisonCorrectionDropDown.Value = 'None'; + + % Create CohensDThresholdCheckBox + app.CohensDThresholdCheckBox = uicheckbox(app.Panel); + app.CohensDThresholdCheckBox.Enable = 'off'; + app.CohensDThresholdCheckBox.Text = 'Cohen''s D Threshold'; + app.CohensDThresholdCheckBox.Position = [257 207 134 22]; + + % Create ROIcentroidsonbrainplotsCheckBox + app.ROIcentroidsonbrainplotsCheckBox = uicheckbox(app.Panel); + app.ROIcentroidsonbrainplotsCheckBox.ValueChangedFcn = createCallbackFcn(app, @ROIcentroidsonbrainplotsCheckBoxValueChanged, true); + app.ROIcentroidsonbrainplotsCheckBox.Text = 'ROI centroids on brain plots'; + app.ROIcentroidsonbrainplotsCheckBox.Position = [7 207 171 22]; + + % Create ViewChordPlotsButton + app.ViewChordPlotsButton = uibutton(app.Panel, 'push'); + app.ViewChordPlotsButton.ButtonPushedFcn = createCallbackFcn(app, @ViewChordPlotsButtonPushed, true); + app.ViewChordPlotsButton.Position = [71 117 107 22]; + app.ViewChordPlotsButton.Text = 'View Chord Plots'; + + % Create ViewEdgeChordPlotsButton + app.ViewEdgeChordPlotsButton = uibutton(app.Panel, 'push'); + app.ViewEdgeChordPlotsButton.ButtonPushedFcn = createCallbackFcn(app, @ViewEdgeChordPlotsButtonPushed, true); + app.ViewEdgeChordPlotsButton.Position = [252 117 139 22]; + app.ViewEdgeChordPlotsButton.Text = 'View Edge Chord Plots'; + + % Create EdgeChordPlotTypeDropDownLabel + app.EdgeChordPlotTypeDropDownLabel = uilabel(app.Panel); + app.EdgeChordPlotTypeDropDownLabel.HorizontalAlignment = 'right'; + app.EdgeChordPlotTypeDropDownLabel.Position = [71 87 123 22]; + app.EdgeChordPlotTypeDropDownLabel.Text = 'Edge Chord Plot Type'; + + % Create EdgeChordPlotTypeDropDown + app.EdgeChordPlotTypeDropDown = uidropdown(app.Panel); + app.EdgeChordPlotTypeDropDown.Items = {}; + app.EdgeChordPlotTypeDropDown.ValueChangedFcn = createCallbackFcn(app, @EdgeChordPlotTypeDropDownValueChanged, true); + app.EdgeChordPlotTypeDropDown.Position = [226 87 165 22]; + app.EdgeChordPlotTypeDropDown.Value = {}; + + % Create ViewConvergenceMapButton + app.ViewConvergenceMapButton = uibutton(app.Panel, 'push'); + app.ViewConvergenceMapButton.ButtonPushedFcn = createCallbackFcn(app, @ViewConvergenceMapButtonPushed, true); + app.ViewConvergenceMapButton.Position = [11 57 143 22]; + app.ViewConvergenceMapButton.Text = 'View Convergence Map'; + + % Create ConvergencePlotColorDropDownLabel + app.ConvergencePlotColorDropDownLabel = uilabel(app.Panel); + app.ConvergencePlotColorDropDownLabel.HorizontalAlignment = 'right'; + app.ConvergencePlotColorDropDownLabel.Position = [161 57 133 22]; + app.ConvergencePlotColorDropDownLabel.Text = 'Convergence Plot Color'; + + % Create ConvergencePlotColorDropDown + app.ConvergencePlotColorDropDown = uidropdown(app.Panel); + app.ConvergencePlotColorDropDown.Items = {'Bone', 'Winter', 'Autumn', 'Copper'}; + app.ConvergencePlotColorDropDown.Position = [309 57 82 22]; + app.ConvergencePlotColorDropDown.Value = 'Autumn'; + + % Create ApplyButton + app.ApplyButton = uibutton(app.Panel, 'push'); + app.ApplyButton.ButtonPushedFcn = createCallbackFcn(app, @ApplyButtonPushed, true); + app.ApplyButton.Position = [21 27 100 22]; + app.ApplyButton.Text = 'Apply'; + + % Create Panel_2 + app.Panel_2 = uipanel(app.UIFigure); + app.Panel_2.AutoResizeChildren = 'off'; + app.Panel_2.BackgroundColor = [1 1 1]; + app.Panel_2.Position = [86 346 510 480]; + + % Show the figure after all components are created + app.UIFigure.Visible = 'on'; + end + end + + % App creation and deletion + methods (Access = public) + + % Construct app + function app = NetworkTestPlotApp(varargin) + + % Create UIFigure and components + createComponents(app) + + % Register the app with App Designer + registerApp(app, app.UIFigure) + + % Execute the startup function + runStartupFcn(app, @(app)startupFcn(app, varargin{:})) + + if nargout == 0 + clear app + end + end + + % Code that executes before app deletion + function delete(app) + + % Delete UIFigure when app is deleted + delete(app.UIFigure) + end + end +end \ No newline at end of file diff --git a/+nla/+net/+result/+plot/PermutationTestPlotter.m b/+nla/+net/+result/+plot/PermutationTestPlotter.m index a1b8486d..a21ec8ff 100644 --- a/+nla/+net/+result/+plot/PermutationTestPlotter.m +++ b/+nla/+net/+result/+plot/PermutationTestPlotter.m @@ -11,7 +11,7 @@ end end - function [w, h] = plotProbability(obj, plot_figure, parameters, x_coordinate, y_coordinate) + function [w, h, matrix_plot] = plotProbability(obj, plot_figure, parameters, x_coordinate, y_coordinate) color_map = parameters.color_map; statistic_matrix = parameters.statistic_plot_matrix; p_value_max = parameters.p_value_plot_max; @@ -47,10 +47,10 @@ function plotProbabilityVsNetworkSize(obj, parameters, axes, plot_title) hold("on"); plot(least_squares_line_x, least_squares_line_y, "r"); - xlabel(axes, "Number of ROI pairs within network pair"); + xlabel(axes, sprintf("Number of ROI pairs\nwithin network pair")); ylabel(axes, "-log_1_0(Asymptotic P-value)"); setTitle(axes, plot_title); - second_title = sprintf('Check if P-values correlate with net-pair size\n(corr: p = %.2f, r = %.2f)', p_values, rho); + second_title = sprintf('Check if P-values correlate with\nnet-pair size (corr: p = %.2f, r = %.2f)', p_values, rho); setTitle(axes, second_title, true); lims = ylim(axes); if lims(2) < 0 diff --git a/+nla/+net/+result/NetworkResultPlotParameter.m b/+nla/+net/+result/NetworkResultPlotParameter.m index de8d506a..c9194a85 100644 --- a/+nla/+net/+result/NetworkResultPlotParameter.m +++ b/+nla/+net/+result/NetworkResultPlotParameter.m @@ -27,7 +27,7 @@ end function result = plotProbabilityParameters(obj, edge_test_options, edge_test_result, test_method, plot_statistic,... - plot_title, fdr_correction, significance_filter) + plot_title, fdr_correction, significance_filter, ranking_method) % plot_title - this will be a string % plot_statistic - this is the stat that will be plotted % significance filter - this will be a boolean or some sort of object (like Cohen's D > D-value) @@ -42,20 +42,27 @@ end % Adding on to the plot title if it's a -log10 plot - if obj.updated_test_options.prob_plot_method == nla.gfx.ProbPlotMethod.NEG_LOG_10 + if obj.updated_test_options.prob_plot_method == nla.gfx.ProbPlotMethod.NEGATIVE_LOG_10 plot_title = sprintf("%s (-log_1_0(P))", plot_title); end % Grab the data from the NetworkTestResult object - statistic_input = obj.getStatsFromMethodAndName(test_method, plot_statistic); + statistic_input = obj.getStatsFromMethodAndName(test_method, plot_statistic, ranking_method); % Get the scale max and the labels + if isstring(fdr_correction) || ischar(fdr_correction) + fdr_correction = nla.net.mcc.(fdr_correction)(); + end p_value_max = fdr_correction.correct(obj.network_atlas, obj.updated_test_options, statistic_input); p_value_breakdown_label = fdr_correction.createLabel(obj.network_atlas, obj.updated_test_options,... statistic_input); name_label = sprintf("%s %s\nP < %.2g (%s)", obj.network_test_results.test_display_name, plot_title,... p_value_max, p_value_breakdown_label); + if p_value_max == 0 + name_label = sprintf("%s %s\nP = %.2g (%s)", obj.network_test_results.test_display_name, plot_title,... + p_value_max, p_value_breakdown_label); + end % Filtering if there's a filter provided significance_plot = TriMatrix(obj.number_of_networks, "logical", TriMatrixDiag.KEEP_DIAGONAL); @@ -69,14 +76,14 @@ % default values for plotting statistic_plot_matrix = statistic_input_scaled; p_value_plot_max = p_value_max; - significance_type = nla.gfx.SigType.DECREASING; + significance_type = "nla.gfx.SigType.DECREASING"; % determine colormap and operate on values if it's -log10 switch obj.updated_test_options.prob_plot_method - case nla.gfx.ProbPlotMethod.LOG + case "LOG" % FUCK Matlab and their enums color_map = nla.net.result.NetworkResultPlotParameter.getLogColormap(obj.default_discrete_colors,... statistic_input, p_value_max); % Here we take a -log10 and change the maximum value to show on the plot - case nla.gfx.ProbPlotMethod.NEG_LOG_10 + case "NEGATIVE_LOG_10" color_map = parula(obj.default_discrete_colors); statistic_matrix = nla.TriMatrix(obj.number_of_networks, "double", nla.TriMatrixDiag.KEEP_DIAGONAL); @@ -87,7 +94,7 @@ else p_value_plot_max = 40; end - significance_type = nla.gfx.SigType.INCREASING; + significance_type = "nla.gfx.SigType.INCREASING"; otherwise color_map = nla.net.result.NetworkResultPlotParameter.getColormap(obj.default_discrete_colors,... p_value_max); @@ -100,9 +107,11 @@ function brainFigureButtonCallback(network1, network2) wait_text = sprintf("Generating %s - %s network-pair brain plot", obj.network_atlas.nets(network1).name,... obj.network_atlas.nets(network2).name); wait_popup = waitbar(0.05, wait_text); - nla.gfx.drawBrainVis(edge_test_options, obj.updated_test_options, obj.network_atlas,... - nla.gfx.MeshType.STD, 0.25, 3, true, edge_test_result, network1, network2,... - any(strcmp(obj.noncorrelation_input_tests, obj.network_test_results.test_name))); + % nla.gfx.drawBrainVis(edge_test_options, obj.updated_test_options, obj.network_atlas,... + % nla.gfx.MeshType.STD, 0.25, 3, true, edge_test_result, network1, network2,... + % any(strcmp(obj.noncorrelation_input_tests, obj.network_test_results.test_name))); + brain_plot = nla.gfx.brain.BrainPlot(edge_test_result, edge_test_options, obj.updated_test_options, network1, network2, edge_test_options.net_atlas); + brain_plot.drawBrainPlots() waitbar(0.95); close(wait_popup); end @@ -122,7 +131,7 @@ function brainFigureButtonCallback(network1, network2) function result = plotProbabilityVsNetworkSize(obj, test_method, plot_statistic) % Two convience methods network_size = obj.getNetworkSizes(); - statistic_input = obj.getStatsFromMethodAndName(test_method, plot_statistic); + statistic_input = obj.getStatsFromMethodAndName(test_method, plot_statistic, obj.updated_test_options.ranking_method); negative_log10_statistics = -log10(statistic_input.v); @@ -164,14 +173,30 @@ function brainFigureButtonCallback(network1, network2) end end - function statistic = getStatsFromMethodAndName(obj, test_method, plot_statistic) - % combining the method and stat name to get the data. With a fail safe for forgetting 'single_sample' - if isequal(test_method, "within_network_pair")... - && ~startsWith(plot_statistic, "single_sample")... - && ~any(ismember(obj.noncorrelation_input_tests, obj.network_test_results.test_name)) - plot_statistic = strcat("single_sample_", plot_statistic); + function statistic = getStatsFromMethodAndName(obj, method, plot_statistic, ranking_method) + import nla.RankingMethod nla.NetworkLevelMethod nla.net.result.NetworkTestResult + + switch method + case "no_permutations" + test_method = "no_permutations"; + case "full_connectome" + test_method = "full_connectome"; + case "within_network_pair" + test_method = "within_network_pair"; end - statistic = obj.network_test_results.(test_method).(plot_statistic); + + switch ranking_method + case "nla.RankingMethod.WINKLER" + ranking = "winkler_"; + case "nla.RankingMethod.WESTFALL_YOUNG" + ranking = "westfall_young_"; + otherwise + ranking = "uncorrected_"; + end + + probability = NetworkTestResult().getPValueNames(method, obj.network_test_results.test_name); + + statistic = obj.network_test_results.(test_method).(strcat(ranking, probability)); end end diff --git a/+nla/+net/+result/NetworkTestResult.m b/+nla/+net/+result/NetworkTestResult.m index 682bb4c3..f060c89a 100644 --- a/+nla/+net/+result/NetworkTestResult.m +++ b/+nla/+net/+result/NetworkTestResult.m @@ -43,6 +43,7 @@ end properties (Constant) + % TODO: replace wtih enums test_methods = ["no_permutations", "full_connectome", "within_network_pair"] noncorrelation_input_tests = ["chi_squared", "hypergeometric"] % These are tests that do not use correlation coefficients as inputs end @@ -60,6 +61,9 @@ % ranking_statistic [String]: Test statistic that will be used in ranking import nla.TriMatrix nla.TriMatrixDiag + if nargin == 0 + return + end if nargin == 6 obj.test_name = test_name; @@ -74,40 +78,19 @@ end function output(obj, edge_test_options, updated_test_options, network_atlas, edge_test_result, flags) - import nla.TriMatrix nla.TriMatrixDiag nla.net.result.NetworkResultPlotParameter - - % This is the object that will do the calculations for the plots - result_plot_parameters = NetworkResultPlotParameter(obj, network_atlas, updated_test_options); + import nla.NetworkLevelMethod - % Cohen's D results for markers - cohens_d_filter = TriMatrix(network_atlas.numNets, 'logical', TriMatrixDiag.KEEP_DIAGONAL); - if ~obj.is_noncorrelation_input - cohens_d_filter.v = (obj.full_connectome.d.v >= updated_test_options.d_max); - end - - %% - % Nonpermuted Plotting if isfield(flags, "show_nonpermuted") && flags.show_nonpermuted - obj.noPermutationsPlotting(result_plot_parameters, edge_test_options, edge_test_result,... - updated_test_options, flags); + test_method = "no_permutations"; + elseif isfield(flags, "show_full_conn") && flags.show_full_conn + test_method = "full_connectome"; + elseif isfield(flags, "show_within_net_pair") && flags.show_within_net_pair + test_method = "within_network_pair"; end - %% - %% - % Full Connectome Plotting - if isfield(flags, "show_full_conn") && flags.show_full_conn - obj.fullConnectomePlotting(network_atlas, edge_test_options, edge_test_result, updated_test_options,... - cohens_d_filter, flags); - end - %% - - %% - % Within network pair plotting - if isfield(flags, "show_within_net_pair") && flags.show_within_net_pair - obj.withinNetworkPairPlotting(network_atlas, edge_test_options, edge_test_result, updated_test_options,... - cohens_d_filter, flags); - end - %% + network_result_plot = nla.net.result.plot.NetworkTestPlot(obj, edge_test_result, network_atlas,... + test_method, edge_test_options, updated_test_options); + network_result_plot.drawFigure(nla.PlotType.FIGURE) end function merge(obj, other_objects) @@ -142,6 +125,35 @@ function concatenateResult(obj, other_object) obj.last_index = obj.last_index + 1; end + function histogram = createHistogram(obj, statistic) + if ~endsWith(statistic, "_permutations") + statistic = strcat(statistic, "_permutations"); + end + permutation_data = obj.permutation_results.(statistic); + histogram = zeros(nla.HistBin.SIZE, "uint32"); + + for permutation = 1:obj.permutation_count + histogram = histogram + uint32(histcounts(permutation_data.v(:, permutation), nla.HistBin.EDGES)'); + end + end + + function runDiagnosticPlots(obj, edge_test_options, updated_test_options, edge_test_result, network_atlas, flags) + import nla.NetworkLevelMethod + + diagnostics_plot = nla.gfx.plots.DiagnosticPlot(edge_test_options, updated_test_options,... + edge_test_result, network_atlas, obj); + + if isfield(flags, "show_nonpermuted") && flags.show_nonpermuted + test_method = "no_permutations"; + elseif isfield(flags, "show_full_conn") && flags.show_full_conn + test_method = "full_connectome"; + elseif isfield(flags, "show_within_net_pair") && flags.show_within_net_pair + test_method = "within_network_pair"; + end + + diagnostics_plot.displayPlots(test_method); + end + % I'm assuming this is Get Significance Matrix. It's used for the convergence plots button, but the naming makes zero sense % Any help on renaming would be great. function [test_number, significance_count_matrix, names] = getSigMat(obj, network_test_options, network_atlas, flags) @@ -154,17 +166,17 @@ function concatenateResult(obj, other_object) if isfield(flags, "show_nonpermuted") && flags.show_nonpermuted title = "Non-Permuted"; - p_values = obj.no_permutations.p_value; + p_values = obj.no_permutations.uncorrected_two_sample_p_value; fdr_method = network_test_options.fdr_correction; end if isfield(flags, "show_full_conn") && flags.show_full_conn title = "Full Connectome"; - p_values = obj.full_connectome.p_value; - fdr_method = nla.net.mcc.None; + p_values = obj.full_connectome.uncorrected_two_sample_p_value; + fdr_method = network_test_options.fdr_correction; end if isfield(flags, "show_within_net_pair") && flags.show_within_net_pair title = "Within Network Pair"; - p_values = obj.within_network_pair.single_sample_p_value; + p_values = obj.within_network_pair.uncorrected_single_sample_p_value; fdr_method = network_test_options.fdr_correction; end [significance, name] = obj.singleSigMat(network_atlas, network_test_options, p_values, fdr_method, title); @@ -172,18 +184,19 @@ function concatenateResult(obj, other_object) names, significance, name); end - %% This is taken directly from old version to maintain functionality. Not sure anyone uses it. function table_new = generateSummaryTable(obj, table_old) - table_new = [table_old, table(obj.full_connectome.p_value.v, 'VariableNames', [obj.test_name + "P-value"])]; + table_new = [table_old, table(... + obj.full_connectome.uncorrected_two_sample_p_value.v, 'VariableNames', [obj.test_display_name + "Full Connectome Two Sample p-value"]... + )]; end %% % getters for dependent properties function value = get.permutation_count(obj) % Convenience method to carry permutation from data through here - if isfield(obj.permutation_results, "p_value_permutations") &&... - ~isequal(obj.permutation_results.p_value_permutations, false) - value = size(obj.permutation_results.p_value_permutations.v, 2); + if isfield(obj.permutation_results, "two_sample_p_value_permutations") &&... + ~isequal(obj.permutation_results.two_sample_p_value_permutations, false) + value = size(obj.permutation_results.two_sample_p_value_permutations.v, 2); elseif isfield(obj.permutation_results, "single_sample_p_value_permutations") &&... ~isequal(obj.permutation_results.single_sample_p_value_permutations, false) value = size(obj.permutation_results.single_sample_p_value_permutations.v, 2); @@ -196,6 +209,12 @@ function concatenateResult(obj, other_object) % Convenience method to determine if inputs were correlation coefficients, or "significance" values value = any(strcmp(obj.noncorrelation_input_tests, obj.test_name)); end + + function set.is_noncorrelation_input(obj, ~) + end + + function set.permutation_count(obj, ~) + end %% end @@ -217,15 +236,8 @@ function createResultsStorage(obj, test_options, number_of_networks, test_specif function createTestSpecificResultsStorage(obj, number_of_networks, test_specific_statistics) %CREATETESTSPECIFICRESULTSSTORAGE Create the substructures for the specific statistical tests - import nla.TriMatrix nla.TriMatrixDiag - obj.permutation_results.p_value_permutations = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); - if ~any(strcmp(obj.test_name, obj.noncorrelation_input_tests)) - obj.permutation_results.single_sample_p_value_permutations = TriMatrix(number_of_networks,... - TriMatrixDiag.KEEP_DIAGONAL); - end - for statistic_index = 1:numel(test_specific_statistics) test_statistic = test_specific_statistics(statistic_index); obj.no_permutations.(test_statistic) = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); @@ -236,227 +248,62 @@ function createTestSpecificResultsStorage(obj, number_of_networks, test_specific function createPValueTriMatrices(obj, number_of_networks, test_method) %CREATEPVALUETRIMATRICES Creates the p-value substructure for the test method - import nla.TriMatrix nla.TriMatrixDiag - obj.(test_method).p_value = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); % p-value by statistic rank - obj.(test_method).statistic_p_value = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); % p-value by statistic rank - if ~isequal(test_method, "full_connectome") && ~any(strcmp(obj.test_name, obj.noncorrelation_input_tests)) - obj.(test_method).single_sample_p_value = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); - end - %Cohen's D results - obj.(test_method).d = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); - end - - function histogram = createHistogram(obj, statistic) - if ~endsWith(statistic, "_permutations") - statistic = strcat(statistic, "_permutations"); - end - permutation_data = obj.permutation_results.(statistic); - histogram = zeros(nla.HistBin.SIZE, "uint32"); - - for permutation = 1:obj.permutation_count - histogram = histogram + uint32(histcounts(permutation_data.v(:, permutation), nla.HistBin.EDGES)'); + non_correlation_test = any(strcmp(obj.test_name, obj.noncorrelation_input_tests)); + uncorrected_names = ["uncorrected_", "legacy_"]; + corrected_names = ["winkler_", "westfall_young_"]; + + switch test_method + case "no_permutations" + for uncorrected_name = uncorrected_names + p_value = "two_sample_p_value"; + obj.(test_method).(strcat(uncorrected_name, p_value)) = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + if isequal(non_correlation_test, false) + p_value = "single_sample_p_value"; + obj.(test_method).(strcat(uncorrected_name, p_value)) = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + end + end + case "full_connectome" + p_value = "two_sample_p_value"; + for name = [corrected_names uncorrected_names] + obj.(test_method).(strcat(name, p_value)) = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + end + case "within_network_pair" + % This is so hacky, but Matlab doesn't play well with logical order-of-operations + if isequal(non_correlation_test, true) + p_value = "two_sample_p_value"; + else + p_value = "single_sample_p_value"; + end + for name = [corrected_names uncorrected_names] + obj.(test_method).(strcat(name, p_value)) = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + end end - end - - function noPermutationsPlotting(obj, plot_parameters, edge_test_options, edge_test_result, updated_test_options, flags) - import nla.gfx.createFigure nla.net.result.plot.PermutationTestPlotter nla.net.result.chord.ChordPlotter - - plot_test_type = "no_permutations"; - - % Get the plot parameters (titles, stats, labels, max, min, etc) - plot_title = sprintf('Non-permuted Method\nNon-permuted Significance'); - - p_value = obj.choosePlottingMethod(updated_test_options, plot_test_type); - p_value_plot_parameters = plot_parameters.plotProbabilityParameters(edge_test_options, edge_test_result,... - plot_test_type, p_value, plot_title, updated_test_options.fdr_correction, false); - - % No permutations results - if flags.plot_type == nla.PlotType.FIGURE - plot_figure = createFigure(500, 900); - - p_value_vs_network_size_parameters = plot_parameters.plotProbabilityVsNetworkSize("no_permutations",... - p_value); - plotter = PermutationTestPlotter(plot_parameters.network_atlas); - % don't need to create a reference to axis since drawMatrixOrg takes a figure as a reference - % plot the probability - - % Hard-coding sucks, but to make this adaptable for every type of test and method, here we are - x_coordinate = 0; - y_coordinate = 425; - plotter.plotProbability(plot_figure, p_value_plot_parameters, x_coordinate, y_coordinate); - - % do need to create a reference here for the axes since this just uses matlab builtins - axes = subplot(2,1,2); - plotter.plotProbabilityVsNetworkSize(p_value_vs_network_size_parameters, axes,... - "Non-permuted P-values vs. Network-Pair Size"); - - elseif flags.plot_type == nla.PlotType.CHORD || flags.plot_type == nla.PlotType.CHORD_EDGE - if isfield(updated_test_options, 'edge_chord_plot_method') - p_value_plot_parameters.edge_chord_plot_method = updated_test_options.edge_chord_plot_method; - end - chord_plotter = ChordPlotter(plot_parameters.network_atlas, edge_test_result); - chord_plotter.generateChordFigure(p_value_plot_parameters, flags.plot_type); - end - end - - function fullConnectomePlotting(obj, network_atlas, edge_test_options, edge_test_result, updated_test_options, cohens_d_filter, flags) - import nla.gfx.createFigure nla.net.result.NetworkResultPlotParameter nla.net.result.plot.PermutationTestPlotter - import nla.net.result.chord.ChordPlotter - - plot_test_type = "full_connectome"; - - plot_title = sprintf("Full Connectome Method\nNetwork vs. Connectome Significance"); - plot_title_threshold = sprintf('%s (D > %g)', plot_title, updated_test_options.d_max); - - p_value = obj.choosePlottingMethod(updated_test_options, plot_test_type); - - % This is the object that will do the calculations for the plots - result_plot_parameters = NetworkResultPlotParameter(obj, edge_test_options.net_atlas, updated_test_options); - - % Get the plot parameters (titles, stats, labels, etc.) - full_connectome_p_value_plot_parameters = result_plot_parameters.plotProbabilityParameters(... - edge_test_options, edge_test_result, plot_test_type, p_value, plot_title,... - nla.net.mcc.None(), false); - - % Mark the probability trimatrix with cohen's d results - full_connectome_p_value_plot_parameters_with_cohensd = result_plot_parameters.plotProbabilityParameters(... - edge_test_options, edge_test_result, plot_test_type, p_value, plot_title_threshold, ... - nla.net.mcc.None(), cohens_d_filter); - - if flags.plot_type == nla.PlotType.FIGURE - - - p_value_vs_network_size_parameters = result_plot_parameters.plotProbabilityVsNetworkSize("no_permutations",... - p_value); - full_connectome_p_value_vs_network_size_parameters = result_plot_parameters.plotProbabilityVsNetworkSize(... - plot_test_type, p_value); - % create a histogram - p_value_histogram = obj.createHistogram(p_value); - - plotter = PermutationTestPlotter(edge_test_options.net_atlas); - - % With the way subplot works, we have to do the plotting this way. I tried assigning variables to the subplots, - % but then the plots get put under different layers. - if obj.is_noncorrelation_input - plot_figure = createFigure(1000, 900); - plotter.plotProbabilityHistogram(subplot(2,2,2), p_value_histogram, obj.full_connectome.p_value.v,... - obj.no_permutations.p_value.v, obj.test_display_name, updated_test_options.prob_max); - plotter.plotProbabilityVsNetworkSize(p_value_vs_network_size_parameters, subplot(2,2,3),... - "Non-permuted P-values vs. Network-Pair Size"); - plotter.plotProbabilityVsNetworkSize(full_connectome_p_value_vs_network_size_parameters, subplot(2,2,4),... - "Permuted P-values vs. Net-Pair Size"); - x_coordinate = 25; - else - plot_figure = createFigure(1200, 900); - plotter.plotProbabilityVsNetworkSize(p_value_vs_network_size_parameters, subplot(2,3,5),... - "Non-permuted P-values vs. Network-Pair Size"); - plotter.plotProbabilityVsNetworkSize(full_connectome_p_value_vs_network_size_parameters, subplot(2,3,6),... - "Permuted P-values vs. Net-Pair Size"); - plotter.plotProbabilityHistogram(subplot(2,3,4), p_value_histogram, obj.full_connectome.p_value.v,... - obj.no_permutations.p_value.v, obj.test_display_name, updated_test_options.prob_max); - x_coordinate = 75; - end - - y_coordinate = 425; - [w, ~] = plotter.plotProbability(plot_figure, full_connectome_p_value_plot_parameters, x_coordinate, y_coordinate); - if ~obj.is_noncorrelation_input - plotter.plotProbability(plot_figure, full_connectome_p_value_plot_parameters_with_cohensd, w + 50, y_coordinate); - end - - elseif flags.plot_type == nla.PlotType.CHORD || flags.plot_type == nla.PlotType.CHORD_EDGE - if isfield(updated_test_options, 'edge_chord_plot_method') - full_connectome_p_value_plot_parameters.edge_chord_plot_method = updated_test_options.edge_chord_plot_method; - full_connectome_p_value_plot_parameters_with_cohensd.edge_chord_plot_method = updated_test_options.edge_chord_plot_method; - end - - chord_plotter = ChordPlotter(network_atlas, edge_test_result); - if obj.is_noncorrelation_input && isfield(updated_test_options, 'd_thresh_chord_plot') && updated_test_options.d_thresh_chord_plot - chord_plotter.generateChordFigure(full_connectome_p_value_plot_parameters_with_cohensd, flags.plot_type); - else - chord_plotter.generateChordFigure(full_connectome_p_value_plot_parameters, flags.plot_type) - end - end - end - - function withinNetworkPairPlotting(obj, network_atlas, edge_test_options, edge_test_result, updated_test_options, cohens_d_filter, flags) - import nla.gfx.createFigure nla.net.result.NetworkResultPlotParameter nla.net.result.plot.PermutationTestPlotter - import nla.net.result.chord.ChordPlotter - - plot_test_type = "within_network_pair"; - - plot_title = sprintf('Within Network Pair Method\nNetwork Pair vs. Permuted Network Pair'); - - result_plot_parameters = NetworkResultPlotParameter(obj, edge_test_options.net_atlas, updated_test_options); - - p_value = obj.choosePlottingMethod(updated_test_options, plot_test_type); - - within_network_pair_p_value_vs_network_parameters = result_plot_parameters.plotProbabilityVsNetworkSize(... - plot_test_type, p_value); - - within_network_pair_p_value_parameters = result_plot_parameters.plotProbabilityParameters(edge_test_options,... - edge_test_result, plot_test_type, p_value, plot_title, updated_test_options.fdr_correction, false); - - plot_title = sprintf("Within Network Pair Method\nNetwork Pair vs. Permuted Network Pair (D > %g)",... - updated_test_options.d_max); - within_network_pair_p_value_parameters_with_cohensd = result_plot_parameters.plotProbabilityParameters(... - edge_test_options, edge_test_result, plot_test_type, p_value, plot_title,... - updated_test_options.fdr_correction, cohens_d_filter); - - if flags.plot_type == nla.PlotType.FIGURE - - plotter = PermutationTestPlotter(edge_test_options.net_atlas); - y_coordinate = 425; - if obj.is_noncorrelation_input - plot_figure = createFigure(500, 900); - x_coordinate = 0; - plotter.plotProbabilityVsNetworkSize(within_network_pair_p_value_vs_network_parameters, subplot(2,1,2),... - "Within Net-Pair P-values vs. Net-Pair Size"); - plotter.plotProbability(plot_figure, within_network_pair_p_value_parameters, x_coordinate, y_coordinate); - else - plot_figure = createFigure(1000,900); - x_coordinate = 25; - plotter.plotProbabilityVsNetworkSize(within_network_pair_p_value_vs_network_parameters, subplot(2,2,3),... - "Within Net-Pair P-values vs. Net-Pair Size"); - [w, ~] = plotter.plotProbability(plot_figure, within_network_pair_p_value_parameters, x_coordinate, y_coordinate); - plotter.plotProbability(plot_figure, within_network_pair_p_value_parameters_with_cohensd, w - 50, y_coordinate); - end - - elseif flags.plot_type == nla.PlotType.CHORD || flags.plot_type == nla.PlotType.CHORD_EDGE - if isfield(updated_test_options, 'edge_chord_plot_method') - within_network_pair_p_value_parameters.edge_chord_plot_method = updated_test_options.edge_chord_plot_method; - within_network_pair_p_value_parameters_with_cohensd.edge_chord_plot_method = updated_test_options.edge_chord_plot_method; - end - - chord_plotter = ChordPlotter(network_atlas, edge_test_result); - if obj.is_noncorrelation_input && isfield(updated_test_options, 'd_thresh_chord_plot') && updated_test_options.d_thresh_chord_plot - chord_plotter.generateChordFigure(within_network_pair_p_value_parameters_with_cohensd, flags.plot_type); - else - chord_plotter.generateChordFigure(within_network_pair_p_value_parameters, flags.plot_type); - end + % We need the permutation fields for all results. We need the two-sample ones for everything + obj.permutation_results.two_sample_p_value_permutations = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + if isequal(non_correlation_test, false) + obj.permutation_results.single_sample_p_value_permutations = TriMatrix(number_of_networks,... + TriMatrixDiag.KEEP_DIAGONAL); end - end - function p_value = choosePlottingMethod(obj, test_options, plot_test_type) - p_value = "p_value"; - if test_options == nla.gfx.ProbPlotMethod.STATISTIC - p_value = strcat("statistic_", p_value); - end - if ~obj.is_noncorrelation_input && plot_test_type == "within_network_pair" - p_value = strcat("single_sample_", p_value); - end + %Cohen's D results + obj.(test_method).d = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); end % I don't really know what these do and haven't really thought about it. Hence the bad naming. function [sig, name] = singleSigMat(obj, network_atlas, edge_test_options, p_value, mcc_method, title_prefix) + mcc_method = nla.net.mcc.(mcc_method)(); p_value_max = mcc_method.correct(network_atlas, edge_test_options, p_value); p_breakdown_labels = mcc_method.createLabel(network_atlas, edge_test_options, p_value); sig = nla.TriMatrix(network_atlas.numNets(), 'double', nla.TriMatrixDiag.KEEP_DIAGONAL); sig.v = (p_value.v < p_value_max); name = sprintf("%s %s P < %.2g (%s)", title_prefix, obj.test_display_name, p_value_max, p_breakdown_labels); + if p_value_max == 0 + name = sprintf("%s %s P = 0 (%s)", title_prefix, obj.test_display_name, p_breakdown_labels); + end end function [number_of_tests, sig_count_mat, names] = appendSignificanceMatrix(... @@ -480,5 +327,193 @@ function withinNetworkPairPlotting(obj, network_atlas, edge_test_options, edge_t Number('d_max', "Cohen's D threshold >", 0, 0.5, 1),... }; end + + function probability = getPValueNames(test_method, test_name) + import nla.NetworkLevelMethod + noncorrelation_input_tests = ["chi_squared", "hypergeometric"]; + non_correlation_test = any(strcmp(test_name, noncorrelation_input_tests)); + + probability = "two_sample_p_value"; + if isequal(non_correlation_test, false) + if isequal(test_method, "no_permutations") || isequal(test_method, "within_network_pair") + probability = "single_sample_p_value"; + end + end + end + + function converted_data_struct = loadOldVersionData(result_struct) + import nla.net.result.NetworkTestResult nla.TriMatrix nla.TriMatrixDiag nla.NetworkAtlas + + number_of_results = numel(result_struct.net_results); + test_options = result_struct.input_struct; + + network_test_options = result_struct.net_input_struct; + network_test_options.ranking_method = "Uncorrected"; + network_test_options.no_permutations = network_test_options.nonpermuted; + network_test_options.full_connectome = network_test_options.full_conn; + network_test_options.within_network_pair = network_test_options.within_net_pair; + network_test_options = rmfield(network_test_options, ["nonpermuted", "full_conn", "within_net_pair"]); + + network_atlas = NetworkAtlas(); + fields = fieldnames(result_struct.net_atlas); + for field_index = 1:numel(fields) + network_atlas.(fields{field_index}) = result_struct.net_atlas.(fields{field_index}); + end + number_of_networks = network_atlas.numNets(); + + converted_data_struct = struct(... + 'test_options', test_options,... + 'network_atlas', network_atlas,... + 'network_test_options', network_test_options,... + 'edge_test_results', result_struct.edge_result,... + 'version', result_struct.version,... + 'commit', result_struct.commit,... + 'commit_short', result_struct.commit_short... + ); + + converted_data_struct.permutation_network_test_results = {}; + d = false; + single_sample_d = false; + + for result_number = 1:number_of_results + switch result_struct.perm_net_results{result_number}.name + case "Chi-Squared" + test_name = "chi_squared"; + test_display_name = "Chi-Squared"; + ranking_statistic = "chi2_statistic"; + is_noncorrelation_input = 1; + case "Hypergeometric" + test_name = "hypergeometric"; + test_display_name = "Hypergeometric"; + ranking_statistic = "two_sample_p_value"; + is_noncorrelation_input = 1; + case "Kolmogorov-Smirnov" + test_name = "kolmogorov_smirnov"; + test_display_name = "Kolmogorov-Smirnov"; + ranking_statistic = "ks_statistic"; + is_noncorrelation_input = 0; + case "Student's T" + test_name = "students_t"; + test_display_name = "Student's T-test"; + ranking_statistic = "t_statistic"; + is_noncorrelation_input = 0; + case "Welch's T" + test_name = "welchs_t"; + test_display_name = "Welch's T-test"; + ranking_statistic = "t_statistic"; + is_noncorrelation_input = 0; + case "Wilcoxon" + test_name = "wilcoxon"; + test_display_name = "Wilcoxon Rank Sum"; + ranking_statistic = "z_statistic"; + is_noncorrelation_input = 0; + case "Cohen's D" + d = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + d.v = result_struct.perm_net_results{result_number}.d.v; + single_sample_d = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + single_sample_d.v = result_struct.perm_net_results{result_number}.within_np_d.v; + end + new_results_struct = struct(... + 'test_name', test_name,... + "test_display_name", test_display_name,... + "ranking_statistic", ranking_statistic,... + "is_noncorrelation_input", is_noncorrelation_input,... + "permutation_count", result_struct.perm_net_results{result_number}.perm_count,... + "test_options", test_options... + ); + no_permutation = struct(); + full_connectome = struct(); + within_network_pair = struct(); + if result_struct.perm_net_results{result_number}.has_nonpermuted + no_permutation.uncorrected_single_sample_p_value = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + no_permutation.uncorrected_single_sample_p_value.v = result_struct.perm_net_results{result_number}.prob.v; + end + if result_struct.perm_net_results{result_number}.has_full_conn + if ~isequal(d, false) + full_connectome.d = d; + end + full_connectome.uncorrected_two_sample_p_value = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + full_connectome.uncorrected_two_sample_p_value.v = result_struct.perm_net_results{result_number}.perm_prob.v; + end + if result_struct.perm_net_results{result_number}.has_within_net_pair + if ~isequal(single_sample_d, false) + within_network_pair.single_sample_d = single_sample_d; + end + within_network_pair.uncorrected_single_sample_p_value = TriMatrix(number_of_networks, TriMatrixDiag.KEEP_DIAGONAL); + within_network_pair.uncorrected_single_sample_p_value.v = result_struct.perm_net_results{result_number}.within_np_prob.v; + end + new_results_struct.no_permutation = no_permutation; + new_results_struct.full_connectome = full_connectome; + new_results_struct.within_network_pair = within_network_pair; + converted_data_struct.permutation_network_test_results = [converted_data_struct.permutation_network_test_results new_results_struct]; + end + end + + function [previous_result_data, old_data] = loadPreviousData(file) + import nla.net.result.NetworkTestResult + + try + results_file = load(file); + + % This shouldn't happen, but just in case... + if isa(results_file.results, 'nla.ResultPool') && ~(isfield(results_file.results_as_struct, 'net_atlas') && isfield(results_file.results_as_struct, 'input_struct')) + previous_result_data = results_file.results; + old_data = false; + else + + % Turn off some warnings we know are going to trigger with old results + warning('off', 'MATLAB:class:EnumerationNameMissing'); + warning('off', 'MATLAB:load:classNotFound'); + previous_result_struct = NetworkTestResult().loadOldVersionData(results_file.results_as_struct); + previous_result_struct_edge_class = NetworkTestResult().edgeResultsStructToClasses(previous_result_struct); + previous_result_struct_class = NetworkTestResult().permutationResultsStructToClasses(previous_result_struct_edge_class); + previous_result_data = nla.ResultPool(); + props = properties(previous_result_data); + for prop = 1:numel(props) + if isfield(previous_result_struct_class, props{prop}) + previous_result_data.(props{prop}) = previous_result_struct_class.(props{prop}); + end + end + old_data = true; + end + catch + error("Failure to load results file"); + end + warning('on', 'MATLAB:class:EnumerationNameMissing'); + warning('on', 'MATLAB:load:classNotFound'); + end + + function class_results = permutationResultsStructToClasses(structure_in) + new_network_results = cell(1,numel(structure_in.permutation_network_test_results)); + for result = 1:numel(structure_in.permutation_network_test_results) + network_result = nla.net.result.NetworkTestResult(); + fields = fieldnames(network_result); + for field_index = 1:numel(fields) + if isfield(structure_in.permutation_network_test_results{result}, (fields{field_index})) && ~isequal(fields{field_index}, "test_methods") + network_result.(fields{field_index}) = structure_in.permutation_network_test_results{result}.(fields{field_index}); + end + end + new_network_results{result} = network_result; + end + structure_in.permutation_network_test_results = new_network_results; + class_results = structure_in; + end + + function class_results = edgeResultsStructToClasses(structure_in) + edge_result = nla.edge.result.Base(); + fields = fieldnames(edge_result); + for field_index = 1:numel(fields) + if isfield(structure_in.edge_test_results, fields{field_index}) + if isequal(fields{field_index}, "coeff") || isequal(fields{field_index}, "prob") || isequal(fields{field_index}, "prob_sig") + edge_result.(fields{field_index}) = nla.TriMatrix(structure_in.edge_test_results.coeff.size, nla.TriMatrixDiag.KEEP_DIAGONAL); + edge_result.(fields{field_index}).v = structure_in.edge_test_results.(fields{field_index}).v; + else + edge_result.(fields{field_index}) = structure_in.edge_test_results.(fields{field_index}); + end + end + end + structure_in.edge_test_results = edge_result; + class_results = structure_in; + end end end \ No newline at end of file diff --git a/+nla/+net/+test/ChiSquaredTest.m b/+nla/+net/+test/ChiSquaredTest.m index 36e6c795..dd350d3a 100644 --- a/+nla/+net/+test/ChiSquaredTest.m +++ b/+nla/+net/+test/ChiSquaredTest.m @@ -43,7 +43,8 @@ network_atlas.nets(network2).indexes); network_ROI_count = numel(network_pair_ROI_significance); observed_significance = sum(network_pair_ROI_significance); - expected_significance = edge_test_results.avg_prob_sig * network_ROI_count; +% expected_significance = edge_test_results.avg_prob_sig * network_ROI_count; + expected_significance = (sum(edge_test_results.prob_sig.v)/size(edge_test_results.prob_sig.v,1)) * network_ROI_count; % expected sig should be based off HITS, AS 250210 chi2_value = ((observed_significance - expected_significance) .^ 2) .* ((expected_significance .^ -1)); %legacy style, AS 240529 result.(permutation_results).(chi2_statistic).set(network, network2, chi2_value); result.(permutation_results).(greater_than_expected).set(network, network2, observed_significance > expected_significance); @@ -59,9 +60,9 @@ % Matlab function for chi-squared cdf to get p-value. "Upper" calculates the upper tail instead of % using 1 - lower tail if permutations - result.permutation_results.p_value_permutations.v = chi2cdf(result.permutation_results.chi2_statistic_permutations.v, 1, "upper"); + result.permutation_results.two_sample_p_value_permutations.v = chi2cdf(result.permutation_results.chi2_statistic_permutations.v, 1, "upper"); else - result.no_permutations.p_value.v = chi2cdf(result.no_permutations.chi2_statistic.v, 1, "upper"); + result.no_permutations.uncorrected_two_sample_p_value.v = chi2cdf(result.no_permutations.chi2_statistic.v, 1, "upper"); end end end diff --git a/+nla/+net/+test/HyperGeometricTest.m b/+nla/+net/+test/HyperGeometricTest.m index 5d1e7eb2..e0a43eb4 100644 --- a/+nla/+net/+test/HyperGeometricTest.m +++ b/+nla/+net/+test/HyperGeometricTest.m @@ -3,7 +3,8 @@ properties (Constant) name = "hypergeometric" display_name = "Hypergeometric" - statistics = ["greater_than_expected"] + statistics = ["two_sample_p_value", "greater_than_expected"] + ranking_statistic = "two_sample_p_value" end methods @@ -23,17 +24,17 @@ % Store results in the 'no_permutations' structure if this is the no-permutation test permutation_results = "no_permutations"; greater_than_expected = "greater_than_expected"; - p_value = "p_value"; + p_value = "two_sample_p_value"; if isequal(permutations, true) % Otherwise, add it on to the back of the 'permutation_results' structure permutation_results = "permutation_results"; greater_than_expected = strcat(greater_than_expected, "_permutations"); - p_value = strcat(p_value, "_permutations"); + p_value = "two_sample_p_value_permutations"; end % Container to hold results % Pass a blank string as ranking statistic since Hypergeometric doesn't have one and we'll be skipping it - result = nla.net.result.NetworkTestResult(test_options, number_of_networks, obj.name, obj.display_name, obj.statistics, ""); + result = nla.net.result.NetworkTestResult(test_options, number_of_networks, obj.name, obj.display_name, obj.statistics, obj.ranking_statistic); % Double for-loop to iterate through trimatrix. Network is the row, network2 the column. Since % we only care about the bottom half, second for-loop is 1:network @@ -43,7 +44,8 @@ network_atlas.nets(network2).indexes); network_ROI_count = numel(network_pair_ROI_significance); observed_significance = sum(network_pair_ROI_significance); - expected_significance = edge_test_results.avg_prob_sig * network_ROI_count; +% expected_significance = edge_test_results.avg_prob_sig * network_ROI_count; + expected_significance = (sum(edge_test_results.prob_sig.v)/size(edge_test_results.prob_sig.v,1)) * network_ROI_count; % expected sig should be based off HITS, AS 250210 result.(permutation_results).(greater_than_expected).set(network, network2, observed_significance > expected_significance) % Matlab function for hypergeometric cdf to get p-value. "Upper" calculates the upper tail instead of % using 1 - lower tail @@ -56,9 +58,10 @@ % This just results in a p-value of 1. Which means no difference between chance and null % hypothesis. if permutations - result.permutation_results.p_value_permutations.v(~result.permutation_results.greater_than_expected_permutations.v) = 1; + result.permutation_results.two_sample_p_value_permutations.v(~result.permutation_results.greater_than_expected_permutations.v) = 1; else - result.no_permutations.p_value.v(~result.no_permutations.greater_than_expected.v) = 1; + result.no_permutations.uncorrected_two_sample_p_value = result.no_permutations.two_sample_p_value; + result.no_permutations.uncorrected_two_sample_p_value.v(~result.no_permutations.greater_than_expected.v) = 1; end end end diff --git a/+nla/+net/+test/KolmogorovSmirnovTest.m b/+nla/+net/+test/KolmogorovSmirnovTest.m index c9edf125..1207f222 100644 --- a/+nla/+net/+test/KolmogorovSmirnovTest.m +++ b/+nla/+net/+test/KolmogorovSmirnovTest.m @@ -23,16 +23,16 @@ % Store results in the 'no_permutations' structure if this is the no-permutation test permutation_results = "no_permutations"; - p_value = "p_value"; ks_statistic = "ks_statistic"; - single_sample_p_value = "single_sample_p_value"; + p_value = "uncorrected_two_sample_p_value"; + single_sample_p_value = "uncorrected_single_sample_p_value"; single_sample_ks_statistic = "single_sample_ks_statistic"; if isequal(permutations, true) % Otherwise, add it on to the back of the 'permutation_results' structure permutation_results = "permutation_results"; - p_value = strcat(p_value, "_permutations"); + p_value = "two_sample_p_value_permutations"; ks_statistic = strcat(ks_statistic, "_permutations"); - single_sample_p_value = strcat(single_sample_p_value, "_permutations"); + single_sample_p_value = "single_sample_p_value_permutations"; single_sample_ks_statistic = strcat(single_sample_ks_statistic, "_permutations"); end @@ -44,11 +44,10 @@ for network2 = 1:network network_rho = edge_test_results.coeff.get(network_atlas.nets(network).indexes,... network_atlas.nets(network2).indexes); - [~, p, ks] = kstest2(network_rho, edge_test_results.coeff.v); result.(permutation_results).(p_value).set(network, network2, p); result.(permutation_results).(ks_statistic).set(network, network2, ks); - + [~, single_sample_p, single_sample_ks] = kstest(network_rho); result.(permutation_results).(single_sample_p_value).set(network, network2, single_sample_p); result.(permutation_results).(single_sample_ks_statistic).set(network, network2, single_sample_ks); diff --git a/+nla/+net/+test/StudentTTest.m b/+nla/+net/+test/StudentTTest.m index 730a16ae..d55d2209 100644 --- a/+nla/+net/+test/StudentTTest.m +++ b/+nla/+net/+test/StudentTTest.m @@ -23,16 +23,16 @@ % Store results in the 'no_permutations' structure if this is the no-permutation test permutation_results = "no_permutations"; - p_value = "p_value"; + p_value = "uncorrected_two_sample_p_value"; t_statistic = "t_statistic"; - single_sample_p_value = "single_sample_p_value"; + single_sample_p_value = "uncorrected_single_sample_p_value"; single_sample_t_statistic = "single_sample_t_statistic"; if isequal(permutations, true) % Otherwise, add it on to the back of the 'permutation_results' structure permutation_results = "permutation_results"; - p_value = strcat(p_value, "_permutations"); + p_value = "two_sample_p_value_permutations"; t_statistic = strcat(t_statistic, "_permutations"); - single_sample_p_value = strcat(single_sample_p_value, "_permutations"); + single_sample_p_value = "single_sample_p_value_permutations"; single_sample_t_statistic = strcat(single_sample_t_statistic, "_permutations"); end @@ -47,9 +47,9 @@ [~, p, ~, stats] = ttest2(network_rho, edge_test_results.coeff.v); [~, single_sample_p, ~, single_sample_stats] = ttest(network_rho); - result.(permutation_results).(p_value).set(network, network2, p); result.(permutation_results).(t_statistic).set(network, network2, stats.tstat); + result.(permutation_results).(single_sample_p_value).set(network, network2, single_sample_p); result.(permutation_results).(single_sample_t_statistic).set(network, network2, single_sample_stats.tstat); end diff --git a/+nla/+net/+test/WelchTTest.m b/+nla/+net/+test/WelchTTest.m index 7f464a90..33133d44 100644 --- a/+nla/+net/+test/WelchTTest.m +++ b/+nla/+net/+test/WelchTTest.m @@ -25,16 +25,16 @@ % Store results in the 'no_permutations' structure if this is the no-permutation test permutation_results = "no_permutations"; - p_value = "p_value"; t_statistic = "t_statistic"; - single_sample_p_value = "single_sample_p_value"; + p_value = "uncorrected_two_sample_p_value"; + single_sample_p_value = "uncorrected_single_sample_p_value"; single_sample_t_statistic = "single_sample_t_statistic"; if isequal(permutations, true) % Otherwise, add it on to the back of the 'permutation_results' structure permutation_results = "permutation_results"; - p_value = strcat(p_value, "_permutations"); + p_value = "two_sample_p_value_permutations"; t_statistic = strcat(t_statistic, "_permutations"); - single_sample_p_value = strcat(single_sample_p_value, "_permutations"); + single_sample_p_value = "single_sample_p_value_permutations"; single_sample_t_statistic = strcat(single_sample_t_statistic, "_permutations"); end @@ -50,8 +50,10 @@ [p, t_stat, ~] = nla.welchT(network_rho, edge_test_results.coeff.v); [~, single_sample_p, ~, single_sample_stats] = ttest(network_rho); + result.(permutation_results).(p_value).set(network, network2, p); result.(permutation_results).(t_statistic).set(network, network2, t_stat); + result.(permutation_results).(single_sample_p_value).set(network, network2, single_sample_p); result.(permutation_results).(single_sample_t_statistic).set(network, network2, single_sample_stats.tstat); end diff --git a/+nla/+net/+test/WilcoxonTest.m b/+nla/+net/+test/WilcoxonTest.m index e8b33023..de490b87 100644 --- a/+nla/+net/+test/WilcoxonTest.m +++ b/+nla/+net/+test/WilcoxonTest.m @@ -23,18 +23,19 @@ % Store results in the 'no_permutations' structure if this is the no-permutation test permutation_results = "no_permutations"; - p_value = "p_value"; ranksum_statistic = "ranksum_statistic"; + p_value = "uncorrected_two_sample_p_value"; z_statistic = "z_statistic"; - single_sample_p_value = "single_sample_p_value"; + p_value = "uncorrected_two_sample_p_value"; + single_sample_p_value = "uncorrected_single_sample_p_value"; single_sample_ranksum_statistic = "single_sample_ranksum_statistic"; if isequal(permutations, true) % Otherwise, add it on to the back of the 'permutation_results' structure permutation_results = "permutation_results"; - p_value = strcat(p_value, "_permutations"); + p_value = "two_sample_p_value_permutations"; ranksum_statistic = strcat(ranksum_statistic, "_permutations"); z_statistic = strcat(z_statistic, "_permutations"); - single_sample_p_value = strcat(single_sample_p_value, "_permutations"); + single_sample_p_value = "single_sample_p_value_permutations"; single_sample_ranksum_statistic = strcat(single_sample_ranksum_statistic, "_permutations"); end @@ -51,7 +52,7 @@ result.(permutation_results).(p_value).set(network, network2, p); result.(permutation_results).(ranksum_statistic).set(network, network2, stats.ranksum); result.(permutation_results).(z_statistic).set(network, network2, stats.zval); - + [single_sample_p, ~, single_sample_stats] = signrank(network_rho); result.(permutation_results).(single_sample_p_value).set(network, network2, single_sample_p); result.(permutation_results).(single_sample_ranksum_statistic).set(network, network2, single_sample_stats.signedrank); diff --git a/+nla/+net/CohenDTest.m b/+nla/+net/CohenDTest.m index 18012f0b..45dfc175 100644 --- a/+nla/+net/CohenDTest.m +++ b/+nla/+net/CohenDTest.m @@ -15,29 +15,40 @@ end function result_object = run(obj, edge_test_results, network_atlas, result_object) - import nla.TriMatrix nla.TriMatrixDiag - - number_of_networks = network_atlas.numNets(); - - for row = 1:number_of_networks - for column = 1:row - - network_rho = edge_test_results.coeff.get(network_atlas.nets(row).indexes,... - network_atlas.nets(column).indexes); - - single_sample_d = abs(mean(network_rho)) / std(network_rho); - d = abs((mean(network_rho) - mean(edge_test_results.coeff.v)) / sqrt(((std(network_rho).^2)) +... - (std(edge_test_results.coeff.v).^2))); - - result_object.no_permutations.d.set(row, column, single_sample_d); - if isprop(result_object, "full_connectome") && ~isequal(result_object.full_connectome, false) - result_object.full_connectome.d.set(row, column, d); - end - if isprop(result_object, "within_network_pair") && ~isequal(result_object.within_network_pair, false) - result_object.within_network_pair.d.set(row, column, single_sample_d); - end - end - end + + + %DETERMINED THAT CURRENT COHEN'S D CALC IS INVALID + %RETURN WITHOUT MODIFYING EXISTING D PLACEHOLDER VALUE FROM 0 + %RECOMMEND THAT, WHEN REENABLED, THIS FN BE CHANGED TO ACCEPT + %VECTOR OF INPUT AND RETURN VECTOR OF OUTPUT RATHER THAN + %RESULTS CLASS + %ADE 2025MAR24 + return; + + + + + %LEAVING COMMENTED CODE BELOW THAT WOULD MODIFY FIELDS AS DONE + %PREVIOUSLY FOR REFERENCE. +% number_of_networks = network_atlas.numNets(); +% +% for row = 1:number_of_networks +% for column = 1:row +% +% if isprop(result_object, "no_permutations") && ~isequal(result_object.no_permutations, false) +% %this_netpair_nonperm_d = COMPUTE NONPERMUTED D HERE; +% result_object.no_permutations.d.set(row, column, this_netpair_nonperm_d); +% end +% if isprop(result_object, "full_connectome") && ~isequal(result_object.full_connectome, false) +% %this_netpair_fullconn_d = COMPUTE FULLCONN D HERE; +% result_object.full_connectome.d.set(row, column, this_netpair_fullconn_d); +% end +% if isprop(result_object, "within_network_pair") && ~isequal(result_object.within_network_pair, false) +% %this_netpair_withinNP_d = COMPUTE WITHIN NET PAIR D HERE +% result_object.within_network_pair.d.set(row, column, this_netpair_withinNP_d); +% end +% end +% end end end end \ No newline at end of file diff --git a/+nla/+net/ResultRank.m b/+nla/+net/ResultRank.m index fa15f67f..8df963e7 100644 --- a/+nla/+net/ResultRank.m +++ b/+nla/+net/ResultRank.m @@ -25,84 +25,87 @@ end function ranking_result = rank(obj) - import nla.TriMatrix nla.TriMatrixDiag + import nla.TriMatrix nla.TriMatrixDiag nla.net.result.NetworkTestResult ranking_result = obj.permuted_network_results.copy(); - for test_type = obj.permuted_network_results.test_methods - if ~isequal(test_type, "no_permutations") && ~isequal(obj.permuted_network_results.test_display_name, "Cohen's D") + for test_method = obj.permuted_network_results.test_methods + if ~isequal(test_method, "no_permutations") && ~isequal(obj.permuted_network_results.test_display_name, "Cohen's D") - [ranking_statistic, probability, denominator] = obj.getTestParameters(test_type); + ranking_statistic = obj.getTestParameters(test_method); + probability = NetworkTestResult().getPValueNames(test_method, obj.permuted_network_results.test_name); permutation_results = obj.permuted_network_results.permutation_results; no_permutation_results = obj.nonpermuted_network_results; - % Eggebrecht ranking - ranking_result = obj.eggebrechtRank(test_type, permutation_results, no_permutation_results, ranking_statistic,... - probability, denominator, ranking_result); - - if ~isequal(obj.permuted_network_results.test_name, "hypergeometric") - % Winkler Method ranking - ranking_result.(test_type).winkler_p_value = TriMatrix(... - obj.number_of_networks, TriMatrixDiag.KEEP_DIAGONAL... - ); - ranking_result = obj.winklerMethodRank(test_type, permutation_results, no_permutation_results, ranking_statistic,... - probability, denominator, ranking_result); - - % Westfall Young ranking - ranking_result.(test_type).westfall_young_p_value = TriMatrix(... - obj.number_of_networks, TriMatrixDiag.KEEP_DIAGONAL... - ); - ranking_result = obj.westfallYoungMethodRank(test_type, permutation_results, no_permutation_results, ranking_statistic,... - probability, denominator, ranking_result); - end + % Uncorrected ranking + ranking_result = obj.uncorrectedRank(test_method, permutation_results, no_permutation_results, ranking_statistic,... + probability, ranking_result); + + % Winkler Method ranking + ranking_result = obj.winklerMethodRank(test_method, permutation_results, no_permutation_results, ranking_statistic,... + probability, ranking_result); + + % Westfall Young ranking + ranking_result = obj.westfallYoungMethodRank(test_method, permutation_results, no_permutation_results, ranking_statistic,... + probability, ranking_result); end end end - function ranking = eggebrechtRank(obj, test_type, permutation_results, no_permutation_results, ranking_statistic,... - probability, denominator, ranking) - - for index = 1:numel(no_permutation_results.(probability).v) - if isequal(test_type, "full_connectome") - combined_probabilities = [... - permutation_results.(strcat((probability), "_permutations")).v(:);... - no_permutation_results.(probability).v(index)... - ]; + function ranking = uncorrectedRank(obj, test_method, permutation_results, no_permutation_results, ranking_statistic,... + probability, ranking) + + for index = 1:numel(no_permutation_results.(strcat("uncorrected_", probability)).v) + combined_probabilities = [... + permutation_results.(strcat((probability), "_permutations")).v(index, :),... + no_permutation_results.(strcat("uncorrected_", probability)).v(index)... + ]; + combined_statistics = [... + permutation_results.(strcat((ranking_statistic), "_permutations")).v(index, :), no_permutation_results.(ranking_statistic).v(index)... + ]; + + legacy_probability = strcat("legacy_", probability); + uncorrected_probability = strcat("uncorrected_", probability); + + % Legacy probability is from the old code and uses the calculated p-values from each individual test + ranking.(test_method).(legacy_probability).v(index) = sum(... + abs(squeeze(combined_probabilities)) <= abs(no_permutation_results.(strcat("uncorrected_", probability)).v(index))... + ) / (1 + obj.permutations); + % Uncorrected probability is the p-value calculated from the individual test statistics (i.e. chi2, t-statistic, etc) + if isequal(ranking.test_name, "hypergeometric") + ranking.(test_method).(uncorrected_probability).v(index) = sum(... + abs(squeeze(combined_statistics)) <= abs(no_permutation_results.(ranking_statistic).v(index))... + ) / (1 + obj.permutations); else - combined_probabilities = [... - permutation_results.(strcat((probability), "_permutations")).v(index, :),... - no_permutation_results.(probability).v(index)... - ]; - end - [~, sorted_combined_probabilites] = sort(combined_probabilities); - ranking.(test_type).(probability).v(index) = find(... - squeeze(sorted_combined_probabilites) == 1 + denominator... - ) / (1 + denominator); - - if ~isequal(obj.permuted_network_results.test_name, "hypergeometric") - combined_statistics = [permutation_results.(strcat((ranking_statistic), "_permutations")).v(:); no_permutation_results.(ranking_statistic).v(index)]; - [~, sorted_combined_statistics] = sort(combined_statistics); - ranking.(test_type).(strcat("statistic_", (probability))).v(index) = find(... - squeeze(sorted_combined_statistics) == 1 + denominator... - ) / (1 + denominator); + ranking.(test_method).(uncorrected_probability).v(index) = sum(... + abs(squeeze(combined_statistics)) >= abs(no_permutation_results.(ranking_statistic).v(index))... + ) / (1 + obj.permutations); end end end - function ranking = winklerMethodRank(obj, test_type, permutation_results, no_permutation_results, ranking_statistic,... - probability, denominator, ranking) + function ranking = winklerMethodRank(obj, test_method, permutation_results, no_permutation_results, ranking_statistic,... + probability, ranking) + winkler_probability = strcat("winkler_", probability); max_statistic_array = max(abs(permutation_results.(strcat(ranking_statistic, "_permutations")).v)); - for index = 1:numel(no_permutation_results.(probability).v) - ranking.(test_type).winkler_p_value.v(index) = sum(... - squeeze(max_statistic_array) >= abs(no_permutation_results.(ranking_statistic).v(index))... - ); + for index = 1:numel(no_permutation_results.(strcat("uncorrected_", probability)).v) + if isequal(ranking.test_name, "hypergeometric") + ranking.(test_method).(winkler_probability).v(index) = sum(... + squeeze(max_statistic_array) <= abs(no_permutation_results.(ranking_statistic).v(index))... + ); + else + ranking.(test_method).(winkler_probability).v(index) = sum(... + squeeze(max_statistic_array) >= abs(no_permutation_results.(ranking_statistic).v(index))... + ); + end end - ranking.(test_type).winkler_p_value.v = ranking.(test_type).winkler_p_value.v ./ obj.permutations; + + ranking.(test_method).(winkler_probability).v = ranking.(test_method).(winkler_probability).v ./ obj.permutations; end - function ranking = westfallYoungMethodRank(obj, test_type, permutation_results, no_permutation_results, ranking_statistic,... - probability, denominator, ranking) + function ranking = westfallYoungMethodRank(obj, test_method, permutation_results, no_permutation_results, ranking_statistic,... + probability, ranking) % sort statistics in ascending order [sorted_no_permutation_results, sorted_statistic_indexes] = sort(... @@ -112,6 +115,8 @@ permutation_results.(strcat((ranking_statistic), "_permutations")).v(sorted_statistic_indexes, :)... ); + westfall_young_probability = strcat("westfall_young_", probability); + % Get max value of each permutation starting from max value of non-permuted statistics % Remove each row of permutations associated with non-permuted statistic % Get max of remaining. The last row of permutations should be with the smallest non-permuted statistic @@ -123,29 +128,26 @@ max_per_permutation_reducing_rows(row_index, :) = max(permutations_sorted_by_non_permuted(1:row_index, :)); end max_per_permutation_reducing_rows(1, :) = permutations_sorted_by_non_permuted(1, :); - - ranking.(test_type).westfall_young_p_value.v = mean(... - sorted_no_permutation_results < max_per_permutation_reducing_rows, 2); - ranking.(test_type).westfall_young_p_value.v(sorted_statistic_indexes) =... - ranking.(test_type).westfall_young_p_value.v; + + if isequal(ranking.test_name, "hypergeometric") + ranking.(test_method).(westfall_young_probability).v = mean(sorted_no_permutation_results > max_per_permutation_reducing_rows, 2); + else + ranking.(test_method).(westfall_young_probability).v = mean(sorted_no_permutation_results < max_per_permutation_reducing_rows, 2); + end + + ranking.(test_method).(westfall_young_probability).v(sorted_statistic_indexes) = ranking.(test_method).(westfall_young_probability).v; end - function [ranking_statistic, probability, denominator] = getTestParameters(obj, test_type) + function ranking_statistic = getTestParameters(obj, test_method) ranking_statistic = obj.permuted_network_results.ranking_statistic; - probability = "p_value"; - denominator = obj.permutations * obj.number_of_network_pairs; % Only use these for within network pair and not Chi-Squared and Hypergeometric. - if isequal(test_type, "within_network_pair") - denominator = obj.permutations; - if ~any(... - strcmp(obj.permuted_network_results.test_name, obj.permuted_network_results.noncorrelation_input_tests)... - ) - ranking_statistic = strcat("single_sample_", ranking_statistic); - if isequal(obj.permuted_network_results.test_name, "wilcoxon") - ranking_statistic = "single_sample_ranksum_statistic"; - end - probability = strcat("single_sample_", probability); + if isequal(test_method, "within_network_pair") && ~any(... + ismember(obj.permuted_network_results.test_name, obj.permuted_network_results.noncorrelation_input_tests)... + ) + ranking_statistic = strcat("single_sample_", ranking_statistic); + if isequal(obj.permuted_network_results.test_name, "wilcoxon") + ranking_statistic = "single_sample_ranksum_statistic"; end end end @@ -153,7 +155,7 @@ %% Getters for dependent properties % This takes the above statistic and gets the property to use its size to find the number of permutations function value = get.permutations(obj) - value = size(obj.permuted_network_results.permutation_results.p_value_permutations.v, 2); + value = size(obj.permuted_network_results.permutation_results.two_sample_p_value_permutations.v, 2); end function value = get.number_of_networks(obj) diff --git a/+nla/+net/genBaseInputs.m b/+nla/+net/genBaseInputs.m index dcf594fc..9a9f570d 100644 --- a/+nla/+net/genBaseInputs.m +++ b/+nla/+net/genBaseInputs.m @@ -6,7 +6,7 @@ 'full_connectome', true,... 'within_network_pair', true,... 'prob_plot_method',nla.gfx.ProbPlotMethod.DEFAULT,... - 'ranking_method', nla.RankingMethod.P_VALUE,... + 'ranking_method', "Uncorrected",... 'edge_chord_plot_method', nla.gfx.EdgeChordPlotMethod.PROB,... 'fdr_correction', nla.net.mcc.Bonferroni(),... 'd_thresh_chord_plot', true... diff --git a/+nla/+net/unittests/NetworkResultPlotParameterTestCase.m b/+nla/+net/unittests/NetworkResultPlotParameterTestCase.m index 8f0299aa..2842a68f 100644 --- a/+nla/+net/unittests/NetworkResultPlotParameterTestCase.m +++ b/+nla/+net/unittests/NetworkResultPlotParameterTestCase.m @@ -82,21 +82,23 @@ function clearData(testCase) methods (Test) function plotProbabilityParametersDefaultPlottingTest(testCase) - import nla.net.result.NetworkResultPlotParameter nla.TriMatrix nla.TriMatrixDiag + import nla.net.result.NetworkResultPlotParameter nla.TriMatrix nla.TriMatrixDiag nla.net.result.NetworkTestResult permutation_result = testCase.permutation_results.permutation_network_test_results{1}; plot_parameters = NetworkResultPlotParameter(permutation_result, testCase.network_atlas,... testCase.network_test_options); + probability = NetworkTestResult().getPValueNames("full_connectome", permutation_result.test_name); + probability_parameters = plot_parameters.plotProbabilityParameters(testCase.edge_test_options,... - testCase.edge_test_result, 'full_connectome', 'p_value', 'Title', nla.net.mcc.Bonferroni(),... - false); + testCase.edge_test_result, "full_connectome", probability, 'Title', nla.net.mcc.Bonferroni(),... + false, nla.RankingMethod.UNCORRECTED); expected_p_value_max = testCase.network_test_options.prob_max / testCase.network_atlas.numNetPairs(); expected_plot = nla.TriMatrix(plot_parameters.number_of_networks, "double", nla.TriMatrixDiag.KEEP_DIAGONAL); - expected_plot.v = permutation_result.full_connectome.p_value.v .*... + expected_plot.v = permutation_result.full_connectome.(strcat("uncorrected_", probability)).v .*... (plot_parameters.default_discrete_colors / (plot_parameters.default_discrete_colors + 1)); - expected_significance_type = nla.gfx.SigType.DECREASING; + expected_significance_type = "nla.gfx.SigType.DECREASING"; % Supposedly this evaluates as a string without the quotes, but NOPE expected_color_map = [flip(parula(plot_parameters.default_discrete_colors)); [1 1 1]]; testCase.verifyEqual(expected_p_value_max, probability_parameters.p_value_plot_max); @@ -106,7 +108,7 @@ function plotProbabilityParametersDefaultPlottingTest(testCase) end function plotProbabilityParametersLogPlottingTest(testCase) - import nla.net.result.NetworkResultPlotParameter + import nla.net.result.NetworkResultPlotParameter nla.net.result.NetworkTestResult permutation_result = testCase.permutation_results.permutation_network_test_results{1}; testCase.network_test_options.prob_plot_method = nla.gfx.ProbPlotMethod.LOG; @@ -114,46 +116,54 @@ function plotProbabilityParametersLogPlottingTest(testCase) plot_parameters = NetworkResultPlotParameter(permutation_result, testCase.network_atlas,... testCase.network_test_options); + probability = NetworkTestResult().getPValueNames("full_connectome", permutation_result.test_name); + probability_parameters = plot_parameters.plotProbabilityParameters(testCase.edge_test_options,... - testCase.edge_test_result, 'full_connectome', 'p_value', 'Title', nla.net.mcc.Bonferroni(),... - false); + testCase.edge_test_result, "full_connectome", probability, 'Title', nla.net.mcc.Bonferroni(),... + false, nla.RankingMethod.UNCORRECTED); expected_p_value_max = testCase.network_test_options.prob_max / testCase.network_atlas.numNetPairs(); expected_plot = nla.TriMatrix(plot_parameters.number_of_networks, "double", nla.TriMatrixDiag.KEEP_DIAGONAL); - expected_plot.v = permutation_result.full_connectome.p_value.v .*... + expected_plot.v = permutation_result.full_connectome.(strcat("uncorrected_", probability)).v .*... (plot_parameters.default_discrete_colors / (plot_parameters.default_discrete_colors + 1)); - expected_significance_type = nla.gfx.SigType.DECREASING; + expected_significance_type = "nla.gfx.SigType.DECREASING"; - expected_log_minimum = log10(min(nonzeros(permutation_result.full_connectome.p_value.v))); + expected_log_minimum = log10(min(nonzeros(permutation_result.full_connectome.(strcat("uncorrected_", probability)).v))); expected_log_minimum = max([-40, expected_log_minimum]); color_map_base = parula(plot_parameters.default_discrete_colors); expected_color_map = flip(color_map_base(ceil(logspace(expected_log_minimum, 0, plot_parameters.default_discrete_colors) .*... plot_parameters.default_discrete_colors), :)); - expected_color_map = [expected_color_map; [1 1 1]]; + if expected_p_value_max ~= 0 + expected_color_map = [expected_color_map; [1 1 1]]; + else + expected_color_map = [1 1 1]; + end testCase.verifyEqual(expected_p_value_max, probability_parameters.p_value_plot_max); testCase.verifyEqual(expected_plot.v, probability_parameters.statistic_plot_matrix.v); testCase.verifyEqual(expected_significance_type, probability_parameters.significance_type); - testCase.verifyEqual(expected_color_map, probability_parameters.color_map); + testCase.verifyEqual(expected_color_map, probability_parameters.color_map) end function plotProbabilityParametersNegLogTest(testCase) - import nla.net.result.NetworkResultPlotParameter + import nla.net.result.NetworkResultPlotParameter nla.net.result.NetworkTestResult permutation_result = testCase.permutation_results.permutation_network_test_results{1}; - testCase.network_test_options.prob_plot_method = nla.gfx.ProbPlotMethod.NEG_LOG_10; + testCase.network_test_options.prob_plot_method = nla.gfx.ProbPlotMethod.NEGATIVE_LOG_10; plot_parameters = NetworkResultPlotParameter(permutation_result, testCase.network_atlas,... testCase.network_test_options); + probability = NetworkTestResult().getPValueNames("full_connectome", permutation_result.test_name); + probability_parameters = plot_parameters.plotProbabilityParameters(testCase.edge_test_options,... - testCase.edge_test_result, 'full_connectome', 'p_value', 'Title', nla.net.mcc.Bonferroni(),... - false); + testCase.edge_test_result, "full_connectome", probability, 'Title', nla.net.mcc.Bonferroni(),... + false, nla.RankingMethod.UNCORRECTED); expected_p_value_max = 2; expected_plot = nla.TriMatrix(plot_parameters.number_of_networks, "double", nla.TriMatrixDiag.KEEP_DIAGONAL); - expected_plot.v = -log10(permutation_result.full_connectome.p_value.v); - expected_significance_type = nla.gfx.SigType.INCREASING; + expected_plot.v = -log10(permutation_result.full_connectome.(strcat("uncorrected_", probability)).v); + expected_significance_type = "nla.gfx.SigType.INCREASING"; expected_color_map = parula(plot_parameters.default_discrete_colors); testCase.verifyEqual(expected_p_value_max, probability_parameters.p_value_plot_max); diff --git a/+nla/+net/unittests/NetworkTestResultTestCase.m b/+nla/+net/unittests/NetworkTestResultTestCase.m index 2e2d4fb9..c04d9c87 100644 --- a/+nla/+net/unittests/NetworkTestResultTestCase.m +++ b/+nla/+net/unittests/NetworkTestResultTestCase.m @@ -44,7 +44,7 @@ function NetworkTestResultNoPermutationsTest(testCase) results = NetworkTestResult(testCase.test_options, testCase.number_of_networks, testCase.test.name,... testCase.test.display_name, testCase.test.statistics, testCase.test.ranking_statistic); % The size of TriMatrices are cast to uint32. Is there a good reason for this? - testCase.verifyEqual(results.no_permutations.p_value.size, uint32(testCase.number_of_networks)); + testCase.verifyEqual(results.no_permutations.uncorrected_single_sample_p_value.size, uint32(testCase.number_of_networks)); end function NetworkTestResultWithinNetworkPairTest(testCase) @@ -53,7 +53,7 @@ function NetworkTestResultWithinNetworkPairTest(testCase) results = NetworkTestResult(testCase.test_options, testCase.number_of_networks, testCase.test.name,... testCase.test.display_name, testCase.test.statistics, testCase.test.ranking_statistic); % The size of TriMatrices are cast to uint32. Is there a good reason for this? - testCase.verifyEqual(results.within_network_pair.p_value.size, uint32(testCase.number_of_networks)); + testCase.verifyEqual(results.within_network_pair.uncorrected_single_sample_p_value.size, uint32(testCase.number_of_networks)); end function NetworkTestResultFullConnectomeTest(testCase) @@ -62,7 +62,7 @@ function NetworkTestResultFullConnectomeTest(testCase) results = NetworkTestResult(testCase.test_options, testCase.number_of_networks, testCase.test.name,... testCase.test.display_name, testCase.test.statistics, testCase.test.ranking_statistic); % The size of TriMatrices are cast to uint32. Is there a good reason for this? - testCase.verifyEqual(results.full_connectome.p_value.size, uint32(testCase.number_of_networks)); + testCase.verifyEqual(results.full_connectome.uncorrected_two_sample_p_value.size, uint32(testCase.number_of_networks)); end function NetworkTestResultPermutationResultsTest(testCase) @@ -71,7 +71,7 @@ function NetworkTestResultPermutationResultsTest(testCase) results = NetworkTestResult(testCase.test_options, testCase.number_of_networks, testCase.test.name,... testCase.test.display_name, testCase.test.statistics, testCase.test.ranking_statistic); % The size of TriMatrices are cast to uint32. Is there a good reason for this? - testCase.verifyEqual(results.permutation_results.p_value_permutations.size, uint32(testCase.number_of_networks)); + testCase.verifyEqual(results.permutation_results.single_sample_p_value_permutations.size, uint32(testCase.number_of_networks)); end function NetworkTestResultPermutationsTest(testCase) @@ -82,7 +82,7 @@ function NetworkTestResultPermutationsTest(testCase) testCase.test_data.v(:, 2) = testCase.test_data.v; testCase.test_data.v(:, 3) = testCase.test_data.v(:, 1); - results.permutation_results.p_value_permutations.v = testCase.test_data.v; + results.permutation_results.two_sample_p_value_permutations.v = testCase.test_data.v; testCase.verifyEqual(results.permutation_count, size(testCase.test_data.v, 2)); end diff --git a/+nla/+net/unittests/NetworkTestsTestCase.m b/+nla/+net/unittests/NetworkTestsTestCase.m index 4230e4cb..8a05dbfc 100644 --- a/+nla/+net/unittests/NetworkTestsTestCase.m +++ b/+nla/+net/unittests/NetworkTestsTestCase.m @@ -78,7 +78,7 @@ function hyperGeometricTestTest(testCase) hypergeometric_test = HyperGeometricTest(); load(strcat(testCase.root_path, fullfile('+nla', '+net', 'unittests', 'HyperGeometricTestResult.mat')), 'hyper_geo_result'); test_result = hypergeometric_test.run(testCase.network_test_options, testCase.edge_test_result, testCase.network_atlas, false); - testCase.verifyEqual(test_result.no_permutations.p_value.v, hyper_geo_result); + testCase.verifyEqual(test_result.no_permutations.uncorrected_two_sample_p_value.v, hyper_geo_result); end function kolmogorovSmirnovTestTest(testCase) diff --git a/+nla/+net/unittests/ResultRankTestCase.m b/+nla/+net/unittests/ResultRankTestCase.m index 5d7f758b..0f65c4dd 100644 --- a/+nla/+net/unittests/ResultRankTestCase.m +++ b/+nla/+net/unittests/ResultRankTestCase.m @@ -8,7 +8,9 @@ network_test_result permutation_results tests - ranking + rank_results + ResultRank + rank permuted_edge_results permuted_network_results end @@ -16,7 +18,7 @@ properties (Constant) number_of_networks = 15 number_of_network_pairs = 120 - permutations = 9 + permutations = 25 end methods (TestClassSetup) @@ -60,8 +62,8 @@ function loadInputData(testCase) testCase.edge_test_options.precalc_perm_coeff.v = permutation_coefficient_file.SIM_perm_coeff; % For unit tests, we're only going to use 10 permutations so they don't take forever - testCase.edge_test_options.precalc_perm_p.v = testCase.edge_test_options.precalc_perm_p.v(:, 1:9); - testCase.edge_test_options.precalc_perm_coeff.v = testCase.edge_test_options.precalc_perm_coeff.v(:, 1:9); + testCase.edge_test_options.precalc_perm_p.v = testCase.edge_test_options.precalc_perm_p.v(:, 1:25); + testCase.edge_test_options.precalc_perm_coeff.v = testCase.edge_test_options.precalc_perm_coeff.v(:, 1:25); testCase.edge_test_options.net_atlas = testCase.network_atlas; testCase.edge_test_options.prob_max = 0.05; @@ -91,25 +93,65 @@ function loadInputData(testCase) testCase.permuted_network_results{1}); testCase.permuted_network_results{1}.no_permutations = testCase.network_test_result{1}.no_permutations; - testCase.ranking = load(strcat(testCase.root_path, fullfile('+nla', '+net', 'unittests', 'resultRank_results.mat'))); - testCase.ranking = testCase.ranking.ranking; + rank_results = load(strcat(testCase.root_path, fullfile('+nla', '+net', 'unittests', 'resultRank_results.mat'))); + testCase.rank_results = rank_results.rank_results; + testCase.ResultRank = nla.net.ResultRank(testCase.permuted_network_results{1}, testCase.number_of_network_pairs); + testCase.rank = testCase.ResultRank.rank(); end end methods (Test) - function fullConnectomeRankTest(testCase) - result_ranker = nla.net.ResultRank(testCase.permuted_network_results{1}, testCase.number_of_network_pairs); - rank_object = result_ranker.rank(); + function fullConnectomeUncorrectedRankingTest(testCase) + legacy_results = testCase.rank.full_connectome.legacy_two_sample_p_value.v; + expected_legacy = testCase.rank_results.full_connectome.legacy_two_sample_p_value.v; - testCase.verifyEqual(rank_object.full_connectome.p_value.v, testCase.ranking.full_connectome.p_value.v); + testCase.verifyEqual(legacy_results, expected_legacy); + + uncorrected_results = testCase.rank.full_connectome.uncorrected_two_sample_p_value.v; + expected_uncorrected = testCase.rank_results.full_connectome.uncorrected_two_sample_p_value.v; + + testCase.verifyEqual(uncorrected_results, expected_uncorrected); end - function withinNetworkPairTest(testCase) - result_ranker = nla.net.ResultRank(testCase.permuted_network_results{1}, testCase.number_of_network_pairs); - rank_object = result_ranker.rank(); - - testCase.verifyEqual(rank_object.within_network_pair.single_sample_p_value.v, testCase.ranking.within_network_pair.single_sample_p_value.v); + function fullConnectomeWinklerRankingTest(testCase) + winkler_results = testCase.rank.full_connectome.winkler_two_sample_p_value.v; + expected_winkler = testCase.rank_results.full_connectome.winkler_two_sample_p_value.v; + + testCase.verifyEqual(winkler_results, expected_winkler); + end + + function fullConnectomeWestfallYoungRankingTest(testCase) + westfall_young_results = testCase.rank.full_connectome.westfall_young_two_sample_p_value.v; + expected_westfall_young = testCase.rank_results.full_connectome.westfall_young_two_sample_p_value.v; + + testCase.verifyEqual(westfall_young_results, expected_westfall_young); + end + + function withinNetworkPairUncorrectedRankingTest(testCase) + legacy_results = testCase.rank.within_network_pair.legacy_single_sample_p_value.v; + expected_legacy = testCase.rank_results.within_network_pair.legacy_single_sample_p_value.v; + + testCase.verifyEqual(legacy_results, expected_legacy); + + uncorrected_results = testCase.rank.within_network_pair.uncorrected_single_sample_p_value.v; + expected_uncorrected = testCase.rank_results.within_network_pair.uncorrected_single_sample_p_value.v; + + testCase.verifyEqual(uncorrected_results, expected_uncorrected); + end + + function withinNetworkPairWinklerRankingTest(testCase) + winkler_results = testCase.rank.within_network_pair.winkler_single_sample_p_value.v; + expected_winkler = testCase.rank_results.within_network_pair.winkler_single_sample_p_value.v; + + testCase.verifyEqual(winkler_results, expected_winkler); + end + + function withinNetworkPairWestfallYoungRankingTest(testCase) + westfall_young_results = testCase.rank.within_network_pair.westfall_young_single_sample_p_value.v; + expected_westfall_young = testCase.rank_results.within_network_pair.westfall_young_single_sample_p_value.v; + + testCase.verifyEqual(westfall_young_results, expected_westfall_young); end end end \ No newline at end of file diff --git a/+nla/+net/unittests/resultRank_results.mat b/+nla/+net/unittests/resultRank_results.mat index badc77c6..7aa5dac6 100644 Binary files a/+nla/+net/unittests/resultRank_results.mat and b/+nla/+net/unittests/resultRank_results.mat differ diff --git a/+nla/+tests/TestPoolTest.m b/+nla/+tests/TestPoolTest.m deleted file mode 100644 index 28139d64..00000000 --- a/+nla/+tests/TestPoolTest.m +++ /dev/null @@ -1,37 +0,0 @@ -classdef TestPoolTest < matlab.unittest.TestCase - - properties - variables - end - - methods (TestClassSetup) - function loadTestData(testCase) - testCase.variables = {}; - - load(fullfile('nla', 'tests', 'inputStruct'), 'input_struct'); - testCase.variables.input_struct = input_struct; - load(fullfile('nla', 'tests', 'edgeResultsPermuted'), 'edge_results_perm'); - testCase.variables.edge_results_perm = edge_results_perm; - load(fullfile('nla', 'tests', 'networkInputStruct'), 'net_input_struct'); - testCase.variables.net_input_struct = net_input_struct; - load(fullfile('nla', 'tests', 'networkAtlas'), 'net_atlas'); - testCase.variables.net_atlas = net_atlas; - end - end - - methods (TestClassTeardown) - function clearTestData(testCase) - clear testCase.variables - end - end - - methods (Test) - function permutationEdgeTest(testCase) - import nla.TestPool - - test_pool = TestPool(); - permuted_edge_results = test_pool.runEdgeTestPerm(testCase.variables.input_struct, 20, 1); - testCase.verifyEqual(permuted_edge_results, testCase.variables.edge_results_perm); - end - end -end \ No newline at end of file diff --git a/+nla/+tests/edgeResultsNonPermuted.mat b/+nla/+tests/edgeResultsNonPermuted.mat deleted file mode 100644 index e15659be..00000000 Binary files a/+nla/+tests/edgeResultsNonPermuted.mat and /dev/null differ diff --git a/+nla/+tests/inputStruct.mat b/+nla/+tests/inputStruct.mat deleted file mode 100644 index 344807be..00000000 Binary files a/+nla/+tests/inputStruct.mat and /dev/null differ diff --git a/+nla/+tests/networkAtlas.mat b/+nla/+tests/networkAtlas.mat deleted file mode 100644 index 936babb8..00000000 Binary files a/+nla/+tests/networkAtlas.mat and /dev/null differ diff --git a/+nla/+tests/networkInputStruct.mat b/+nla/+tests/networkInputStruct.mat deleted file mode 100644 index 0d8be50e..00000000 Binary files a/+nla/+tests/networkInputStruct.mat and /dev/null differ diff --git a/+nla/+tests/networkResultsNonPermuted.mat b/+nla/+tests/networkResultsNonPermuted.mat deleted file mode 100644 index 1ed4a2c7..00000000 Binary files a/+nla/+tests/networkResultsNonPermuted.mat and /dev/null differ diff --git a/+nla/+tests/networkResultsPermuted.mat b/+nla/+tests/networkResultsPermuted.mat deleted file mode 100644 index 2976b756..00000000 Binary files a/+nla/+tests/networkResultsPermuted.mat and /dev/null differ diff --git a/+nla/Dir.m b/+nla/Direction.m similarity index 69% rename from +nla/Dir.m rename to +nla/Direction.m index 16b29536..e841ca17 100755 --- a/+nla/Dir.m +++ b/+nla/Direction.m @@ -1,4 +1,4 @@ -classdef Dir +classdef Direction enumeration X, Y, Z end diff --git a/+nla/Method.m b/+nla/Method.m deleted file mode 100755 index c31347f4..00000000 --- a/+nla/Method.m +++ /dev/null @@ -1,6 +0,0 @@ -classdef Method - enumeration - NONPERMUTED, FULL_CONN, WITHIN_NET_PAIR - end -end - diff --git a/+nla/NetworkAtlas.m b/+nla/NetworkAtlas.m index df18c9a0..c5696060 100755 --- a/+nla/NetworkAtlas.m +++ b/+nla/NetworkAtlas.m @@ -11,9 +11,9 @@ % :param parcels: Optional MATLAB struct field for surface parcellations. Contains two sub-fields ``ctx_l`` and ``ctx_r``. N\ :sub:`verts`\ x 1 vectors. Each element of a vector corresponds to a vertex within the spatial mesh and contains the index of the ROI for that vertex. % :param space: Optional The mesh that the atlas` ROI locations/parcels are in. Two options - ``TT`` or ``MNI`` - properties (SetAccess = private) nets % This is the net_names + ROIs ROI_order name @@ -24,6 +24,10 @@ methods function obj = NetworkAtlas(fname) + if nargin == 0 + return + end + %% IM structure if ischar(fname) || isstring(fname) net_struct = load(fname); diff --git a/+nla/NetworkLevelMethod.m b/+nla/NetworkLevelMethod.m new file mode 100644 index 00000000..000a7370 --- /dev/null +++ b/+nla/NetworkLevelMethod.m @@ -0,0 +1,5 @@ +classdef NetworkLevelMethod + enumeration + NO_PERMUTATIONS, FULL_CONNECTOME, WITHIN_NETWORK_PAIR + end +end \ No newline at end of file diff --git a/+nla/RankingMethod.m b/+nla/RankingMethod.m old mode 100755 new mode 100644 index e4d2c79a..cb3f98b1 --- a/+nla/RankingMethod.m +++ b/+nla/RankingMethod.m @@ -1,6 +1,5 @@ classdef RankingMethod enumeration - P_VALUE, TEST_STATISTIC + UNCORRECTED, WINKLER, WESTFALL_YOUNG end -end - +end \ No newline at end of file diff --git a/+nla/ResultPool.m b/+nla/ResultPool.m index d66dc714..39958e3a 100755 --- a/+nla/ResultPool.m +++ b/+nla/ResultPool.m @@ -18,6 +18,9 @@ methods function obj = ResultPool(test_options, network_test_options, network_atlas, edge_test_results,... network_test_results, permutation_edge_test_results, permutation_network_test_results) + if nargin == 0 + return + end obj.test_options = test_options; obj.network_test_options = network_test_options; obj.network_atlas = network_atlas; diff --git a/+nla/TestPool.m b/+nla/TestPool.m index b3f54f76..fdca9536 100755 --- a/+nla/TestPool.m +++ b/+nla/TestPool.m @@ -7,6 +7,10 @@ data_queue = false end + properties (Constant) + correlation_input_tests = ["kolmogorov_smirnov", "students_t", "welchs_t", "wilcoxon"] + end + methods (Access = private) function [number_processes, blocks] = initializeParallelPool(obj, number_permutations) @@ -60,16 +64,11 @@ function ranked_results = collateNetworkPermutationResults(obj, nonpermuted_edge_test_results, network_atlas, nonpermuted_network_test_results,... permuted_network_test_results, network_test_options) - % Run Cohen's D - cohen_d_test = nla.net.CohenDTest(); + % Warning: Hacky code. Because of the way non-permuted network tests and permuted are called from the front, they are stored % in different objects. (Notice the input argument for non-permuted network results). Eventually, it should probably be done % that we do them all here. That may be another ticket. For now, we're copying over. - for test_index = 1:numNetTests(obj) - permuted_network_test_results{test_index} = cohen_d_test.run(nonpermuted_edge_test_results, network_atlas,... - permuted_network_test_results{test_index}); - end for test_index = 1:numNetTests(obj) for test_index2 = 1:numNetTests(obj) if nonpermuted_network_test_results{test_index2}.test_name == permuted_network_test_results{test_index}.test_name @@ -78,9 +77,17 @@ end end end + + % REMOVE CALL TO COHENS D UNTIL WE DETERMINE CORRECT CALC FOR IT ADE 2025MAR24 + % Run Cohen's D +% cohen_d_test = nla.net.CohenDTest(); +% for test_index = 1:numNetTests(obj) +% +% permuted_network_test_results{test_index} = cohen_d_test.run(nonpermuted_edge_test_results, network_atlas,... +% permuted_network_test_results{test_index}); +% end - ranked_results = obj.rankResults(network_test_options, permuted_network_test_results,... - network_atlas.numNetPairs()); + ranked_results = obj.rankResults(network_test_options, permuted_network_test_results, network_atlas.numNetPairs()); end function [permuted_edge_test_results, permuted_network_test_results] = runPermSeparateEdgeAndNet(obj, input_struct, net_input_struct,... @@ -271,14 +278,17 @@ end function ranked_results = rankResults(obj, input_options, permuted_network_results, number_of_network_pairs) - import nla.net.ResultRank + ranked_results = permuted_network_results; for test = 1:numNetTests(obj) - ranker = ResultRank(permuted_network_results{test}, number_of_network_pairs); + ranker = nla.net.ResultRank(permuted_network_results{test}, number_of_network_pairs); ranked_results_object = ranker.rank(); ranked_results{test} = ranked_results_object; - ranked_results{test}.permutation_results = permuted_network_results{test}.permutation_results; + if any(strcmp(ranked_results{test}.test_name, obj.correlation_input_tests)) + ranked_results{test}.no_permutations = rmfield(ranked_results{test}.no_permutations, "legacy_two_sample_p_value"); + ranked_results{test}.no_permutations = rmfield(ranked_results{test}.no_permutations, "uncorrected_two_sample_p_value"); + end end end end diff --git a/+nla/unittests/TestPoolTest.m b/+nla/unittests/TestPoolTest.m new file mode 100644 index 00000000..1f2a9814 --- /dev/null +++ b/+nla/unittests/TestPoolTest.m @@ -0,0 +1,184 @@ +classdef TestPoolTest < matlab.unittest.TestCase + + properties + edge_test_options + network_test_options + tests + root_path + end + + properties (Constant) + permutations = 20 + end + + methods (TestClassSetup) + function loadTestData(testCase) + import nla.TriMatrix + + testCase.root_path = nla.findRootPath(); + testCase.tests = nla.TestPool(); + + % load functional connectivity + fc_path = strcat(testCase.root_path, fullfile("examples", "fc_and_behavior", "sample_func_conn.mat")); + fc_unordered = load(fc_path); + fc_unordered = double(fc_unordered.fc); + if all(abs(fc_unordered(:)) <= 1) + fc_unordered = nla.fisherR2Z(fc_unordered); + end + + % load network atlas + network_atlas_path = strcat(testCase.root_path, fullfile("support_files", "Wheelock_2020_CerebralCortex_17nets_300ROI_on_MNI.mat")); + network_atlas_loaded = load(network_atlas_path); + network_to_remove = ["US"]; + [network_atlas] = nla.removeNetworks(network_atlas_loaded, network_to_remove, "Wheelock_2020_CerebralCortex_16nets_288ROI_on_MNI"); + network_atlas = nla.NetworkAtlas(network_atlas); + + % load behavior file + behavior_path = strcat(testCase.root_path, fullfile("examples", "fc_and_behavior", "sample_behavior.mat")); + behavior = load(behavior_path); + behavior = behavior.Bx; + + testCase.edge_test_options = struct(); + testCase.edge_test_options.net_atlas = network_atlas; + testCase.edge_test_options.func_conn = TriMatrix(fc_unordered(network_atlas.ROI_order, network_atlas.ROI_order, :)); + testCase.edge_test_options.behavior = behavior(:, 10).Variables; + testCase.edge_test_options.prob_max = 0.05; + testCase.edge_test_options.permute_method = nla.edge.permutationMethods.BehaviorVec(); + testCase.edge_test_options.iteration = 0; + + + testCase.network_test_options = nla.net.genBaseInputs(); + testCase.network_test_options.prob_max = 0.05; + testCase.network_test_options.behavior_count = 1; + testCase.network_test_options.d_max = 0.5; + testCase.network_test_options.prob_plot_method = nla.gfx.ProbPlotMethod.DEFAULT; + testCase.network_test_options.full_connectome = true; + testCase.network_test_options.no_permutations = true; + testCase.network_test_options.within_network_pair = true; + end + end + + methods (TestClassTeardown) + function clearTestData(testCase) + clear + end + end + + methods (Test) + function spearmanEdgeTest(testCase) + testCase.tests.edge_test = nla.edge.test.Spearman(); + edge_result = testCase.tests.runEdgeTestPerm(testCase.edge_test_options, testCase.permutations, 0); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "spearman_result.mat"))); + property_names = properties(edge_result); + for prop_name = property_names + testCase.verifyEqual(expected_result.edge_result.(prop_name{1}), edge_result.(prop_name{1})); + end + end + + function pearsonEdgeTest(testCase) + testCase.tests.edge_test = nla.edge.test.Pearson(); + edge_result = testCase.tests.runEdgeTestPerm(testCase.edge_test_options, testCase.permutations, 0); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "pearson_result.mat"))); + property_names = properties(edge_result); + for prop_name = property_names + testCase.verifyEqual(expected_result.edge_result.(prop_name{1}), edge_result.(prop_name{1})); + end + end + + function kendallBTest(testCase) + testCase.tests.edge_test = nla.edge.test.KendallB(); + edge_result = testCase.tests.runEdgeTestPerm(testCase.edge_test_options, testCase.permutations, 0); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "kendallb_result.mat"))); + property_names = properties(edge_result); + for prop_name = property_names + testCase.verifyEqual(expected_result.edge_result.(prop_name{1}), edge_result.(prop_name{1})); + end + end + + function spearmanEstimatorTest(testCase) + testCase.tests.edge_test = nla.edge.test.SpearmanEstimator(); + edge_result = testCase.tests.runEdgeTestPerm(testCase.edge_test_options, testCase.permutations, 0); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "spearman_estimator_result.mat"))); + property_names = properties(edge_result); + for prop_name = property_names + testCase.verifyEqual(expected_result.edge_result.(prop_name{1}), edge_result.(prop_name{1})); + end + end + + function chiSquaredTest(testCase) + edge_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "pearson_result.mat"))); + testCase.tests.net_tests = {nla.net.test.ChiSquaredTest()}; + network_result = testCase.tests.runNetTestsPerm(testCase.network_test_options, testCase.edge_test_options.net_atlas, edge_result.edge_result); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "chi_squared_result.mat"))); + property_names = properties(network_result{1}); + for prop_name = property_names + testCase.verifyEqual(expected_result.network_result.(prop_name{1}), network_result{1}.(prop_name{1})); + end + end + + function hyperGeometricTest(testCase) + edge_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "pearson_result.mat"))); + testCase.tests.net_tests = {nla.net.test.HyperGeometricTest()}; + network_result = testCase.tests.runNetTestsPerm(testCase.network_test_options, testCase.edge_test_options.net_atlas, edge_result.edge_result); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "hypergeometric_result.mat"))); + property_names = properties(network_result{1}); + for prop_name = property_names + testCase.verifyEqual(expected_result.network_result.(prop_name{1}), network_result{1}.(prop_name{1})); + end + end + + function kolmogorovSmirnovTest(testCase) + edge_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "pearson_result.mat"))); + testCase.tests.net_tests = {nla.net.test.KolmogorovSmirnovTest()}; + network_result = testCase.tests.runNetTestsPerm(testCase.network_test_options, testCase.edge_test_options.net_atlas, edge_result.edge_result); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "kolmogorov_smirnov_result.mat"))); + property_names = properties(network_result{1}); + for prop_name = property_names + testCase.verifyEqual(expected_result.network_result.(prop_name{1}), network_result{1}.(prop_name{1})); + end + end + + function studentTTest(testCase) + edge_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "pearson_result.mat"))); + testCase.tests.net_tests = {nla.net.test.StudentTTest()}; + network_result = testCase.tests.runNetTestsPerm(testCase.network_test_options, testCase.edge_test_options.net_atlas, edge_result.edge_result); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "student_t_result.mat"))); + property_names = properties(network_result{1}); + for prop_name = property_names + testCase.verifyEqual(expected_result.network_result.(prop_name{1}), network_result{1}.(prop_name{1})); + end + end + + function welchTTest(testCase) + edge_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "pearson_result.mat"))); + testCase.tests.net_tests = {nla.net.test.WelchTTest()}; + network_result = testCase.tests.runNetTestsPerm(testCase.network_test_options, testCase.edge_test_options.net_atlas, edge_result.edge_result); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "welch_t_result.mat"))); + property_names = properties(network_result{1}); + for prop_name = property_names + testCase.verifyEqual(expected_result.network_result.(prop_name{1}), network_result{1}.(prop_name{1})); + end + end + + function wilcoxonTest(testCase) + edge_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "pearson_result.mat"))); + testCase.tests.net_tests = {nla.net.test.WilcoxonTest()}; + network_result = testCase.tests.runNetTestsPerm(testCase.network_test_options, testCase.edge_test_options.net_atlas, edge_result.edge_result); + + expected_result = load(strcat(testCase.root_path, fullfile("+nla", "unittests", "wilocoxon_result.mat"))); + property_names = properties(network_result{1}); + for prop_name = property_names + testCase.verifyEqual(expected_result.network_result.(prop_name{1}), network_result{1}.(prop_name{1})); + end + end + end +end \ No newline at end of file diff --git a/+nla/unittests/chi_squared_result.mat b/+nla/unittests/chi_squared_result.mat new file mode 100644 index 00000000..0d89b4c3 Binary files /dev/null and b/+nla/unittests/chi_squared_result.mat differ diff --git a/+nla/unittests/hypergeometric_result.mat b/+nla/unittests/hypergeometric_result.mat new file mode 100644 index 00000000..587b8890 Binary files /dev/null and b/+nla/unittests/hypergeometric_result.mat differ diff --git a/+nla/unittests/kendallb_result.mat b/+nla/unittests/kendallb_result.mat new file mode 100644 index 00000000..ea9a1a4b Binary files /dev/null and b/+nla/unittests/kendallb_result.mat differ diff --git a/+nla/unittests/kolmogorov_smirnov_result.mat b/+nla/unittests/kolmogorov_smirnov_result.mat new file mode 100644 index 00000000..20906dba Binary files /dev/null and b/+nla/unittests/kolmogorov_smirnov_result.mat differ diff --git a/+nla/+tests/edgeResultsPermuted.mat b/+nla/unittests/pearson_result.mat similarity index 60% rename from +nla/+tests/edgeResultsPermuted.mat rename to +nla/unittests/pearson_result.mat index dbfd32fc..6c814375 100644 Binary files a/+nla/+tests/edgeResultsPermuted.mat and b/+nla/unittests/pearson_result.mat differ diff --git a/+nla/unittests/spearman_estimator_result.mat b/+nla/unittests/spearman_estimator_result.mat new file mode 100644 index 00000000..0c8c6f14 Binary files /dev/null and b/+nla/unittests/spearman_estimator_result.mat differ diff --git a/+nla/unittests/spearman_result.mat b/+nla/unittests/spearman_result.mat new file mode 100644 index 00000000..023a8d05 Binary files /dev/null and b/+nla/unittests/spearman_result.mat differ diff --git a/+nla/unittests/student_t_result.mat b/+nla/unittests/student_t_result.mat new file mode 100644 index 00000000..ca315a31 Binary files /dev/null and b/+nla/unittests/student_t_result.mat differ diff --git a/+nla/unittests/welch_t_result.mat b/+nla/unittests/welch_t_result.mat new file mode 100644 index 00000000..7136c11b Binary files /dev/null and b/+nla/unittests/welch_t_result.mat differ diff --git a/+nla/unittests/wilocoxon_result.mat b/+nla/unittests/wilocoxon_result.mat new file mode 100644 index 00000000..faae4082 Binary files /dev/null and b/+nla/unittests/wilocoxon_result.mat differ diff --git a/.github/workflows/run_matlab_tests.yml b/.github/workflows/run_matlab_tests.yml index ea611498..67d3fad7 100644 --- a/.github/workflows/run_matlab_tests.yml +++ b/.github/workflows/run_matlab_tests.yml @@ -1,29 +1,30 @@ name: Run Matlab Tests on: [push] -#env: -# MLM_LICENSE_TOKEN: ${{ secrets.MLM_LICENSE_TOKEN }} -#jobs: -# matlab-test-job: -# name: Run Matlab tests and generate reports -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v4 -# - name: Setup MATLAB -# uses: matlab-actions/setup-matlab@v2 -# with: -# release: R2023b #We want coverage reports, so we bump up our version -# cache: true -# products: -# MATLAB -# Bioinformatics_Toolbox -# Parallel_Computing_Toolbox -# Statistics_and_Machine_Learning_Toolbox -# Image_Processing_Toolbox -# - name: Check Matlab Install -# uses: matlab-actions/run-command@v2 -# with: -# command: ver -# - name: Run script -# uses: matlab-actions/run-command@v2 -# with: -# command: addpath(genpath(pwd)); results = runTests(); assertSuccess(results) +env: + MLM_LICENSE_TOKEN: ${{ secrets.MLM_LICENSE_TOKEN }} +jobs: + matlab-test-job: + name: Run Matlab tests and generate reports + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup MATLAB + uses: matlab-actions/setup-matlab@v2 + with: + release: R2023a + cache: true + products: > + MATLAB + Bioinformatics_Toolbox + Parallel_Computing_Toolbox + Statistics_and_Machine_Learning_Toolbox + Image_Processing_Toolbox + - name: Check Matlab Install + uses: matlab-actions/run-command@v2 + with: + command: ver + - name: Run script + uses: matlab-actions/run-command@v2 + with: + command: addpath(genpath(pwd)); results = runTests(); assertSuccess(results) + use-parallel: true diff --git a/.gitignore b/.gitignore index 6fba5d4f..7e0afc10 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ test/ +# we can build these on git +docs/build + + # Ignoring files by extension # All files with these extensions will be ignored in # this directory and all its sub-directories. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..e4e34d6e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,25 @@ +---LICENSE--- +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), +for non-commercial rights to use, copy, modify, merge, publish, distribute, +sublicense, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +-You must give appropriate credit to the original authors of this software, and +indicate if changes were made to the original code. +-The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +A commercial version of the software is available, and licensed through the +Washington University Office of Technology Management. +For more information, please contact: +otm@wustl.edu (314) 747-0920 + + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/NLAResult.mlapp b/NLAResult.mlapp index f7c2eafe..9d636739 100644 Binary files a/NLAResult.mlapp and b/NLAResult.mlapp differ diff --git a/NLAResult_exported.m b/NLAResult_exported.m index b519471b..5ce66538 100644 --- a/NLAResult_exported.m +++ b/NLAResult_exported.m @@ -2,32 +2,20 @@ % Properties that correspond to app components properties (Access = public) - UIFigure matlab.ui.Figure - FileMenu matlab.ui.container.Menu - SaveButton matlab.ui.container.Menu - ResultTree matlab.ui.container.Tree - FlipNestingButton matlab.ui.control.Button - EdgeLevelLabel matlab.ui.control.Label - ViewEdgeLevelButton matlab.ui.control.Button - NetLevelLabel matlab.ui.control.Label - RunButton matlab.ui.control.Button - DisplaySelectedButton matlab.ui.control.Button - NetlevelplottingDropDownLabel matlab.ui.control.Label - NetlevelplottingDropDown matlab.ui.control.DropDown - DisplayConvergenceButton matlab.ui.control.Button - ConvergencecolormapDropDownLabel matlab.ui.control.Label - ColormapDropDown matlab.ui.control.DropDown - DisplayChordNet matlab.ui.control.Button - DisplayChordEdge matlab.ui.control.Button - SaveSummaryTable matlab.ui.control.Button - EdgelevelchordplottingLabel matlab.ui.control.Label - EdgeLevelTypeDropDown matlab.ui.control.DropDown - AdjustableNetParamsPanel matlab.ui.container.Panel - MultiplecomparisonscorrectionLabel matlab.ui.control.Label - FDRCorrection matlab.ui.control.DropDown - showROIcentroidsinbrainplotsCheckBox matlab.ui.control.CheckBox - CohensDthresholdchordplotsCheckBox matlab.ui.control.CheckBox - BranchLabel matlab.ui.control.Label + UIFigure matlab.ui.Figure + FileMenu matlab.ui.container.Menu + SaveButton matlab.ui.container.Menu + SaveSummaryTableMenu matlab.ui.container.Menu + ResultTree matlab.ui.container.Tree + FlipNestingButton matlab.ui.control.Button + EdgeLevelLabel matlab.ui.control.Label + ViewEdgeLevelButton matlab.ui.control.Button + NetLevelLabel matlab.ui.control.Label + RunButton matlab.ui.control.Button + AdjustableNetParamsPanel matlab.ui.container.Panel + BranchLabel matlab.ui.control.Label + OpenTriMatrixPlotButton matlab.ui.control.Button + OpenDiagnosticPlotsButton matlab.ui.control.Button end @@ -41,6 +29,7 @@ prog_bar = false net_adjustable_fields cur_iter = 0 + old_data = false end methods (Access = private) @@ -76,8 +65,7 @@ function moveCurrFigToParentLocation(app) end - function setNesting(app, nesting_by_method) - import nla.* % required due to matlab package system quirks + function setNesting(app) % clear old nodes for i = 1:size(app.ResultTree.Children, 1) for j = 1:size(app.ResultTree.Children(1).Children, 1) @@ -87,7 +75,7 @@ function setNesting(app, nesting_by_method) end % add new nodes - if nesting_by_method + if app.nesting_by_method if app.net_input_struct.no_permutations root = app.createNode(app.ResultTree, 'Non-permuted'); for i = 1:size(app.results.network_test_results, 2) @@ -133,7 +121,7 @@ function setNesting(app, nesting_by_method) app.createNode(root, 'Non-permuted', {result, flags}); end - if app.net_input_struct.full_connectome && result.has_full_conn + if app.net_input_struct.full_connectome && ~isequal(result.full_connectome, false) perm_result = app.results.permutation_network_test_results{i}; if app.net_input_struct.full_connectome && ~isequal(result.full_connectome, false) flags = struct(); @@ -230,7 +218,7 @@ function readNetParamAdjustments(app) end end - function initFromInputs(app, test_pool, input_struct, net_input_struct) + function initFromInputs(app, test_pool, input_struct, net_input_struct, ~) import nla.* % required due to matlab package system quirks app.ViewEdgeLevelButton.Enable = false; app.RunButton.Enable = false; @@ -260,7 +248,7 @@ function initFromInputs(app, test_pool, input_struct, net_input_struct) close(prog); end - function initFromResult(app, result, file_name) + function initFromResult(app, result, file_name, ~, old_data) import nla.* % required due to matlab package system quirks app.UIFigure.Name = sprintf('%s - NLA Result', file_name); @@ -288,7 +276,8 @@ function initFromResult(app, result, file_name) end app.results = result; - + app.old_data = old_data; + app.ViewEdgeLevelButton.Enable = true; app.SaveButton.Enable = true; app.RunButton.Enable = false; @@ -299,19 +288,17 @@ function initFromResult(app, result, file_name) drawnow(); if ~islogical(result.network_test_results) - app.setNesting(true); + app.setNesting(); else app.results = false; end - if ~islogical(result.network_test_results) - app.genadjustableNetParams(); - end + end function enableNetButtons(app, val) % buttons that need net-level data to be used - net_buttons = {app.ResultTree, app.FlipNestingButton, app.DisplaySelectedButton, app.DisplayConvergenceButton, app.DisplayChordNet, app.DisplayChordEdge, app.SaveSummaryTable, app.ColormapDropDown}; + net_buttons = {app.ResultTree, app.FlipNestingButton, app.SaveSummaryTableMenu}; for i = 1:numel(net_buttons) net_buttons{i}.Enable = val; end @@ -326,13 +313,6 @@ function enableNetButtons(app, val) else app.AdjustableNetParamsPanel.Enable = 'off'; end - - % dropdowns that need net-level data to be used - net_dropdowns = {app.FDRCorrection, app.EdgeLevelTypeDropDown, app.NetlevelplottingDropDown}; - for i = 1:numel(net_dropdowns) - net_dropdowns{i}.Enable = val; - net_dropdowns{i}.ValueChangedFcn(app, true); - end end function displayManyPlots(app, extra_flags, plot_type) @@ -352,10 +332,10 @@ function displayManyPlots(app, extra_flags, plot_type) prog.Message = sprintf('Generating %s %s', result.test_display_name, plot_type); - result.output(app.input_struct, app.net_input_struct, app.input_struct.net_atlas, app.edge_result, helpers.mergeStruct(node_flags, extra_flags)); - +% result.output(app.input_struct, app.net_input_struct, app.input_struct.net_atlas, app.edge_result, helpers.mergeStruct(node_flags, extra_flags)); + nla.net.result.plot.NetworkTestPlotApp(result, app.edge_result, node_flags, app.input_struct, app.net_input_struct, app.old_data) prog.Value = i / size(selected_nodes, 1); - app.moveCurrFigToParentLocation(); +% app.moveCurrFigToParentLocation(); end end @@ -371,16 +351,16 @@ function branchLabel(app, gui, result) methods (Access = private) % Code that executes after component creation - function startupFcn(app, test_pool, input_struct, net_input_struct) + function startupFcn(app, test_pool, input_struct, net_input_struct, old_data) import nla.* % required due to matlab package system quirks app.UIFigure.Name = 'NLA Result'; app.UIFigure.Icon = [findRootPath() 'thumb.png']; if isa(test_pool, 'nla.ResultPool') - initFromResult(app, test_pool, input_struct); + initFromResult(app, test_pool, input_struct, net_input_struct, old_data); else - initFromInputs(app, test_pool, input_struct, net_input_struct); + initFromInputs(app, test_pool, input_struct, net_input_struct, false); end end @@ -433,8 +413,7 @@ function RunButtonPushed(app, event) drawnow(); - app.setNesting(true); - app.genadjustableNetParams(); + app.setNesting(); close(prog); end @@ -471,9 +450,8 @@ function SaveButtonPushed(app, event) % Button pushed function: FlipNestingButton function FlipNestingButtonPushed(app, event) - import nla.* % required due to matlab package system quirks app.nesting_by_method = ~app.nesting_by_method; - app.setNesting(app.nesting_by_method) + app.setNesting() end % Button pushed function: ViewEdgeLevelButton @@ -489,17 +467,35 @@ function ViewEdgeLevelButtonPushed(app, event) close(prog); - app.moveCurrFigToParentLocation(); +% app.moveCurrFigToParentLocation(); end - % Button pushed function: DisplaySelectedButton - function DisplaySelectedButtonPushed(app, event) - import nla.* % required due to matlab package system quirks - displayManyPlots(app, struct('plot_type', PlotType.FIGURE), 'figures'); + % Button pushed function: OpenTriMatrixPlotButton + function OpenTriMatrixPlotButtonPushed(app, event) + displayManyPlots(app, struct('plot_type', nla.PlotType.FIGURE), 'figures'); end - % Value changed function: NetlevelplottingDropDown + % Button pushed function: OpenDiagnosticPlotsButton + function OpenDiagnosticPlotsButtonPushed(app, event) + prog = uiprogressdlg(app.UIFigure, 'Title', sprintf('Generating Diagnostic Plots'), 'Message', sprintf('Generating Diagnostic Plots')); + prog.Value = 0.02; + drawnow; + + selected_nodes = app.ResultTree.SelectedNodes; + for i = 1:size(selected_nodes, 1) + if ~isempty(selected_nodes(i).NodeData) + result = selected_nodes(i).NodeData{1}; + node_flags = selected_nodes(i).NodeData{2}; + + result.runDiagnosticPlots(app.input_struct, app.net_input_struct, app.edge_result, app.input_struct.net_atlas, node_flags); + prog.Value = i / size(selected_nodes, 1); + end + end + close(prog) + end + + % Callback function function PValModeDropDownValueChanged(app, event) import nla.* % required due to matlab package system quirks value = app.NetlevelplottingDropDown.Value; @@ -517,7 +513,7 @@ function PValModeDropDownValueChanged(app, event) end end - % Button pushed function: DisplayConvergenceButton + % Callback function function DisplayConvergenceButtonPushed(app, event) import nla.* % required due to matlab package system quirks @@ -562,23 +558,23 @@ function DisplayConvergenceButtonPushed(app, event) close(prog); - app.moveCurrFigToParentLocation(); +% app.moveCurrFigToParentLocation(); %These mlapp files are really just the worst end - % Button pushed function: DisplayChordNet + % Callback function function DisplayChordNetButtonPushed(app, event) import nla.* % required due to matlab package system quirks displayManyPlots(app, struct('plot_type', PlotType.CHORD), 'chord plots'); end - % Button pushed function: DisplayChordEdge + % Callback function function DisplayChordEdgeButtonPushed(app, event) import nla.* % required due to matlab package system quirks displayManyPlots(app, struct('plot_type', PlotType.CHORD_EDGE), 'chord plots'); end - % Button pushed function: SaveSummaryTable + % Menu selected function: SaveSummaryTableMenu function SaveSummaryTableButtonPushed(app, event) import nla.* % required due to matlab package system quirks [file, path] = uiputfile({'*.txt', 'Summary Table (*.txt)'}, 'Save Summary Table', 'result.txt'); @@ -592,7 +588,7 @@ function SaveSummaryTableButtonPushed(app, event) end end - % Value changed function: EdgeLevelTypeDropDown + % Callback function function EdgeLevelTypeDropDownValueChanged(app, event) import nla.* % required due to matlab package system quirks value = app.EdgeLevelTypeDropDown.Value; @@ -609,7 +605,7 @@ function EdgeLevelTypeDropDownValueChanged(app, event) end end - % Value changed function: FDRCorrection + % Callback function function FDRCorrectionValueChanged(app, event) import nla.* % required due to matlab package system quirks value = app.FDRCorrection.Value; @@ -622,14 +618,13 @@ function FDRCorrectionValueChanged(app, event) end end - % Value changed function: - % showROIcentroidsinbrainplotsCheckBox + % Callback function function showROIcentroidsinbrainplotsCheckBoxValueChanged(app, event) include_centroids = app.showROIcentroidsinbrainplotsCheckBox.Value; app.net_input_struct.show_ROI_centroids = include_centroids; end - % Value changed function: CohensDthresholdchordplotsCheckBox + % Callback function function CohensDthresholdchordplotsCheckBoxValueChanged(app, event) d_thresh = app.CohensDthresholdchordplotsCheckBox.Value; app.net_input_struct.d_thresh_chord_plot = d_thresh; @@ -658,6 +653,11 @@ function createComponents(app) app.SaveButton.Accelerator = 's'; app.SaveButton.Text = 'Save'; + % Create SaveSummaryTableMenu + app.SaveSummaryTableMenu = uimenu(app.FileMenu); + app.SaveSummaryTableMenu.MenuSelectedFcn = createCallbackFcn(app, @SaveSummaryTableButtonPushed, true); + app.SaveSummaryTableMenu.Text = 'Save Summary Table'; + % Create ResultTree app.ResultTree = uitree(app.UIFigure); app.ResultTree.Multiselect = 'on'; @@ -691,107 +691,11 @@ function createComponents(app) app.RunButton.Position = [18 523 49 40]; app.RunButton.Text = 'Run'; - % Create DisplaySelectedButton - app.DisplaySelectedButton = uibutton(app.UIFigure, 'push'); - app.DisplaySelectedButton.ButtonPushedFcn = createCallbackFcn(app, @DisplaySelectedButtonPushed, true); - app.DisplaySelectedButton.Position = [434 62 81 22]; - app.DisplaySelectedButton.Text = 'View figures'; - - % Create NetlevelplottingDropDownLabel - app.NetlevelplottingDropDownLabel = uilabel(app.UIFigure); - app.NetlevelplottingDropDownLabel.HorizontalAlignment = 'right'; - app.NetlevelplottingDropDownLabel.Position = [434 114 98 22]; - app.NetlevelplottingDropDownLabel.Text = 'Net-level plotting:'; - - % Create NetlevelplottingDropDown - app.NetlevelplottingDropDown = uidropdown(app.UIFigure); - app.NetlevelplottingDropDown.Items = {'p-value linear', 'p-value log', 'p-value -log10', 'stat ranked'}; - app.NetlevelplottingDropDown.ValueChangedFcn = createCallbackFcn(app, @PValModeDropDownValueChanged, true); - app.NetlevelplottingDropDown.Position = [533 114 118 22]; - app.NetlevelplottingDropDown.Value = 'p-value linear'; - - % Create DisplayConvergenceButton - app.DisplayConvergenceButton = uibutton(app.UIFigure, 'push'); - app.DisplayConvergenceButton.ButtonPushedFcn = createCallbackFcn(app, @DisplayConvergenceButtonPushed, true); - app.DisplayConvergenceButton.Position = [434 36 202 22]; - app.DisplayConvergenceButton.Text = 'View convergence map of selected'; - - % Create ConvergencecolormapDropDownLabel - app.ConvergencecolormapDropDownLabel = uilabel(app.UIFigure); - app.ConvergencecolormapDropDownLabel.HorizontalAlignment = 'right'; - app.ConvergencecolormapDropDownLabel.Position = [640 36 133 22]; - app.ConvergencecolormapDropDownLabel.Text = 'Convergence colormap:'; - - % Create ColormapDropDown - app.ColormapDropDown = uidropdown(app.UIFigure); - app.ColormapDropDown.Items = {'bone', 'winter', 'autumn', 'copper'}; - app.ColormapDropDown.ItemsData = [1 2 3 4]; - app.ColormapDropDown.Position = [779 36 73 22]; - app.ColormapDropDown.Value = 1; - - % Create DisplayChordNet - app.DisplayChordNet = uibutton(app.UIFigure, 'push'); - app.DisplayChordNet.ButtonPushedFcn = createCallbackFcn(app, @DisplayChordNetButtonPushed, true); - app.DisplayChordNet.Position = [519 62 103 22]; - app.DisplayChordNet.Text = 'View chord plots'; - - % Create DisplayChordEdge - app.DisplayChordEdge = uibutton(app.UIFigure, 'push'); - app.DisplayChordEdge.ButtonPushedFcn = createCallbackFcn(app, @DisplayChordEdgeButtonPushed, true); - app.DisplayChordEdge.Position = [626 62 162 22]; - app.DisplayChordEdge.Text = 'View edge-level chord plots'; - - % Create SaveSummaryTable - app.SaveSummaryTable = uibutton(app.UIFigure, 'push'); - app.SaveSummaryTable.ButtonPushedFcn = createCallbackFcn(app, @SaveSummaryTableButtonPushed, true); - app.SaveSummaryTable.Position = [434 10 125 22]; - app.SaveSummaryTable.Text = 'Save summary table'; - - % Create EdgelevelchordplottingLabel - app.EdgelevelchordplottingLabel = uilabel(app.UIFigure); - app.EdgelevelchordplottingLabel.HorizontalAlignment = 'right'; - app.EdgelevelchordplottingLabel.Position = [434 89 141 22]; - app.EdgelevelchordplottingLabel.Text = 'Edge-level chord plotting:'; - - % Create EdgeLevelTypeDropDown - app.EdgeLevelTypeDropDown = uidropdown(app.UIFigure); - app.EdgeLevelTypeDropDown.Items = {'prob', 'coeff', 'coeff (split)', 'coeff (basic)', 'coeff (basic, split)'}; - app.EdgeLevelTypeDropDown.ValueChangedFcn = createCallbackFcn(app, @EdgeLevelTypeDropDownValueChanged, true); - app.EdgeLevelTypeDropDown.Position = [581 89 69 22]; - app.EdgeLevelTypeDropDown.Value = 'prob'; - % Create AdjustableNetParamsPanel app.AdjustableNetParamsPanel = uipanel(app.UIFigure); app.AdjustableNetParamsPanel.Title = 'Adjustable network-level parameters'; app.AdjustableNetParamsPanel.Position = [434 170 416 427]; - % Create MultiplecomparisonscorrectionLabel - app.MultiplecomparisonscorrectionLabel = uilabel(app.UIFigure); - app.MultiplecomparisonscorrectionLabel.HorizontalAlignment = 'right'; - app.MultiplecomparisonscorrectionLabel.Position = [434 140 178 22]; - app.MultiplecomparisonscorrectionLabel.Text = 'Multiple comparisons correction:'; - - % Create FDRCorrection - app.FDRCorrection = uidropdown(app.UIFigure); - app.FDRCorrection.Items = {'Bonferroni', 'Benjamini-Hochberg', 'Benjamini-Yekutieli'}; - app.FDRCorrection.ValueChangedFcn = createCallbackFcn(app, @FDRCorrectionValueChanged, true); - app.FDRCorrection.Position = [618 140 148 22]; - app.FDRCorrection.Value = 'Bonferroni'; - - % Create showROIcentroidsinbrainplotsCheckBox - app.showROIcentroidsinbrainplotsCheckBox = uicheckbox(app.UIFigure); - app.showROIcentroidsinbrainplotsCheckBox.ValueChangedFcn = createCallbackFcn(app, @showROIcentroidsinbrainplotsCheckBoxValueChanged, true); - app.showROIcentroidsinbrainplotsCheckBox.Text = 'show ROI centroids in brain plots'; - app.showROIcentroidsinbrainplotsCheckBox.Position = [657 89 199 22]; - app.showROIcentroidsinbrainplotsCheckBox.Value = true; - - % Create CohensDthresholdchordplotsCheckBox - app.CohensDthresholdchordplotsCheckBox = uicheckbox(app.UIFigure); - app.CohensDthresholdchordplotsCheckBox.ValueChangedFcn = createCallbackFcn(app, @CohensDthresholdchordplotsCheckBoxValueChanged, true); - app.CohensDthresholdchordplotsCheckBox.Text = 'Cohen''s D threshold chord plots'; - app.CohensDthresholdchordplotsCheckBox.Position = [657 114 193 22]; - app.CohensDthresholdchordplotsCheckBox.Value = true; - % Create BranchLabel app.BranchLabel = uilabel(app.UIFigure); app.BranchLabel.HorizontalAlignment = 'right'; @@ -800,6 +704,18 @@ function createComponents(app) app.BranchLabel.Position = [434 627 418 29]; app.BranchLabel.Text = {'gui | unknown_branch:0000000'; 'result produced by | unknown_branch:0000000'}; + % Create OpenTriMatrixPlotButton + app.OpenTriMatrixPlotButton = uibutton(app.UIFigure, 'push'); + app.OpenTriMatrixPlotButton.ButtonPushedFcn = createCallbackFcn(app, @OpenTriMatrixPlotButtonPushed, true); + app.OpenTriMatrixPlotButton.Position = [435 127 146 22]; + app.OpenTriMatrixPlotButton.Text = 'Open TriMatrix Plot'; + + % Create OpenDiagnosticPlotsButton + app.OpenDiagnosticPlotsButton = uibutton(app.UIFigure, 'push'); + app.OpenDiagnosticPlotsButton.ButtonPushedFcn = createCallbackFcn(app, @OpenDiagnosticPlotsButtonPushed, true); + app.OpenDiagnosticPlotsButton.Position = [435 98 147 22]; + app.OpenDiagnosticPlotsButton.Text = 'Open Diagnostic Plots'; + % Show the figure after all components are created app.UIFigure.Visible = 'on'; end diff --git a/NLA_GUI.mlapp b/NLA_GUI.mlapp index f256dc39..1de4bde8 100755 Binary files a/NLA_GUI.mlapp and b/NLA_GUI.mlapp differ diff --git a/NLA_GUI_exported.m b/NLA_GUI_exported.m index 7ef8d61d..54d33b66 100644 --- a/NLA_GUI_exported.m +++ b/NLA_GUI_exported.m @@ -138,7 +138,7 @@ function startupFcn(app) app.EdgeInputsPanel.AutoResizeChildren = 'off'; end - % Callback function: EdgeInputsPanel, EdgeTestSelector + % Value changed function: EdgeTestSelector function EdgeTestSelectorValueChanged(app, event) import nla.* % required due to matlab package system quirks @@ -167,11 +167,6 @@ function EdgeTestSelectorValueChanged(app, event) app.input_fields{i}.read(app.input_struct); y = y - h; end - - % All current edge tests will permute behavior. When adding SWE - % or changing permutation behavior, this will need to be put - % into requiredInputs for the edge test - app.input_struct.permute_method = nla.edge.permutationMethods.BehaviorVec(); end % Value changed function: NetTestSelector @@ -239,7 +234,12 @@ function PermutationcountEditFieldValueChanged(app, event) % Button pushed function: RunButton function RunButtonPushed(app, event) - import nla.* + if ~isfield(app.input_struct, "permutation_groups") || isfield(app.input_struct, "permutation_groups") && (isequal(app.input_struct.permutation_groups, false) || isempty(app.input_struct.permutation_groups)) + app.input_struct.permute_method = nla.edge.permutationMethods.BehaviorVec(); + else + app.input_struct.permute_method = nla.edge.permutationMethods.MultiLevel(); + app.input_struct.permute_method = app.input_struct.permute_method.createPermutationTree(app.input_struct); + end runWithInputs(app, 0); end @@ -269,16 +269,18 @@ function OpenpreviousresultMenuSelected(app, event) prog = uiprogressdlg(app.NetworkLevelAnalysisUIFigure, 'Title', 'Loading previous result', 'Message', sprintf('Loading %s', file), 'Indeterminate', true); drawnow; + [results, old_data] = nla.net.result.NetworkTestResult().loadPreviousData([path file]); + try - results_file = load([path file]); - if isa(results_file.results, 'nla.ResultPool') - NLAResult(results_file.results, file, false); + if isa(results, 'nla.ResultPool') + NLAResult(results, file, false, old_data); end close(prog); catch ex close(prog); uialert(app.NetworkLevelAnalysisUIFigure, ex.message, 'Error while loading previous result'); end + close(prog) end end @@ -354,7 +356,6 @@ function createComponents(app) % Create EdgeInputsPanel app.EdgeInputsPanel = uipanel(app.GridLayout); app.EdgeInputsPanel.Title = 'Edge-level inputs'; - app.EdgeInputsPanel.SizeChangedFcn = createCallbackFcn(app, @EdgeTestSelectorValueChanged, true); app.EdgeInputsPanel.Layout.Row = [2 4]; app.EdgeInputsPanel.Layout.Column = 1; diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..e69de29b diff --git a/docs/source/edge_level_tests.rst b/docs/source/edge_level_tests.rst index b7b3fea2..2e2bf0f7 100644 --- a/docs/source/edge_level_tests.rst +++ b/docs/source/edge_level_tests.rst @@ -73,9 +73,6 @@ Provided Tests :Permuted p: ``.mat`` file containing N\ :sub:`ROI_pairs`\ x N\ :sub:`permutations`\ of logical values. Observed, thresholded, permuted p-values. :Permuted coeff: ``.mat`` file containing N\ :sub:`ROI_pairs`\ x N\ :sub:`permutations`\ of permuted edge-level coefficients. -Creating additional edge-level tests ------------------------------------------------ - Creating an edge-level test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -136,4 +133,4 @@ Creating a result function merge(obj, results) - * The ``results`` argument is a result to merge the object with. Afterwards, the current object will be the two merged blocks \ No newline at end of file + * The ``results`` argument is a result to merge the object with. Afterwards, the current object will be the two merged blocks diff --git a/export_mlapp.py b/export_mlapp.py index 5b8cf784..30096503 100644 --- a/export_mlapp.py +++ b/export_mlapp.py @@ -1,6 +1,6 @@ import os, zipfile -for filename in [('./NLA_GUI.mlapp', './NLA_GUI_exported.m'), ('./NLAResult.mlapp', './NLAResult_exported.m')]: +for filename in [('./NLA_GUI.mlapp', './NLA_GUI_exported.m'), ('./NLAResult.mlapp', './NLAResult_exported.m'), ('./+nla/+net/+result/+plot/NetworkTestPlotApp.mlapp', './+nla/+net/+result/+plot/NetworkTestPlotApp_exported.m')]: with zipfile.ZipFile(filename[0]) as zip_object: zip_object.extract('matlab/document.xml') with open('matlab/document.xml') as filedata: @@ -8,7 +8,7 @@ classdef_index = data[0].find('classdef') open(filename[1], 'w').close() # This deletes the contents of the file - with open(filename[1], 'w') as newfile: + with open(filename[1], 'w+') as newfile: newfile.write(data[0][classdef_index:]) newfile.writelines(data[1:-1]) newfile.write('end') \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..e69de29b diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..cdce0ed3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,222 @@ +alabaster==0.7.13 +anyio==4.1.0 +apturl==0.5.2 +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 +arrow==1.3.0 +asttokens==2.4.1 +async-lru==2.0.4 +attrs==23.1.0 +Babel==2.13.1 +backcall==0.2.0 +bcrypt==3.1.7 +beautifulsoup4==4.12.2 +bids-validator==1.14.0 +bleach==6.1.0 +blinker==1.4 +Brlapi==0.7.0 +cajarename==19.7.15 +certifi==2019.11.28 +cffi==1.16.0 +cfgv==3.4.0 +chardet==3.0.4 +charset-normalizer==3.3.2 +chrome-gnome-shell==0.0.0 +Click==7.0 +colorama==0.4.3 +comm==0.2.0 +command-not-found==0.3 +configobj==5.0.6 +cryptography==2.8 +cupshelpers==1.0 +cycler==0.10.0 +dbus-python==1.2.16 +debugpy==1.8.0 +decorator==5.1.1 +defer==1.0.6 +defusedxml==0.7.1 +deja-dup-caja==0.0.6 +distlib==0.3.7 +distro==1.4.0 +distro-info==0.23+ubuntu1.1 +docutils==0.20.1 +duplicity==0.8.12.0 +Endgame-Singularity==1.0b1 +entrypoints==0.3 +exceptiongroup==1.2.0 +executing==2.0.1 +fasteners==0.14.1 +fastjsonschema==2.19.0 +filelock==3.13.1 +fmriprep-docker==24.1.0 +folder-color-caja==0.0.86 +folder-color-common==0.0.86 +fqdn==1.5.1 +future==0.18.2 +gpg==1.13.1 +httplib2==0.14.0 +identify==2.5.35 +idna==2.8 +imagesize==1.4.1 +importlib-metadata==6.8.0 +importlib-resources==6.1.1 +ipykernel==6.27.1 +ipython==8.12.3 +ipywidgets==8.1.1 +isoduration==20.11.0 +itsdangerous==1.1.0 +jedi==0.19.1 +Jinja2==3.1.2 +json5==0.9.14 +jsonpointer==2.4 +jsonschema==4.20.0 +jsonschema-specifications==2023.11.2 +jupyter==1.0.0 +jupyter-client==8.6.0 +jupyter-console==6.6.3 +jupyter-core==5.5.0 +jupyter-events==0.9.0 +jupyter-lsp==2.2.1 +jupyter-server==2.11.1 +jupyter-server-terminals==0.4.4 +jupyterlab==4.0.9 +jupyterlab-pygments==0.3.0 +jupyterlab-server==2.25.2 +jupyterlab-widgets==3.0.9 +keyring==18.0.1 +kiwisolver==1.0.1 +language-selector==0.1 +latexcodec==3.0.0 +launchpadlib==1.10.13 +lazr.restfulclient==0.14.2 +lazr.uri==1.0.3 +lockfile==0.12.2 +louis==3.12.0 +macaroonbakery==1.3.1 +Magnus==1.0.3 +Mako==1.1.0 +MarkupSafe==2.1.3 +mate-hud==19.10.0 +mate-menu==20.4.1 +mate-tweak==20.4.0 +matplotlib-inline==0.1.6 +mistune==3.0.2 +monotonic==1.5 +nbclient==0.9.0 +nbconvert==7.11.0 +nbformat==5.9.2 +nest-asyncio==1.5.8 +netifaces==0.10.4 +nibabel==5.1.0 +niix2bids==2.5.0 +nodeenv==1.8.0 +notebook==7.0.6 +notebook-shim==0.2.3 +numpy==1.24.4 +oauthlib==3.1.0 +olefile==0.46 +onboard==1.4.1 +overrides==7.4.0 +packaging==23.2 +pandas==2.0.3 +pandocfilters==1.5.0 +paramiko==2.6.0 +parso==0.8.3 +pexpect==4.6.0 +pickleshare==0.7.5 +Pillow==7.0.0 +pkgutil-resolve-name==1.3.10 +platformdirs==4.0.0 +polib==1.1.0 +pre-commit==3.5.0 +prometheus-client==0.19.0 +prompt-toolkit==3.0.41 +protobuf==3.6.1 +psutil==5.5.1 +ptyprocess==0.7.0 +pulsemixer==1.5.0 +pure-eval==0.2.2 +pybtex==0.24.0 +pybtex-docutils==1.0.3 +pycairo==1.16.2 +pycparser==2.21 +pycrypto==2.6.1 +pycups==1.9.73 +pygame==1.9.6 +pygments==2.17.2 +PyGObject==3.36.0 +pyinotify==0.9.6 +PyJWT==1.7.1 +pymacaroons==0.13.0 +PyNaCl==1.3.0 +pyOpenSSL==19.0.0 +pyparsing==2.4.6 +pyRFC3339==1.1 +PySocks==1.7.1 +python-apt==2.0.1+ubuntu0.20.4.1 +python-dateutil==2.8.2 +python-debian==0.1.36+ubuntu1.1 +python-json-logger==2.0.7 +python-xapp==1.8.1 +python-xlib==0.23 +pytz==2024.2 +pyxattr==0.6.1 +pyxdg==0.26 +PyYAML==5.3.1 +pyzmq==25.1.1 +qtconsole==5.5.1 +QtPy==2.4.1 +referencing==0.31.1 +reportlab==3.5.34 +requests==2.31.0 +requests-unixsocket==0.2.0 +rfc3339-validator==0.1.4 +rfc3986-validator==0.1.1 +rpds-py==0.13.2 +SecretStorage==2.3.1 +Send2Trash==1.8.2 +setproctitle==1.1.10 +simplejson==3.16.0 +six==1.14.0 +sniffio==1.3.0 +snowballstemmer==2.2.0 +soupsieve==2.5 +sphinx==7.1.2 +sphinx-rtd-theme==3.0.2 +sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-bibtex==2.6.3 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-jquery==4.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-matlabdomain==0.22.1 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.5 +ssh-import-id==5.10 +stack-data==0.6.3 +systemd-python==234 +terminado==0.18.0 +tinycss2==1.2.1 +tomli==2.0.1 +tornado==6.4 +traitlets==5.14.0 +types-python-dateutil==2.8.19.14 +typing-extensions==4.8.0 +tzdata==2024.2 +ubuntu-advantage-tools==8001 +ubuntu-drivers-common==0.0.0 +ufw==0.36 +unattended-upgrades==0.1 +uri-template==1.3.0 +urllib3==1.25.8 +usb-creator==0.3.7 +virtualenv==20.24.7 +wadllib==1.3.3 +wcwidth==0.2.12 +webcolors==1.13 +webencodings==0.5.1 +websocket-client==1.6.4 +widgetsnbextension==4.0.9 +xkit==0.0.0 +youtube-dl==2020.3.24 +zipp==3.17.0