Skip to content

Release 1.1.2

Choose a tag to compare

@Julian-Patzner Julian-Patzner released this 19 Jun 12:04
· 305 commits to main since this release
7772623

This patch adds an optional firing condition to tick triggers and reworks intervention follow-up handling and the event queue to be type-stable and allocation-optimized.


Conditional Tick Triggers

ITickTrigger and STickTrigger now accept an optional condition predicate on the Simulation, evaluated alongside switch_tick/interval to decide whether the trigger fires at all.

  • condition keyword: Pass condition = sim -> ... (default sim -> true) to suppress a trigger based on simulation-wide state, independently of its schedule. This is separate from the per-individual/per-setting condition on the trigger's strategy. The predicate is wrapped in a FunctionWrapper for type-stable evaluation, and a non-boolean return raises a clear error, matching how strategy conditions behave.
  • is_disease_active(sim): A new exported helper returning true while any individuals are exposed or infectious. Its purpose is to be used as a tick-trigger condition: a recurring trigger otherwise keeps the simulation permanently awake, but gating it on is_disease_active lets the trigger stop firing once the disease dies out, so is_dormant can fast-forward the simulation again.
  • should_fire(trigger, sim): A new method that consults the schedule and the trigger-level condition together; step! and is_dormant now use it.

This is additive and backward-compatible — the default condition preserves the previous always-fire behavior.


Direct Follow-Up Triggering

Built-in measures no longer return a Handover for the event queue to act on later. Each now triggers its follow-up strategy directly and returns nothing.

  • apply_followup!: A new helper that triggers a follow-up strategy for a single focal object (Individual/Setting) or a vector of them, dispatched on the concrete focal/strategy type, and a no-op when the follow-up is nothing. All built-in measures (Test, PoolTest, TestAll, FindSetting, FindSettingMembers, IsOpen, Vaccinate, TraceInfectiousContacts) now call it instead of constructing a Handover.
  • FindMembers / member-resolving measures: FindSettingMembers, FindMembers, and TraceInfectiousContacts now trigger the follow-up per member inline rather than collecting members into a Handover. FindMembers reuses internal filter_buffer/sample_buffer buffers and a new in-place sample_individuals!, avoiding a fresh member-list allocation on each run.
  • Handover is unchanged for custom measures. process_event still checks for a returned Handover and applies its follow-up, so user-defined measures that return a Handover continue to work exactly as before. Only the built-in measures changed.

Event Queue Optimization

The EventQueue now keeps individual and setting events in separate, concretely-typed buckets so that draining and processing stay statically dispatched. It now also utilizes free lists to reduce memory allocations.

  • Separate typed buckets: IMeasureEvents and SMeasureEvents are stored in their own bucket and staging vectors (i_buckets/s_buckets, i_staging/s_staging) rather than a shared Vector{Event}. enqueue!, stage!, and _insert! dispatch on the concrete event type.
  • process_due!: A new draining path used by process_events! that processes every event due for a tick directly from the typed buckets, keeping process_event statically dispatched and avoiding boxing each event into a Union{IMeasureEvent, SMeasureEvent}.
  • Bucket recycling: Drained bucket vectors are pushed onto per-type free lists (i_free/s_free) and reused for future tick slots, so growing the queue reuses existing capacity rather than repeatedly reallocating.
  • Defined intra-tick ordering: Within a tick, setting events now drain before individual events (each LIFO within its own bucket). This is now a specified behavior of the queue.

Reproducibility Notes

  • Contact tracing RNG: TraceInfectiousContacts now draws one random value per traced infectee in sequence rather than drawing them all at once and filtering. The selection logic is unchanged, but the draw pattern differs, so seeded runs that use contact tracing will produce different (but equally valid) results than 1.1.1. Runs without contact tracing are unaffected.
  • Seeded RNG construction: Per-thread RNGs are now derived from a single master Xoshiro constructed once (in both Simulation construction and reset!), rather than re-seeding a fresh Xoshiro with the same seed per thread.