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