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