v2.0.0
Loading...
Searching...
No Matches
sts_cluster.cpp
Go to the documentation of this file.
1//=============================================================================================================
35
36//=============================================================================================================
37// INCLUDES
38//=============================================================================================================
39
40#include "sts_cluster.h"
41#include "sts_ttest.h"
42#include "sts_ftest.h"
43
44#include <QtConcurrent>
45#include <QRandomGenerator>
46
47#include <cmath>
48#include <algorithm>
49#include <queue>
50#include <vector>
51
52//=============================================================================================================
53// USED NAMESPACES
54//=============================================================================================================
55
56using namespace STSLIB;
57using namespace Eigen;
58
59//=============================================================================================================
60// DEFINE METHODS
61//=============================================================================================================
62
64 const QVector<MatrixXd>& dataA,
65 const QVector<MatrixXd>& dataB,
66 const SparseMatrix<int>& adjacency,
67 int nPermutations,
68 double clusterAlpha,
69 double pThreshold,
70 StatsTailType tail)
71{
72 Q_UNUSED(pThreshold);
73
74 const int nA = dataA.size();
75
76 // Step 1: Compute the observed t-map
77 MatrixXd tObs = computeTMap(dataA, dataB);
78
79 // Step 2: Determine cluster-forming threshold from clusterAlpha and df
80 int df = nA - 1;
81 double threshold = inverseTCdf(clusterAlpha, df);
82
83 // Step 3: Find observed clusters
84 auto [clusterIds, clusterStats] = findClusters(tObs, threshold, adjacency, tail);
85
86 // Step 4: Combine all data for permutation
87 QVector<MatrixXd> allData;
88 allData.reserve(nA + dataB.size());
89 for (const auto& m : dataA) allData.append(m);
90 for (const auto& m : dataB) allData.append(m);
91
92 // Step 5: Build null distribution via permutations (using QtConcurrent)
93 QVector<double> nullDist(nPermutations);
94
95 // Perform permutations in parallel
96 QVector<int> permIndices(nPermutations);
97 std::iota(permIndices.begin(), permIndices.end(), 0);
98
99 std::function<double(int)> permuteFunc = [&](int) -> double {
100 return permuteOnce(allData, nA, adjacency, threshold, tail);
101 };
102
103 QFuture<double> future = QtConcurrent::mapped(permIndices, permuteFunc);
104 future.waitForFinished();
105
106 for (int i = 0; i < nPermutations; ++i) {
107 nullDist[i] = future.resultAt(i);
108 }
109
110 // Sort null distribution for percentile computation
111 std::sort(nullDist.begin(), nullDist.end());
112
113 // Step 6: Compute cluster p-values
114 QVector<double> clusterPvals(clusterStats.size());
115 for (int c = 0; c < clusterStats.size(); ++c) {
116 double obsStat = std::fabs(clusterStats[c]);
117 int count = 0;
118 for (int p = 0; p < nPermutations; ++p) {
119 if (nullDist[p] >= obsStat) {
120 count++;
121 }
122 }
123 clusterPvals[c] = static_cast<double>(count) / static_cast<double>(nPermutations);
124 }
125
126 StatsClusterResult result;
127 result.matTObs = tObs;
128 result.vecClusterStats = clusterStats;
129 result.vecClusterPvals = clusterPvals;
130 result.matClusterIds = clusterIds;
131 result.clusterThreshold = threshold;
132 return result;
133}
134
135//=============================================================================================================
136
137MatrixXd StatsCluster::computeTMap(const QVector<MatrixXd>& dataA, const QVector<MatrixXd>& dataB)
138{
139 const int nSubjects = dataA.size();
140 const int nChannels = static_cast<int>(dataA[0].rows());
141 const int nTimes = static_cast<int>(dataA[0].cols());
142
143 // Compute difference for each subject
144 // Then compute paired t-statistic at each (channel, time)
145 MatrixXd sumDiff = MatrixXd::Zero(nChannels, nTimes);
146 MatrixXd sumDiffSq = MatrixXd::Zero(nChannels, nTimes);
147
148 for (int s = 0; s < nSubjects; ++s) {
149 MatrixXd diff = dataA[s] - dataB[s];
150 sumDiff += diff;
151 sumDiffSq += diff.cwiseProduct(diff);
152 }
153
154 double n = static_cast<double>(nSubjects);
155 MatrixXd mean = sumDiff / n;
156 MatrixXd variance = (sumDiffSq - sumDiff.cwiseProduct(sumDiff) / n) / (n - 1.0);
157
158 // Clamp variance to avoid division by zero
159 variance = variance.cwiseMax(1.0e-30);
160
161 MatrixXd tMap = mean.array() / (variance.array().sqrt() / std::sqrt(n));
162 return tMap;
163}
164
165//=============================================================================================================
166
167QPair<MatrixXi, QVector<double>> StatsCluster::findClusters(
168 const MatrixXd& tMap,
169 double threshold,
170 const SparseMatrix<int>& adjacency,
171 StatsTailType tail)
172{
173 const int nChannels = static_cast<int>(tMap.rows());
174 const int nTimes = static_cast<int>(tMap.cols());
175
176 MatrixXi clusterIds = MatrixXi::Zero(nChannels, nTimes);
177 QVector<double> clusterStats;
178 int currentClusterId = 0;
179
180 // Helper lambda: BFS on combined spatial+temporal adjacency for one polarity
181 auto bfsClusters = [&](bool positive) {
182 // Build a visited mask
183 MatrixXi visited = MatrixXi::Zero(nChannels, nTimes);
184
185 for (int ch = 0; ch < nChannels; ++ch) {
186 for (int t = 0; t < nTimes; ++t) {
187 double val = tMap(ch, t);
188 bool suprathreshold = positive ? (val > threshold) : (val < -threshold);
189
190 if (!suprathreshold || visited(ch, t)) continue;
191
192 // Start BFS for a new cluster
193 currentClusterId++;
194 double clusterSum = 0.0;
195 std::queue<std::pair<int, int>> queue;
196 queue.push({ch, t});
197 visited(ch, t) = 1;
198
199 while (!queue.empty()) {
200 auto [curCh, curT] = queue.front();
201 queue.pop();
202
203 clusterIds(curCh, curT) = positive ? currentClusterId : -currentClusterId;
204 clusterSum += tMap(curCh, curT);
205
206 // Temporal neighbors (consecutive time points)
207 for (int dt = -1; dt <= 1; dt += 2) {
208 int nt = curT + dt;
209 if (nt < 0 || nt >= nTimes) continue;
210 double nval = tMap(curCh, nt);
211 bool nSupra = positive ? (nval > threshold) : (nval < -threshold);
212 if (nSupra && !visited(curCh, nt)) {
213 visited(curCh, nt) = 1;
214 queue.push({curCh, nt});
215 }
216 }
217
218 // Spatial neighbors at the same time point
219 for (SparseMatrix<int>::InnerIterator it(adjacency, curCh); it; ++it) {
220 int nCh = static_cast<int>(it.row());
221 double nval = tMap(nCh, curT);
222 bool nSupra = positive ? (nval > threshold) : (nval < -threshold);
223 if (nSupra && !visited(nCh, curT)) {
224 visited(nCh, curT) = 1;
225 queue.push({nCh, curT});
226 }
227 }
228 }
229
230 clusterStats.append(clusterSum);
231 }
232 }
233 };
234
235 if (tail == StatsTailType::Both || tail == StatsTailType::Right) {
236 bfsClusters(true);
237 }
238 if (tail == StatsTailType::Both || tail == StatsTailType::Left) {
239 bfsClusters(false);
240 }
241
242 return {clusterIds, clusterStats};
243}
244
245//=============================================================================================================
246
247double StatsCluster::permuteOnce(
248 const QVector<MatrixXd>& allData,
249 int nA,
250 const SparseMatrix<int>& adjacency,
251 double threshold,
252 StatsTailType tail)
253{
254 const int nTotal = allData.size();
255
256 // Generate a random permutation of indices
257 std::vector<int> indices(nTotal);
258 std::iota(indices.begin(), indices.end(), 0);
259
260 // Use thread-safe random generator
261 QRandomGenerator rng(QRandomGenerator::global()->generate());
262 for (int i = nTotal - 1; i > 0; --i) {
263 int j = static_cast<int>(rng.bounded(i + 1));
264 std::swap(indices[i], indices[j]);
265 }
266
267 // Split into permuted groups
268 QVector<MatrixXd> permA, permB;
269 permA.reserve(nA);
270 permB.reserve(nTotal - nA);
271 for (int i = 0; i < nTotal; ++i) {
272 if (i < nA) {
273 permA.append(allData[indices[i]]);
274 } else {
275 permB.append(allData[indices[i]]);
276 }
277 }
278
279 // Compute t-map and find clusters
280 MatrixXd tMap = computeTMap(permA, permB);
281 auto [clusterIds, clusterStats] = findClusters(tMap, threshold, adjacency, tail);
282
283 // Return the maximum absolute cluster statistic
284 double maxStat = 0.0;
285 for (double s : clusterStats) {
286 double absS = std::fabs(s);
287 if (absS > maxStat) {
288 maxStat = absS;
289 }
290 }
291 return maxStat;
292}
293
294//=============================================================================================================
295
296double StatsCluster::inverseTCdf(double p, int df)
297{
298 // Approximate inverse t CDF for two-tailed threshold.
299 // We want the t-value such that P(|T| > t) = p, i.e., P(T > t) = p/2.
300 // This means we want the (1 - p/2) quantile.
301
302 // Use a simple bisection on the tCdf function.
303 double targetCdf = 1.0 - p / 2.0;
304
305 double lo = 0.0;
306 double hi = 100.0;
307
308 for (int iter = 0; iter < 100; ++iter) {
309 double mid = (lo + hi) / 2.0;
310 double cdf = StatsTtest::tCdf(mid, df);
311 if (cdf < targetCdf) {
312 lo = mid;
313 } else {
314 hi = mid;
315 }
316 }
317
318 return (lo + hi) / 2.0;
319}
320
321//=============================================================================================================
322
323MatrixXd StatsCluster::computeOneSampleTMap(const QVector<MatrixXd>& data)
324{
325 const int nSubjects = data.size();
326 const int nVertices = static_cast<int>(data[0].rows());
327 const int nTimes = static_cast<int>(data[0].cols());
328
329 MatrixXd sum = MatrixXd::Zero(nVertices, nTimes);
330 MatrixXd sumSq = MatrixXd::Zero(nVertices, nTimes);
331
332 for (int s = 0; s < nSubjects; ++s) {
333 sum += data[s];
334 sumSq += data[s].cwiseProduct(data[s]);
335 }
336
337 double n = static_cast<double>(nSubjects);
338 MatrixXd mean = sum / n;
339 MatrixXd variance = (sumSq - sum.cwiseProduct(sum) / n) / (n - 1.0);
340 variance = variance.cwiseMax(1.0e-30);
341
342 MatrixXd tMap = mean.array() / (variance.array().sqrt() / std::sqrt(n));
343 return tMap;
344}
345
346//=============================================================================================================
347
348MatrixXd StatsCluster::computeFMap(const QVector<QVector<MatrixXd>>& conditions)
349{
350 const int nConditions = conditions.size();
351 const int nVertices = static_cast<int>(conditions[0][0].rows());
352 const int nTimes = static_cast<int>(conditions[0][0].cols());
353
354 // Count total subjects and build per-condition means
355 int nTotal = 0;
356 for (int c = 0; c < nConditions; ++c) {
357 nTotal += conditions[c].size();
358 }
359
360 // Grand mean
361 MatrixXd grandSum = MatrixXd::Zero(nVertices, nTimes);
362 for (int c = 0; c < nConditions; ++c) {
363 for (int s = 0; s < conditions[c].size(); ++s) {
364 grandSum += conditions[c][s];
365 }
366 }
367 MatrixXd grandMean = grandSum / static_cast<double>(nTotal);
368
369 // SS between and SS within
370 MatrixXd ssBetween = MatrixXd::Zero(nVertices, nTimes);
371 MatrixXd ssWithin = MatrixXd::Zero(nVertices, nTimes);
372
373 for (int c = 0; c < nConditions; ++c) {
374 int nc = conditions[c].size();
375 MatrixXd condSum = MatrixXd::Zero(nVertices, nTimes);
376 for (int s = 0; s < nc; ++s) {
377 condSum += conditions[c][s];
378 }
379 MatrixXd condMean = condSum / static_cast<double>(nc);
380
381 MatrixXd diff = condMean - grandMean;
382 ssBetween += static_cast<double>(nc) * diff.cwiseProduct(diff);
383
384 for (int s = 0; s < nc; ++s) {
385 MatrixXd residual = conditions[c][s] - condMean;
386 ssWithin += residual.cwiseProduct(residual);
387 }
388 }
389
390 int dfBetween = nConditions - 1;
391 int dfWithin = nTotal - nConditions;
392
393 MatrixXd msBetween = ssBetween / static_cast<double>(dfBetween);
394 MatrixXd msWithin = ssWithin / static_cast<double>(dfWithin);
395 msWithin = msWithin.cwiseMax(1.0e-30);
396
397 MatrixXd fMap = msBetween.array() / msWithin.array();
398 return fMap;
399}
400
401//=============================================================================================================
402
403QPair<MatrixXi, QVector<double>> StatsCluster::findClustersFlat(
404 const MatrixXd& statMap,
405 double threshold,
406 const SparseMatrix<int>& adjacency,
407 bool positiveOnly)
408{
409 const int nVertices = static_cast<int>(statMap.rows());
410 const int nTimes = static_cast<int>(statMap.cols());
411 const int nTotal = nVertices * nTimes;
412
413 MatrixXi clusterIds = MatrixXi::Zero(nVertices, nTimes);
414 QVector<double> clusterStats;
415 int currentClusterId = 0;
416
417 // BFS on the flat spatio-temporal adjacency
418 std::vector<bool> visited(nTotal, false);
419
420 for (int idx = 0; idx < nTotal; ++idx) {
421 int v = idx / nTimes;
422 int t = idx % nTimes;
423 double val = statMap(v, t);
424
425 bool suprathreshold = positiveOnly ? (val > threshold) : (val < -threshold);
426 if (!suprathreshold || visited[idx]) continue;
427
428 currentClusterId++;
429 double clusterSum = 0.0;
430 std::queue<int> queue;
431 queue.push(idx);
432 visited[idx] = true;
433
434 while (!queue.empty()) {
435 int curIdx = queue.front();
436 queue.pop();
437
438 int curV = curIdx / nTimes;
439 int curT = curIdx % nTimes;
440
441 clusterIds(curV, curT) = positiveOnly ? currentClusterId : -currentClusterId;
442 clusterSum += statMap(curV, curT);
443
444 // Iterate over adjacency neighbors
445 for (SparseMatrix<int>::InnerIterator it(adjacency, curIdx); it; ++it) {
446 int nIdx = static_cast<int>(it.row());
447 if (visited[nIdx]) continue;
448 int nV = nIdx / nTimes;
449 int nT = nIdx % nTimes;
450 double nval = statMap(nV, nT);
451 bool nSupra = positiveOnly ? (nval > threshold) : (nval < -threshold);
452 if (nSupra) {
453 visited[nIdx] = true;
454 queue.push(nIdx);
455 }
456 }
457 }
458
459 clusterStats.append(clusterSum);
460 }
461
462 return {clusterIds, clusterStats};
463}
464
465//=============================================================================================================
466
467double StatsCluster::permuteOnceOneSample(
468 const QVector<MatrixXd>& data,
469 const SparseMatrix<int>& adjacency,
470 double threshold,
471 StatsTailType tail)
472{
473 const int nSubjects = data.size();
474
475 // Generate random sign flips
476 QRandomGenerator rng(QRandomGenerator::global()->generate());
477 QVector<int> signs(nSubjects);
478 for (int s = 0; s < nSubjects; ++s) {
479 signs[s] = rng.bounded(2) == 0 ? 1 : -1;
480 }
481
482 // Apply sign flips
483 QVector<MatrixXd> flipped(nSubjects);
484 for (int s = 0; s < nSubjects; ++s) {
485 flipped[s] = data[s] * static_cast<double>(signs[s]);
486 }
487
488 // Compute t-map and find clusters
489 MatrixXd tMap = computeOneSampleTMap(flipped);
490
491 double maxStat = 0.0;
492
493 if (tail == StatsTailType::Both || tail == StatsTailType::Right) {
494 auto [ids, stats] = findClustersFlat(tMap, threshold, adjacency, true);
495 for (double s : stats) {
496 if (std::fabs(s) > maxStat) maxStat = std::fabs(s);
497 }
498 }
499 if (tail == StatsTailType::Both || tail == StatsTailType::Left) {
500 auto [ids, stats] = findClustersFlat(tMap, threshold, adjacency, false);
501 for (double s : stats) {
502 if (std::fabs(s) > maxStat) maxStat = std::fabs(s);
503 }
504 }
505
506 return maxStat;
507}
508
509//=============================================================================================================
510
511double StatsCluster::permuteOnceFTest(
512 const QVector<MatrixXd>& allData,
513 const QVector<int>& groupSizes,
514 const SparseMatrix<int>& adjacency,
515 double threshold)
516{
517 const int nTotal = allData.size();
518
519 // Random permutation of indices
520 std::vector<int> indices(nTotal);
521 std::iota(indices.begin(), indices.end(), 0);
522
523 QRandomGenerator rng(QRandomGenerator::global()->generate());
524 for (int i = nTotal - 1; i > 0; --i) {
525 int j = static_cast<int>(rng.bounded(i + 1));
526 std::swap(indices[i], indices[j]);
527 }
528
529 // Rebuild condition groups
530 QVector<QVector<MatrixXd>> permConditions;
531 int offset = 0;
532 for (int c = 0; c < groupSizes.size(); ++c) {
533 QVector<MatrixXd> group;
534 group.reserve(groupSizes[c]);
535 for (int s = 0; s < groupSizes[c]; ++s) {
536 group.append(allData[indices[offset + s]]);
537 }
538 permConditions.append(group);
539 offset += groupSizes[c];
540 }
541
542 // Compute F-map and find clusters (F is always positive)
543 MatrixXd fMap = computeFMap(permConditions);
544 auto [ids, stats] = findClustersFlat(fMap, threshold, adjacency, true);
545
546 double maxStat = 0.0;
547 for (double s : stats) {
548 if (s > maxStat) maxStat = s;
549 }
550 return maxStat;
551}
552
553//=============================================================================================================
554
556 const QVector<MatrixXd>& data,
557 const SparseMatrix<int>& adjacency,
558 double threshold,
559 int nPermutations,
560 StatsTailType tail)
561{
562 // Step 1: Compute observed one-sample t-map
563 MatrixXd tObs = computeOneSampleTMap(data);
564
565 // Step 2: Find observed clusters
566 MatrixXi clusterIdsPos, clusterIdsNeg;
567 QVector<double> clusterStats;
568
569 if (tail == StatsTailType::Both || tail == StatsTailType::Right) {
570 auto [ids, stats] = findClustersFlat(tObs, threshold, adjacency, true);
571 clusterIdsPos = ids;
572 clusterStats.append(stats);
573 }
574 if (tail == StatsTailType::Both || tail == StatsTailType::Left) {
575 auto [ids, stats] = findClustersFlat(tObs, threshold, adjacency, false);
576 clusterIdsNeg = ids;
577 clusterStats.append(stats);
578 }
579
580 // Merge cluster ID maps
581 MatrixXi clusterIds = MatrixXi::Zero(tObs.rows(), tObs.cols());
582 if (clusterIdsPos.size() > 0) clusterIds += clusterIdsPos;
583 if (clusterIdsNeg.size() > 0) clusterIds += clusterIdsNeg;
584
585 // Step 3: Build null distribution via sign-flip permutations
586 QVector<int> permIndices(nPermutations);
587 std::iota(permIndices.begin(), permIndices.end(), 0);
588
589 std::function<double(int)> permuteFunc = [&](int) -> double {
590 return permuteOnceOneSample(data, adjacency, threshold, tail);
591 };
592
593 QFuture<double> future = QtConcurrent::mapped(permIndices, permuteFunc);
594 future.waitForFinished();
595
596 QVector<double> nullDist(nPermutations);
597 for (int i = 0; i < nPermutations; ++i) {
598 nullDist[i] = future.resultAt(i);
599 }
600 std::sort(nullDist.begin(), nullDist.end());
601
602 // Step 4: Compute cluster p-values
603 QVector<double> clusterPvals(clusterStats.size());
604 for (int c = 0; c < clusterStats.size(); ++c) {
605 double obsStat = std::fabs(clusterStats[c]);
606 int count = 0;
607 for (int p = 0; p < nPermutations; ++p) {
608 if (nullDist[p] >= obsStat) {
609 count++;
610 }
611 }
612 clusterPvals[c] = static_cast<double>(count) / static_cast<double>(nPermutations);
613 }
614
615 StatsClusterResult result;
616 result.matTObs = tObs;
617 result.vecClusterStats = clusterStats;
618 result.vecClusterPvals = clusterPvals;
619 result.matClusterIds = clusterIds;
620 result.clusterThreshold = threshold;
621 return result;
622}
623
624//=============================================================================================================
625
627 const QVector<QVector<MatrixXd>>& conditions,
628 const SparseMatrix<int>& adjacency,
629 double threshold,
630 int nPermutations)
631{
632 // Step 1: Compute observed F-map
633 MatrixXd fObs = computeFMap(conditions);
634
635 // Step 2: Find observed clusters (F is always positive, one-tailed)
636 auto [clusterIds, clusterStats] = findClustersFlat(fObs, threshold, adjacency, true);
637
638 // Step 3: Flatten all data and record group sizes
639 QVector<MatrixXd> allData;
640 QVector<int> groupSizes;
641 for (int c = 0; c < conditions.size(); ++c) {
642 groupSizes.append(conditions[c].size());
643 for (int s = 0; s < conditions[c].size(); ++s) {
644 allData.append(conditions[c][s]);
645 }
646 }
647
648 // Step 4: Build null distribution via label-shuffle permutations
649 QVector<int> permIndices(nPermutations);
650 std::iota(permIndices.begin(), permIndices.end(), 0);
651
652 std::function<double(int)> permuteFunc = [&](int) -> double {
653 return permuteOnceFTest(allData, groupSizes, adjacency, threshold);
654 };
655
656 QFuture<double> future = QtConcurrent::mapped(permIndices, permuteFunc);
657 future.waitForFinished();
658
659 QVector<double> nullDist(nPermutations);
660 for (int i = 0; i < nPermutations; ++i) {
661 nullDist[i] = future.resultAt(i);
662 }
663 std::sort(nullDist.begin(), nullDist.end());
664
665 // Step 5: Compute cluster p-values
666 QVector<double> clusterPvals(clusterStats.size());
667 for (int c = 0; c < clusterStats.size(); ++c) {
668 double obsStat = clusterStats[c];
669 int count = 0;
670 for (int p = 0; p < nPermutations; ++p) {
671 if (nullDist[p] >= obsStat) {
672 count++;
673 }
674 }
675 clusterPvals[c] = static_cast<double>(count) / static_cast<double>(nPermutations);
676 }
677
678 StatsClusterResult result;
679 result.matTObs = fObs;
680 result.vecClusterStats = clusterStats;
681 result.vecClusterPvals = clusterPvals;
682 result.matClusterIds = clusterIds;
683 result.clusterThreshold = threshold;
684 return result;
685}
686
687//=============================================================================================================
688
690 const MatrixXd& statMap,
691 const SparseMatrix<int>& adjacency,
692 double E,
693 double H,
694 int nSteps)
695{
696 const int nVertices = static_cast<int>(statMap.rows());
697 const int nTimes = static_cast<int>(statMap.cols());
698 const int nTotal = nVertices * nTimes;
699
700 MatrixXd tfceMap = MatrixXd::Zero(nVertices, nTimes);
701
702 // Helper: run TFCE on one polarity
703 auto runTfce = [&](const MatrixXd& absMap, bool isPositive) {
704 double maxVal = absMap.maxCoeff();
705 if (maxVal <= 0.0) return;
706
707 double dh = maxVal / static_cast<double>(nSteps);
708
709 for (int step = 1; step <= nSteps; ++step) {
710 double h = dh * static_cast<double>(step);
711
712 // Find connected components above threshold h
713 std::vector<bool> visited(nTotal, false);
714
715 for (int idx = 0; idx < nTotal; ++idx) {
716 int v = idx / nTimes;
717 int t = idx % nTimes;
718
719 if (absMap(v, t) < h || visited[idx]) continue;
720
721 // BFS to find cluster
722 std::vector<int> cluster;
723 std::queue<int> queue;
724 queue.push(idx);
725 visited[idx] = true;
726
727 while (!queue.empty()) {
728 int curIdx = queue.front();
729 queue.pop();
730 cluster.push_back(curIdx);
731
732 for (SparseMatrix<int>::InnerIterator it(adjacency, curIdx); it; ++it) {
733 int nIdx = static_cast<int>(it.row());
734 if (visited[nIdx]) continue;
735 int nV = nIdx / nTimes;
736 int nT = nIdx % nTimes;
737 if (absMap(nV, nT) >= h) {
738 visited[nIdx] = true;
739 queue.push(nIdx);
740 }
741 }
742 }
743
744 // Compute contribution: e^E * h^H * dh
745 double extent = static_cast<double>(cluster.size());
746 double contribution = std::pow(extent, E) * std::pow(h, H) * dh;
747
748 for (int cIdx : cluster) {
749 int cv = cIdx / nTimes;
750 int ct = cIdx % nTimes;
751 if (isPositive) {
752 tfceMap(cv, ct) += contribution;
753 } else {
754 tfceMap(cv, ct) -= contribution;
755 }
756 }
757 }
758 }
759 };
760
761 // Positive values
762 MatrixXd posMap = statMap.cwiseMax(0.0);
763 runTfce(posMap, true);
764
765 // Negative values (use absolute values, then negate contributions)
766 MatrixXd negMap = (-statMap).cwiseMax(0.0);
767 runTfce(negMap, false);
768
769 return tfceMap;
770}
Frequentist Student's t-tests with exact p-values via the regularised incomplete beta function.
One-way ANOVA F-test with exact p-values for comparing two or more independent groups.
Maris-Oostenveld cluster-mass permutation tests and Threshold-Free Cluster Enhancement for M/EEG infe...
Statistical testing (t-tests, F-tests, cluster permutation, multiple comparison correction).
StatsTailType
Direction of the alternative hypothesis for a t- or F-test (left, right, or two-sided).
Definition sts_types.h:37
Per-call output of a cluster permutation test: observed statistic map, cluster masses,...
Definition sts_cluster.h:72
Eigen::MatrixXd matTObs
Definition sts_cluster.h:73
Eigen::MatrixXi matClusterIds
Definition sts_cluster.h:76
QVector< double > vecClusterStats
Definition sts_cluster.h:74
QVector< double > vecClusterPvals
Definition sts_cluster.h:75
static StatsClusterResult permutationTest(const QVector< Eigen::MatrixXd > &dataA, const QVector< Eigen::MatrixXd > &dataB, const Eigen::SparseMatrix< int > &adjacency, int nPermutations=1024, double clusterAlpha=0.05, double pThreshold=0.05, StatsTailType tail=StatsTailType::Both)
static Eigen::MatrixXd tfce(const Eigen::MatrixXd &statMap, const Eigen::SparseMatrix< int > &adjacency, double E=0.5, double H=2.0, int nSteps=100)
static StatsClusterResult oneSamplePermutationTest(const QVector< Eigen::MatrixXd > &data, const Eigen::SparseMatrix< int > &adjacency, double threshold, int nPermutations, StatsTailType tail)
static StatsClusterResult fTestPermutationTest(const QVector< QVector< Eigen::MatrixXd > > &conditions, const Eigen::SparseMatrix< int > &adjacency, double threshold, int nPermutations)
static double tCdf(double t, int df)