Add the naive per-condition baseline arm and manuscript Figure 3 - #311
Merged
Conversation
The naive baseline arm (EPIC #290 Phase 4) needs two knobs: the times_seen threshold and the list of conditions to fit. Both go in the downstream tier, never config.yaml, which is input: on all four fit rules -- Snakemake 9.19 keys reruns on content checksums, so any edit there forces the 2 h 20 min refit. times_seen_threshold is 1 to match evaluate.ipynb's joint-arm call. Filtering the two arms differently rigs the headline comparison: joint shift_Delta replicate R^2 reads 0.543 at threshold 3 but 0.410 at 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four helpers in _downstream.py, the downstream-tier sibling of _common.py (which is input: on all four fit rules, so a helper added there would invalidate the cached fit): fit_single_condition one (replicate, condition) model fit_naive_arm all six, with convergence and GE diagnostics derive_naive_shifts beta_condition - beta_reference on the inner join assert_wt_agreement guards the intersection premise fusionreg and beta0_ridge are not passed at all: every fusion term in jaxmodels is guarded by d != reference_condition, so with one condition they are inert. test_regularizers_are_inert_for_a_single_condition pins that with a bit-identical assertion -- if it ever drifts, the arm has acquired a lambda dependence and the no-ladder cost model is void. Shifts are derived on an inner join because 32 spike sites carry different wildtype letters across conditions; a union would pair non-equivalent labels. assert_wt_agreement verifies the premise instead of trusting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three fixes from the fresh-context plan review: 1. fit_single_condition now forwards fusionreg and beta0_ridge instead of letting them sit at Model.__init__'s 0.0 defaults. The V1 test compared configs at 0.0 and 1e6 but neither value ever reached the model, so it passed vacuously and could not have failed even if the d != reference_condition guard were removed. It now exercises the real mechanism. 2. assert_wt_agreement rejects sites absent from site_map. nunique skips NaN, so an all-NaN merge row scored 0 distinct letters and slipped through the > 1 disagreement test unchecked. An unverifiable site is not an agreeing one. 3. derive_naive_shifts raises a named error when the reference condition was fitted for no replicate, rather than failing inside pd.concat with 'No objects to concatenate'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reads only training_functional_scores.csv -- never fit_collection.pkl (1.76 GB, ~7 GB RSS). Fits one model per (replicate, condition), derives naive shifts on the inner-joined mutation index, exports three tables. Verified against the cached prod run: 6/6 fits converge in 4.5 minutes total, and the shared mutation index comes to 5,926 (rep 1) and 5,969 (rep 2) against the manuscript's reported 5,934 -- close agreement that would not hold at times_seen_threshold=3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new rule declares the fit-tier config and _common.py as inputs, which rule evaluate already does: it is a fit, not a figure rule. That cannot invalidate fit_collection.pkl, since invalidation flows along edges into a rule's outputs and this rule produces no input of any fit-tier rule. Verified on the cached prod run -- the dry-run DAG lists naive_baseline, evaluate and manuscript_figures, and does not list prepare_data, fit_models or cross_validation. Figure 3 targets shift_distribution_correlation_naive. The manuscript labels it fig:shift_distribution_correlation_effect and an orphaned ..._effect.pdf exists alongside it, which is an older version carrying no naive panels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six panels in one mosaic, matching the legacy figure's layout and the single \includegraphics in main.tex. A/B are class-split distributions; C-F are replicate-agreement scatters pairing the joint arm against the naive baseline on one shared mutation index at times_seen_threshold=1. Two departures from the legacy code, both because the legacy constants no longer fit this pipeline's fit: * Axis limits are derived from the data rather than hard-coded. The legacy (-6, 3) and (-11, 6) ranges were sized for the legacy beta spread and leave these panels ~70% empty. * Empty mutation classes are skipped. This dataset carries no deletions, and a legend reading 'n = 0' implies the class was measured rather than absent. A following cell measures the two corrections that keep the pooled numbers from being over-read: the permutation null for the shared-beta_ref confound (null 0.70/0.81, HIGHER than observed) and the coverage stratification. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Placed between spike_cross_validation and spike_manuscript_figures to match the pipeline's execution order -- naive_baseline runs after evaluate and feeds manuscript_figures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test profile fits two outer sweeps on a small dataset, so its top coverage strata hold fewer than two mutations and pearsonr raised 'x and y must have length at least 2', killing manuscript_figures and with it the whole test-profile run. _r2 now returns NaN on degenerate input, _residualize passes short inputs through, and the stratification loop skips and reports thresholds that keep fewer than two mutations. Prod output is unchanged -- every printed R^2 matches the pre-guard run exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fresh-context review found the figure's central claim was not implemented. Panels C/D read the joint arm's 5,809 mutations while E/F read the naive arm's, and the naive side was itself a union: derive_naive_shifts intersects conditions WITHIN a replicate but not ACROSS them, so rep 1 carried 5,926 mutations and rep 2 carried 5,969, and pivot_table unioned them to 6,386 rows with NaN columns. The panels therefore compared methods and coverage at once, while the prose claimed a shared index. Intersect once, explicitly, to 5,428 mutations and read every panel and the diagnostics block from it. The printed n and the figure's own annotation now agree; the coverage strata are counted on the paired index rather than the union. Also from the review: * V3 (assert_wt_agreement) was dead code -- the spec required it as an in-notebook assertion. Now wired into manuscript_figures, where site_map is already an input. It is not vacuous: 5,428 mutations checked against a site map carrying 35 sites whose wildtype letter differs by background, none of which reach the shared index. That IS the intersection-safety premise, now verified against real data instead of assumed. * naive_convergence.csv was a declared input that nothing read, a spurious dependency edge. It now reports the equal-convergence claim the fairness argument rests on. * Panel B plotted missense only while its caption promised the same class partition as panel A, silently dropping 137 stop-codon shifts. Restored, with colour carrying condition and line style carrying class so all four series stay distinguishable. * A test pins the per-replicate index contract, so the union behaviour downstream code must compensate for cannot regress silently. Prod numbers move slightly because the index is now correct: F_Delta 0.340 -> 0.349, F_BA2 0.161 -> 0.169, n 6,386 -> 5,428. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Figure 3 was drawn with derived axis limits and a bespoke panel layout. The legacy notebook (matsengrp/SARS-CoV-2_spike_multidms@6c98b7b, cells 84-95) is the format the manuscript publishes, and Figure 4's heatmap already follows the convention of porting that structure verbatim. Port the same way: the 7-row / 10-column mosaic, the 3x3 distribution block, the three-panel correlation rows, the hard-coded limits and ticks, the panel letters and titles are the legacy code as written. Only the data plumbing is adapted to this pipeline's CSV schema. Three departures, each commented at its site: both arms read one shared mutation index (legacy left them on different indices); the chained clip is assigned rather than passed inplace, which is a no-op under modern pandas copy-on-write; and an empty mutation class draws nothing while keeping its axes in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The legacy figure's 3x3 distribution block splits mutations into nonsynonymous / in-frame codon deletion / stop. Ported verbatim, the middle row rendered as an empty labelled panel: this pipeline deletes every deletion-bearing variant during data prep (prepare_data.ipynb, "Remove deletions and invalid variants" -- on main since #226), so no deletion mutation can reach the model. Two classes remain because two are all the model has. Beta is indexed by observed amino-acid substitution; synonymous variants carry no substitution, so prep folds them into the wildtype row used for func-score subtraction and they have no beta to plot. Verified by re-rendering against results-prod-294-naive-baseline-arm: all six replicate R^2 unchanged (C 0.83, D 0.40/0.38, E 0.84, F 0.35/0.17) -- the change touches only the distribution block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The legacy limits were sized for the legacy fit's much wider parameter spread. Carried over verbatim they left every subpanel mostly empty and rendered E and F as small off-centre blobs: panel legacy limits data range wasted C beta [-6, 3] -1.97 .. 1.11 ~66% D shifts [-2.8, 2.8] -1.48 .. 1.83 ~41% E naive beta [-11, 6] -2.25 .. 1.25 ~79% F naive sh. [-8.2, 12.2] -2.74 .. 3.59 ~69% Limits are now the observed range of both replicates padded ~5% and rounded to clean ticks, kept square so the identity line stays diagonal, and shared across the two columns of D and of F so those pairs remain directly comparable. Distribution bins likewise: the legacy ranges gave ~5 populated 0.5-wide bins per panel. Halving the width to 0.25 over the observed range resolves the distributions. Verified no mutation falls outside the new bins and the tallest bin is 0.97, so the [0, 1] probability axis holds. Also notes that the naive beta clip(lower=-10) does not bind on this fit (beta bottoms out at -2.25); retained as a guard. All six replicate R^2 unchanged (C 0.83, D 0.40/0.38, E 0.84, F 0.35/0.17) -- presentation only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 17, 2026
Figure S16 plots convergence across the lasso ladder, and it was calling drop_excluded() on the trajectory frame -- silently removing the 1.28e-3 rung, which contains the single non-converged fit in the study (rep_1, 500-sweep cap, drift_frac 0.10). A convergence figure that deletes the only non-converged trace asserts the opposite of the truth: every remaining line drops below tol, so the figure read as "all fits converged". config_downstream.yaml already stated the correct intent -- figures that plot "all rungs" should "mark it rather than silently showing it as a normal point" -- so this is the code catching up to the documented contract, not a new policy. S16 now iterates FULL_LADDER and marks the excluded rung grey/dashed, with the non-converged trace annotated in place. Two details worth noting: - The mark is driven by fit_convergence.csv, not by fusionreg. The instability is per-FIT, not per-rung: rep_2 converges at 1.28e-3 in 109 sweeps. Greying its trace as "did not converge" would swap one dishonesty for another, so the legend says "0.00128 (excluded)" and only rep_1 gets the annotation. - Colours are still indexed off the 9-rung analysed ladder, so the existing palette is unchanged and the marked trace takes no colour slot. fit_convergence.csv is added to the rule's input: -- it is an evaluate output in the same downstream tier as convergence_trajectory.csv, which the rule already consumes, so this adds no new dependency edge and cannot reach the fit tier. S9 keeps dropping the rung via drop_excluded(), which is correct there: its panels are the lambda-selection criteria, only defined for converged fits, on a categorical axis over the analysed ladder. drop_excluded()'s docstring now records why the two figures differ, since the config's "(S9, S16)" wording elides it. Marking S9's slot needs rung_x() to gain a tenth position and is left out of scope. Refs #294
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #294
EPIC #290 Phase 4. Builds the naive per-condition baseline the manuscript
contrasts against joint fitting, and renders manuscript Figure 3 (six
panels, A–F) from it.
The naive approach is the obvious alternative to joint modelling: fit each
condition alone, then call the difference of the two independently inferred
effects a "shift". This PR makes that baseline reproducible inside the
pipeline so the two arms can be compared on equal footing, and — this turned
out to be the substantive part of the work — measures whether the resulting
comparison actually supports the claim the manuscript makes with it.
What landed
spike.naiveconfig block (times_seen_threshold: 1, three conditions)config/*_downstream.yamlfit_single_condition,fit_naive_arm,derive_naive_shifts,assert_wt_agreementnotebooks/_downstream.pytests/test_naive_baseline.pynaive_baseline.ipynb— six single-condition fits, shift derivation, α sensitivity checknotebooks/rule naive_baseline+fig3registry entry +manuscript_figuresinputsSnakefilesavefig, plus the confound diagnosticsnotebooks/manuscript_figures.ipynbdocs/spike_naive_baseline.nblink,docs/index.rstnotebooks/manuscript_figures.ipynb,SnakefileThree design choices make the comparison fair, and all three are deliberate:
tolandmaxiterfrom the same config. The original analysis gave the naive fits atenth of the iteration budget, so part of its poor showing was budget
rather than method. All 6 fits converge (max final error 9.4e-07 against
tol1e-06), andmanuscript_figuresprints that rather than assuming it.times_seen_threshold=1. Every scatterpanel reads the same 5,428 mutations. This matches
evaluate.ipynb'sjoint-arm call; filtering the arms differently rigs the comparison —
joint
shift_Deltareads 0.543 at threshold 3 but 0.405 at 1.checked against a site map carrying 35 sites whose wildtype letter
differs by background; none reach the shared index.
No refit.
config/config.yamlis untouched (git diff main -- <it>isempty), so the 1.76 GB
fit_collection.pkland the ~2h20m fit are notinvalidated. The DAG is
naive_baseline → evaluate → manuscript_figures;fit_modelsis absent.evaluatereruns (~7 min) because it legitimatelydeclares
config_downstream.yamlas an input.Measured results
Against
results-prod-292-spike-fit-tuning, λ = 8e-05, shared indexn = 5,428, times_seen ≥ 1.
Deviations from spec
1. The headline statistic is confounded — for both arms
The spec framed the deliverable as reproducing the published gap (naive 0.14
vs joint 0.48, "3.4× worse") and expected the equal-convergence correction to
narrow it. It does narrow it — but the narrowed number rests on the same
broken statistic as the original.
A naive shift is
β_cond − β_ref. Both replicates subtract the samewell-replicating reference beta vector, and that shared term manufactures
correlation on its own. Permuting the condition betas identically in both
replicates — destroying every condition-specific signal while leaving β_ref
paired — gives:
The null is higher than what is observed. Replicate R² of a
difference-of-betas is not a clean measure of shift reproducibility. So do
not report "3.4× collapsed to 1.2×" as the finding.
Residualizing each arm's shift on β_ref isolates the real signal:
Joint beats naive by a consistent ~2.1–2.2× in both conditions — unlike the
published asymmetry. This is the defensible statement, and the notebook
computes it next to the headline numbers rather than asserting it.
2. The advantage lives entirely in low-coverage mutations
Stratified by min
times_seenacross both replicates and all conditions:The joint advantage vanishes by
ts >= 10; for Delta it inverts atts >= 3. The defensible story is "joint fitting helps where data is thin"— which is the manuscript's own thesis, but a narrower claim than "3.4×
better replication", and it should be stated as the narrower one.
Supporting evidence: naive
β_Deltareplicate R² is 0.461 against joint's0.718, while BA.1 and BA.2 naive betas are fine (0.843, 0.784). Delta has
the least data (28k variants vs 60–69k; median times_seen 8 vs 16), and
independent fitting cannot borrow strength.
3. ⚠ The spec's alpha lower-bound argument is refuted
The issue body argued that per-condition α mismatch is replicate-stable,
therefore adds a correlated component to both replicates' naive shifts,
therefore inflates naive R² — making the measured gap a lower bound on
the true gap.
Measured, this runs backwards.
naive_baseline.ipynbnow rescales eachcondition's betas by its fitted α before subtracting and prints the result:
α mismatch is suppressing the naive correlation, not inflating it, and the
gap is not a lower bound. α is confirmed replicate-stable (Delta
11.72/11.41, BA.1 13.06/13.14, BA.2 12.47/11.79) — the premise holds; the
inference from it does not.
Issue #294's body carries a ⚠ CORRECTION block, and the notebook's prose has
been rewritten to state the measured result rather than the refuted argument
— it publishes as method documentation via the docs toctree, so shipping the
false version was not an option. (These two numbers are on the naive arm's
own paired index, 0.340/0.161, not the joint-intersected 5,428 used by the
figure; the direction is identical either way.)
This needs a call on whether the manuscript text carries the same
argument.
4. The joint number is materially λ-dependent
Delta's joint R² peaks at λ = 0 (0.431), above the chosen 8e-05. BA.2
climbs monotonically to 3.2e-04, but by then 4,489 of 5,428 shifts are zero
in both replicates. The two conditions want opposite λ. 8e-05 was not
chosen to maximize shift replicate R², so reporting one joint number without
the curve overstates its stability. (Excluding exactly-(0,0) pairs moves
little: Delta 0.405 → 0.392, BA.2 0.378 → 0.376.)
5. Figure 3 reproduces the legacy layout verbatim
Figure 3 was first drawn with derived axis limits and a bespoke panel
layout. It is now a verbatim port of the legacy figure
(
matsengrp/SARS-CoV-2_spike_multidms@6c98b7b,spike-analysis.ipynbcells 84–95), matching the convention Figure 4's heatmap already follows:
legacy structure as written, only the data plumbing adapted to this
pipeline's CSV schema. The mosaic, the three-panel correlation rows, the
hard-coded limits and ticks, the panel letters and titles are the legacy
code.
Three departures remain, each commented at its site:
One shared mutation index across both arms. Legacy called
combine_replicate_muts(how="inner")per arm, leaving the joint andnaive panels on different mutation sets. Every panel here reads the
same 5,428 mutations — filtering the arms differently rigs the
comparison the figure exists to make.
The clip is assigned, not passed
inplace. Legacy'smut_df_replicates["avg_beta"].clip(lower=-5, inplace=True)is a no-opunder modern pandas copy-on-write, so the clip silently never happened.
The distribution block is 2 rows, not the legacy 3. The legacy
middle row was in-frame codon deletion muts (
muts == '-'). Thispipeline deletes every deletion-bearing variant during data prep
(
prepare_data.ipynb, "Remove deletions and invalid variants" — onmainsince Improve spike data prep: post_count threshold, nonsense truncation, log2 scores #226), so no deletion mutation can reach the model andthat row rendered as an empty labelled panel. It is dropped.
Two classes remain because two are all the model has: β is indexed by
observed amino-acid substitution, and synonymous variants carry no
substitution — prep folds them into the wildtype row used for
func-score subtraction, so they have no β to plot. Showing synonymous
mutations in this block would require both a prep change (retain the
class) and a full refit; that is a modelling decision, not a figure
one, and is left to the epic.
Re-rendered against
results-prod-294-naive-baseline-arm: all sixreplicate R² unchanged, confirming the change touches only the
distribution block.
Axis limits and bins refit to this pipeline's data. The legacy
ranges were sized for the legacy fit's much wider parameter spread.
Carried over verbatim they left every subpanel mostly empty and
rendered E and F as small off-centre blobs:
[-6, 3][-2.8, 2.8][-11, 6][-8.2, 12.2]Limits are now the observed range across both replicates padded ~5% and
rounded to clean ticks, kept square so the identity line stays a
true diagonal, and shared across the two columns of D and of F so
those pairs remain directly comparable.
Distribution bins likewise: the legacy ranges gave only ~5 populated
0.5-wide bins per panel. Halving the width to 0.25 over the observed
range resolves the distributions. Verified no mutation falls outside
the new bins, and the tallest bin is 0.97 — so the legacy
[0, 1]probability axis still holds.
Noted at its site: the naive
clip(lower=-10)does not bind on thisfit (β bottoms out at −2.25). Retained as a guard rather than removed,
since it would matter on a fit where β does run away.
All six replicate R² unchanged — presentation only.
6. The prod fit was re-run remotely rather than reused from cache
The plan anticipated shipping a cached checkpoint to a remote host, and an
earlier draft of this section reported that no refit was needed. That is
no longer accurate.
The local
results-prod-292-spike-fit-tuningcache was destroyed duringthis work by a
snakemake --forcerun manuscript_figuresinvocation runwithout a preceding
--dry-run:config.yamlis declared as a ruleinput:, the Phase 2 PR (32d7ffb) had added a comment block to it, andSnakemake hashes whole files — so it planned a 7-rule rerun and deleted
fit_collection.pklandcv_fit_collection.pklat job start.The remote copies on orca04 were unaffected, and the arm was re-run there
in a fresh worktree (
results-prod-294-naive-baseline-arm, commit147e057): 7/7 rules, 51 minutes,fit_modelsalone taking 41. Theregenerated
training_functional_scores.csvis byte-identical to the Aug12 version (17,210,919 B) and both pickles match their originals to within
3 bytes of pickle metadata, confirming the config change was inert and the
numbers in this PR are unchanged.
The measured results below were computed against the original cache and
have been re-verified against the re-run.
7. Figure S16 no longer hides the one fit that did not converge
Scope beyond #294, folded in at the user's request after the dashboard and
S16 appeared to contradict each other:
fit_convergence.csvreports 19/20 fitsconverged, but S16 showed every line dropping below
tol.Both artifacts were correct. S16 called
drop_excluded()on the trajectoryframe, removing the 1.28e-3 rung — which contains the study's only
non-converged fit (
rep_1, 500-sweep cap,drift_frac0.10). A convergencefigure that deletes the only non-converged trace asserts the opposite of the
truth.
config_downstream.yamlalready stated the correct intent — figures that plot"all rungs" should "mark it rather than silently showing it as a normal point"
— so this is the code catching up to the documented contract, not new policy.
S16 now iterates the full ladder and marks the excluded rung grey/dashed, with
the non-converged trace annotated in place. Two judgement calls:
fit_convergence.csv, not byfusionreg. Theinstability is per-fit, not per-rung:
rep_2converges at 1.28e-3 in 109sweeps. Greying its trace as "did not converge" would swap one dishonesty for
another, so the legend reads
0.00128 (excluded)and onlyrep_1isannotated
did not converge.palette is unchanged and the marked trace takes no colour slot. The other
nine lines are pixel-identical to the previous version.
fit_convergence.csvis added to the rule'sinput:. It is anevaluateoutput in the same downstream tier as
convergence_trajectory.csv, which therule already consumed, so this adds no new dependency edge and cannot reach the
fit tier.
S9 keeps dropping the rung via
drop_excluded(), which is correct there:its panels are the λ-selection criteria, only defined for converged fits, on a
categorical axis over the analysed ladder. Marking S9's slot needs
rung_x()to gain a tenth position and is deliberately out of scope.
drop_excluded()'sdocstring now records why the two figures differ, since the config's
"(S9, S16)" wording elides it.
Everything else: no deviation
D1–D7 landed as written, including validations V1–V3.
Checked, and cleared as non-artifacts
shared index. Negligible.
|shift| > 2gives 0.370.β_cond − β_refto 7e-16; thereference condition's shift is identically 0; no clipping at beta bounds.
Split out of this PR
In-frame codon deletions are dropped at the prep stage and have been since
baaa0dc, the first commit of the spike pipeline. The legacy analysis keptthem. Restoring them would add 881 mutations (354 with ≥5-variant
support, led by NTD antigenic-supersite residues
P25-,N164-,F140-),and would require re-deriving λ, since
shift_sparsityuses stop codons asits false-positive denominator and cannot see deletions.
That is out of scope here and is filed as #313. A working restoration edit
is parked as git stash
8d911b5and referenced from that issue.Open question for Phase 7
Under-convergence alone does not explain the published naive 0.14. An
under-converged Delta fit shrinks β_Delta toward its init, pushing
β_Delta − β_BA1toward−β_BA1, which per the decomposition pushes naiveR² up toward the 0.70 null — not down to 0.14. The legacy 0.14 more
likely came from a different index, a different
times_seenthreshold, orper-replicate rather than shared-index pairing. Resolving it needs the legacy
naive betas, not just its reported correlation.
Verification
pixi run pytest --doctest-modules multidms tests→ 142 passed (8 intest_naive_baseline.py)pixi run ruff check .→ clean; explicit run on_downstream.py→ clean(ruff's config excludes
experiments/)pixi run black --check→ clean, 13 filespixi run docs→ build succeeded,spike_naive_baselinepresentsnakemake --config profile=test --forceall,7/7 rules, Figure 3 renders. The naive fits report
0/6 convergedthereby design (2 outer sweeps) — reported, not asserted, per spec.
naive_baseline,evaluate,manuscript_figures;fit_modelsabsent from the DAGgit diff main -- experiments/scv2-spike/config/config.yaml→ emptyorca01ata4a7536.--dry-rungate first: DAG was exactlymanuscript_figures+all, reason"Updated input files: manuscript_figures.ipynb"; no
fit_models,cross_validation,evaluate, ornaive_baseline. Run took 50 s, 30/30cells, zero errors. Post-run parity: all 7 fit-tier CSVs and all 20 figures
md5-identical local↔remote; both pickles retain their Aug 16 16:39/16:40
mtimes, proving the fit was never re-run.
A fresh-context reviewer audited the completed diff and found five real
defects, all fixed in
6e71a4c: the shared-index violation (panels C/D andE/F were on different mutation sets despite the prose claiming otherwise),
the per-replicate union inflating every reported
nfrom 5,428 to 6,386,V3 shipped as dead code,
naive_convergence.csvdeclared as an input butnever read, and panel B silently dropping the stop class.
Yolo trail: spec gate ✓, plan gate ✓ — full log in
_ignore/yolo/294.log🤖 Generated with Claude Code