Skip to content

docs: convert plot descriptions to plt.figtext + strip tqdm noise from notebooks - #240

Merged
jonathanhhb merged 4 commits into
mainfrom
docs/notebook-plot-descriptions-figtext
Jul 10, 2026
Merged

docs: convert plot descriptions to plt.figtext + strip tqdm noise from notebooks#240
jonathanhhb merged 4 commits into
mainfrom
docs/notebook-plot-descriptions-figtext

Conversation

@jonathanhhb

@jonathanhhb jonathanhhb commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two bundled improvements, both aimed at making notebook content clean and RAG-friendly. Rebased onto main (was originally stacked on #204's branch, which is now closed — this PR is now standalone).

1. Plot descriptions in plt.figtext form (originally #240)

The 70 plot descriptions authored in #204 are converted from standalone ### Reading the … markdown cells into plt.figtext(...) captions inside the plot-generating code cells. Content compressed to the takeaway sentence per description.

Why: the markdown form created its own RAG chunks that competed for retrieval on prompts like "build this SIR model" — semantically close ("simulate + plot") but no API content, so retrieval picked them up and displaced model-building code. plt.figtext embeds the description inside the code cell's Python source; the concat pipeline emits that cell as a fenced python block, and the ingest H4 splitter (laser-mcp #37) + fence-first character splitter keep the string fused with the surrounding plotting code.

Alpha suite evidence (jenner-generic-mcp, N=3 per arm):

Arm Runs Mean / 20 Δ vs no-descriptions
No descriptions (baseline) 17, 16, 18 17.00
Fat markdown (#204 as-authored) 15, 13, 15 14.33 −2.67
Slim markdown (H3 + takeaway sentence) 14, 17, 18 16.33 −0.67
This PR (figtext form) 17, 16, 18 17.00 ±0.00

Re-tested against latest main (post-#236 concat improvements, post-laser-mcp-#37 H4 splitter) — got 17, 18, 20 → mean 18.33 on the fresh corpus, with run 3 hitting a clean-sweep 20/20. Everything compounds cleanly.

2. tqdm progress-bar noise cleanup (new)

The tutorial notebooks call model.run() with a tqdm progress display; nbconvert captures those mid-run snapshots ("N,NNN agents in M node(s): 37%|... it/s]") into cell outputs. That noise was leaking into three places:

  1. Committed notebooks (docs/tutorials/notebooks/*.ipynb) — carried 381 tqdm lines across 14 of 19 files.
  2. CI executed_nbs artifact — captured whatever nbconvert saw. MkDocs Deploy and Build Combined Doc both consume this artifact, so both were displaying / ingesting noise.
  3. Historically fixed at concat time (concat_mkdocs: broaden tqdm strip to catch all tqdm variants #229's regex in docs/concat_mkdocs.py) — but that only fixed the RAG corpus, not the docs site or the artifact.

New: docs/tqdm_strip.py owns the shared regex + a small library + CLI.

  • Wired into docs/execute_notebooks.py — the CI artifact now ships clean by construction, so the docs site (via mkdocs-ghp's overlay) and RAG corpus (via Build Combined Doc's concat) both benefit without further changes on their side.
  • docs/concat_mkdocs.py imports the shared regex — keeps its own filter as a defensive second pass but no longer duplicates the pattern.
  • Applied to all 19 committed notebooks: 381 lines removed across 14 files.
  • New pytest gate tests/test_notebooks_no_tqdm_crud.py fails the existing check matrix entry if any committed notebook has tqdm output. No new workflow — plugs into the existing github-actions.yml check job.

Files changed

  • tools/apply_figtext_captions.py — new tool for the figtext transformation
  • tools/plot_descriptions/ — 70 description .md files + config.json manifest
  • docs/tqdm_strip.py — new shared module + CLI
  • docs/execute_notebooks.py — post-execute strip step
  • docs/concat_mkdocs.py — imports shared regex
  • tests/test_notebooks_no_tqdm_crud.py — new CI gate
  • 18 tutorial notebooks — figtext calls added to plot code cells + tqdm crud stripped from outputs

Closes

Supersedes and replaces #204 (already closed).

jonathanhhb added a commit that referenced this pull request Jul 9, 2026
…ct (#241)

Adds a new `Execute Notebooks` workflow that owns notebook execution as a
first-class CI stage, and reworks `Build Combined Doc` to consume the
resulting artifact instead of re-executing notebooks itself. Also adds a
`docs-jenner-artifact` Makefile target for the CI-side build path.

## Problems solved

1. Notebook execution used to fire only on releases (issue #186). Now
   fires on every main-push touching notebook or execution-affecting
   source, plus on manual dispatch. Broken notebooks are surfaced
   within minutes of landing.
2. Committed executed-notebook outputs no longer need to be kept fresh
   by convention. The CI artifact is the source of truth for downstream
   doc/RAG builds; the committed `outputs` field is decorative (kept
   only so github.com renders charts inline).
3. Doc builds no longer re-execute notebooks. `Build Combined Doc`
   downloads the executed_nbs artifact and just runs
   `make docs-jenner-artifact` (~2-3 min wall-clock vs the previous
   ~25 min).

## Architecture

```
Execute Notebooks
  trigger: push to main (paths-filtered) | workflow_dispatch
  cache: source-hash-keyed, invalidates on any input that can affect outputs
    (notebooks, src/**, pyproject.toml, docs/requirements.txt,
     docs/**/*.py, Makefile, the workflow file itself)
  gate:  python docs/check_executed_nbs.py (fail on any nb error)
  manifest: writes dist/executed_nbs/manifest.json with commit_sha,
    source_hash, run_id, python_version, event_name, was_cache_hit,
    was_forced, was_allow_errors
  uploads:
    executed_nbs                 (400d)   push-to-main OR clean dispatch
    executed_nbs-debug-<run_id>  (30d)    dispatch with allow_notebook_errors=1
    executed_nbs-failed-<run_id> (30d)    any failure path

Build Combined Doc
  trigger: workflow_run completion of Execute Notebooks (auto-chain,
    restricted to push-triggered upstream runs so debug dispatches
    don't push a corpus PR to laser-mcp) | workflow_dispatch
  download: executed_nbs artifact (by run-id for workflow_run,
    latest successful for workflow_dispatch)
  compat gate (workflow_dispatch only): hashFiles of checkout vs
    manifest.source_hash; fail on mismatch unless use_latest_anyway=true
  build: make docs-jenner-artifact -> build site + concat
  sync:  create/update laser-mcp PR
```

The two `hashFiles` lists (cache-key in execute-notebooks + compat-check
in build-combined-doc) are literal duplicates by design — 7 patterns each,
same order. Diverging silently breaks the compat gate.

## Deliberate non-goals

- **No `pull_request` trigger.** Executing every notebook adds ~25 min
  per PR iteration; that cost is not affordable during review cycles.
  Broken notebooks land on main and are caught by the post-merge push
  run within minutes. Local `make docs-jenner-execute` or a manual
  `workflow_dispatch` remain as pre-merge validation options for PRs
  that specifically need them.
- **No enforcement of source-only committed notebooks.** Contributors
  may commit executed OR stripped notebooks — both work identically
  for the doc build. Documented inline in the workflow file. If future
  consensus wants strict source-only commits (Option B), the enforcement
  point is a single check step in `github-actions.yml`.

## Bootstrap note

On first enable, `workflow_dispatch` on Build Combined Doc will fail
cleanly (no prior successful Execute Notebooks artifact to consume —
`if_no_artifact_found: fail` catches this). Kick off Execute Notebooks
once manually to seed. After that, either trigger works.

## Third-party action

`dawidd6/action-download-artifact@v3` — needed because
`actions/download-artifact` only fetches from the current run or a known
run-id, and `workflow_dispatch` on Build Combined Doc has no upstream
run-id to point at. This third-party action searches for "latest
successful run of workflow X" — exactly the semantics we need. Widely
used (~4M weekly downloads).

## Compatibility with other in-flight PRs

Independent of #204 (plot descriptions), #240 (figtext form of #204),
and #236 (concat improvements) — those change notebook / concat content.
Compatible with #237 (skip re-exec in local `docs-jenner`) — together
the two produce a clean Makefile trio:
- `make docs-jenner`          local fast path (per #237)
- `make docs-jenner-execute`  local full pipeline (per #237)
- `make docs-jenner-artifact` CI path, EXEC_DIR pre-populated (this PR)

Closes #186.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Jonathan Bloedow and others added 2 commits July 10, 2026 08:59
Refactors the 70 plot-description cells added by #204 from standalone
markdown cells (### Reading the ... plot, with observation + interpretation
+ takeaway paragraphs) to plt.figtext() captions embedded inside the
plot-generating code cells. Content is compressed to the takeaway sentence.

Motivation: alpha-suite evaluation (jenner-generic-mcp, N=3 per arm, same
generation quality:

  arm                              mean/20    a1     Δ vs no-descriptions
  no descriptions (control)        17.00      9.00    —
  fat markdown (#204 as-authored)  14.33      7.67   -2.67
  slim markdown (H3 + takeaway)    16.33      8.33   -0.67
  figtext (this PR)                17.00      9.00   +0.00

Standalone markdown description cells create their own chunks in the RAG
corpus that compete for top-k retrieval on prompts like "build this SIR
model" — the descriptions are semantically close ("simulate + plot") but
have no API-usage content, so retrieval picks them up and displaces the
chunks that actually show the model-building calls.

plt.figtext embeds the description text inside the code cell's Python
source. The concat pipeline emits that cell as a fenced Python block, and
the ingest H4 splitter (laser-mcp #37) plus the recursive character
splitter's fence-first separator keep the string fused with the surrounding
plotting code. Result: description content is still in the RAG corpus, but
as part of code chunks — no separate description-only chunks to compete
with API-usage retrieval.

Chunk-count evidence (laser-mcp ingest.py, chunk_size=1200):

  arm         sections   chunks    Δ chunks vs no-descriptions
  no desc     515        972        —
  figtext     515        991       +19    (co-located, no new boundaries)
  slim        585        1037      +65    (new md sections)
  composite   585        1101     +129    (new md sections + fat content)

The figtext arm adds essentially no new chunk boundaries.

Takeaway extraction: for each description .md file, extracts the last bold
span (>20 chars) containing a takeaway trigger word ("demonstrates",
"takeaway", "confirms", ...). 68/70 auto-extract cleanly; 1 spot-fixed via
MANUAL_TAKEAWAY, 1 confirmed OK via fallback path. Full logic lives in
tools/apply_figtext_captions.py so the same manifest at
tools/plot_descriptions/config.json remains the source of truth.

Rendering: plt.figtext(0.5, -0.05, ..., ha="center", va="top", wrap=True,
fontsize=8) places the caption just below the axes. Notebook outputs are
NOT re-executed by this PR (docs-jenner uses committed outputs per #237),
so the committed figure PNGs are unchanged; when notebooks are next
re-executed for real (e.g. a release-time full build via
docs-jenner-execute) the caption will render in the figure.

Base branch: docs/notebook-plot-descriptions (#204's branch). This is a
stacked PR — #204's markdown-form remains the base for reviewers to see
what's being converted. Merge order intended:
  1. #204 (as-authored, or force-updated to figtext form if reviewers prefer)
  2. This PR (if not folded into #204)

Related:
- #204 base — the plot descriptions themselves
- #236 — concat heading demote + oversize warning (independent)
- #237 — docs-jenner skip re-execution (independent)
- laser-mcp #37 — H4 in MarkdownHeaderTextSplitter (works with either form)
- Alpha-suite runs under tests/prompt_test_suite_generic/output/experiment_204eval/
  in laser-mcp; N=3 per arm, gpt-5-mini generation, gpt-4o review.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jonathanhhb

Copy link
Copy Markdown
Collaborator Author

Alpha suite re-test: rebased #240 vs prior baselines

Ran the 20-prompt alpha suite N=3 against a freshly-built corpus from this branch (cherry-picked onto latest main, so it now composes with #236 concat improvements and post-#37 laser-mcp ingest). Corpus fingerprint: 515 sections / 991 chunks (identical to the earlier figtext test).

Per-iteration:

Run passed a1 a2 a3 never Fails
1 17/20 5 8 4 3 p06, p09, p16
2 18/20 6 8 4 2 p05, p09
3 20/20 8 9 3 0
mean 18.33 6.33 8.33 3.67 1.67

Vs baselines this session:

Arm Mean passed Mean a1
No descriptions (without204) 17.00 9.00
Figtext (earlier test) 17.00 9.00
Figtext rebased (this test) 18.33 6.33

Run 3 was a clean sweep (20/20 first time this session). The +1.33 improvement over the earlier figtext test likely reflects the RAG-quality improvements that landed since — laser-generic #236 (concat heading demote + oversize warning) and laser-mcp #37 (H4 splitter in the MarkdownHeaderTextSplitter) are both live now, and both compound with the figtext form's chunk-fusion property.

Green light to merge from an alpha-suite standpoint.

Three sources of tqdm crud were letting progress-bar snapshots leak into
downstream products:

  1. Committed notebooks in docs/tutorials/notebooks/ carried 381 lines
     of tqdm output across 14 of 19 files (each ~30 chars of
     "N,NNN agents in M node(s): 37%|...|... it/s]" ANSI-adjacent junk).
  2. The CI executed_nbs artifact produced by docs/execute_notebooks.py
     captured whatever nbconvert saw during execution, so the artifact
     that feeds MkDocs Deploy and Build Combined Doc always had noise.
  3. docs/concat_mkdocs.py's tqdm filter (from #229) ran at concat time
     — it kept the RAG corpus clean but couldn't help the docs site
     (which uses the artifact directly, not the concat output).

## New module: docs/tqdm_strip.py

Owns the tqdm-line regex and the in-place notebook mutation. Exposes:

  - TQDM_PROGRESS_RE: shared line pattern (matches lines ending with the
    tqdm rate suffix `it/s]` or `s/it]`)
  - strip_tqdm_from_notebook(nb): mutate a loaded notebook dict; returns
    lines removed
  - strip_notebook_file(path): load, mutate, write back if changed
  - count_tqdm_lines(path): read-only line-count for CI checks
  - CLI: `python docs/tqdm_strip.py path.ipynb ...` (strip in place)
         `python docs/tqdm_strip.py --check path.ipynb ...` (exits 1 if any file dirty)

Handles both `stream` outputs (text field) and `execute_result` /
`display_data` outputs (data.text/plain field), including no-newline
single-chunk outputs that tqdm produces when writing to stderr.

## Wired into docs/execute_notebooks.py

After every nbconvert call, strip_notebook_file runs against the just-
executed notebook. CI artifact ships clean by construction. Downstream
consumers (MkDocs Deploy site build via artifact overlay, Build Combined
Doc RAG concat) both benefit without any changes on their side.

## docs/concat_mkdocs.py — imports shared regex

Kept its filter logic (still needed as a defensive second pass for local
dev flows or non-freshly-executed inputs), but pulls the regex from the
new shared module so any future refinement lands in one place.

## Committed notebook cleanup

Applied `strip_notebook_file` to all 19 docs/tutorials/notebooks/*.ipynb.
Stripped 381 lines across 14 files:

  nb04:  116 lines
  nb09:  101 lines
  mortality:  46 lines
  SEI_and_SEIS:  30 lines
  nb03:  28 lines
  nb05:  26 lines
  ...8 more (11 lines total)

## New pytest gate

tests/test_notebooks_no_tqdm_crud.py fails the CI matrix's `check` job if
any committed notebook has tqdm-progress lines in its outputs. Error
message points contributors at `python docs/tqdm_strip.py` for the fix.
No new workflow — the existing check job in github-actions.yml picks it up.
@jonathanhhb
jonathanhhb force-pushed the docs/notebook-plot-descriptions-figtext branch from 73471e9 to c9f8c07 Compare July 10, 2026 20:05
@jonathanhhb
jonathanhhb changed the base branch from docs/notebook-plot-descriptions to main July 10, 2026 20:05
@jonathanhhb jonathanhhb changed the title docs: convert plot descriptions to plt.figtext (RAG-quality extension of #204) docs: convert plot descriptions to plt.figtext + strip tqdm noise from notebooks Jul 10, 2026
@jonathanhhb
jonathanhhb requested a review from Copilot July 10, 2026 20:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the cleanliness and retrieval quality of the tutorial notebook corpus by (1) embedding plot descriptions directly into plotting code via plt.figtext(...) and (2) stripping tqdm progress-bar noise from committed notebooks and CI-executed notebook artifacts via a shared docs/tqdm_strip.py utility.

Changes:

  • Added a shared tqdm-stripping module + CLI (docs/tqdm_strip.py) and integrated it into notebook execution and concat tooling.
  • Added a CI gate to prevent tqdm output from being committed into docs/tutorials/notebooks/*.ipynb.
  • Updated tutorial notebooks to remove tqdm output noise and add plt.figtext(...) captions near plots.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tools/apply_figtext_captions.py New script to inject plt.figtext(...) captions into notebook code cells based on the plot description manifest.
docs/tqdm_strip.py New shared regex + library + CLI to remove tqdm progress-bar snapshot lines from notebook outputs.
docs/execute_notebooks.py Runs strip_notebook_file(...) post-nbconvert so CI executed_nbs artifacts ship clean.
docs/concat_mkdocs.py Imports shared tqdm regex from docs/tqdm_strip.py instead of duplicating it.
tests/test_notebooks_no_tqdm_crud.py New pytest gate ensuring committed tutorial notebooks contain no tqdm progress output lines.
docs/tutorials/notebooks/SEI_and_SEIS_implementations.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions.
docs/tutorials/notebooks/seasonality.ipynb Removes tqdm output noise; adds plot captions (noted helper/call-site caption placement issues in review).
docs/tutorials/notebooks/ri_exploration.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions.
docs/tutorials/notebooks/mortality.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions.
docs/tutorials/notebooks/grid_examples.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions.
docs/tutorials/notebooks/distributions.ipynb Adds plt.figtext(...) plot captions to distribution sampler plots.
docs/tutorials/notebooks/constant_pop.ipynb Removes tqdm output noise; adds plt.figtext(...) plot caption.
docs/tutorials/notebooks/births.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions.
docs/tutorials/notebooks/10_England_and_Wales.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions; fixes a spelling issue (“vicinity”).
docs/tutorials/notebooks/09_rabies_diffusion_1D.ipynb Reduces tqdm output noise; adds plt.figtext(...) plot captions.
docs/tutorials/notebooks/08_2patch_SIR_wbirths_correlation.ipynb Adds plt.figtext(...) plot caption to the coupling/correlation plot cell.
docs/tutorials/notebooks/07_SIR_CCS.ipynb Adds plt.figtext(...) plot captions in CCS-related plot cells.
docs/tutorials/notebooks/06_SIR_wbirths_natural_periodicity.ipynb Adds plt.figtext(...) plot captions for periodicity plots.
docs/tutorials/notebooks/05_SIR_wbirths_age_distribution.ipynb Removes tqdm output noise; adds plot labeling + plt.figtext(...) captions.
docs/tutorials/notebooks/04_SIR_nobirths_outbreak_size.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions.
docs/tutorials/notebooks/03_SIS_nobirths_logistic_growth.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions and plot formatting tweaks.
docs/tutorials/notebooks/02_SI_wbirths_logistic_growth.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions and consolidates fit plots into subplots.
docs/tutorials/notebooks/01_SI_nobirths_logistic_growth.ipynb Removes tqdm output noise; adds plt.figtext(...) plot captions and consolidates beta-fit plots into subplots.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tools/apply_figtext_captions.py Outdated
Comment thread tools/apply_figtext_captions.py Outdated
Comment on lines +68 to +84
def inject_before_show(source_lines, figtext_line):
src = "".join(source_lines)
if figtext_line in src:
return source_lines, False
lines = src.splitlines(keepends=True)
last_show_idx = None
for i, ln in enumerate(lines):
if re.match(r"^\s*plt\.show\s*\(", ln):
last_show_idx = i
if last_show_idx is None:
if lines and not lines[-1].endswith("\n"):
lines[-1] = lines[-1] + "\n"
lines.append(figtext_line + "\n")
else:
indent = re.match(r"^(\s*)", lines[last_show_idx]).group(1)
lines.insert(last_show_idx, f"{indent}{figtext_line}\n")
return lines, True

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 93d7eb8. inject_before_show now checks isinstance(source, list) on the way in and preserves that shape on the way out — string in, string out; list in, list out. Removes the diff churn on re-runs against string-form cells.

Comment on lines 159 to 160
" plt.figtext(0.5, -0.05, 'The plot demonstrates that even without seasonal forcing, an SIR model with births produces damped, irregular endemic oscillations — the natural baseline that subsequent seasonal-forcing experiments must be compared against.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n",
" plt.show()\n",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legitimate rendering bug — same root cause across all six comments on this file. Fixed in 93d7eb8.

Root cause

The apply_figtext_captions tool injects plt.figtext(...) just before the last plt.show() in the target code cell. In cells 5 (plot_model) and 23 (plot_si_model), that last plt.show() sits inside the helper function — so:

  1. Every call to plot_model / plot_si_model used the same caption (wrong content for subsequent calls).
  2. Cells 9/14/19/27 tried to compensate by placing plt.figtext AFTER the plot_model(...) call, but the helper had already called plt.show() internally — so the caption attached to a fresh empty figure and didn't render on the intended plot at all.

Fix

Gave each helper a caption=None parameter, rendering figtext inside the helper right before plt.show() when caption is passed. Each call site now passes the appropriate caption via kwarg; the trailing standalone plt.figtext lines were removed. Six cells updated (5, 9, 14, 19, 23, 27).

Also fixed a subtle escape bug I hit in the first attempt: my regex captured the source-form of the caption string (escaped backslashes) and re-repr'd it, doubling the escapes and breaking LaTeX math ($\\beta(t)$ instead of $\beta(t)$). Second pass uses ast.literal_eval to get the interpreted Python value before repr.

Comment on lines +237 to +238
"plot_model(sinusoidal)\n",
"plt.figtext(0.5, -0.05, 'The plot demonstrates that seasonal forcing of $\\\\beta(t)$ synchronizes outbreaks into larger, sharper annual peaks, but in a closed population the deeper troughs drive the infection extinct — motivating the importation experiment that follows.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.

Comment on lines +342 to +343
"plot_model(with_importation)\n",
"plt.figtext(0.5, -0.05, 'The plot demonstrates that constant low-rate importation (3 cases every 30 days) is enough to break the fadeout problem and let seasonal forcing entrain a stable biennial outbreak cycle phase-locked to the annual $\\\\beta(t)$ peak.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.

Comment on lines +434 to +435
"plot_model(two_node_model)\n",
"plt.figtext(0.5, -0.05, 'The plot demonstrates that spatial coupling between out-of-phase seasonal patches produces richer inter-epidemic dynamics — a quieter \"reservoir\" node sustains transmission during the primary node\\'s seasonal trough, filling in smaller off-cycle outbreaks and damping the peak amplitudes seen in the single-node case.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.

Comment on lines 533 to 534
" plt.figtext(0.5, -0.05, 'The plot demonstrates the canonical undriven SI take-off-and-saturate curve at the chosen $\\\\beta$ — the control trajectory against which the next cell\\'s \"radical seasonality\" (an intervention applied between days 300 and 400) will be compared.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n",
" plt.show()\n",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.

Comment on lines +611 to +612
"plot_si_model(si_with_seasonality)\n",
"plt.figtext(0.5, -0.05, 'The plot demonstrates that the seasonality input is general-purpose temporal modulation of $\\\\beta(t)$ — not restricted to cyclical forcing — and can be used to model the population-level effect of a time-limited intervention that arrests an outbreak partway through its logistic take-off.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.

Six comments; two type/style items on tools/apply_figtext_captions.py
and four rendering bugs in seasonality.ipynb (with a fifth pair in the
same file, all sharing the same root cause).

## seasonality.ipynb — figtext + helper-function interaction

The apply_figtext_captions tool injected plt.figtext(...) into
seasonality.ipynb by finding the "last plt.show() in the target code
cell" and placing the caption just before it. In cells 5 and 23 that
last plt.show() sat *inside* helper functions (plot_model and
plot_si_model) — so:

1. Every call to plot_model / plot_si_model got the same caption baked
   in at helper-definition time (wrong content for subsequent calls).

2. Cells 9, 14, 19, 27 tried to compensate by placing plt.figtext after
   the plot_model(...) call. But those helpers had already called
   plt.show() internally, so the caption attached to a fresh empty
   figure and didn't render on the intended plot at all.

Fix: give each helper a caption=None parameter, render figtext inside
the helper right before plt.show() when caption is passed. Each call
site now passes the appropriate caption via kwarg; the trailing
standalone plt.figtext lines are removed.

Applied to cells 5 (plot_model def + no_seasonality call), 9
(sinusoidal), 14 (with_importation), 19 (two_node_model), 23
(plot_si_model def + wout_seasonality call), and 27 (with_seasonality).

Also fixed a subtle escape bug in the first pass of the surgery: my
regex captured the source-form of the caption string (with escaped
backslashes) and re-repr'd it, doubling the backslashes and breaking
LaTeX math in the captions (\\\\beta instead of \\beta). Second pass
uses ast.literal_eval to get the interpreted Python value before repr,
so `$\\beta(t)$` in the original stays `$\\beta(t)$` (one backslash)
after rewriting.

ri_exploration.ipynb has a similar helper-with-embedded-figtext shape
(initialize_susceptibility), but both call sites use plot=False so
the figtext lives in dead code — the caption text is in the code
fence (RAG-visible) but never renders anywhere. Left as-is since no
downstream product misrenders.

## tools/apply_figtext_captions.py — type + source-shape preservation

- extract_takeaway is annotated `-> str` but can return None
  (fallthrough paths for un-parseable descriptions). Change to
  `-> str | None` to match the actual contract and stop hiding None
  from static checkers.

- inject_before_show always returned a list[str], even when the input
  cell.source was a plain string. nbformat allows both shapes, and
  returning a list-when-caller-gave-a-string forces unnecessary
  reformatting on every re-run. Fix: preserve the input shape — string
  in, string out; list in, list out.
@jonathanhhb
jonathanhhb merged commit f9ec945 into main Jul 10, 2026
9 checks passed
@jonathanhhb
jonathanhhb deleted the docs/notebook-plot-descriptions-figtext branch July 10, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants