Gen.jl and CompetingClocks.jl
From Gen's point of view, there are two main “hooks” CompetingClocks.jl can plug into:
- probability distributions (
Distribution{T}withrandom/logpdf), and - generative functions (things that implement the Generative Function Interface and produce traces with scores).
CompetingClocks already the operations Gen needs: forward simulation of event paths, and step‑ or path‑wise log‑likelihoods.
Below is an overview of the main integration modes.
1. CompetingClocks as a Gen distribution over event paths
What CompetingClocks gives you
Relevant features:
A
SamplingContext/SamplerBuilderwhere you can turn onstep_likelihood=true(forsteploglikelihood) and/orpath_likelihood=true(forpathloglikelihood), optionally withlikelihood_cntfor multiple distributions / IS.
TrajectoryWatcherandPathLikelihoodsobjects that do not sample but compute the log‑likelihood of a full path of(event, time)pairs.An integration pattern where you replay a sequence of events and call
pathloglikelihood(sampler, end_time)to get the log probability of that entire trajectory.
This is exactly what Gen’s distribution API wants as random (sampling a path) and logpdf (log‑likelihood of a path).
How this looks conceptually in Gen
Define a concrete Julia type for a path, e.g. a vector of records
(time, clock_key)whereclock_keyis whatever you already use.Implement a custom
Distribution{PathType}(e.g.,GsmpPath) for Gen:random(::GsmpPath, params, T, rng)- Build a
SamplingContextwithpath_likelihood=true. - Run your usual integration loop (
initialize_events!,next,fire!,handle_event!) up to horizonT, collecting(time, key)into a path. - Return that path.
- Build a
logpdf(::GsmpPath, path, params, T)- Rebuild the same sampler, but now replay the given path (possibly using
TrajectoryWatcher/PathLikelihoodsinstead of a sampler). - After replay, return
pathloglikelihood(sampler_or_watcher, T), which is aFloat64whenlikelihood_cnt == 1.
- Rebuild the same sampler, but now replay the given path (possibly using
In a Gen model you then treat the whole continuous‑time trajectory as one random choice:
As a latent path:
path ~ gsmp_path(params, T)inside an@genfunction.
As an observed path:
- Use
generate(model, (params, T), choicemap((:path, observed_path))); Gen will call yourlogpdfto score that trace.
- Use
Once you do this, all Gen inference methods that operate on generative functions calling distributions—importance sampling, SMC, generic MH, etc.—can work with your GSMP path as just another random variable.
2. CompetingClocks as a custom Generative Function
If you want finer control over the trace (addressable choices per event, incremental updates, gradient hooks), you can wrap CompetingClocks as a full Gen generative function rather than just a single distribution.
Core idea
Gen’s generative function should:
- take simulation parameters and possibly a time horizon as arguments,
- internally run that loop using a
SamplingContext, - construct a trace whose choice map encodes the discrete events and/or times,
- set its score to the path log‑likelihood from CompetingClocks.
Key points:
Trace structure and addresses
Gen allows arbitrary address types (not just symbols), and your doc explicitly suggests using a
Pairkey like:i => 37“for use with Gen.jl”.You can align your CompetingClocks clock keys with Gen trace addresses directly:
- e.g. event key
ClockKey(:infect, i, j)maps to Gen address(:infect, i, j)or to the suggested:infect => i/ similar scheme.
- e.g. event key
The generative function’s
get_choices(trace)then gives a choice map whose addresses are your event identifiers.
Implementing the Generative Function Interface
At minimum you implement:
simulate: build theSamplingContext(withpath_likelihood=true), run the main loop, record events into the trace, and storepathloglikelihoodas the trace score.get_args,get_retval(e.g. the final state or full path),get_choices,get_score.
If you want full use of Gen’s MCMC/SMC:
- add
generate(to create traces that satisfy constraints on some events/times), update/regenerate(for efficient incremental proposals that adjust only part of the path),- optionally
choice_gradientsandaccumulate_param_gradients!if you expose derivatives of the path log‑likelihood w.r.t. parameters or event times.
When this is attractive
- You want to expose internal events as addressable random choices for custom proposals.
- You need incremental updates of long trajectories inside MCMC (rather than resimulating whole paths).
- You want Gen’s gradient‑based machinery to see through to your likelihood (after you supply
logpdf_grad/choice_gradients).
3. Using CompetingClocks as a likelihood engine inside Gen inference
Even without fully wrapping your simulator as a distribution or generative function, Gen inference code can delegate likelihood calculations to CompetingClocks.
a) Observation likelihood for event data
Given observed event times/types:
Represent the data as a list of
(clock_key, time)events.Build a
TrajectoryWatcheror a sampler created withpath_likelihood=trueandlikelihood_cnt=1.In a Gen model, after sampling parameters, call a pure Julia function that:
- replays the observed sequence into the watcher/sampler, and
- returns
log_prob = pathloglikelihood(watcher_or_sampler, end_time).
The clean way to plug this into Gen is still via a custom distribution or generative function whose logpdf / get_score calls this function. That keeps the likelihood inside Gen’s bookkeeping, so all standard inference algorithms see the correct score.
b) Importance sampling and mixture proposals
We have already seen importance sampling with CompetingClocks.
Within Gen:
A custom inference procedure (e.g. an SMC or MH kernel) can:
- draw candidate paths using a biased CompetingClocks sampler,
- use
PathLikelihoodsto evaluate both the target and proposal path log‑likelihoods, - compute importance weights or Metropolis–Hastings acceptance ratios, and
- attach the resulting weight as the trace score (or as an incremental factor inside a custom generative function).
This lets you reuse your variance‑reduction machinery (common random numbers, mixtures, control variates) inside Gen’s programmable inference.
c) HMC/gradient‑based parameter inference
Two ways this interacts with Gen:
External HMC over latent event times, Gen for everything else
- Use your HMC driver to update a latent event list given parameters.
- Wrap that event list as a variable in a Gen model (e.g. through a distribution over paths).
- Combine Gen’s inference for other latent variables/parameters with your HMC over the CTDES part.
Gen‑native gradients
- Because
pathloglikelihoodis ordinary Julia code, it can in principle be differentiated using AD; Gen’s distribution and generative‑function APIs have hooks (logpdf_grad,choice_gradients) to expose these gradients to its optimizers and variational methods.
- Because
Summary
At a high level, the main interaction patterns are:
- Custom Gen distribution over GSMP paths, with
randomandlogpdfimplemented via CompetingClocks simulation andpathloglikelihood. - Custom Gen generative function that internally runs your simulation, uses your key types as Gen addresses, and sets its score from your path likelihood.
- Use of CompetingClocks as a likelihood / weighting engine in Gen inference code: observation likelihoods for event data, importance‑sampling/mixture proposals (with
PathLikelihoodsand CRNs), and HMC‑style gradient‑based updates driven bypathloglikelihood.
All three rely on the same core feature set you already built: configurable samplers/contexts, step‑ and path‑wise log‑likelihoods, watchers, and importance‑sampling tools.
References
- https://www.gen.dev/docs/stable/ref/modeling/distributions/ "Probability Distributions · Gen.jl"
- https://www.gen.dev/docs/stable/howto/customgen_fns/ "Adding New Generative Functions · Gen.jl"
- https://www.gen.dev/docs/stable/ref/core/gfi/ "Generative Function Interface · Gen.jl"
- https://www.gen.dev/docs/dev/howto/customdistributions/ "Adding New Distributions · Gen.jl"