Dependencies & provenance
Analysis scripts are copied into a study’s code subdataset and adapted there, so
every project keeps its own frozen copy. Their dependencies are not frozen:
CanlabCore, our
fork of CANlab_help_examples,
CanlabPrivate, canlab_single_trials and the rest are shared clones that keep moving
underneath the scripts.
That gap is the practical reproducibility problem in this workflow. A result from eighteen months ago can be re-read in its HTML report, but until recently nothing recorded which version of CanlabCore produced it.
Two questions, two tools
Going forward — record it at run time. Publish a script with
LaBGAScore_prov_publish instead of MATLAB’s publish, and the HTML report gains a
Provenance section listing the commit of every dependency the script reaches, plus a
machine-readable copy in the model’s results/notes/.
% instead of
publish('cfs_secondlevel_m14_s7_c2a_second_level_regression', 'outputDir', htmlsavedir)
% use
LaBGAScore_prov_publish('cfs_secondlevel_m14_s7_c2a_second_level_regression', htmlsavedir)
Looking back — reconstruct it. For analyses already run,
LaBGAScore_prov_resolve_retrospective recovers the same information after the fact.
It works because two records survive independently. Every artifact carries its own date:
a published report has its run date, the MATLAB version and its complete source embedded
in the HTML; a result .mat has a Created on: stamp in its file header, with a time of
day, which survives git-annex. And every clone keeps a git reflog recording which
commit it actually had checked out, and when. Intersecting the two gives the commit that
was genuinely in place.
This matters more than it sounds: only a handful of scripts per model are ever published
to HTML — the prep_ scripts write .mat files instead — so resolving reports alone
would leave most of the pipeline undocumented. Output is one page per run, grouping a
script’s report with the .mat files it wrote. Existing files are never modified.
Protect the reflogs first. Git prunes reflog entries after 90 days by default, and
it happens silently during garbage collection. The reflog cannot be reconstructed
afterwards — a commit’s own date says nothing about when your clone moved to it. Run
clean/labgascore_prov_protect_reflogs.sh once per machine to disable expiry.
Figures depend on the screen that made them
publish() captures figures from the screen, so the size and DPI of your X2go session
decide how figures in a report come out — and a figure larger than the session window is
captured at display size instead. Two consequences the workflow now handles explicitly:
- Figure size is set in inches, not pixels, because MATLAB font sizes are in points. The request is treated as a maximum and scaled down to fit the display, preserving the aspect ratio.
- Every report records the session’s screen geometry, DPI and the resulting figure dimensions, and flags figures whose size was decided by the display rather than the script — so a difference between two people’s reports is measurable rather than puzzling.
Run LaBGAScore_check_display to see what your own session can produce. Recommended X2go
settings per screen size are in the
fMRI analysis workflow.
What a commit hash does and does not tell you
A hash alone is not enough, because shared clones accumulate uncommitted local edits.
The tooling therefore also reports which modified files a given script actually
reaches, using a call graph built with mtree. In practice most repository/script
pairs come back clean, and the few that do not are named precisely rather than left as
a blanket warning.
What genuinely cannot be recovered is whether a dependency was dirty at the time — uncommitted work is not timestamped anywhere. That limit is stated in the output rather than glossed over.
Dependency overview
The tables below are generated by LaBGAScore_dep_report, which parses each script
with mtree and resolves every called name against an index of the installed toolboxes.
Full per-script detail lives in each repository’s DEPENDENCIES.md — at the root for
LaBGAScore, and in Second_level_analysis_template_scripts/ for the CANlab fork, where it
covers the 19 second-level templates LaBGAS actively maintains rather than the ~113
scripts present.
Resolution is deliberately conservative: only an unambiguous, repository-unique call can
introduce a dependency. Names that several toolboxes define, MATLAB’s own builtins, and
obj.name reads (which are usually object properties, not method calls) can reinforce a
dependency the script already has, but never create one.
Dependency data has not been collected yet. It is refreshed from each repository’s
published dependencies.yml by scripts/refresh_dependencies.py.
Reading the caveats
Resolution is static analysis, and MATLAB makes some of it genuinely undecidable. The generated documents mark every uncertain edge rather than guessing:
ambiguous— several classes define the name.thresholdexists in@atlas,@glm_map,@image_vectorand@statistic_image; deciding which one runs needs type inference these workspace-chained scripts do not support. All candidates are listed.dotcall— called asobj.name(...)and matched to a class method by name; it could also be a struct field.dynamic— the file usesfeval/eval/str2func, so its real call set cannot be recovered statically at all.unparseable— the file has a syntax error. There are a handful of these in the CANlab tree, which is why MATLAB’s ownrequiredFilesAndProductsis not used: it aborts on the first one it meets.