Release 1.1: Batch Rework, Custom Individuals & Intervention Overhaul #85
Julian-Patzner
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Welcome to the 1.1 release! This release rebuilds the batch API around a streaming, memory-bounded processing model, adds support for custom per-individual attributes, and overhauls the intervention engine for type-stable, allocation-free execution of measures.
Note that the batch rewrite and parts of the intervention internals contain breaking changes. Please read the relevant sections below before upgrading.
Batch Rework
The
Batchworkflow has been redesigned so that memory no longer scales with the number of runs. Instead of instantiating and retaining aSimulation(and later aResultData) per run, aBatchnow stores lightweight simulation configurations and folds each run's results into running accumulators as it goes.BatchProcessornow aggregates results using Welford online statistics (mean, variance, min/max, 95% CI). Each run is executed, accumulated, and discarded, so peak memory is proportional to a single run rather than to the size of the batch. A newWelfordStateaccumulator and helpers (welford_update!,welford_to_aggregate,welford_df_to_stats_df,welford_df_to_stats_df_multicol) back this model and produce the same aggregate schema as the existingaggregate_*utilities.Batch:Batchnow holds a vector of simulation-configNamedTuples and per-run setup functions instead ofSimulationobjects. Create one withBatch(n_runs = N; simargs...), wheresimargsare any keyword arguments accepted bySimulation(). An optionalsetup = sim -> ...hook lets you attach interventions, strategies, or triggers to each run after construction but before it executes.process!replacesrun!'s old semantics:process!(batch; ...)runs every configuration sequentially and returns aBatchProcessor.run!(::Batch)is retained as an alias forprocess!(it now returns aBatchProcessorrather than mutating and returning theBatch).seed = Ntoprocess!/BatchDatafor fully reproducible batches, and retrieve the seed actually used viaseed(bp).median_by = pp -> ...to select a criterion (e.g. total infections); the run whose criterion is closest to the median across the batch is re-run in full and stored asmedian_run(bp). For multi-group batches, one median run is computed per group. Disable withmedian_by = nothing(the default).group_by = :label(or any config field) to accumulate statistics separately per group, accessible viaper_label(bd)/per_group.gemsplot(bd)then draws one mean±CI ribbon per label automatically.BatchData-native plots:gemsplot(bd::BatchData)now dispatches directly to mean±CI ribbon plots built from the streamed accumulators — no need to retain every per-runResultData.BatchData(b)remains a convenient shorthand forBatchData(process!(b)).keep_rundatafor individual runs: Individual run traces are retained by default in the lightweightLightRDformat and are available viaruns(bd). Passkeep_rundata = falseto drop them entirely for minimum memory.customloggerpassthrough:process!(batch; customlogger = ...)attaches an independent copy of aCustomLoggerto each run, so logged data is never shared across runs.BatchProcessor/BatchDatagaincumulative_cases,dark_figure,generation_times,n_runs,r0,median_run,seed, and more.Breaking changes
Batchno longer storesSimulationobjects. UseBatch(n_runs = N, kwarg = val, ...)andsimconfigs(batch)/simsetups(batch).simulations(batch)has been removed; usesimconfigs(batch).add!(::Simulation, ::Batch)andremove!(::Simulation, ::Batch)have been removed; useadd!(::NamedTuple, ::Batch).ResultData(::Batch)andPostProcessor(::Batch)have been removed; useBatchData(::Batch).gemsplot'ssplitlabelis renamedsplitgroup, and thecombined = :bylabeloption is renamedcombined = :bygroup. The old names are no longer exported.customlogger!(::Batch, ::CustomLogger)has been removed; passcustomloggertoprocess!instead.BatchProcessoraccessors that required storing everyResultData(config_files,pathogens,settingdata,population_pyramid,setting_age_contacts,strategies,run_ids,runtime,allocations,extract,extract_unique, …) have been removed. Usesimconfigs(batch)for configuration metadata, or process withkeep_rundata = trueand read per-run data viarundata(bp).See the [batch tutorial](https://immidd.github.io/GEMS/stable/tut_batches/) for the updated workflow and migration examples.
Custom Individual Extensions
You can now attach arbitrary per-agent attributes to
Individuals without modifying the core struct, by passingind_extensionto thePopulationorSimulationconstructor. Extension fields behave exactly like built-in fields, and every existing GEMS function that accepts an::Individualcontinues to work unchanged.ind_extension = [:my_attr]), whose values are read from each row and wrapped automatically; (2) a separate extensionDataFramejoined byid(individuals absent from the table receive zero-filled values with a warning); (3) a factory functionind -> extthat computes each individual's extension from its base attributes — it may return any struct, including a@kwdefstruct constructed by keyword arguments when the struct has many fields.ind.my_attr,ind.my_attr = 0.9). Access is implemented viagetproperty/setproperty!overloads; for literal core-field names the dispatch is constant-folded and inlines to a singlegetfield, so built-in field access keeps its original performance. Extension data is held in a boxedextensions::Anyslot, wrapped in anAutoExtensionNamedTuple for the named-column/DataFrame paths.Populationfrom aDataFrame:Simulation'spopulationargument now also accepts aDataFramedirectly (in addition to a path, identifier, orPopulationobject), andPopulation(df; ind_extension = ...)builds individuals in parallel as before.DataFrame:dataframe(population)appends extension fields as additional columns after the base columns, taking the schema from the first extended individual.Individualfields. Collisions are rejected at load time with an explicit error, preventing a custom field from silently shadowing a built-in attribute and keeping the population exportable.extensionsslot is typedAny, reading custom fields is type-unstable. This is negligible in dynamically-dispatched code such as customtransmission_probabilityorsample_contacts!methods, but for heavy per-contact computation on custom fields you can recover full type stability with a function barrier.See the [Custom Individual Extensions](https://immidd.github.io/GEMS/stable/tut_configfiles/#Custom-Individual-Extension) section of the config-file tutorial.
Intervention Engine Optimization
The intervention pipeline — the event queue, strategy callbacks, and per-tick setting iteration — has been reworked to be type-stable and allocation-free on the hot path. This adds a dependency on
FunctionWrappers.jl.EventQueuehas been rewritten from a sorted-vector priority queue (withO(N)searchsortedfirst+insert!) into a "calendar" queue of per-tick buckets.enqueue!is now anO(1)push!into the relevant tick bucket, and the queue is drained in tick order via a forward-advancing head pointer. Newpeektickandempty!(capacity-retaining) functions are provided;firstis no longer part of the public queue API (usepeek).process_measurecalls are now wrapped in concreteFunctionWrappertypes (IPredicate/SPredicate,IDelayFn/SDelayFn,IProcessFn/SProcessFn).MeasureEntryis now parameterized on the focal object type (IndividualorSetting), keeping each strategy's measure vector concretely typed. Theprocess_measurecallback for each measure is built once atadd_measure!time — where the concrete measure type is statically known — and stored on the event, turning the former dynamicprocess_measuredispatch into a fixed indirect call.IMeasureEvent/SMeasureEventare now immutable structs carrying this prebuilt callback.SettingsContainernow stores each setting type in a concretely-typedVector{T}(the dict value type was widened fromVector{Setting}toVector). A newforeach_setting_vectoriterates the built-in setting types (BUILTIN_SETTING_TYPES) with full type stability and no dynamic dispatch, and a newsettings(sim, ::Type{T})accessor returns a typedVector{T}.step!,open!,close!, and the tick triggers now route through these type-stable paths via function barriers.@debuglogging on the intervention hot path is now gated behind theINTERVENTION_DEBUGconstant (defaultfalse), so the logging machinery is fully elided in normal runs. Flip it totruewhen debugging.FindMembersfast path: Theselectionfilteris now wrapped as anIPredicatewith ahas_filterflag and a_select_allsentinel default. When no custom filter is supplied, the filtering pass is skipped entirely, andprocess_measureno longer rebuilds the member list multiple times.Note for API users
condition(strategy)andcondition(measure_entry)now return a wrapped predicate (IPredicate/SPredicate) rather than a bareFunction. The wrappers are callable exactly like the original closures, but code that assertedcondition(str) isa Functionor compared the stored condition by identity will need updating.Bugfixes
InexactError: Transition-time calculations across all progression categories (Asymptomatic,Symptomatic,Severe,Hospitalized,Critical) now useround(Int16, ...)instead ofInt16(...). Previously, a sampled transition duration that wasn't already a whole number would throw anInexactError; the values are now rounded to the nearest tick.death_probabilityis also explicitly cast toFloat64before comparison, andDiseaseProgression's default keyword values are nowInt16-typed.Other Changes & API Cleanup
PostProcessorconstructor removed: The undocumented 7-argumentPostProcessor(simulation, population, infections, vaccinations, deaths, tests, quarantines)reconstruction constructor has been removed.Dict-basedSimulationconstructor, and the batch streaming pipeline.This discussion was created from the release Release 1.1: Batch Rework, Custom Individuals & Intervention Overhaul.
All reactions