Here is an example. I want to echo what Chris said: I’m not sure this is the best path since representing constraints in different ways can lead to different results. But for illustration here is an example:
## Example of adding a sampler that obeys a linear constraint
library(nimble)
## Make a model with two nodes, a and b.
## Suppose we want to sample with constraint a + b = c, where c will be data
## and we'll take care that we provide initial values that follow the constraint
sillyModel <- nimbleModel( nimbleCode({a ~ dnorm(0, 1); b ~ dnorm(0,1); c ~ dnorm(0, sd = 1000)}), inits = list(a = 0.3, b = 0.4), data = list(c = 0.7) )
## We don't really need a distribution for c. It is just written as a way to get c as a node into the model.
## Maybe we should make a better way to do that.
## To do constrained sampling, we'll work temporarily in the space
## w1 = (a + b) ## i.e. w1 = c
## w2 = (a - b)
## Then we'll sample w2 as a random walk Metropolis-Hastings and convert back to a and b
RW_constrained <- nimbleFunction(
contains = sampler_BASE,
## assume target is a vector of two nodes and control$constraint gives the constraint node
setup = function(model, mvSaved, target, control) {
scale = control$scale
constraintNode <- control$constraintNode
calcNodes <- model$getDependencies(target)
},
run = function() {
target_vals <- values(model, target)
current_w2 <- target_vals[2] - target_vals[1] ## could use linear algebra for more general case
current_w1 <- sum(target_vals)
if(abs(model[[constraintNode]] - current_w1) > 1e-6) print('Looks like the constraint is being violated\n');
proposal_w2 <- rnorm(1, current_w2, scale)
target_vals[1] <- 0.5*(current_w1 - proposal_w2)
target_vals[2] <- 0.5*(current_w1 + proposal_w2)
values(model, target) <<- target_vals
accept <- decide(calculateDiff(model, calcNodes))
if(accept) {
copy(from = model, to = mvSaved, row = 1, nodes = calcNodes, logProb = TRUE)
} else {
copy(from = mvSaved, to = model, row = 1, nodes = calcNodes, logProb = TRUE)
}
},
methods = list(
reset = function() {}
)
)
## Make an empty configuration by creating one and then removing the samplers
confMCMC <- configureMCMC(sillyModel)
confMCMC$getSamplers()
confMCMC$removeSamplers('a')
confMCMC$removeSamplers('b')
## alternatively we could do configureMCMC(sillyModel, nodes = NULL) to get an empty MCMC configuration directly (i.e. one with no samplers)
## now add one instance of our constrained sampler
confMCMC$addSampler(target = c('a','b'), type = RW_constrained, control = list(scale = 0.5, constraintNode = 'c'))
sillyMCMC <- buildMCMC(confMCMC)
cConstrainedExample <- compileNimble(sillyModel, sillyMCMC)
cConstrainedExample$sillyMCMC$run(10000)
samp <- as.matrix(cConstrainedExample$sillyMCMC$mvSamples)
plot(samp[,'a'])
plot(samp[,'b'])
plot(samp[,'a'], samp[, 'b'])
abline(0.7, -1, col = 'red') ## show the constraint
## end of example
Now one might want to use adaptive random walk instead of choosing a scale parameter as above. To do that one could copy from samplers_RW in MCMC_samplers.R in the nimble/R directory of the source code. One would then replace the first four lines of the run function with the first nine lines of the run function above. (The error check could be removed). A more general case of constraints could be done using linear algebra.
Perry