MATLAB's minor grid ignored MinorTickValues and auto-subdivided finer than 0.02. Make 0.02 the actual (major) tick spacing for rate, label only every 0.1, and let grid on draw the fine 0.02 gridlines deterministically. Regenerated all figures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tDCS reaching study — MATLAB GLM port
This directory is a MATLAB port of the Python tDCS GLM analysis
(analysis/tdcs_glm.py, documented in analysis/METHODS.md), driven by raw
app-export matrices rather than the pre-joined tdcs_reach_data.csv. It
reproduces the same three models (count level, rate level, learning rate) and
the same H2 anchor check / unknown-condition classification, but replaces the
Python's population-average GEE with a fitglme GLMM (Poisson/Binomial,
subject random intercept) — see "GLMM vs GEE" below.
Requirements
-
MATLAB R2025b + Statistics and Machine Learning Toolbox.
-
A license activated at
~/matlab/licenses/license.dat(already in place in this environment). If MATLAB ever reports it is unlicensed, activate with:~/matlab/bin/activate_matlab.sh
Layout
tdcs_config.m— curated 12-animal group map, reference group, merge maps.tdcs_load_data.m— parsesraw/raw_success.csvandraw/raw_total.csv(app-export matrix format), joins them, and applies the curated group map.tdcs_scenario_data.m— builds one of the 6 scenarios (merge map + day window), and saves the scenario table toderived/<scenario>.csv.tdcs_models.m— fits the count/rate/learning GLMEs, the per-animal Welch/Mann-Whitney tests, the H2 anchor check, and (unmerged scenarios only) the unknown-condition classification.tdcs_report.m— prints descriptives, the raw GLME fixed-effects tables, and an interpretation section (H2 verdict, per-animal p-values, classification, caveats); also writesresults/<scenario>.txt.tdcs_glm.m— the entry point:tdcs_glm(scenario)runstdcs_scenario_data→tdcs_models→tdcs_reportfor one named scenario, via an explicitswitch/caseover all 6 scenario names.run_all.m— runs all 6 scenarios in one script.run_tests.m— runs thematlab.unittestsuite intests/and exits nonzero on any failure.tests/—matlab.unittesttest classes (tLoadData,tScenarioData,tModels) with golden values cross-checked against the Python.raw/— pre-generated app-export CSVs (input; not modified by this code).derived/— scenario tables written bytdcs_scenario_data(generated).results/— report text files written bytdcs_report/tdcs_glm(generated).
Running
Run one scenario (valid names: unmerged_full, unmerged_d0_10,
mergeA2_full, mergeA2_d0_10, mergeB2_full, mergeB2_d0_10):
cd analysis/matlab
~/matlab/bin/matlab -batch "addpath(pwd); tdcs_glm('mergeB2_full')"
This prints the full report to the console and writes it to
results/mergeB2_full.txt.
Run all 6 scenarios:
~/matlab/bin/matlab -batch "addpath(pwd); run_all"
Run the test suite:
~/matlab/bin/matlab -batch "addpath(pwd); run_tests"
(run_tests calls error(...) on any failure, so the matlab -batch
process exits with a nonzero status — suitable for CI.)
Validation against the Python
Per-animal statistics (per-subject Welch t-test / one-sided Mann-Whitney U,
Box-B2 vs Box-A2) are pure stats independent of the GLM choice, and were
matched to the Python's golden values to 3 decimals across all 6 scenarios
(see docs/superpowers/plans/2026-07-20-matlab-tdcs-analysis.md, Task 3).
GLMM vs GEE (read this before comparing numbers to the Python)
The Python reference uses GEE (population-average effects, exchangeable
working correlation, robust/sandwich SEs) for the count and learning-rate
models, and a cluster-robust Binomial GLM for the rate model. This MATLAB
port instead uses GLMM (fitglme, subject-specific effects via a
(1|subject) random intercept, Laplace/PL approximation) for all three
models, because MATLAB's Statistics and Machine Learning Toolbox has no GEE
implementation.
GLMM and GEE estimate related but distinct quantities (subject-specific vs. population-average effects), so exact coefficients, ratios, and p-values will differ between the two implementations — sometimes by a fair amount for the small, unbalanced groups in this study. What should agree is the interpretation: the direction of the H2 anchor effect, which unknown condition classifies as A2-like/B2-like/ambiguous, and whether the learning rates differ across groups. Treat the MATLAB numbers as a sensitivity check on the Python's conclusions, not as a numerically-identical reproduction.
Scope vs. the Python
The MATLAB port reproduces the Python's three core models (count, rate,
learning-rate), per-animal tests, unknown-group classification, and the H2
verdict (read off the main model's A2-vs-B2 term). The Python's two auxiliary
sensitivity sections — report_anchor_only (a two-anchor-only refit) and
report_mixed (a Bayesian MAP random-intercept model) — are intentionally
not ported: the anchor-only refit is closely tracked by the main-model H2
term, and MATLAB's native fitglme already is the random-intercept model.
Phased learning analysis and power
-
Phased days × tDCS LME — the same
success ~ day*tDCS + (1|subject)model refit within learning phases (0–5, 6–10, 6–13), Box-B2 vs Box-A2, with per-phase slopes and the interaction 95% CI:matlab -batch "tdcs_glm('phase_mergeA2')" # or phase_unmerged / phase_mergeB2The Box-B2 faster-acquisition signal sits in the early (0–5) phase; late phases converge. The interaction p/CI use fitlme observation-level DF and are anticonservative at these small n (see power analysis).
-
Monte-Carlo power — power to detect the early-phase interaction, scored with the cluster-honest per-animal test and the LME test, across subjects-per-group and effect sizes:
matlab -batch "run_power" # default: mergeA2, phase 0–5Result: at our n (3–5/group) honest power is ~0.3–0.7 even at the observed effect; ~8/group reaches ~80–90% if the effect is as large as observed, ~20/group if it is half that. The LME test is anticonservative only at small n and converges to the honest test by n≈12.
Figure and data export
- Figure — two-panel learning figure (curves + per-phase slopes, Box-B2 vs Box-A2),
written to
results/figure_learning.png:matlab -batch "make_figure" - Export all data variations to CSV — organized under
export/:Producesmatlab -batch "run_export"curated_all.csv,scenarios/<merge>_<window>.csv(9),anchors/<merge>_B2vsA2.csv(3, with the binary tDCS factor), andphases/<merge>_phase_<lo>-<hi>.csv(9), plus a MANIFEST.
Paper replication (verbatim formula)
The paper's model, fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)'), is
replicated verbatim (behavior = successful reaches, stim = tDCS [Box-B2=1 vs Box-A2=0],
rat = subject) over the full training range:
matlab -batch "tdcs_glm('paper_mergeA2')" # or paper_unmerged / paper_mergeB2
paper_mergeA2 reproduces the paper's interaction (F(1)=7.09 vs 7.12, p=0.009 vs 0.008); it
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,
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.