Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 KiB
MATLAB tDCS Analysis Port — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax.
Goal: Port the Python tDCS GLM analysis (analysis/tdcs_glm.py) to MATLAB under analysis/matlab/, driven by raw app exports, with 6 self-contained switch/case scenarios that print raw GLM output + interpretation, plus a matlab.unittest suite.
Architecture: fitglme (GLMM, per-subject random intercept) replaces the Python GEE. Two raw matrices (Success, Total attempts) are parsed and joined to a long table; a curated 12-animal map defines groups; each scenario applies a merge map + day window, fits the models, and reports.
Tech Stack: MATLAB R2025b + Statistics and Machine Learning Toolbox (installed at ~/matlab).
Global Constraints
- MATLAB is NOT runnable in this environment until the user's license is activated (
~/matlab/bin/activate_matlab.sh). Write all code and tests; they are executed later via~/matlab/bin/matlab -batch "run_tests". In each task, "run the test" means: the test is authored and self-consistent; note it is unrun pending license. Do not claim tests pass. - Reference implementation:
analysis/tdcs_glm.pyandanalysis/METHODS.md. Port equivalent behavior; do not change the Python. - Reference group =
Electrode-Box-B2;day_c= mean-centered day;day_c2 = day_c.^2. - Analysis restricted to day ≥ 0. The rate model only drops
total==0sessions; the count model keeps them. - Curated 12-animal group map (by animal name; all other animals in the raw files are dropped):
- Naive:
Vu-vuong, Khoai-lang-2, Khoai-tay-2, OM-2 - Electrode-Box-A:
Khoai-tay-1 - Electrode-Box-A2:
Banh-mi-2, Egg-tart-2, Root-beer-2 - Electrode-Box-B2:
Banh-mi-1, Egg-tart-1, Root-beer-1 - Right-Electrode:
Khoai-lang-1
- Naive:
- Merge maps:
mergeA2 = {Electrode-Box-A→Electrode-Box-A2, Right-Electrode→Electrode-Box-B2};mergeB2 = {Electrode-Box-A→Electrode-Box-B2, Right-Electrode→Electrode-Box-B2};unmerged = {}(5 groups, classify unknowns). - Raw files already generated at
analysis/matlab/raw/raw_success.csvandraw_total.csv(app export format:Data,<metric>/ blank /Group,…/# Days Reach,<subjects>/<day>,<values>). Contain all 15 animals incl. negative days. - Load-check cells (post-join, day≥0):
Vu-vuongday 0 → success=18, total=61;Banh-mi-1day 0 → success=13, total=84.
Task 1: Data loading + curated map
Files:
- Create:
analysis/matlab/tdcs_config.m(constants: analysis group map, merge maps, ref group) - Create:
analysis/matlab/tdcs_load_data.m - Create:
analysis/matlab/tests/tLoadData.m
Interfaces:
- Produces
cfg = tdcs_config()returning a struct:cfg.groups(containers.Map name→group for the 12),cfg.ref='Electrode-Box-B2',cfg.mergeA2,cfg.mergeB2(containers.Map),cfg.anchorLow='Electrode-Box-A2',cfg.anchorHigh='Electrode-Box-B2',cfg.unknowns={'Electrode-Box-A','Right-Electrode'}. - Produces
T = tdcs_load_data()returning a table with variablessubject(string),group(categorical),day(double),success(double),total(double); only the 12 curated animals; onlyday >= 0.
Steps:
- Step 1: Write
tdcs_config.m— return the struct above using the Global Constraints values.containers.Mapfor the name→group map (all 12 entries) and the two merge maps. - Step 2: Write the failing test
tests/tLoadData.m(matlab.unittest). Assertions:Thas 12 unique subjects;height(T)matches the non-blank day≥0 rows.- Group subject counts: Naive=4, Electrode-Box-A2=3, Electrode-Box-B2=3, Electrode-Box-A=1, Right-Electrode=1.
T(T.subject=="Vu-vuong" & T.day==0, {'success','total'})equals[18 61].T(T.subject=="Banh-mi-1" & T.day==0, {'success','total'})equals[13 84].- No rows with
day < 0.
- Step 3: Write
tdcs_load_data.m. Port the CSV-matrix parsing:- Read
raw/raw_success.csvandraw/raw_total.csvas raw lines (readlinesorfileread+splitlines). - Line 1 =
Data,<metric>(skip); line 2 blank (skip); line 3 =Group,<…>(skip — grouping comes from the curated map, not this row); line 4 =# Days Reach,<subject headers>→ subject names (columns 2:end); lines 5+ =<day>,<value…>. - Build long form: for each (day, subject) cell that is non-empty, emit a row. Parse success matrix and total matrix separately, then
innerjoin/outerjoinon (subject, day). A cell present in one metric but blank in the other → treat blank as missing; keep the row only if success present (matches Python: rows come from Success; total is fully populated where success is). - Apply the curated map: keep only subjects in
cfg.groups; assigngroupfrom the map (overriding the file's Group row). Dropday < 0. Return the table.
- Read
- Step 4: (Deferred) run
~/matlab/bin/matlab -batch "run_tests"once licensed; expecttLoadDatagreen. For now, self-review the parsing against the two raw files and the load-check cells by reading. - Step 5: Commit
git add analysis/matlab && git commit -m "matlab(tdcs): data loading + curated group map"
Task 2: Scenario derivation
Files:
- Create:
analysis/matlab/tdcs_scenario_data.m - Create:
analysis/matlab/tests/tScenarioData.m
Interfaces:
- Consumes
tdcs_load_data,tdcs_config. - Produces
[S, meta] = tdcs_scenario_data(scenario)wherescenario∈ {unmerged_full,unmerged_d0_10,mergeA2_full,mergeA2_d0_10,mergeB2_full,mergeB2_d0_10}.S= scenario table (group remapped, day window applied,day_c/day_c2added,groupcategorical withcfg.refas the FIRST/reference category).meta= struct with.mergeKey,.maxDay,.isUnmerged. Also savesStoanalysis/matlab/derived/<scenario>.csv(create the folder if missing).
Steps:
-
Step 1: Write the failing test
tests/tScenarioData.m. For each scenario, assert the per-group subject counts match this table, and thatmax(S.day) <= maxDay:scenario subj/group maxDay unmerged_full A=1,A2=3,B2=3,Naive=4,Right=1 26 unmerged_d0_10 A=1,A2=3,B2=3,Naive=4,Right=1 10 mergeA2_full A2=4,B2=4,Naive=4 26 mergeA2_d0_10 A2=4,B2=4,Naive=4 10 mergeB2_full A2=3,B2=5,Naive=4 26 mergeB2_d0_10 A2=3,B2=5,Naive=4 10 Also assert
derived/mergeB2_full.csvis written and reloads to the same subject/group counts, and that the reference level ofS.groupisElectrode-Box-B2. -
Step 2: Write
tdcs_scenario_data.m. Parsescenariointo(mergeKey, maxDay)by splitting on_full/_d0_10. LoadT, apply the mergecontainers.Map(relabel groups;unmerged= identity), filterday <= maxDay, center day, addday_c2, setgroupcategorical withreordercatssocfg.refis first. Writederived/<scenario>.csvviawritetable. -
Step 3: (Deferred) run tests once licensed. Self-review the counts against the table by reading.
-
Step 4: Commit
git commit -am "matlab(tdcs): scenario derivation + derived data export"
Task 3: Models (GLMM + per-animal + classification)
Files:
- Create:
analysis/matlab/tdcs_models.m - Create:
analysis/matlab/tests/tModels.m
Interfaces:
- Consumes
tdcs_scenario_data,tdcs_config. - Produces
R = tdcs_models(S, meta, cfg)returning a struct of results:R.countGLME,R.rateGLME,R.learnGLME— theGeneralizedLinearMixedModelobjects.R.perAnimal— struct with.countWelchP, .countMWUp, .rateWelchP, .rateMWUp, .nB, .nA(B2 vs A2).R.h2— struct with.countIRR, .countOneSidedP, .rateOR, .rateOneSidedP(Box-A2 vs B2 from the GLMEs).R.classify— (unmerged only) struct per unknown with vs-A2 and vs-B2 ratios/p and a label; empty otherwise.
Steps:
-
Step 1: Write the failing test
tests/tModels.musing these Python golden values (per-animal tests are pure stats and must match to 3 decimals; GLME checks are loose direction/magnitude):Per-animal (B2 vs A2),
assertEqual(..., 'AbsTol', 0.005):scenario nB,nA rateWelchP rateMWUp countWelchP countMWUp unmerged_full 3,3 0.072 0.050 0.055 0.050 mergeA2_full 4,4 0.056 0.029 0.035 0.029 mergeB2_full 5,3 0.044 0.018 0.020 0.018 unmerged_d0_10 3,3 0.070 0.050 0.041 0.050 mergeA2_d0_10 4,4 0.069 0.057 0.023 0.014 mergeB2_d0_10 5,3 0.094 0.071 0.023 0.018 GLME sanity (loose): for
mergeB2_full,R.h2.rateORin[1.3, 1.7]andR.h2.rateOneSidedP < 0.05andR.h2.countIRR > 1. Forunmerged_full,R.classifyhas entries for both unknowns. -
Step 2: Write
tdcs_models.m. Port the models:- Count:
fitglme(S, 'success ~ group + day_c + day_c2 + (1|subject)', 'Distribution','Poisson','Link','log'). - Rate: drop
total==0rows, thenfitglme(Sr, 'success ~ group + day_c + day_c2 + (1|subject)', 'Distribution','Binomial','BinomialSize',Sr.total,'Link','logit'). - Learning:
fitglme(S, 'success ~ group*day_c + day_c2 + (1|subject)', 'Distribution','Poisson'); joint test on the interaction terms viacoefTest. - H2: from each GLME read the
[T.Electrode-Box-A2]coefficient (vs B2 ref); IRR/OR =exp(coef); one-sided p =normcdf(coef/se)(tests coef<0). Report1/exp(coef)as the B2 advantage. - Per-animal: build per-subject summaries (mean success; pooled Σsuccess/Σtotal), then Welch via
ttest2(b,a,'Vartype','unequal')and one-sided MWU viaranksum(b,a,'tail','right'). (Direction: B2 vs A2.) - Classification (unmerged): for each unknown, contrast vs A2 and vs B2 (use
coefTestwith a contrast vector, or read coefficients + covariance fromR.<model>.CoefficientCovariance), label A2-like/B2-like/ambiguous per METHODS.md.
- Count:
-
Step 3: (Deferred) run tests once licensed. Self-review the estimator calls and the per-animal math against the Python.
-
Step 4: Commit
git commit -am "matlab(tdcs): GLMM + per-animal + classification models"
Task 4: Report, entry point, runners
Files:
- Create:
analysis/matlab/tdcs_report.m,analysis/matlab/tdcs_glm.m,analysis/matlab/run_all.m,analysis/matlab/run_tests.m - Create:
analysis/matlab/README.md
Interfaces:
- Consumes all prior. Produces
tdcs_glm(scenario)— theswitch/caseentry that validates the scenario name, callstdcs_scenario_data→tdcs_models→tdcs_report, prints to console and writesresults/<scenario>.txt.
Steps:
- Step 1: Write
tdcs_report.m— givenS,R,meta,cfg, print: (1) descriptives (per group: n_subj, n_sessions, mean success, pooled rate); (2) raw GLM —disp(R.countGLME)anddisp(R.rateGLME)fixed-effects tables + the learning joint test; (3) interpretation mirroringverdicts()/classify()in the Python (H2 verdict with IRR/OR + one-sided p; per-animal p's; classification for unmerged; the standing caveats). Usefprintf; also capture toresults/<scenario>.txtviadiaryor an output string. - Step 2: Write
tdcs_glm.mwith an explicitswitch scenarioover the 6 names (each case is a self-contained call;otherwiseerrors with the valid list). Even though cases share the pipeline, enumerate all 6 so each is independently invocable and greppable, per the spec. - Step 3: Write
run_all.m(loop the 6 scenarios callingtdcs_glm) andrun_tests.m(results = runtests('analysis/matlab/tests'); disp(results);with a nonzero exit on failure for-batch). - Step 4: Write
README.md— how to activate the license, run one scenario (matlab -batch "tdcs_glm('mergeB2_full')"), run all, run tests; note results were validated against the Python golden values and that GLMM ≠ GEE (interpretation matches, exact numbers differ). - Step 5: (Deferred) once licensed, run
matlab -batch "run_tests"andmatlab -batch "run_all", capture output, fix failures. Until then, self-review by reading. - Step 6: Commit
git commit -am "matlab(tdcs): report, switch/case entry, runners, README"
Self-Review Notes
- Spec coverage: raw files in website format (Task 1 input, pre-generated) · parse+join+curated map (Task 1) · day windows + merge scenarios saved (Task 2) ·
fitglmecount/rate/learning + per-animal + classification (Task 3) · switch/case per scenario + raw GLM + interpretation (Task 4) ·matlab.unittestsuite with golden values (Tasks 1–3). All covered. - Runtime caveat is pervasive: every task defers actual execution to post-licensing; reviews are by reading + golden-value cross-checks, and no task may claim tests pass.
- Golden values are authoritative (computed from the Python this session). Per-animal stats must match to 3 decimals; GLME magnitudes are checked loosely because GLMM ≠ GEE.
- Out of scope (per spec): GEE/sandwich SEs, MATLAB plots, Bayesian MAP model, editing the Python or the DB.