Choosing a Sampler

How to make a sampler for events in CompetingClocks.jl.

High-level Interface

Every event in a simulation has an identifying key which can be any immutable Julia type. A Tuple key type works fine, but it will be more performant to use a concrete type for the KeyType. The second argument is a type to use for time. Basic usage:

sampler = SamplingContext(KeyType, Float64, rng)

The sampler takes a Random.AbstractRNG as its second argument.

Specify features for the sampler with keyword arguments. These features determine the type of the sampler.

  • method=nothing–-Setting this chooses a single sampling algorithm for all clocks.
  • step_likelihood=false–-Setting this to true let's you calculate the the likelihood of the next event and time before firing it.
  • path_likelihood=false–-Set to true in order to calculate likelihood of a whole trajectory at any point in the simulation. This is more efficient than adding up steps in the likelihood along the way.
  • likelihood_cnt=1–-Applies when likelihoods are enabled and supports importance sampling. Specifies how many event distributions will be used to calculate a vector of likelihoods.
  • common_random=false–-For variance reduction, turn on recording of random number usage during sampling.
  • start_time=0–-Sometimes you want a simulation to start at a different time.
  • debug=false–-Whether to print debug messages using the Logging package.
  • recording=false–-This will create a vector of every enable and disable event for test and debug.

If you want to try a particular sampler instead of having the SamplingContext pick one for you, use the method parameter.

sampler = SamplingContext(KeyType, Float64, rng; method=DirectMethod(:keep, :tree))

Quick Reference

These are all of the samplers.

NameDistribution supportBest use cases
FirstToFireMethodAllFastest for simple simulation
NextReactionMethodAllBest for common random numbers
FirstReactionMethodAllFastest for very few events
DirectMethodExponential-onlyQuicker likelihood calculation
RejectionMethodExponential-onlyRSSA for large systems
PartialPropensityMethodExponential-onlyFor reaction networks.
PetriMethodAllDebug rare errors

Every sampler in this table is validated by a statistical test matrix against a trusted reference; see Sampler Correctness for the methodology and current results.

Spec-to-Sampler Crosswalk

Each method spec on the left constructs the developer-level sampler type on the right. The right column is the developer-level type, reachable as CompetingClocks.<Name>; users normally pick the left column in a SamplerBuilder.

Method specUnderlying sampler type
FirstToFireMethodCompetingClocks.FirstToFire
NextReactionMethodCompetingClocks.CombinedNextReaction
DirectMethodCompetingClocks.DirectCall
RejectionMethodCompetingClocks.RSSA
PartialPropensityMethodCompetingClocks.PSSACR
FirstReactionMethodCompetingClocks.FirstReaction
PetriMethodCompetingClocks.Petri
Time-varying hazard rates

Samplers that continuously track a time-inhomogeneous hazard (tRSSA-style bounded rates) are deferred, not denied. In the meantime, re-enabling a clock with a shifted enabling time covers many cases where a rate changes at discrete events. See Re-enabling and Memory for the three idioms.

Hierarchical Samplers

For spatial simulations, chemical simulations that aren't well-mixed, or simulations with some fast and many slow clocks, it can help to split a sampler into multiple buckets so that each bucket can update its own list of what fires next. This package does this by adding sampler groups.

A sampler group is

  • A Symbol name for the sampler.
  • An inclusion function from (ClockKey, Distribution) to Bool that decides whether a given event belongs to this sampler.
  • An optional method to say what kind of sampler this group should use.

This example sends events with keys like (:infect, 3) tto the sampler called :forthright and sends keys like (:recover, 7) to the sampler called :sparky.

const KeyType = Tuple{Symbol,Integer}
builder = SamplerBuilder(KeyType, Float64)
add_group!(builder, :sparky => (x,d) -> x[1] == :recover, method=NextReaction())
add_group!(builder, :forthright=>(x,d) -> x[1] == :infect)
sampler = SamplingContext(builder, rng)

The resulting sampler will be hierarchical. For every event, it will choose the soonest time among the soonest times from all sampler groups.