Starsim port 2 - #444
Merged
Merged
Conversation
Add the internal variant axis to the single cv.COVID module (Design B):
- nv/variant_map/variant_pars (wild=index 0, 5 keys from get_variant_pars)
- scalar per-agent tags exposed_variant/infectious_variant/recovered_variant
(set on infection/infectious-transition/recovery, cleared on death)
- 2D sus_imm/symp_imm/sev_imm (nv, n_raw) allocated in init_post; births
rejected (arrays not growth-aware; deferred to M4/M6)
- the 12-key 2D by_variant results sub-dict as a nested ss.Results (float),
with _flow_variant accumulators (new_{infections,symptomatic,severe} counted
at infection, new_infectious at the infectious transition -- the v3 quirk)
- variant-aware set_prognoses: per-uid rel_*_prob and cross-immunity reductions
fold into M2's exact four bernoulli draws
At nv==1 the factors are all 1.0 and immunity all-zero, so the draw sequence and
results are bit-for-bit identical to M2 (verified on random + hybrid, fixed seed).
The infect() override, cv.variant, and cv.CrossImmunity land in Tasks 2-3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…v.variant - Override cv.COVID.infect() to loop variants (v3 sim.py:622-649): per-variant source mask (infectious_variant==vi), beta scaled by the variant's rel_beta, rel_sus reduced by (1-sus_imm[vi]); trans_rng.rvs(src,trg) preserved verbatim (multi_random auto-jumps per call => independent per-variant draws). Dedup keeps the lowest-variant-index per target (v3 sequential-loop tie-break); the surviving per-target variant is recorded by UID in _new_case_variant for set_prognoses. - cv.COVID.import_variant() routes seeds/imports to set_prognoses + bumps n_imports. - Restore cv.variant in new covasim/immunity.py (Design B: a registration+seeding descriptor, not a module): parse / initialize (grow nv) / apply (mid-run + t0 introduction of n_imports susceptibles). Also build_immunity_matrix (used in Task 3). - cv.Sim(variants=...) registers variants into the single module before state allocation; cv.COVID drives apply() each step in step_state. Export cv.variant. - set_prognoses tags immediately-infectious agents (dur_exp2inf==0 edge case) so the by_variant stock stays exact; update_results NaN-guards the bincounts. nv==1 remains byte-for-byte identical to M2 (verified: the overridden infect equals stock ss.Infection.infect on a fixed seed). A 2-variant run transmits both variants independently with no DistNotReadyError; per-variant rel_beta scales transmission; mid-run imports land on the right day. Cross-immunity + reinfection are Task 3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New covasim/connectors.py: cv.CrossImmunity(ss.Connector). At the connector loop slot (after step_state, before transmission -- where v3 ran check_immunity) it reads every ever-recovered agent's recovered_variant and the asymmetric cross-immunity matrix and writes the module's 2D sus_imm/symp_imm/sev_imm (all three axes, NAb-free). Ever-recovered is keyed off finite ti_recovered<=ti (v3 was_inf), not the transient recovered BoolState (adversary #4). Matrix built via build_immunity_matrix. - Attaching the connector flips covid.cross_immunity_active on (Open Q B): recovery then restores susceptibility (reinfection), without touching the public use_waning flag (reserved for M4). cv.Sim auto-attaches CrossImmunity when nv>1; connectors= overrides. - Export cv.CrossImmunity. Documented divergence (adversary #6): same-variant protection is the diagonal (1.0) so same-variant reinfection is exactly 0 (v3's NAb numerics permit a tiny amount). Tests: matrix written correctly to all three axes; same-variant sus_imm==1.0; reinfection occurs under cross-immunity; weaker cross-immunity permits more heterologous reinfection; host exclusivity (one SEIR chain: exposed_variant==infectious_variant); nv==1 keeps no connector + permanent immunity (still byte-identical to M2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- cv.Sim.finalize bridges the multi-variant results to the v3 top-level path (Open Q E): references sim.results['variant'] + sim.results['n_imports']; manually scales the count-type by_variant Results by pop_scale (the auto-scaler skips nested results); adds the initial-wild seed-offset to cum_infections_by_variant[0]; recomputes the prevalence/incidence_by_variant rate denominators against the scaled population. - Fold per-variant rel_beta + sus_imm into the ACTIVE values in infect() (not rel_trans.raw) so the uninitialized inactive raw slots are never multiplied -- fixes a garbage*rel_beta overflow that COVASIM_WARNINGS=error rejected. nv==1 stays byte-identical to M2. - Defensive: cv.CrossImmunity filters to finite recovered_variant before the matrix index (adversarial-review hardening); clarifying comment on the exposed-vs-infected tagging gate. - Regression harness: anchor_m3.py (wild + alpha@d10 + delta@d30, dual-version, v3 baseline with cross-immunity active), build_summary_m3 + METRIC_KEYS_M3, m3 anchors in multi_seed_v3.py / compare.py, test_m3_parity.py, .gitignore + README M3 section. Acceptance (documented static-vs-NAb divergence, M3 design spec Open Q D): the gate hard-gates the convergent subset -- cum_infections_wild (|z|~0), peak_n_infectious, peak_prevalence (|z|<3.3 both backends) -- validating the multi-variant machinery, and reports the per-variant alpha/delta absolute counts + aggregate cum_infections as informational (the late escape variant diverges ~7-10x because M3's static matrix lacks v3's NAb weighting; re-converged in M4). See NOTES_FOR_CLIFF.md. Demo: /tmp/m3_demo.png. v3.1.8 baselines generated locally (gitignored); the gate skips when absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan/spec for M4 following the M0-M3 pattern. Design: use_waning gates the M3 static cross-immunity (default, byte-identical to M2/M3) vs the M4 NAb-weighted path. NAb state (peak_nab/nab/t_nab_event) is host-level on cv.COVID; the kinetics + calc_VE mapping fold into the existing cv.CrossImmunity connector (the M3 landing zone). Acceptance is directional (waning => more reinfections + nonzero nabs) plus M3-anchor re-convergence toward the v3.1.8 use_waning=True baseline. Vaccines stay in M6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port precompute_waning + nab_growth_decay + nab_decay + exp_decay + calc_VE from _v2_legacy/immunity.py into the active covasim/immunity.py (cvu.true -> np.nonzero, numpy default dtypes). The default nab_growth_decay kernel is bit-identical to v3 (max abs diff 0.0 over 180 days). calc_VE(0)==0, monotonic, bounded in [0,1). Imported but not yet wired into the live engine -- the M3-sanctioned NAb landing zone. Wiring (NAb state + acquisition + connector NAb-weighting) lands in M4 Tasks 2-3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iring - Add host-level NAb state (peak_nab/nab/t_nab_event/n_breakthroughs) + the NAb pars (use_waning, nab_init/nab_decay/nab_boost/nab_eff/rel_imm_symp/trans_redux) from v3. - set_prognoses (under use_waning) acquires/boosts peak NAb at infection via _update_peak_nab: prior-NAb agents boosted by nab_boost; the rest draw init_nab (an ss.normal Dist, created LAST so the branch bernoullis keep their CRN slots), scaled by symptom severity (rel_imm_symp) and normalized by (1+alpha_inf_diff). First breakthrough reduces rel_trans_base by trans_redux. - Precompute the nab_kin waning kernel once in init_post; cv.Sim(use_waning=...) forwards to cv.COVID and attaches cv.CrossImmunity when use_waning OR nv>1. - infect() now folds sus_imm whenever cross_immunity_active (not just nv>1), so use_waning=True single-variant reinfection is protected; sus_imm is all-zero with no connector, so use_waning= False nv==1 stays byte-identical to M2/M3 (verified, both backends). The connector still writes the static matrix; NAb-WEIGHTING of sus_imm (calc_VE(nab x matrix)) is M4 Task 3. Tests: NAb acquisition, severity scaling, nab_boost on reinfection, kernel precompute. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…convergence - cv.CrossImmunity.step branches on use_waning: static matrix (M3) vs NAb-weighted (M4). Under waning it advances NAb kinetics (nab += nab_kin[ti-t_nab_event]*peak_nab, clamped) then writes sus_imm/symp_imm/sev_imm = calc_VE(nab x matrix, axis) per axis for ever-recovered agents. - pop_nabs / pop_protection results (mean over alive agents), filled under use_waning. - anchor_m4.py (M3 scenario + use_waning=True) reuses the M3 v3.1.8 baseline (same use_waning=True regime); test_m4_parity.py hard-gates the FULL metric set at |z|<5; compare.py m4 anchor; README. Validation: the M3 anchor's documented static-vs-NAb divergence CLOSES under use_waning=True -- every pinned metric (aggregate + per-variant wild/alpha/delta) re-converges to the v3.1.8 baseline within |z|<3.5 (delta: |z| 25->0.3, 46->0.1; cum_infections ratio 1.58->1.00). Directional test_waning passes (waning => more reinfection + nonzero NAbs/protection). use_waning=False stays byte-identical to M2/M3. Demo /tmp/m4_demo.png. Vaccines (the max(natural,vaccine) branch) -> M6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Starting-point spec + task plan for M5, grounded in the v3 reference surface (test_num/test_prob/ contact_tracing in _v2_legacy/interventions.py; test/check_diagnosed/check_quar/check_enter_iso in people.py). Proposed design (additive, states inert until a testing intervention is attached, so M1-M4 stay byte-identical): host testing/quarantine states + state machines on cv.COVID; a restored active interventions.py with cv.Intervention(ss.Intervention) + the concrete interventions at loop slot 7; quar_factor/iso_factor fold into infect(). Open questions (per-layer quar factor, state location, test_num rescaling) flagged for sign-off. NOT a full design-panel spec -- a solid stub. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ction Additive scaffolding on cv.COVID (inert until a testing/tracing intervention drives it, so M1-M4 stay byte-identical -- verified both backends): - States: tested/diagnosed/known_contact/quarantined/isolated BoolStates (auto n_* results) + date_tested/date_pos_test/date_diagnosed/date_quarantined/date_end_quarantine/date_end_isolation; quar_period par. The test bernoulli is created LAST so existing CRN slots are unchanged. - covid.test() action (v3 People.test): infectious agents test positive at test_sensitivity; not-already-diagnosed, not-lost positives get date_diagnosed = ti + test_delay. - schedule_quarantine + _step_testing (run in step_state): the v3 check_diagnosed/check_enter_iso/ check_exit_iso/check_quar machines -- diagnosis, diagnosed->isolation-until-recovery, the pending- quarantine queue (eligibility re-checked at start_day), and release at end. step_die clears the flags. The test_num/test_prob/contact_tracing interventions + the quar/iso beta modifiers are M5 Tasks 2-3. Tests: states inert with no intervention; test() diagnoses infectious agents (sensitivity scaling, no double-diagnosis); diagnosis->isolation; schedule_quarantine->quarantine->release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… flow results - New active covasim/interventions.py: cv.Intervention(ss.Intervention) base (same public name) + cv.test_prob (per-agent daily test probability by symptom/quarantine state) and cv.test_num (fixed daily budget, symptomatic-weighted selection). They run at the intervention loop slot, resolve sim.diseases['covid'], and call covid.test(); selection draws use ss.bernoulli (CRN). Honour start_day/end_day. Exported. - cv.COVID: new_tests/cum_tests/new_diagnoses/cum_diagnoses flow results (tests counted in test(), diagnoses in _step_testing, written in update_results, cumsum in finalize). Without an intervention everything is inert => byte-identical to M1-M4 (verified both backends). Tests: test_prob/test_num produce diagnoses + cum_tests/cum_diagnoses; respect start_day; test_num honours the daily budget; only ever-infected agents are diagnosed; more aggressive testing => more diagnoses. contact_tracing + quar/iso beta modifiers + the parity gate are M5 Task 3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- cv.contact_tracing(ss.Intervention): on each active day, finds the network contacts (via the per-layer edge lists, _find_contacts) of the newly-diagnosed (or presumptive: newly-tested exposed), keeps a per-layer fraction (trace_probs, ss.bernoulli CRN), marks known_contact, and schedules quarantine starting trace_time days later (capacity limit; excludes the dead). Exported. - iso_factor/quar_factor (scalar M5 approximation of v3's per-layer factors; spec Open Q A): isolated and quarantined agents have their rel_trans reduced each step in step_state. Inert with no testing intervention (no agent isolated/quarantined) => M1-M4 byte-identical (verified). Pipeline validated: test_prob + contact_tracing => diagnoses -> isolation, contacts -> quarantine (peak ~11k), and tracing+quarantine roughly halves the epidemic (14550 -> 7542 ever-infected). Tests: tracing creates quarantine; no effect without testing; tracing reduces transmission; iso/quar factor reduces rel_trans. Demo /tmp/m5_demo.png. The formal multi-seed M5 parity gate (anchor + v3 baseline with testing+tracing) is the remaining M5 piece (capability + directional validation done). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ull v3 parity) - anchor_m5.py (M2 scenario + test_prob + contact_tracing, dual-version), build_summary_m5 + METRIC_KEYS_M5, m5 anchors in multi_seed_v3/compare, test_m5_parity.py, .gitignore + README. - FIX: v3's quar_factor reduces both transmissibility AND susceptibility, but infect() only applied it to rel_trans -- quarantined agents stayed fully susceptible, so the epidemic over-shot v3 by ~17-30% (cum_infections z~20). Applying quar_factor to rel_sus too (folded into the active values alongside sus_imm) brings every gated metric to |z|<2 (cum_infections z~-0.1, cum_diagnoses ~0, quarantine/isolation/deaths/peak all <2). nv==1 / no-intervention stays byte-identical (quar_mult is None when nobody is quarantined). Gate (both backends pass): cum_infections/cum_deaths/cum_diagnoses/peak_n_infectious/quarantined/ isolated at |z|<5. cum_tests informational (testing volume matches to ~2%; tiny SE -> |z|~8, the documented CRN residual). The iso/quar factors are a scalar approximation of v3's per-layer values (spec Open Q A); aggregate already matches. v3.1.8 baselines gitignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan/spec for M6 following the project pattern. Vaccines are the last consumer of the M4 NAb pipeline: a dose calls update_peak_nab with the vaccine's nab_init/nab_boost (no symptom scaling) and the connector's check_immunity combines natural + vaccine immunity as max(natural_cross, vaccine_eff) then effective_nabs = nab x imm. Additive + gated behind use_waning (no vaccination => M1-M5 byte-identical; the vaccine NAb draw uses a separate Dist so M4 is unchanged). Covers BaseVaccination/vaccinate_prob/vaccinate_num/vaccinate + products + target_eff + simple_vaccine. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…accine branch - cv.COVID: vaccinated/doses/vaccine_source/date_vaccinated states + vaccine_pars/vaccine_map registry + a separate _vacc_nab_init Dist (distinct from the natural _nab_init, so a dose drawn at the intervention slot does not perturb the natural-infection NAb draw => M4 byte-identical). - _update_peak_nab generalised: nab_pars=None is the M4 natural path (unchanged); a vaccine dose uses the vaccine's nab_init/nab_boost with no symptom scaling / no natural normalisation. - covid.vaccinate_agents() applies a dose (sets state + NAb boost); new_doses/cum_doses/ new_vaccinated/cum_vaccinated flow results. - cv.CrossImmunity (use_waning) now computes protection over ALL active agents as imm = max(natural_cross_immunity, vaccine_efficacy) per variant, then calc_VE(nab x imm) -- natural and vaccine immunity share one NAb-weighted curve (v3 check_immunity). No vaccine => vaccine term 0 => byte-identical to M4. calc_VE_symp ported into immunity.py (for target_eff). Verified: M1-M5 byte-identical; M4 parity gate still passes (re-convergence intact); vaccinated agents acquire peak NAb and gain protection via the vaccine branch. Interventions land in M6 Task 2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… products - cv.BaseVaccination(cv.Intervention): parse a predefined product (pfizer/moderna/az/jj/novavax/ sinovac/sinopharm via get_vaccine_choices/variant_pars/dose_pars) or a pars dict; register vaccine_pars/vaccine_map + per-variant efficacy into the module at init; target_eff back-calc (calc_VE_symp on a NAb grid); check_doses; the dose action (skip dead/fully-dosed, then covid.vaccinate_agents). Requires use_waning=True. - cv.vaccinate_prob (per-day prob on days + second-dose scheduling at +interval; booster) and cv.vaccinate_num (num_doses/day in a priority sequence, second doses prioritised, pop_scale dose accounting). cv.vaccinate wrapper. Selection via ss.bernoulli (CRN); num ordering deterministic. - Export BaseVaccination/vaccinate/vaccinate_prob/vaccinate_num. Tests: no-vaccine byte-identical; vaccinate_prob reduces the epidemic + 2-dose scheduling (~2 doses/person); vaccinate_num honours the daily budget; per-variant efficacy (pfizer protects vaccinated agents more vs wild than the escape variant beta); requires use_waning. simple_vaccine + the parity gate are M6 Task 3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- cv.simple_vaccine (non-NAb, use_waning=False): directly scales vaccinated agents' rel_sus and
symptomatic probability by the efficacy factors (cumulative per-dose), bypassing the NAb pipeline;
preserves the v3 public API. Exported.
- anchor_m6 (single-variant, use_waning=True, vaccinate_prob('pfizer')) dual-version, build_summary_m6
(cum_infections/cum_severe/cum_deaths/peak_n_infectious/cum_doses/cum_vaccinated), m6 anchors in
multi_seed_v3/compare, test_m6_parity, .gitignore + README.
M6 parity (both backends, |z|<5 gate): EVERY metric matches v3.1.8 within |z|<2.1 -- cum_infections
~-2.0, cum_severe ~0, cum_deaths ~-1, peak_n_infectious ~-1.8, cum_doses/cum_vaccinated ~-0.9. The
vaccine NAb pipeline + the connector's max(natural,vaccine) immunity faithfully reproduce v3.
(cum_infections counts infection EVENTS = sum of cum_infections_by_variant, since use_waning produces
reinfections.) Tests: simple_vaccine reduces infections without NAbs + scales rel_sus. Demo
/tmp/m6_demo.png (pfizer 1500/day: 20847->836 infections, 105->3 deaths). Full suite: 101 passed,
0 warnings. historical_*/prior_immunity deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Flat results bridge (completes the deferred Open Q E): cv.Sim.finalize references every top-level Result of the covid module at the sim root, so v3-style sim.results['cum_deaths'] etc. resolve. Additive (references only) -> M1-M6 byte-identical/gates unaffected. - cv.Fit (new covasim/analysis.py): the v3 model-vs-data goodness-of-fit -- reconcile_inputs (date/ day matching) -> compute_diffs -> compute_gofs -> compute_losses -> compute_mismatch, reading the bridged flat results + accepting a data DataFrame; reuses the already-active cv.compute_gof. Default weights cum_deaths:10/cum_diagnoses:5; custom series supported. Exported. cv.Fit's logic is engine-independent (compute_gof is shared with v3), so it is validated deterministically: custom-series mismatch == weight*sum(compute_gof), perfect fit -> 0, default weights applied to matched losses, date-matched fitting on a real sim. cv.Calibration (the Optuna ss.Calibration wrapper) is the remaining M7 piece -- design captured in the spec, deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cv.Calibration wraps Starsim's Optuna-based ss.Calibration: calib_pars=dict(key=[best,low,high]) (v3 form) -> ss sampler specs; a build_fn applies each trial's sampled value to the COVID module's pars (init-then-apply -- the severity scalers are read at set_prognoses runtime, so post-init application takes effect); the eval returns cv.Fit(sim, data).mismatch (minimised by Optuna). Exposes best_pars + df. Exported. Acceptance: a 15-trial calibration of rel_severe_prob to data generated at the true value 1.6 (start 1.0, range [0.5,2.5]) converges toward the truth and beats the starting-guess mismatch (slow-marked). M7 (Fit + Calibration) complete. Init-time-par calibration (e.g. the per-layer beta dict) would need a pre-init hook -- noted as a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- cv.MultiSim: runs a sim across seeds via ss.MultiSim, then reduces over the per-seed COVID time-series Results to a median/mean trajectory + low/high quantile bands (ss.MultiSim.run works with cv.Sim, but its reduce() does not handle Covasim's bridged/nested results, so cv.MultiSim reduces itself). .mean()/.median()/.plot() (median line + fill_between band). - cv.parallel (run a list of distinct sims), cv.multi_run (list of N seeded runs), cv.single_run. - cv.Scenarios: named parameter-set comparison -- builds a fresh cv.Sim per scenario (basepars + override), runs a cv.MultiSim, stores the reduced UQ; .plot() overlays scenarios. - UQ demo examples/m8_uq_sweep.py (median + 10/90 band) + smoke tests. Acceptance: multi-seed CIs produced (band width>0, median within band); Scenarios baseline-vs-pfizer gives a clear reduction. The "retrofit onto multi-seed z-score gates" sub-task is already satisfied (the M1-M6 parity gates are multi-seed z-score gates via parity_gate). All names preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing) Default cv.MultiSim.run(parallel=False) deep-copies the base sim and runs n_runs consecutive seeds serially (or runs explicit sims), which is deterministic and emits no multiprocess fork DeprecationWarning (Python 3.13). parallel=True dispatches via ss.MultiSim for large sweeps. Keeps the strict-warnings bar perfectly clean (the suite now emits 0 warnings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sTree - cv.Analyzer(ss.Analyzer) base + cv.snapshot (per-agent state on given days), cv.age_histogram (state counts by age bin), cv.nab_histogram (NAb distribution, use_waning) -- ss.Analyzer subclasses running at the analyzer loop slot, recording on matched days. Exported. - cv.TransTree: reconstructs the transmission tree from a transmission log that cv.COVID.infect() records ONLY when a TransTree analyzer switches it on (gated flag) -- so M1-M8 stay byte-identical. Exposes infection_events (source,target,day,variant), n_targets (offspring per infector), r0 (mean offspring per infector), make_detailed(). Full networkx graph + tree plotting -> plotting pass. Tests: snapshot/age-hist counts match the result series; analyzers are observational (byte-identical); nab_histogram bins positive NAbs; TransTree records events + computes R0; logging off without the analyzer. Remaining M9: synthpops backend (BLOCKED -- not installed) + daily_age_stats + the Covasim-specific sim.plot()/Fit.plot views (plotting pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…portunistic) Round out the intervention API (architecture map: M5-or-opportunistic) and enable the M10 default-baseline scenario. All additive (inert until their day => M1-M9 byte-identical): - cv.change_beta: scale the disease's per-layer beta by a factor on given days (relative to the original; scales the per-day beta value via ss.probperday to avoid the rate/prob non-linearity). - cv.clip_edges: clip a fraction of network edges on given days (subsample the original edge set; restored when the fraction rises). Unlike change_beta this also reduces traceable contacts. - cv.dynamic_pars: set parameters on given days (resolved against COVID/sim pars). - cv.sequence: switch between a sequence of interventions over time (applies the most-recent one). Tests: change_beta/clip_edges shrink the epidemic (and change_beta=0 stops onward transmission, restores on reset); clip_edges keeps ~the requested edge fraction; dynamic_pars(rel_severe_prob=2) roughly doubles cum_severe; sequence switches test_prob->test_num. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M9 plotting pass: - cv.Sim.plot: Covasim-specific headline panels (stock ss.Sim.plot breaks on the 2D by-variant results); finalize() split out _finalize_variant_bridge. - Fit.plot (data-vs-sim per key) and TransTree.plot (offspring histogram). - cv.daily_age_stats already present; tests added for it + the plots. M10 save/load: - cv.Sim.save defaults shrink=False (cv.COVID carries large legitimate state, so stock shrink-on-save trips Starsim's size check); matches v3 semantics. - cv.Sim.load restored; cv.load returns v4 (ss.Base) objects natively, skipping the v3 migration path that crashed on the Starsim version string. - tests/test_m10_saveload.py: round-trip across cv.Sim.load/cv.load/sc.load. synthpops backend is the only remaining M9 piece (optional dep, not installed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion guide Backwards compatibility: - cv.Sim(pars_dict) restored (the canonical v3 form): the constructor pulls the Covasim sim-level keys out of a pars dict; explicit keywords override dict entries; the rest forwards to ss.Sim. Keyword form is byte-identical. - Added cv.Sim.initialize (alias -> init), cv.Sim.export_pars (config + COVID scalar pars -> JSON), and cv.diff_sims (summary comparison). Release readiness: - Version bumped 3.1.8 -> 4.0.0 (+ versiondate) with a full v4.0.0 CHANGELOG entry (Starsim port; preserved API; CRN-vs-global-RNG regression note). - Regenerated tests/baseline.json + benchmark.json + regression/pars_v4.0.0.json via tests/update_baseline. test_baselines un-skipped + rewritten to v4 idioms; both baseline + benchmark pass (v4 is deterministic per fixed seed). - Retired test_regression / test_migration (the v1.7.0 pickle predates the Starsim object model); v4 regression guard is the multi-seed parity gate. - Added docs/migration.md (v3 -> v4 guide) and wired it into the Quarto navbar. Full suite: 137 passed, 9 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records that M10 is substantively complete; the wholesale quarantine deletion (_v2_legacy + tests/_legacy) and the v4.0.0 tag/push are consciously left for Cliff (release-coupled; the quarantines are still cited as in-repo provenance). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ran all 11 docs/tutorials notebooks under v3.1.8 and v4.0.0, compared the
executed outputs cell-by-cell (a 26-agent analysis workflow classified every
difference and adversarially verified each claimed bug), and fixed the genuine
compat regressions. v4 tutorial-cell errors: 56 -> 20. Full suite stays green
(137 passed, 9 skipped); byte-identity preserved (additive shims only).
cv.Sim: end_day=/datafile=/n_agents= + route disease pars (rel_death_prob,
nab_decay, ...) from dict/kwargs to the COVID module; verbose defaults from
cv.options.verbose; sim.version=4.0.0 + sim.git_info; day()/date()/brief()/
get_analyzer(s)/get_intervention(s)/compute_fit()/calibrate()/make_transtree()/
export_pars()/to_excel()/plot_result()/initialize(reset=); plot() accepts a
string key, to_plot= alias, 'variant'/'overview' meta-keys, honours
options.returnfig; results expose v3 date/t keys.
Interventions: accept+ignore cosmetic v3 kwargs (do_plot/show_label/line_args);
convert date-string days + start_day/end_day to indices; test_num('data') reads
sim.data['new_tests']; change_beta tolerates callable days; age_histogram(days=
None) defaults to the final day + real .plot().
MultiSim: list construction, run(n_runs=), plot_result, combine, merge,
returnfig. Calibration: custom_fn hook + plot_trend/plot_sims/plot_all.
Documented in NOTES_FOR_CLIFF (tutorial validation section) + migration.md: the
use_waning default flip (v3 True -> v4 False), people-level disease state,
bare-function interventions/analyzers, namespaced summary/CRN RNG, and the
deeper confirmed gaps left for follow-up (r_eff result, quarantine result
series, n_imports seeding, sim['par'] access, re-init). Executed notebooks +
22 HTML exports + comparator live under tutorial_validation/ (git-excluded).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ults, n_imports
Follow-up to the tutorial validation. All COVID-module changes; default-off
features stay byte-identical (parity gates + suite green: 143 passed).
- r_eff: ported v3's 'daily' compute_r_eff into finalize_results (new scale=False
Result, bridged to sim.results); sim.plot('r_eff') works.
- Contact-tracing quarantine BUG: with the default trace_time=0, tracing never
quarantined anyone -- _step_testing pops the quarantine queue (loop slot 4,
in step_state) before contact_tracing.step schedules it (slot 7), so same-day
requests were lost. schedule_quarantine now clamps a same-day/past start_date
to ti+1. trace_time>=1 (incl. the M5 parity anchor's trace_time=2) is unchanged,
so parity holds; baseline.json regenerated (default sim uses trace_time=0).
- Quarantine results: added new_quarantined/cum_quarantined/test_yield
(n_quarantined/n_isolated were already auto-counted).
- n_imports: registered as a COVID par (default 0) + _seed_imports (Poisson draw
of susceptibles -> wild infection each step). Inert at 0 (byte-identical);
settable via cv.Sim(n_imports=) / cv.dynamic_pars.
Tests: tests/test_confirmed_gaps.py (6). Baselines regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…yzer apply()
Follow-up to the tutorial validation. All additive (inert when unused) -> the
parity gates + suite stay green (149 passed).
- Vaccination subtarget=: ported the v3 get_subtargets helper + subtarget on
vaccinate_prob / vaccinate_num / simple_vaccine. {'inds': fn/array, 'vals':
per-agent-prob} (or callable); for vaccinate_num, vals==0 excludes (the
booster-targeting idiom). booster= already existed.
- location=: cv.Sim(location='Japan') draws ages from the country pyramid via
cv.data.get_age_distribution (Japan ~47 vs default ~38 vs Bangladesh ~30).
- Custom analyzers: cv.Analyzer.step() now dispatches to a subclass-defined
v3-style apply(self, sim).
- Custom nab_decay forms already worked in the waning engine; the disease-pars
routing (prior commit) lets cv.Sim(nab_decay=...) reach them.
Tests: tests/test_unported_features.py (6). NOTES + migration guide updated;
historical immunity (pre-t0 NAb imprinting) and people-level disease-state
access remain documented deferrals with recipes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s alias Ports the v3 historical_vaccinate_prob / historical_wave / prior_immunity. All require use_waning=True; inert otherwise (suite green: 152 passed). - cv.COVID.imprint_historical_nab(uids, event_day): replays the connector's clamped NAb accumulation from a back-dated (negative) event_day to t=0, extending the nab_kin kernel to cover the offset. So historically-immunised agents start with correctly-decayed NAbs (360-day-old dose -> NAb 0.43 vs a 30-day-old dose -> 1.40). - historical_vaccinate_prob(vaccinate_prob): negative days imprint at init (single back-dated dose); non-negative days behave like vaccinate_prob. - historical_wave: prior natural-infection wave -> recovered state + back-dated natural NAbs. prior_immunity(): the v3 dispatch wrapper. - nab_histogram now accepts the v3 edges= alias for bins=. NOTES + migration guide updated. Remaining deferrals (documented): the use_waning default (v3 True vs v4 False -- Cliff's call, blocks the immunity tutorial's unset-use_waning cells), people-level disease-state access, and the Layer/contacts internals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mpat) Both are __getattr__ fallbacks (only consulted when normal lookup fails, so they cannot shadow real attributes) -> byte-identity preserved (155 passed). - cv.People.__getattr__: proxies a whitelist of per-agent disease-state names (exposed/infectious/rel_sus/doses/...) through to sim.diseases.covid, so v3 custom interventions/analyzers using sim.people.<state> keep working. Reads return the live disease array, so write-through (sim.people.rel_sus[inds]=0) works -- a protect_elderly custom intervention reduces deaths again. - cv.Sim.__getattr__: exposes the Covasim sim-level config + COVID pars as attributes (sim.beta / sim.start_day / sim.rel_death_prob), and since ss.Sim.__getitem__ delegates to getattr, sim['beta'] works too. Reads only. Tutorial v4 cell errors now 56 -> 15 across the whole follow-up; the remaining 15 are 5x the use_waning default (Cliff's call; features work with use_waning=True) + documented architectural items (Layer internals, reserved analyzer attrs, re-init, save/load idioms). NOTES + migration guide updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- use_waning default flipped False -> True in cv.Sim, matching v3: cv.Sim() now runs with waning + cross-immunity by default, so vaccinate_* and the immunity tutorial work without an explicit use_waning=True. - sim['rel_death_prob'] = 4 now routes to the COVID module (cv.Sim.__setitem__ + _resolve_covid), so a fresh cv.Sim(); sim['par']=v; sim.run() applies it; the __getattr__ read path resolves through the same helper (reflects the set). The flip exposed code that relied on the old default: the no-waning anchors (m1/m2/m5) and the M3/M4 static-path unit tests set use_waning explicitly on their v4 sims now (they were implicitly depending on default=False). M5 parity re-confirmed (anchor_m5's v4 branch had omitted use_waning=False); all anchors + baseline set use_waning explicitly, so the regression infrastructure is robust to the default. pars_v4.0.0.json regenerated. Full suite: 155 passed, 9 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- nab_histogram now defines its own .plot() (a bar chart per recorded day), so it no longer hits Starsim's generic analyzer plot (which assumed a flat results dict and broke on a single Axes) -- fixes the immunity tutorial's historical-vaccination + NAb-histogram cell. With the use_waning=True default, tutorial v4 cell errors are down to 10 (from 56): 5 of 11 notebooks fully clean. The remaining 10 are documented architectural / notebook-idiom items (Layer internals, reserved analyzer attrs, re-init, bare-function sim.t/.tvec, ss.Arr.copy idiom, duplicate intervention names) -- no remaining easy fixes. Full suite: 155 passed, 9 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
No description provided.