Details behind the multiple_tasks and lesion figure modes of
paper_plot.py: which caches each figure reads, what its statistics are, and
the conventions of the clustering caches. python multiple_task/run_pipeline.py
produces those caches (clustering, lesions, lesion plots); the commands are in
the README.
# multi-task: train, analyze (clustering), lesion, lesion plots. Every training
# script (one_task, one_task_rnn, two_task, multiple_task, pretraining) grows
# its cohort the same way (core/seed_selection.py): a run adds N_TRIALS seeds
# new to the setting configured in the script, never redrawing a seed with a
# saved checkpoint or a started run, so rerun it to add trials; --n-trials N
# changes the count and --seed S [S ...] trains explicit seeds (refused when
# their checkpoint exists). The runs just trained are listed in the output
# directory's last_run_anames.txt, which the pipelines read.
python multiple_task/multiple_task.py
python multiple_task/multiple_task.py --n-trials 3
python multiple_task/multiple_task.py --seed 11 42
python multiple_task/run_pipeline.py --seed 749 --feature L21e4
# several seeds of one cohort, run in the given order
python multiple_task/run_pipeline.py --feature L21e3 --seed 299 365 487 501 934
# every checkpoint of a cohort that has no pipeline outputs yet (rerun after
# more training). A run counts as completed when all three steps wrote their
# last file: cluster_info and cluster_info_mod, lesion_prune_results, and the
# two lesion_interaction pickles; a run missing any is redone from step 1.
# --skip-completed needs --feature or --seed; to spread a cohort over nodes,
# give each process its own --seed list (two bare runs would pick the same
# checkpoints). Each run takes about an hour of clustering plus four to six
# hours of lesions.
python multiple_task/run_pipeline.py --feature L21e4 --skip-completedThe lesion figure mode contains lesion_heatmap, lesion_cluster_sizes,
cluster_corr_vs_lesion, cluster_corr_vs_lesion_weighted,
cluster_corr_vs_lesion_seeds, om_vs_lesion, om_vs_lesion_seeds,
plasticity_share, plasticity_share_cohort, causal_vs_activity_tasks,
causal_vs_activity_seeds, task_specificity, task_sharing_cohort,
state_space_vs_lesion and state_space_vs_lesion_seeds. All but the four
_seeds and two _cohort figures read one run,
LESION_ANAME, independently of ANAME, which selects the multiple_tasks
clustering figures. These figures no longer run under multiple_tasks; use
python figures/paper_plot.py multiple_tasks lesion for both groups. Existing figure
names and --only FIGURE commands are unchanged. The historical leison
spelling is now lesion in commands, module names, output names and cache keys:
use multiple_task/lesion.py, multiple_task/lesion_plot.py and the lesion
paper mode. Existing caches have NOT been migrated or renamed. Readers accept
their legacy filenames, keys and condition labels through a read-only adapter;
correctly spelled files take precedence when both exist. Conflicting new/old
keys in one cache raise an error rather than losing data. New results use only
the corrected spelling; this compatibility does not bypass schema validation.
Mode runs retain the existing
behavior of clearing top-level paper_plot/ outputs; --only preserves other
figures.
The multiple_tasks and lesion figures read one L2 cohort of the
everything_seed*_L2<l2>+hidden300+batch128+angle runs, selected by
MULTITASK_L2 in paper_plot.py (currently 1e3) or per invocation with
python figures/paper_plot.py --multitask-l2 1e4 multiple_tasks lesion. The aggregate
figures (overmembership, cluster_corr_vs_lesion_seeds, om_vs_lesion_seeds,
plasticity_share_cohort, causal_vs_activity_seeds, task_sharing_cohort,
state_space_vs_lesion_seeds) summarize every cached seed of that cohort. The single-run figures use the
cohort's designated seed from MULTITASK_DESIGNATED_SEEDS (749 for clustering
and 921 for lesions in the 1e4 cohort; none for 1e3) when its cache directory
holds pickles; otherwise paper_plot picks the cached seed covering the most of
the category's cache directories, lowest seed on ties, and prints the switch.
Each category resolves once, so its figures never mix runs, and a run is never
taken from another cohort; this also applies to --only and to imported plotting
functions. Empty directories and pickles of other runs do not count as cached
data. Missing or incompatible individual caches still use the existing per-figure
checks rather than switching seeds per figure.
python figures/paper_plot.py --only cluster_corr_vs_lesion draws the four input/hidden
scatter figures and three modulation figures:
multitask_cluster_corr_vs_lesion_modulation_norm_freeze_M_n.png,
multitask_cluster_corr_vs_lesion_modulation_var_weighted_unnorm_freeze_M_n.png
and multitask_cluster_corr_vs_lesion_modulation_abs_weighted_unnorm_freeze_M_n.png
(--only cluster_corr_vs_lesion_weighted adds the Var(W·M) one,
..._modulation_weighted_unnorm_freeze_M_n.png). Every modulation figure uses
cached freeze_M effects, the intervention of the lesion heatmap
(paper_plot.LESION_HEATMAP_MODE; changing it there moves all of them), and
reads only that mode's cache, never zero_W as a substitute. The
two unnormalized figures use var-weighted (W·Var(M)) and abs-weighted
(|W|·Var(M)) modulation and drop the unresponsive cluster. A run whose
lesion_plot.py caches predate the abs-weighted variant skips only that
figure. Each figure is scale-free
on both axes: x is the Pearson correlation between cluster mean tuning
profiles, y is the Pearson correlation between per-task lesion effects after
z-scoring each effect against its stored random-control repeats (control SD
floored at one accuracy point). The caches store the dissimilarity 1 − r and
the rank statistics of that quantity; paper_plot.py shows the similarity r,
so it flips y, the sign of the saved Spearman rho and the saved bin medians
(their quartiles swap; the two-sided permutation p is unchanged) without
refitting anything. Only clusters whose summed |z|
exceeds the 95th percentile of a control-only, leave-one-repeat-out null are
compared. The legend reports Spearman rho with a two-sided cluster-label
(Mantel-type) permutation p from 10,000 permutations, because the cluster pairs
share clusters and are not independent samples. The red connector through the
points joins the saved quantile-bin medians (CLUSTER_CORR_N_BINS = 4 bins of
the tuning correlation; tied values stay together) with the interquartile range
of each bin as a bar. It describes the shape of the relation, which is
concentrated among the most similarly tuned pairs rather than linear across the
range; it is not the reported statistic. lesion_plot.py still saves its OLS
guide for its own diagnostic figure, but paper_plot.py no longer draws it.
Caches written before the bin medians were added draw without the connector
until lesion_plot.py is rerun. All entries come from
LESION_ANAME. Coordinates
and statistics are read from the schema_version=2 caches written by
lesion_plot.py; legacy caches holding OLS regressions are skipped, never
refitted. Missing or incompatible caches skip only the affected figure, with no
fallback to other variants or the other lesion mode. The caches also hold an L1
supplement (the former tuning-cosine vs lesion-effect L1 scatter and L1 vs the
pair's summed effect magnitude, which is what L1 mostly measures) that
lesion_plot.py draws in its per-run diagnostic figure; paper_plot.py does
not export it. The _n suffix is omitted with --no-legend.
--only cluster_corr_vs_lesion_seeds reads the same unnormalized hidden and
input caches for every sibling run of LESION_ANAME and plots each run's
similarity rho as a joined hidden/input pair, filled when the saved permutation
p is below 0.05, with the median over runs as a dash; a single run has only
8-17 clusters, so the cohort view is where the cluster-level relation should
be judged. The legend title carries the cohort-level test, a two-sided
Wilcoxon signed-rank p of the runs' rhos against zero for each cluster type,
computed in paper_plot.py from the saved per-run rhos (nothing below the run
level is recomputed). Like every lesion figure it describes the one cohort
selected by --multitask-l2; render it once per cohort to compare them.
python figures/paper_plot.py --only cluster_corr_vs_lesion_weighted draws the separate
Var(WM) comparison from LESION_ANAME, exporting only
multitask_cluster_corr_vs_lesion_modulation_weighted_unnorm_zero_W_n.png.
The upstream modulation_all_weighted_unnormalized
features take variance after multiplying modulation by static W, giving
W**2 * Var(M), unlike var_weighted, which gives W * Var(M).
The plot reads only the weighted-unnormalized_zero-W scatter cache, drops
the last unresponsive cluster, and reuses the saved profile correlations,
z-scored lesion dissimilarities and permutation statistics without refitting.
No clustering or lesion experiment is rerun. Use an allocated compute node for
rendering; this --only command preserves all other figures.
The lesion_heatmap compares hidden neuron lesions with var-weighted,
unnormalized modulation freeze_M lesions (the selected synapses keep W and
their modulation is held at its initial value, M(t) <- M(0);
paper_plot.LESION_HEATMAP_MODE), as two task-by-cluster panels; the input
neuron effects saved in the same cache are not drawn, and the shared color
scale covers only the two drawn panels. Its lesion_cluster_sizes companion
reads the memberships of the same freeze_M entry; zero_W and freeze_M lesion
the same synapse clusters, so the sizes do not depend on the mode.
lesion_plot.py exports both zero_W and freeze_M effects with explicit mode
metadata; missing freeze_M results are never replaced by zero_W in these two
figures. Every modulation tuning-vs-lesion scatter and the modulation side of
causal-vs-activity (below) follow the same mode. The OM scatter
(paper_plot.OM_LESION_MODE), task specificity and the synapse column of
state_space_vs_lesion still read zero_W (selected synaptic weights set to
zero); plasticity share compares the two modes by definition.
lesion_plot.py records freeze_M as primary_modulation_mode in the
normalized-effect cache.
The OM-versus-lesion-profile scatter uses Spearman rank correlation and a
one-sided (negative association) modulation-cluster-footprint permutation test,
not linear regression. lesion_plot.py saves the scatter, association
(rho, matching permutation p, null statistics and test metadata), and
binned_medians in both single-mode and combined schema_version=2 caches;
paper_plot.py takes the zero_W entry. Run selection differs from the other
lesion figures: the single-run scatter is drawn from ANAME, the clustering
example (seed 749 under --multitask-l2 1e4), the run of the
overmembership_examples_dots_* panels, so the example clusters are among its
points; every other single-run lesion figure uses LESION_ANAME (seed 921).
paper_plot._om_scatter_run holds the choice and the saved message names the
run. om_vs_lesion_seeds pools every sibling run either way. Because of the
shared run, the deciding block of overmembership_examples_dots_1 (its most
significant input × hidden block; both panels take it from
_om_example_selection) is drawn on top of the scatter in the viridis color
the examples give its OM, with a black ring ("Example block"). The cache keeps
no block identity per point, so the point is found by its OM value, which the
scatter copies from the same om_stack; the highlight is skipped with a note
unless exactly one point matches.
Up to five quantile bins provide median OM/L1 points and counts; tied OM values
stay together. The median connector is descriptive and is not forced to decrease.
paper_plot.py only reads these values, draws pale scatter points and the saved
median connector, and starts the L1 axis at zero. Pearson-only caches are skipped,
not silently reused. python figures/paper_plot.py --only om_vs_lesion_seeds draws the
cohort view, multitask_om_vs_lesion_seeds_n.png: one point per sibling run of
LESION_ANAME (same feature, any seed), the saved zero_W Spearman rho of that
run's OM scatter, filled when its saved one-sided permutation p is below 0.05,
with the median over runs as a dash. Because the per-run test is one-sided
(negative association), the legend title carries the one-sided Wilcoxon
signed-rank p of the runs' rhos being below zero; the other _seeds figures
keep their two-sided test. Runs whose OM cache is missing, legacy or has an
undefined rho are left out with a note.
python figures/paper_plot.py --only plasticity_share draws
multitask_plasticity_share_n.png from plasticity_share_var-weighted-unnormalized_<aname>.pkl.
For every (task, synapse cluster) cell whose zero_W effect is significant
(one-sided BH-FDR q = 0.05 against the stored random-control repeats),
lesion_plot.py saves the plasticity share = freeze_M effect / zero_W effect:
1 means freezing the cluster's plasticity costs the task as much as removing
its weights, 0 means the static wiring suffices. The figure shows one column
per task (no-working-memory tasks fdgo/fdanti/reactgo/reactanti first), the
saved cells as pale grey points and the saved task median as a red dash;
values outside the fixed y-limits appear as hollow grey triangles at the edge.
Task labels slant at 45° (_PLASTICITY_SHARE_TICK_ROTATION), low enough to
keep the long names short and high enough that their colored boxes do not
overlap. The figure shows
the values only: the one-sided task-level Mann-Whitney U test that
lesion_plot.py also saves (memory tasks exceeding no-memory tasks) is not
reported, and the former per-run plasticity_share_seeds figure was removed.
--only plasticity_share_cohort draws multitask_plasticity_share_cohort_n.png,
the same layout with every sibling run of LESION_ANAME (same feature, any
seed) overlaid: the saved significant cells of all runs pooled per task as
pale grey points, each run's saved task median as a short near-black dash (so
it stays apart from the grey cells), and the median of the pooled cells as
the red dash; the legend title gives the run count. A
run whose task list differs from the others is left out with a note, and the
figure is skipped below two runs. The share compares two interventions and is
not an exact additive partition of static and plastic contributions.
python figures/paper_plot.py --only causal_vs_activity_tasks draws
multitask_causal_vs_activity_tasks_hidden_n.png and ..._input_n.png from
causal_vs_activity_tasksim_<side>_<aname>.pkl: one point per task pair, x the
Pearson correlation of the two tasks' period-averaged raw (unnormalized) task
variance profiles over that side's neurons, y the correlation of their
lesion-effect profiles over all unnormalized input and hidden neuron clusters
(the causal_dependency_unnorm matrix). Per-neuron normalization was dropped
because it discards the amplitude that predicts causal impact; the normalized
pairing is kept in the cache under reference_variants. The
legend reports the saved two-sided Spearman task-label (Mantel-type)
permutation p, and the line is the saved OLS guide (the cluster-level
tuning-vs-lesion scatters show bin medians instead, see above).
A third side, ..._tasks_modulation_n.png from
causal_vs_activity_tasksim_modulation_<aname>.pkl, asks the same of the
synapses: x the correlation of the two tasks' period-averaged var-weighted
modulation features over all synapses, taken by magnitude (|W| Var(M); the
signed W Var(M) carries the sign of W in every task alike and would inflate
every pair), y the correlation of their freeze_M lesion-effect profiles
over the var-weighted unnormalized synapse clusters, the intervention of the
lesion heatmap. The unresponsive class is left out of y: it holds most
synapses and impairs every task, which would raise every pair together
(state_space_vs_lesion.py drops it for the same reason). A run gets this
cache only from a lesion_plot.py run made after the side was added; until
then the figure is skipped for it.
--only causal_vs_activity_seeds reads the same
caches for every sibling run of LESION_ANAME (same feature, any seed) and
plots one point per run in a hidden, an input and a modulation column (fixed
random jitter; the runs have no order), filled when the saved permutation p is
below 0.05 and a hollow grey ring otherwise, with each column's median (dash)
and interquartile range (bar).
Hidden and input share their lesion basis, so input is hidden's control: a
thin line joins each run's pair and a bracket gives the paired Wilcoxon
signed-rank p of hidden minus input, computed from the saved per-run rhos.
Modulation pairs other features with other lesions, so it stands apart behind
a dotted divider and is not joined. A run needs both neuron sides; the
modulation column holds the runs that have its cache. The panel has the
figure size and the square axes box (about 1.49 in) of the single-run
scatters, so it sits beside ..._tasks_hidden_n.png at one scale; it carries
no legend or run count, and its legend (filled and hollow markers, median and
IQR) is saved as multitask_causal_vs_activity_seeds_legend_n.png. The
bracket's test, the paired Wilcoxon signed-rank, belongs in the caption.
A response-period-only
activity vector is not used: it also makes the input side correlate. Both
figures need schema_version=2 caches written
by the current lesion_plot.py.
python figures/paper_plot.py --only task_specificity draws
multitask_task_specificity_counts_n.png from task_specificity_<aname>.pkl
for input and hidden neuron clusters (unnormalized clustering) and
var-weighted zero_W synapse clusters, the unresponsive class excluded. A
cluster "impairs" a task when its zero_W lesion effect is significant
(one-sided z against the control repeats, BH-FDR q = 0.05, the
causal-dependency mask). The figure has one column per cluster type: every
cluster is a dot at the number of tasks it impairs (ties spread sideways, a
dot histogram), the dash is the median, and the gray band is the spread the
counts would have under the saved mixing null, which shuffles cluster identity
within each task and so keeps each task's number of impaired clusters and the
mean count (mean ± one SD, the square root of the median null variance).
Dots spreading beyond the band mean specialized and hub-like clusters rather
than a uniform mix; the tick label gives the cluster count and the saved
one-sided permutation p of the observed variance exceeding that null. Nothing
is recomputed in paper_plot.py. The same cache holds, per cluster type, the
task-pair Jaccard overlap of impaired clusters grouped by the component the
two tasks differ in (TASK_PAIR_RELATIONS in lesion_plot.py) with one-sided
task-label permutation tests. python figures/paper_plot.py --only task_sharing_cohort
draws them at the cohort level, multitask_task_sharing_cohort_n.png: one
panel per cluster type and one column per relation (plus the pooled "All
related" column), each sibling run of LESION_ANAME a point at its excess
sharing, the saved mean Jaccard of the relation's pairs minus that of the
unrelated pairs, filled when the run's saved permutation p is below 0.05,
with the median over runs as a dash and, above each column, the one-sided
Wilcoxon signed-rank p of the runs' excess being above zero. Runs whose
cluster types or relation lists disagree are left out with a note; the
figure is skipped below two runs. The former per-run sharing figures were
removed.
python multiple_task/state_space_vs_lesion.py links the state-space and
lesion analyses over the task pairs. For every run with both a state-space
structure cache (state_space/state_space_structure_<aname>_noise0.01.pkl)
and lesion caches (multiple_tasks_norm/<aname>/), it relates the Euclidean
distance between the two tasks' full-dimensional context-end centroids (hidden
state and W ⊙ M, the saved distance matrices) to the similarity of their
lesion-effect profiles: for neuron clusters the saved causal_similarity of
causal_vs_activity_tasksim_hidden_<aname>.pkl (Pearson correlation of the
random-minus-lesion effects over all unnormalized input and hidden neuron
clusters, the causal-vs-activity matrix), for synapse clusters the Pearson
correlation of the var-weighted zero_W effects in
normalized_lesion_effects_<aname>.pkl over the synapse clusters without the
unresponsive class, whose label is read from the lesion pickle in
multiple_tasks_perf/ (the class holds most synapses, so its size-matched
random control collapses the network and its effect is near −0.8 for every
task). Each (representation, cluster type) pair gets a two-sided Spearman
task-label permutation (Mantel) test (10,000 permutations, seed 0;
core.state_space_geometry.mantel_spearman_permutation), a negative rho
meaning that tasks whose context-end states lie closer depend on more similar
clusters. Nothing is recomputed from network activity, so it runs in seconds;
the results go to state_space/state_space_vs_lesion_results.pkl (one entry
per run with the pair coordinates, the tests with their null arrays and the
L2), which the state-space batch run clears like every top-level file there,
so rerun it after state_space_structure.py or lesion_plot.py.
python figures/paper_plot.py --only state_space_vs_lesion draws
multitask_state_space_vs_lesion_n.png for LESION_ANAME: a 2 × 2 grid
with one row per representation (hidden-state and W ⊙ M centroid distance)
and one column per cluster type (neuron clusters, synapse clusters), one
point per task pair, x the centroid distance, y the lesion-profile
similarity, each panel's saved rho and permutation p in its legend and no
fitted line (the statistic is a rank correlation). --only state_space_vs_lesion_seeds draws
multitask_state_space_vs_lesion_seeds_n.png: per cluster type, every
sibling run's rho for the hidden-state and the W ⊙ M distance, filled when
its saved permutation p is below 0.05, the median over runs as a dash and
under each tick the two-sided Wilcoxon signed-rank p of the runs' rhos
against zero, computed in paper_plot.py from the saved per-run rhos; it is
skipped below two runs with results.
On a compute node, regenerate the post-processing cache and then the paper figure:
python multiple_task/lesion_plot.py --seed 921 --feature L21e4
python figures/paper_plot.py --only om_vs_lesionUse --seed all in the first command to update every matching run's OM results.
This reuses completed lesion experiments, but clears and regenerates the selected
runs' post-processing outputs in multiple_tasks_norm/.
cluster_info_mod_{aname}.pkl stores each grouped modulation clustering
result with col_labels_by_k only at the fixed lesion k (20) and at that
result's tolerance-selected k (clustering.prune_labels_by_k, recorded in
col_labels_by_k_kept). These are the only k values lesion.py reads back;
the between-modulation metric sweep over other k runs inside
multiple_task_analysis.py before saving. Pickles written before this change
hold one 90,000-synapse label array per candidate k (up to k = G = 1000) and
are several GB; they remain readable.
Every clustering appends the silent (unresponsive) neurons or synapses as one
extra class, labelled k + 1. Since 2026-09-29 that class is recorded
explicitly instead of being inferred from its position: each clustering result
carries col_unresponsive_mask / col_unresponsive_label (and
col_unresponsive_label_by_k for the grouped modulation clusterings), each
cluster_info_{aname}.pkl entry carries unresponsive_label and
unresponsive_neurons, the OM caches carry unresponsive_input_index /
unresponsive_hidden_index, and lesion_prune_results_{aname}.pkl records
unresponsive_labels / unresponsive_conditions per neuron variant,
unresponsive_pre_label / unresponsive_post_label per combined lesion and
unresponsive_label per modulation lesion (None when nothing was flagged).
lesion_plot.py and paper_plot.py read these fields; caches written before
they existed fall back to the former rule (the last cluster of an unnormalized
variant). The cluster_corr_vs_lesion
caches keep their exclude_last_cluster flag, now meaning that an unresponsive
exclusion was applied, and add unresponsive_source.
The plastic synapses of mp_layer1 are clustered under five task-variance
feature definitions, registered once in core/modulation_variants.py and
shared by multiple_task_analysis.py (clustering), lesion.py (lesions) and
lesion_plot.py (per-variant comparisons, in this order and with fixed colors):
| saved name | feature |
|---|---|
modulation_all_normalized |
Var(M), each synapse divided by its max over rules |
modulation_all_unnormalized |
Var(M) |
modulation_all_weighted_unnormalized |
Var(W·M), i.e. W²·Var(M) |
modulation_all_var_weighted_unnormalized |
W·Var(M), signed: positive- and negative-W synapses form separate clusters (in caches produced before 2026-09-25 every negative-W synapse had been classified as unresponsive, so those results describe positive-W synapses only) |
modulation_all_abs_weighted_unnormalized |
|W|·Var(M), magnitude-weighted and sign-blind |
All three weighted variants are computed from Var(M) by
weight_modulation_variance; since 2026-10-27 that includes weighted, as
W²·Var(M), where the script used to take the variance of a weighted copy of the
M trajectory. The two agree up to rounding (on seed 749: median relative
difference 5e-9, largest absolute difference 3e-8 of the feature's maximum),
so weighted caches clustered before and after the change compare.
Memory. multiple_task_analysis.py holds the recorded M trajectory once,
750 trials x T steps x 300 x 300 float32 (T is the longest evaluation trial,
which varies from run to run: 24 GiB at T = 96, 32 GiB at T = 126), plus the
clustering's working memory. record_tracked_states writes every chunk of the
forward pass into one preallocated array. Before 2026-10-27 the script also
held the concatenated chunks and the weighted copy, two to three trajectories
in all, and a 100 GiB job was killed (out of memory, the shell printing
Killed) on long trials. The script prints the job's cgroup memory limit and
use at the start and after the forward pass (core/memory_limit.py); psutil's
node-wide numbers, printed before, hid the limit.
Two sign-aware steps are identities for the non-negative variants and matter
only for var_weighted: clustering.unresponsive_row_mask judges a synapse
unresponsive by its mean |feature| (the previous signed mean treated every
negative-W synapse as unresponsive), and modulation_variants.signed_log1p
compresses unnormalized features symmetrically before clustering. Because the
signed variant's negative clusters have negative mean profiles, lesion_plot.py
compares its cluster tuning profiles by absolute value (tuning_profile in the
cluster-correlation caches). abs_weighted was added after the others; existing
cluster_info_mod_*.pkl and lesion_prune_results_*.pkl files do not contain
it, and their var_weighted entries predate the sign fixes. To obtain it, rerun
the pipeline for the run (python multiple_task/run_pipeline.py --seed 921 --feature L21e4), which reclusters, lesions and re-plots all five variants.
The lesion paper figures keep reading the var-weighted variant, while the
multiple_tasks mode draws one aggregate over-membership figure per variant
(overmembership_norm, overmembership_unnorm, overmembership_weighted,
overmembership_var_weighted, overmembership_abs_weighted); a run without
the abs_weighted prepost-belonging pickle is skipped by that figure only.
--only modulation_abs_weighted draws the clustered task-variance heatmap of
the abs_weighted variant (multitask_clustered_modulation_abs_weighted_n.png),
the |W|·Var(M) counterpart of the normalized modulation heatmap: same layout
and G=300 grouping, but the unnormalized cells are log-scaled from a 1e-4 floor
(the analysis script's own floor) to the run's maximum, so it exports its own
colorbar, multitask_clustered_modulation_abs_weighted_colorbar_n.png, instead
of sharing heatmap_colorbar.
python multiple_task/synapse_variance_decomposition.py asks how much of a
synapse's task tuning is the cluster identity of its two endpoint neurons.
For every run with cluster_info_<aname>.pkl and cluster_info_mod_<aname>.pkl
(--seed N --feature TAG for one run, --seed all --feature TAG for a
cohort, no arguments for every run), it takes the active synapses of the
var-weighted clustering (the unresponsive synapse class excluded, as the
over-membership analysis does), their task-period features in two versions,
magnitude = log1p(|W|·Var(M)) (primary; the sign of W is a per-synapse
attribute, not tuning: the signed clustering gives sign-pure clusters and the
sign alone accounts for about a fifth of the signed variance) and signed =
signed_log1p(W·Var(M)) (what the clustering saw), and reports the share of
the total sum of squares explained by least-squares fits of: the presynaptic
(input) neuron's cluster, the postsynaptic (hidden) neuron's cluster, both
additively, the pre × post block (interaction = block − additive), the
data-driven synapse clusters themselves (reference), the joint (block,
synapse cluster) cells (synapse_beyond_block = joint − block, the
structure the synapse clusters hold beyond the endpoint identity), and, at
the neuron level, the pre neuron, the post neuron and both additively (the
strongest endpoint model without an interaction). Two label sets are used
per version: fixed_k20 (the lesion convention, k = 20 plus the
unresponsive class, G = 300 grouping) and tolerance (the analysis figures'
tolerance-selected clusterings). Nulls (1,000 permutations, seed 0,
one-sided p): the cluster models permute the cluster labels over the neurons
(same cluster sizes, so the specific clusters are tested against arbitrary
groupings), the synapse model permutes the synapse labels, the beyond-block
model permutes them within each block (which removes the mechanical gain of
the extra cells), and the neuron-level models permute the synapse positions.
Per run the result goes to
multiple_tasks_analysis/<aname>/synapse_variance_decomposition_<aname>.pkl
(every R², null array and p, the sign-of-W share and the group counts);
the run prints a per-cohort summary of the medians. Nothing is recomputed
from network activity; a run takes a few minutes on a compute node.
multiple_task/multiple_task_diagnosis.py asks whether two sibling tasks
land in the same row cluster of the task-variance clustering, the rows of the
clustered input, hidden and modulation heatmaps (input/hidden
*_normalized, modulation modulation_all_normalized with the G=300
grouping). It reads only the saved cluster_info* pickles of every clustered
run of one cohort, so it takes a few minutes on a CPU node:
python multiple_task/multiple_task_diagnosis.py --feature L21e4
# seven sibling pairs and their summary by phase type
python multiple_task/sibling_row_sharing.py --feature L21e4
# untrained control: cluster fresh networks of one run's config (about 80 min
# each on a GPU node; add --skip-completed to resume a killed batch), then
# their row-cluster organization
python multiple_task/untrained_clustering.py --feature L21e4 --seed 749 --init-seed 0 1 2 3 4
python multiple_task/sibling_row_sharing.py --feature L21e4 --untrained
# paper figures: rows grouped by period or by task, and the per-task and
# per-period views
python figures/paper_plot.py --multitask-l2 1e4 --only row_cluster_organization
python figures/paper_plot.py --multitask-l2 1e4 --only row_cluster_same_task
python figures/paper_plot.py --multitask-l2 1e4 --only row_cluster_same_periodThree pairs are checked: ReactCategoryPro vs ReactCategoryAnti (dmcgo/dmcnogo),
ContextIntModality1 vs 2 and IntegrationModality1 vs 2, in every trial phase
both tasks have. For each pair, phase and representation the result is the
number of runs whose two rows share a row cluster. Each count is read against
a chance level: the fraction of pairs of rows of that phase from two
different tasks that share a cluster in that run and representation,
averaged over the runs (_same_phase_chance), with an exact two-sided
Poisson-binomial test over the runs' own chance levels
(_poisson_binomial_p). The chance level matters: on the L21e4 runs every
memory-period row of every task falls in one input and one hidden cluster
(chance 100%), so sharing in the memory periods is not specific to the
sibling pair, while the modulation chance stays between about 7% and 20%.
The script writes multiple_tasks_diagnosis/diagnosis_<tag>.{pkl,json} and
its own figure same_cluster_diagnoses_<tag>.png (tag <feature> or
<feature>_seed<seed>), replacing earlier files of the same tag only, so the
cohorts' results sit side by side. No paper figure draws these results;
they are an intermediate check.
multiple_task/sibling_row_sharing.py runs the same per-pair test on seven
sibling pairs in two groups, the pro / anti pairs (DelayPro / DelayAnti,
MemoryPro / MemoryAnti, ReactPro / ReactAnti, ReactMatch2Sample /
ReactNonMatch2Sample, ReactCategoryPro / ReactCategoryAnti) and the modality
pairs (ContextIntModality1 / 2, IntegrationModality1 / 2), reusing the
diagnosis's loading, chance levels and test. It then pools each group by
phase type, Stimulus (stim1, stim2), Memory (delay1, delay2) and Response
(go1): per run, the number of the group's (pair, phase) cells of that type
whose two rows share a cluster minus the number chance predicts, tested
over the runs with a two-sided Wilcoxon signed-rank test (the cells of one
run share one clustering, so their per-cell tests are not pooled). A pooled
cell whose mean chance is at least 95% (input and hidden in the memory
periods) is reported but not tested. A third check asks whether rows are
grouped by task rather than by period: for every task of the pairs and every
two of its phases, whether the task's two rows share a row cluster, against
the chance that a row of the one phase and a row of the other from two
different tasks share one (_cross_phase_chance, so the phase combination is
controlled for), pooled per task and per group and tested the same way;
cells whose own chance is at least 95% (the two memory periods in input and
hidden) are left out and counted. It writes
multiple_tasks_diagnosis/sibling_row_sharing_<tag>.{pkl,json}, the
per-pair figure sibling_row_sharing_pairs_<tag>.png, the summary figure
sibling_row_sharing_summary_<tag>.png and the same-task figure
sibling_row_sharing_same_task_<tag>.png, replacing only its own tag's
files. A star marks a per-run excess that differs from zero in either
direction; the bar against its chance dash gives the direction.
On the 25 L21e4 runs the pooled pro / anti stimulus cells sit at chance in
every representation because the pairs disagree: DelayPro / DelayAnti and
MemoryPro / MemoryAnti share their stimulus rows in input and hidden (15-17
of 25 runs, chance about 33%) but not in modulation (2-3 of 25), the
category pair the other way round (2 of 25 in input and hidden, 16 of 25 in
modulation), and the match pair in none. Within a period, input and hidden
group the rows by period and input (every task's memory rows in one
cluster), modulation by neither. Across periods, modulation keeps a task's
rows together: 71% of the pro / anti tasks' phase pairs and 48% of the
modality tasks' share a cluster, against chances of 11% and 10%, while input
and hidden stay near chance (20% and 24% against 15% and 18% for pro / anti;
16% and 27% against 9% and 20% for the modality tasks). DelayPro and
DelayAnti are the exception: their stimulus and response rows share an input
cluster in every run (the stimulus stays on) but a modulation cluster in
none. The modulation keeps a 4 s memory of the trial (lambda = 0.99), longer
than a period, so part of this grouping may be the trial's history carried
in M rather than a separate task code.
The organization check (summarize_organization) puts period and task on
one scale with every task's rows: per run and representation, the fold
enrichment of two rows of one period from different tasks, and of two rows
of one task from different periods, sharing a row cluster over any two rows
sharing one (the chance the run's cluster sizes give, so the
representations, with their different numbers of row clusters, compare),
tested against 1 over the runs and, as task minus period enrichment, between
representations (paired Wilcoxon). On the 25 L21e4 runs the medians are, for
period and task, input 1.85x and 0.90x, hidden 1.57x and 0.95x, synapses
1.10x and 3.51x, and every pair of representations differs (p <= 7.5e-5).
The same-task check is also run over every task (same_task_all; ReactPro
and ReactAnti have one clustered period and so no cells). Its mirror, the
same-period check (same_period_all), asks for every phase and every two
tasks with a row of it whether the two rows share a row cluster, against the
chance that a row of the one task and a row of the other from two different
phases share one (_cross_period_chance, so how alike the two tasks are
anyway is controlled for), pooled per phase and tested the same way; cells at
chance of at least 95% are left out (on the L21e4 runs 118 input, 92 hidden
and 445 synapse cells: task pairs whose rows sit in one cluster in every
period). On the 25 L21e4 runs the period grouping of input and hidden is
mostly the memory periods: every two tasks' Memory 1 and Memory 2 rows share
a cluster (input 100%, hidden 99-100%, chance 18-31%), because nothing in the
input varies there. Stimulus rows share more than chance but far less
(input 32% and 39% against 15% and 22%, hidden 32% and 46% against 21% and
31%), and Response rows hardly (input 13% against 16%, below chance; hidden
27% against 21%). Synapse rows share 2-20% against 4-8% in every period. The
untrained networks show the same pattern (memory 100%, stimulus 38-55%,
response 20-25%). Result version 4 holds all three checks.
paper_plot.py (multiple_tasks mode) draws three figures from
multiple_tasks_diagnosis/sibling_row_sharing_<MULTITASK_L2 feature>.pkl,
skipped with the command to run when it is missing or older than version 4:
multitask_row_cluster_organization.png, the two enrichments side by side
(same period, different task; same task, different period), one point per
run in the representation colors, the median as a red dash, chance (1) as a
dashed line on a log axis and the Wilcoxon p against 1 above each column
(the Mann-Whitney p against the untrained networks once they exist, below);
and multitask_row_cluster_same_task.png, the per-task view of its right
panel: for every task with two clustered periods, three bars giving the % of
its (run, period pair) cells in one row cluster, a dash at the
period-combination chance of the same-task check (a stricter baseline than
the organization figure's) and a star where the per-run excess differs from
zero in either direction; and multitask_row_cluster_same_period.png, the
per-period view of its left panel: for every period, three bars giving the %
of its (run, task pair) cells in one row cluster with the task-pair chance
of the same-period check, the period labels in the heatmap phase colors.
The untrained control asks whether training makes this organization or the
architecture alone does, for instance because M, with its 4 s memory, keeps
a trial's earlier periods in every later one. untrained_clustering.py
builds, for one run's config, an untrained network per init seed
(core/network_loading.build_network: the same architecture, weights drawn
after seeding NumPy and torch with the init seed, none of the trained ones;
the same builder the state-space control uses) and runs the complete
multiple_task_analysis.main clustering on it (untrained_init_seed), with
every output under
multiple_tasks_analysis/untrained/<aname>_init<k>/, named
..._init<k> (multiple_task_analysis.output_names), where no trained-run
reader looks. --skip-completed leaves out the networks whose two cluster
pickles exist; no lesion step runs. sibling_row_sharing.py --untrained
then analyzes those networks (tag <feature>_untrained). An untrained
network of seed 749's config is not degenerate: hidden std 0.12 (trained
0.16), M std 0.38, and up to about a quarter of its synapses reach their bounds
late in long trials, as in the trained network (modulation_feedback.py). The
organization figure draws the untrained networks, hollow with a gray median,
right of the trained runs in each representation's column, with the
two-sided Mann-Whitney p of trained against untrained above the column. The
per-task and per-period figures give every trained bar a hollow twin on its
right with the untrained networks' % and chance (a Wilcoxon star over the
networks needs at least six of them). All three draw the trained runs alone,
with the commands to run, while the untrained result is missing.
multiple_task/modulation_feedback.py asks how long M remembers, given
that M feeds back on itself through h. One step of the plastic layer is
h_t = tanh((W * (1 + M_{t-1})) x_t + b), then
M_t = clip(lam M_{t-1} + eta h_t x_t^T, -1, 1). Linearized, the row M_i
of post neuron i is lam times the identity plus a rank-one term, so on its
unclipped synapses every direction the neuron cannot read (orthogonal to
W_i * x) decays by exactly lam, whatever the training, and only the one it
reads, <W_i * x, M_i>, decays by
mu_i = lam + eta (1 - h_i^2) sum_{j unclipped} W_ij x_j^2; of the 90,000
synapse directions at most 300 are not set by lam. A clipped synapse stays at
its bound while eta h_i x_j exceeds 1 - lam (0.01 at lam = 0.99).
python multiple_task/modulation_feedback.py --feature L21e4 --seed 749
python multiple_task/modulation_feedback.py --feature L21e4 --seed 749 --untrained-init-seed 0
python multiple_task/modulation_feedback.py --feature L21e4 --seed allThe script steps the network through the clustering analysis's evaluation
trials (50 per task, mode_input="random", long_all=False), about a minute
per network on a CPU, and records at every step mu_i of every post neuron and
the share of synapses clipped to each bound. A finite-difference check
through the real network step, in double precision, comes first: one step's
response to a perturbation of M along a random direction, along the readable
mode and along an unreadable direction must match the linearized map, mu_i
and lam. The readable mode's time constant is dt / (1 - |mu|) (the
convention that gives lam its 4000 ms). It writes
multiple_tasks_diagnosis/modulation_feedback_<aname>[_init<k>].{pkl,png}:
per task and period the quantiles of mu_i, its shares above 1, below -1 and
below lam, the median time constant and the shares of synapses at each bound;
the same pooled by period type (Context, Stimulus, Memory, Response); each
task's saturation time course; and the check's errors.
On seed 749 (L21e4, eta = 2.15) the check holds to 1.2e-7 per unit perturbation. The readable mode's mu has a median of 0.986-0.987 in every period type, below lam for 80-84% of the post neurons and steps, so the median time constant is 2.9-3.1 s against lam's 4 s; mu exceeds 1 for 4% (Memory) to 14% (Response) and is never below -1, and a tail reaches mu of about 0.4 (5th percentile in Memory). Synapses at a bound rise from about 1% in the Context period to peaks of 15-25% late in the stimulus and memory periods. An untrained network of the same config (init seed 0, eta = 1) has mu symmetric about lam (median 0.988, 5-95% 0.94-1.03, above 1 for 18-28%), median time constants of 3.3-3.8 s and up to about 25% of synapses at a bound late in long trials. In neither network does the feedback lengthen M's typical memory: the unreadable directions keep lam's 4 s, the median readable mode decays a little faster (more so after training), and where mu exceeds 1 the mode grows until its synapses clip, which the saturation share counts.