Skip to contents

This vignette describes how one can write samplers that can be used within monty. A sampler describes how to propagate a set of parameters one step along the Markov chain used in MCMC. Once you have written a custom sampler, you can use it with your monty_model along with all the monty runners via monty_sample().

In this vignette, we will design a sampler that samples by taking a series of independent steps for each parameter; it forms (approximately) a very poor form of a random walk sampler with a diagonal variance-covariance matrix, but has the advantage of being simple to implement and not useful enough to include in the package!

When might you want to create a new sampler?

We don’t anticipate that many people will want to create a new sampler, but you can do this if you have some new algorithm that you want to try. This can be as simple as wanting to have a more flexible update scheme that moves around your parameters in a different way than (say) the random walk sampler, but it might be a sampler for a very different sort of process. Anything where your process involves integer values will want a new sampler, because all our existing ones assume that parameters are all on the real line.

Our intention is that it will be easy enough to create a sampler, and that these can function perfectly well outside of the monty package, making the package easy to extend.

A toy problem

Let’s draw samples from a 5-dimensional Gaussian with unit variance and mean of 0:

model <- monty_example("gaussian", diag(5))

A toy sampler

Our toy sampler will sample a point on each axis, sequentially, accepting or rejecting each. We’ll use a random normal proposal in each dimension and our control parameter will be the standard deviation of this proposal; a vector of length 5 in this case. This sampler is stateless for now - that is, nothing is stored in state. Initialisation is therefore trivial too as there is almost nothing to do. However, this is where we can check that our control parameters make sense with the model that we have been given:

toy_sampler_initialise <- function(state_chain, control, model, rng) {
  n_sd <- length(control$sd)
  n_pars <- length(model$parameters)
  if (n_sd != n_pars) {
    cli::cli_abort("'control$sd' has length {n_sd} but your model has {n_pars} parameter{?s}")
  }
  NULL
}

The step function is where the actual interesting work happens:

toy_sampler_step <- function(state_chain, state_sampler, control, model, rng) {
  pars <- state_chain$pars
  density <- state_chain$density
  sd <- control$sd
  n_pars <- length(sd) # same as length(model$parameters)

  for (i in seq_len(n_pars)) {
    pars_next <- pars
    pars_next[[i]] <- monty_random_normal(pars[[i]], sd[[i]], rng)
    density_next <- monty_model_density(model, pars_next)
    accept <- density_next > density ||
      density_next - density > log(monty_random_real(rng))
    if (accept) {
      pars <- pars_next
      density <- density_next
    }
  }

  # Put state back together and return
  state_chain$pars <- pars
  state_chain$density <- density
  state_chain
}

Then we can write a constructor, which takes sd as the argument:

toy_sampler <- function(sd) {
  control <- list(sd = sd)
  monty_sampler(
    "Toy Sampler",
    "toy_sampler",
    control,
    toy_sampler_initialise,
    toy_sampler_step)
}

That’s it. We can now use our new sampler on the toy problem:

sampler <- toy_sampler(rep(0.2, 5))
samples <- monty_sample(model, sampler, 100)
#> ⡀⠀ Sampling   |   1% ETA:  3s
#>  Sampled 100 steps across 1 chain in 75ms
#> 
samples
#> 
#> ── <monty_samples: 5 parameters x 100 samples x 1 chain> ───────────────────────
#>  Parameters: 'a', 'b', 'c', 'd', and 'e'
#>  Conversion to other types is possible:
#> → ! posterior::as_draws_array() [package installed, but not loaded]
#> → ! posterior::as_draws_df() [package installed, but not loaded]
#> → ! coda::as.mcmc.list() [package installed, but not loaded]
#>  See `?monty_sample()` and `vignette("samples")` for more information