check_adapter <- function(adapter) { expect_named( adapter, c("initialize", "update", "finalize", "state"), ignore.order = TRUE ) expect_type(adapter$initialize, "closure") expect_type(adapter$update, "closure") if (!is.null(adapter$finalize)) { expect_type(adapter$finalize, "closure") } expect_type(adapter$state, "closure") } dummy_proposal_with_scale_parameter <- function(scale = NULL) { list( update = function(scale) scale <<- scale, parameters = function() list(scale = scale), default_target_accept_prob = function() 0.234, default_initial_scale = function(dimension) 1 / sqrt(dimension) ) } dummy_proposal_with_shape_parameter <- function(shape = NULL) { list( update = function(shape) shape <<- shape, parameters = function() list(shape = shape), default_target_accept_prob = function() 0.234, default_initial_scale = function(dimension) 1 / sqrt(dimension) ) } check_scale_adapter_coerces_to_target_accept_prob <- function( adapter, proposal, target_accept_prob, initial_scale) { # For a smooth decreasing relation between accept probability and # scale should adapt over long run to give close to target accept # probability scale <- initial_scale for (sample_index in 1:2000) { accept_prob <- exp(-scale) adapter$update( proposal, sample_index, list(statistics = list(accept_prob = accept_prob)) ) scale <- proposal$parameters()$scale } if (!is.null(adapter$finalize)) { adapter$finalize(proposal) scale <- proposal$parameters()$scale } expect_equal(scale, -log(target_accept_prob), tolerance = 1e-2) } check_scale_adapter_with_default_args_works <- function( adapter, dimension, check_adapter_state) { check_adapter(adapter) proposal <- dummy_proposal_with_scale_parameter() adapter$initialize(proposal, chain_state(rep(0, dimension))) adapter_state <- adapter$state() check_adapter_state(adapter_state) expected_log_scale <- log(proposal$default_initial_scale(dimension)) expect_equal(adapter_state$log_scale, expected_log_scale) adapter$update( proposal, 1, list(statistics = list(accept_prob = 1.)) ) adapter_state <- adapter$state() expect_gte(adapter_state$log_scale, expected_log_scale) } check_stochastic_approximation_scale_adapter_state <- function(adapter_state) { expect_named(adapter_state, c("log_scale")) expect_length(adapter_state$log_scale, 1) } for (target_accept_prob in c(0.2, 0.4, 0.6)) { for (initial_scale in c(0.5, 1., 2.)) { for (kappa in c(0.5, 0.6, 0.8)) { test_that( sprintf( paste0( "Stochastic approximation scale adapter works with target_accept_prob %.1f ", "initial_scale %.1f kappa %.1f" ), target_accept_prob, initial_scale, kappa ), { adapter <- scale_adapter( algorithm = "stochastic_approximation", initial_scale = initial_scale, target_accept_prob = target_accept_prob, kappa = kappa ) check_adapter(adapter) proposal <- dummy_proposal_with_scale_parameter() adapter$initialize(proposal, chain_state(rep(0, dimension))) adapter_state <- adapter$state() check_stochastic_approximation_scale_adapter_state(adapter_state) old_scale <- initial_scale # If accept probability higher than target scale should be increased for (sample_index in 1:2) { adapter$update( proposal, sample_index, list(statistics = list(accept_prob = target_accept_prob + 0.1)) ) expect_type(adapter$state(), "list") scale <- proposal$parameters()$scale expect_gt(scale, old_scale) old_scale <- scale } # If accept probability lower than target scale should be decreased adapter$initialize(proposal, chain_state(rep(0, dimension))) old_scale <- initial_scale for (sample_index in 1:2) { adapter$update( proposal, sample_index, list(statistics = list(accept_prob = target_accept_prob - 0.1)) ) scale <- proposal$parameters()$scale expect_lt(scale, old_scale) old_scale <- scale } check_scale_adapter_coerces_to_target_accept_prob( adapter, proposal, target_accept_prob, initial_scale ) } ) } } } for (dimension in c(1L, 2L, 5L)) { test_that( sprintf( "Stochastic approximation scale adapter with default args works in dimension %i", dimension ), { check_scale_adapter_with_default_args_works( scale_adapter(algorithm = "stochastic_approximation"), dimension, check_stochastic_approximation_scale_adapter_state ) } ) } check_dual_averaging_scale_adapter_state <- function(adapter_state) { expect_named( adapter_state, c("log_scale", "smoothed_log_scale", "accept_prob_error") ) expect_length(adapter_state$log_scale, 1) expect_length(adapter_state$smoothed_log_scale, 1) expect_length(adapter_state$accept_prob_error, 1) } for (target_accept_prob in c(0.2, 0.4, 0.6)) { for (initial_scale in c(0.5, 1., 2.)) { for (kappa in c(0.6, 0.8)) { for (gamma in c(0.01, 0.05)) { test_that( sprintf( paste0( "Dual averaging scale adapter works with target_accept_prob %.1f ", "initial_scale %.1f kappa %.1f gamma %.2f" ), target_accept_prob, initial_scale, kappa, gamma ), { adapter <- scale_adapter( algorithm = "dual_averaging", initial_scale = initial_scale, target_accept_prob = target_accept_prob, kappa = kappa, gamma = gamma ) check_adapter(adapter) proposal <- dummy_proposal_with_scale_parameter() adapter$initialize(proposal, chain_state(rep(0, dimension))) adapter_state <- adapter$state() check_dual_averaging_scale_adapter_state(adapter_state) check_scale_adapter_coerces_to_target_accept_prob( adapter, proposal, target_accept_prob, initial_scale ) } ) } } } } for (dimension in c(1L, 2L, 5L)) { test_that( sprintf( "Dual averaging scale adapter with default args works in dimension %i", dimension ), { check_scale_adapter_with_default_args_works( scale_adapter(algorithm = "dual_averaging"), dimension, check_dual_averaging_scale_adapter_state ) } ) } for (dimension in c(1L, 2L, 5L)) { for (kappa in c(0.5, 0.6, 0.8)) { for (correlation in c(0.0, 0.5, 0.8)) { test_that( sprintf( paste0( "Variance adapter works for AR1 process with correlation %.1f in ", "dimension %i with kappa %.1f" ), correlation, dimension, kappa ), { proposal <- dummy_proposal_with_shape_parameter() adapter <- shape_adapter(type = "variance", kappa = kappa) check_adapter(adapter) withr::local_seed(default_seed()) target_scales <- exp(2 * rnorm(dimension)) position <- rnorm(dimension) * target_scales state <- chain_state(position = position) adapter$initialize(proposal, state) adapter_state <- adapter$state() expect_named( adapter_state, c("mean_estimate", "variance_estimate"), ignore.order = TRUE ) expect_length(adapter_state$mean_estimate, dimension) expect_length(adapter_state$variance_estimate, dimension) # Proposal shape parameter should be adapted to close to target scales # over long run for (s in 1:3000) { # Sample new position from AR1 process with stationary distribution # zero-mean normal with standard deviations equal to target scales position <- ( correlation * position + sqrt(1 - correlation^2) * target_scales * rnorm(dimension) ) state$update(position = position) adapter$update(proposal, s, list(state = state)) } expect_equal( proposal$parameters()$shape, target_scales, tolerance = 1e-1 ) } ) } } } for (dimension in c(1L, 2L, 3L)) { for (kappa in c(0.6, 1.)) { test_that( sprintf( "Covariance shape adapter works in dimension %i with kappa %.1f", dimension, kappa ), { withr::local_seed(default_seed()) covariance <- random_covariance_matrix(dimension) chol_covariance <- t(chol(covariance)) target_distribution <- multivariate_normal_target_distribution( mean = 0, covariance = covariance ) proposal <- random_walk_proposal(scale = 2.4 / sqrt(dimension)) adapter <- shape_adapter(type = "covariance", kappa = kappa) check_adapter(adapter) state <- chain_state(position = rnorm(dimension)) adapter$initialize(proposal, state) adapter_state <- adapter$state() expect_named( adapter_state, c("mean_estimate", "chol_covariance_estimate") ) expect_length(adapter_state$mean_estimate, dimension) expect_nrow(adapter_state$chol_covariance_estimate, dimension) expect_ncol(adapter_state$chol_covariance_estimate, dimension) for (sample_index in 1:10000) { state_and_statistics <- sample_metropolis_hastings( state, target_distribution, proposal ) adapter$update(proposal, sample_index, state_and_statistics) state <- state_and_statistics$state } expect_equal( proposal$parameters()$shape, chol_covariance, tolerance = 0.1 ) } ) } } for (dimension in c(1L, 2L, 3L)) { for (target_accept_prob in c(0.234, 0.4)) { test_that( sprintf( paste0( "Robust shape adapter works in dimension %i with ", "target_accept_prob %.2f " ), dimension, target_accept_prob ), { withr::local_seed(default_seed()) covariance <- random_covariance_matrix(dimension) chol_covariance <- t(chol(covariance)) target_distribution <- multivariate_normal_target_distribution( mean = 0, covariance = covariance ) proposal <- random_walk_proposal() adapter <- robust_shape_adapter( kappa = 0.6, target_accept_prob = target_accept_prob ) check_adapter(adapter) state <- chain_state(position = rnorm(dimension)) adapter$initialize(proposal, state) adapter_state <- adapter$state() expect_named(adapter_state, "shape") expect_nrow(adapter_state$shape, dimension) expect_ncol(adapter_state$shape, dimension) mean_accept_prob <- 0. for (sample_index in 1:10000) { state_and_statistics <- sample_metropolis_hastings( state, target_distribution, proposal ) adapter$update(proposal, sample_index, state_and_statistics) state <- state_and_statistics$state mean_accept_prob <- mean_accept_prob + ( state_and_statistics$statistics$accept_prob - mean_accept_prob ) / sample_index } expect_equal(mean_accept_prob, target_accept_prob, tolerance = 0.1) # Proposal shape parameter should be adapted to close to Cholesky # factor of covariance of target distribution modulo a scaling # constant over long run in_lower_triangle <- lower.tri(chol_covariance, TRUE) ratios <- ( proposal$parameters()$shape[in_lower_triangle] / chol_covariance[in_lower_triangle] ) expect_lt(diff(range(ratios)) / median(ratios), 0.1) } ) } } for (dimension in c(1L, 2L, 5L)) { test_that( sprintf( "Robust shape adapter default args works in dimension %i", dimension ), { adapter <- robust_shape_adapter() check_adapter(adapter) proposal <- dummy_proposal_with_shape_parameter() adapter$initialize(proposal, chain_state(rep(0, dimension))) adapter_state <- adapter$state() expect_named(adapter_state, "shape") initial_shape <- adapter_state$shape expect_nrow(initial_shape, dimension) expect_ncol(initial_shape, dimension) expect_equal(initial_shape, diag(dimension) / sqrt(dimension)) adapter$update( proposal, 1, list( proposed_state = chain_state( position = NULL, momentum = rep(1, dimension) ), statistics = list(accept_prob = 1.) ) ) adapter_state <- adapter$state() expect_gt(norm(initial_shape - adapter_state$shape), 0) } ) } test_that("sample_chain works with dummy adapter with required interface", { dummy_adapter <- list( initialize = function(proposal, initial_state) {}, update = function(proposal, sample_index, state_and_statistics) {}, finalize = function(proposal) {}, state = function() list() ) target_distribution <- standard_normal_target_distribution() proposal <- barker_proposal(scale = 1) expect_no_error( sample_chain( target_distribution = target_distribution, proposal = proposal, initial_state = chain_state(0), n_warm_up_iteration = 1, n_main_iteration = 0, adapters = list(dummy_adapter) ) ) })