Skip to content

Portfolio hardening: analytical ODE validation, tests + CI, config, docs - #1

Merged
ethanbuckley merged 6 commits into
mainfrom
portfolio-hardening
Jul 3, 2026
Merged

Portfolio hardening: analytical ODE validation, tests + CI, config, docs#1
ethanbuckley merged 6 commits into
mainfrom
portfolio-hardening

Conversation

@ethanbuckley

Copy link
Copy Markdown
Owner

Note

Drafted with an LLM-based AI tool (Claude Code / Claude Fable 5).

Turns the coursework-origin SEIQR cellular automaton into a more defensible portfolio piece, in five focused commits. The deployed dashboard and the group-vs-individual attribution are preserved, and seiqr.py (the group original) is left untouched.

What's in it

1. Analytical ODE validation (the headline). New ode_reference.py adds the classical well-mixed SEIQR ODE (scipy.solve_ivp) and validates the CA against it, so the "checked against the analytical model" claim is real rather than asserted. The CA's local rules reduce to the ODE in the mean-field limit: a global-coupling (well-mixed) CA ensemble reproduces the exact discrete recursion to about 2% of peak (RMSE) and the ODE peak height to about 2%, while the standard spatial CA departs (a roughly 3.5x lower, about 50-step-later peak), which is the genuine effect of spatial structure. Per-step probabilities map to rates by -ln(1-p) (not p, which is 28% wrong at p_infect=0.50); the saturating exposure rule gives a force of infection that linearises to beta = 8*p_expose (R0 about 15 at p_expose=0.30). Wired into run_experiments.py (scenario 7) and the dashboard Performance tab.

2. Tests + CI. A pytest suite (23 tests) over the model core (transitions, boundary preservation, density-map zone counts, the half-open lockdown window and its guard, vaccination, seed reproducibility, and statistical equivalence with seiqr.py) and the ODE reference (rate mapping, combined-hazard split, saturating force of infection, conservation, and the well-mixed-limit agreement). A GitHub Actions workflow runs ruff and pytest on push and PR.

3. Config refactor. A SimConfig dataclass replaces about 20 scattered module constants; the zone radii (8, 16) are deduped into named constants in model.py; the fragile lockdown-window handling now fails loudly instead of relying on short-circuit evaluation. Behaviour-preserving: the regenerated curve data is bit-identical to the committed results.json (verified), so model.py public signatures are unchanged and the dashboard is unaffected.

4. Docs + demo GIF. A reproducible make_demo_gif.py renders assets/demo.gif and embeds it at the top of the README (the live dashboard is Plotly, which GitHub cannot embed). The README gains a Validation section, a CI badge, and updated structure and run instructions.

5. Licence + hygiene. An MIT licence for the individual extension, with a note that seiqr.py's copyright stays with the group. The original report PDF is gitignored so it is not published to the public repo.

Decisions for you to confirm

  • Licence scoping. MIT covers the individual extension; the LICENSE notes that seiqr.py originates from the group project and its copyright stays with the team. If you would rather licence the whole repo, use a different licence, or clear it with the team, adjust as you see fit.
  • Report PDF. Gitignored, because it contains other team members' work; team credit stays in the README. If the group is happy to publish it, it can be committed instead.
  • README house style. I normalised the existing README to British spelling and removed em dashes (your stated preference). Revert any wording you would rather keep.
  • Validation framing is deliberately narrow. It validates the rule-to-ODE reduction, not that the spatial CA matches the ODE (it does not, and that departure is the result). The dashboard and README state this explicitly; the continuous ODE peaks a few steps earlier than the discrete CA, which is a discretisation effect at high R0 (the discrete recursion, matched to about 2%, is the tight reference).

Verification

  • ruff check . clean; pytest 23 passed.
  • python run_experiments.py regenerates results.json; curve data is bit-identical to the previous version (only the speedup timings and the generated_at timestamp differ).
  • The dashboard loads all tabs headless (Streamlit AppTest), including the new validation figure.

🤖 Generated with Claude Code

ethanbuckley and others added 6 commits July 3, 2026 21:13
…d lockdown

Introduce config.SimConfig to hold the tunable model parameters that were
scattered as module-level constants in run_experiments.py; defaults reproduce
the report Table 1 values so the published scenarios are unchanged. Define the
zone radii (8, 16) once as CENTRE_RADIUS/MIDDLE_RADIUS in model.py and reuse
them in make_density_map, vaccinate and the experiment zone masks. Add a guard
in run_seiqr so a lockdown map without a window fails loudly instead of relying
on short-circuit evaluation in the step loop.

model.py public signatures are unchanged (make_density_map gains optional
radius kwargs with defaults), so the dashboard is unaffected. Verified the
regenerated curve data is bit-identical to the committed results.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add ode_reference.py: the classical well-mixed SEIQR ODE (scipy solve_ivp)
plus the tools to validate the CA against it. Per-step probabilities map to
continuous rates by rho = -ln(1 - p) (the p ~= p shortcut is 28% wrong at
p_infect = 0.50); the saturating exposure rule maps to a force of infection
-ln(1 - p(1 - (1 - i)**8)) that linearises to beta = 8p; the single-draw
I->Q / I->R competition maps to a combined-hazard split. A global-coupling CA
variant drives exposure by the global infected fraction and reproduces the
exact discrete recursion to ~2% of peak and the ODE peak to ~2%, while the
standard local CA departs (a ~3.5x lower, ~50-step-later peak) as the genuine
spatial effect. Comparisons use the interior 2304-cell population.

Wire scenario 7 into run_experiments.py (results['ode_validation']) and add a
guarded Validation figure and metrics to the dashboard Performance tab. Trim
now-unused state-constant imports in the touched files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add tests/ covering the model core (density-map zone counts, S->E neighbour
gating, E->I/I->Q thresholds, Q non-transmission, boundary preservation, the
half-open lockdown window and its guard, vaccination dose/targeting/copy
semantics, seed reproducibility, and statistical equivalence with the original
seiqr.py) and the ODE reference (rate mapping, combined-hazard split, saturating
force of infection, R0, compartment conservation, and the well-mixed-limit
agreement of the CA against both the exact discrete recursion and the ODE).

Add pyproject.toml with the ruff config (ignore E741 for the standard SEIQR
state names; leave the frozen seiqr.py's legacy imports unlinted) and the pytest
path config, and a GitHub Actions workflow running ruff and pytest on push and
pull request. Split the remaining semicolon statements in run_experiments.py so
the tree is clean under default ruff rules; curve data is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add make_demo_gif.py and the rendered assets/demo.gif preview (GitHub cannot
embed the live Plotly dashboard) at the top of the README. Add a Validation
section stating the mean-field-limit argument and the measured agreement, a CI
badge, updated project structure and run instructions, and flip the roadmap to
mark analytical validation and tests + CI as done. Normalise the README to the
project's house style (British spelling, no em dashes). Give run_all a docstring
noting the seeded reproducibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add an MIT licence for the individual extension, with a note that the preserved
seiqr.py originates from the UCL group project and its copyright remains with
the original team. Gitignore the original report PDF so it is not published to
the public repository (it contains other team members' work); team credit stays
in the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Streamlit deprecated use_container_width; switch all plotly_chart and button
calls to width='stretch'. No behaviour change; the dashboard loads cleanly with
no deprecation warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ethanbuckley
ethanbuckley merged commit 02021e0 into main Jul 3, 2026
1 check passed
@ethanbuckley
ethanbuckley deleted the portfolio-hardening branch July 11, 2026 00:17
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.

1 participant