feat(matlab): mergeNaive grouping (control=A2+Naive, tDCS=B2+Right) + GLM/power

Adds cfg.mergeNaive and scenario/paper/phase/power support. This pooled grouping
is the closest replication of the paper's full pattern: paper LME interaction
F(1)=7.30, p=0.008 (paper 7.12/0.008), stim main n.s. (p=0.21 = equal on Day 1),
strong day effect; the overall tDCS advantage is significant at the cluster-honest
per-animal level (rate p=0.003) and well powered. Adds a grouping test. Suite 38/38.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Experiments DB Dev
2026-07-20 12:50:35 -04:00
parent 7b5c27df8c
commit 77b631bcac
6 changed files with 62 additions and 2 deletions
+16
View File
@@ -155,3 +155,19 @@ matlab -batch "tdcs_glm('paper_mergeA2')" # or paper_unmerged / paper_mergeB2
also warns that the full-range interaction is confounded by unequal day coverage (Box-A2 ends also warns that the full-range interaction is confounded by unequal day coverage (Box-A2 ends
~day 13) — see the _d0_13 and phased analyses. The Monte-Carlo power analysis (`run_power`, ~day 13) — see the _d0_13 and phased analyses. The Monte-Carlo power analysis (`run_power`,
`tdcs_power_sim`) uses this same `behavior ~ stim + day + stim:day + (1|rat)` model. `tdcs_power_sim`) uses this same `behavior ~ stim + day + stim:day + (1|rat)` model.
## mergeNaive: pooled control vs tDCS
`mergeNaive` pools Naive into the control side (control = Box-A2 + Naive, n=7) and
Right-Electrode into the tDCS side (tDCS = Box-B2 + Right-Electrode, n=4); Box-A is left
out of the anchor contrast. Run the GLMMs, the paper LME, and power:
```
matlab -batch "tdcs_glm('mergeNaive_full')" # GLMM (rate/count) tDCS vs control
matlab -batch "tdcs_glm('paper_mergeNaive')" # paper LME: behavior ~ stim+day+stim:day+(1|rat)
matlab -batch "tdcs_power_sim('mergeNaive',[0 5])"
```
With the larger pooled control this is the closest replication of the paper's full pattern:
a significant days x tDCS interaction (F(1)=7.30, p=0.008 vs paper 7.12/0.008), an equal-on-Day-1
stim main effect (p=0.21, matching the paper's n.s.), and a strong day effect. The overall tDCS
advantage is significant even at the cluster-honest per-animal level (rate p=0.003). Caveat:
pooling Naive (no-stimulation control) with Box-A2 (an electrode condition) is a modeling choice.
+2 -1
View File
@@ -10,7 +10,8 @@ scenarios = {'unmerged_full', 'unmerged_d0_10', ...
'lme_mergeB2_full', 'lme_mergeB2_d0_10', ... 'lme_mergeB2_full', 'lme_mergeB2_d0_10', ...
'lme_unmerged_d0_13', 'lme_mergeA2_d0_13', 'lme_mergeB2_d0_13', ... 'lme_unmerged_d0_13', 'lme_mergeA2_d0_13', 'lme_mergeB2_d0_13', ...
'phase_unmerged', 'phase_mergeA2', 'phase_mergeB2', ... 'phase_unmerged', 'phase_mergeA2', 'phase_mergeB2', ...
'paper_unmerged', 'paper_mergeA2', 'paper_mergeB2'}; 'paper_unmerged', 'paper_mergeA2', 'paper_mergeB2', ...
'mergeNaive_full', 'paper_mergeNaive', 'phase_mergeNaive'};
for i = 1:numel(scenarios) for i = 1:numel(scenarios)
fprintf('\n\n### Running scenario: %s ###\n', scenarios{i}); fprintf('\n\n### Running scenario: %s ###\n', scenarios{i});
+7
View File
@@ -54,4 +54,11 @@ cfg.mergeB2 = containers.Map( ...
{'Electrode-Box-A', 'Right-Electrode'}, ... {'Electrode-Box-A', 'Right-Electrode'}, ...
{'Electrode-Box-B2', 'Electrode-Box-B2'}); {'Electrode-Box-B2', 'Electrode-Box-B2'});
% mergeNaive: pool Naive into the Box-A2 (control) side and Right-Electrode
% into the Box-B2 (tDCS) side. Control = Box-A2 + Naive (n=7), tDCS = Box-B2 +
% Right-Electrode (n=4); Electrode-Box-A is left unchanged.
cfg.mergeNaive = containers.Map( ...
{'Naive', 'Right-Electrode'}, ...
{'Electrode-Box-A2', 'Electrode-Box-B2'});
end end
+22
View File
@@ -60,6 +60,22 @@ switch scenario
R = tdcs_models(S, meta, cfg); R = tdcs_models(S, meta, cfg);
tdcs_report(S, R, meta, cfg, 'mergeB2_d0_10'); tdcs_report(S, R, meta, cfg, 'mergeB2_d0_10');
% --- mergeNaive: control = Box-A2 + Naive, tDCS = Box-B2 + Right-Electrode ---
case 'mergeNaive_full'
[S, meta] = tdcs_scenario_data('mergeNaive_full');
R = tdcs_models(S, meta, cfg);
tdcs_report(S, R, meta, cfg, 'mergeNaive_full');
case 'mergeNaive_d0_10'
[S, meta] = tdcs_scenario_data('mergeNaive_d0_10');
R = tdcs_models(S, meta, cfg);
tdcs_report(S, R, meta, cfg, 'mergeNaive_d0_10');
case 'mergeNaive_d0_13'
[S, meta] = tdcs_scenario_data('mergeNaive_d0_13');
R = tdcs_models(S, meta, cfg);
tdcs_report(S, R, meta, cfg, 'mergeNaive_d0_13');
% --- Linear "days x tDCS" mixed model (Box-B2 vs Box-A2), per the paper --- % --- Linear "days x tDCS" mixed model (Box-B2 vs Box-A2), per the paper ---
case 'lme_unmerged_full' case 'lme_unmerged_full'
L = tdcs_lme(tdcs_scenario_data('unmerged_full'), cfg); L = tdcs_lme(tdcs_scenario_data('unmerged_full'), cfg);
@@ -117,6 +133,12 @@ switch scenario
case 'paper_mergeB2' case 'paper_mergeB2'
tdcs_paper_lme('mergeB2', cfg); tdcs_paper_lme('mergeB2', cfg);
case 'paper_mergeNaive'
tdcs_paper_lme('mergeNaive', cfg);
case 'phase_mergeNaive'
tdcs_phase_lme('mergeNaive', cfg);
otherwise otherwise
error('tdcs_glm:badScenario', ... error('tdcs_glm:badScenario', ...
['Unrecognized scenario "%s". Valid scenarios are: ' ... ['Unrecognized scenario "%s". Valid scenarios are: ' ...
+3 -1
View File
@@ -41,6 +41,8 @@ switch mergeKey
mergeMap = cfg.mergeA2; mergeMap = cfg.mergeA2;
case 'mergeB2' case 'mergeB2'
mergeMap = cfg.mergeB2; mergeMap = cfg.mergeB2;
case 'mergeNaive'
mergeMap = cfg.mergeNaive;
otherwise otherwise
error('tdcs_scenario_data:badScenario', ... error('tdcs_scenario_data:badScenario', ...
'Unrecognized scenario "%s".', scenario); 'Unrecognized scenario "%s".', scenario);
@@ -108,7 +110,7 @@ else
'Scenario "%s" does not end in "_full", "_d0_10", or "_d0_13".', scenario); 'Scenario "%s" does not end in "_full", "_d0_10", or "_d0_13".', scenario);
end end
if ~ismember(mergeKey, {'unmerged', 'mergeA2', 'mergeB2'}) if ~ismember(mergeKey, {'unmerged', 'mergeA2', 'mergeB2', 'mergeNaive'})
error('tdcs_scenario_data:badScenario', ... error('tdcs_scenario_data:badScenario', ...
'Scenario "%s" has unrecognized merge key "%s".', scenario, mergeKey); 'Scenario "%s" has unrecognized merge key "%s".', scenario, mergeKey);
end end
+12
View File
@@ -59,6 +59,18 @@ classdef tLme < matlab.unittest.TestCase
testCase.verifyError(@() tdcs_glm('lme_nonsense'), 'tdcs_glm:badScenario'); testCase.verifyError(@() tdcs_glm('lme_nonsense'), 'tdcs_glm:badScenario');
end end
function testMergeNaiveGrouping(testCase)
% mergeNaive: control = Box-A2 + Naive (7), tDCS = Box-B2 + Right (4),
% Box-A unchanged (1); Naive is relabeled away.
[S, ~] = tdcs_scenario_data('mergeNaive_full');
sg = unique(S(:, {'subject','group'}));
n = @(g) sum(sg.group == g);
testCase.verifyEqual(n('Electrode-Box-A2'), 7);
testCase.verifyEqual(n('Electrode-Box-B2'), 4);
testCase.verifyEqual(n('Electrode-Box-A'), 1);
testCase.verifyFalse(any(sg.group == 'Naive'));
end
function testPaperFormulaReplicatesInteraction(testCase) function testPaperFormulaReplicatesInteraction(testCase)
% The paper's exact formula (behavior ~ stim + day + stim:day + % The paper's exact formula (behavior ~ stim + day + stim:day +
% (1|rat)) on mergeA2 reproduces the paper's interaction % (1|rat)) on mergeA2 reproduces the paper's interaction