Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve test coverage and automate reporting #40

Merged
merged 7 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/test-coverage.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

name: test-coverage

permissions: read-all

jobs:
test-coverage:
runs-on: ubuntu-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}

steps:
- uses: actions/checkout@v4

- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true

- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::covr, any::xml2
needs: coverage

- name: Test coverage
run: |
cov <- covr::package_coverage(
quiet = FALSE,
clean = FALSE,
install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package")
)
covr::to_cobertura(cov)
shell: Rscript {0}

- uses: codecov/codecov-action@v4
with:
fail_ci_if_error: ${{ github.event_name != 'pull_request' && true || false }}
file: ./cobertura.xml
plugin: noop
disable_search: true
token: ${{ secrets.CODECOV_TOKEN }}

- name: Show testthat output
if: always()
run: |
## --------------------------------------------------------------------
find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true
shell: bash

- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: coverage-test-failures
path: ${{ runner.temp }}/package
1 change: 1 addition & 0 deletions README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ knitr::opts_chunk$set(
[![pkgdown](https://github.com/UCL/rmcmc/actions/workflows/pkgdown.yaml/badge.svg)](https://github.com/UCL/rmcmc/actions/workflows/pkgdown.yaml)
[![lint](https://github.com/UCL/rmcmc/actions/workflows/lint.yaml/badge.svg)](https://github.com/UCL/rmcmc/actions/workflows/lint.yaml)
[![pre-commit](https://github.com/UCL/rmcmc/actions/workflows/pre-commit.yaml/badge.svg)](https://github.com/UCL/rmcmc/actions/workflows/pre-commit.yaml)
[![codecov](https://codecov.io/github/UCL/rmcmc/graph/badge.svg?token=PL8557fpgT)](https://codecov.io/github/UCL/rmcmc)
<!-- badges: end -->

`rmcmc` is an R package for simulating Markov chains using the Barker proposal to compute _Markov chain Monte Carlo_ (MCMC) estimates of expectations with respect to a target distribution on a real-valued vector space.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[![pkgdown](https://github.com/UCL/rmcmc/actions/workflows/pkgdown.yaml/badge.svg)](https://github.com/UCL/rmcmc/actions/workflows/pkgdown.yaml)
[![lint](https://github.com/UCL/rmcmc/actions/workflows/lint.yaml/badge.svg)](https://github.com/UCL/rmcmc/actions/workflows/lint.yaml)
[![pre-commit](https://github.com/UCL/rmcmc/actions/workflows/pre-commit.yaml/badge.svg)](https://github.com/UCL/rmcmc/actions/workflows/pre-commit.yaml)
[![codecov](https://codecov.io/github/UCL/rmcmc/graph/badge.svg?token=PL8557fpgT)](https://codecov.io/github/UCL/rmcmc)
<!-- badges: end -->

`rmcmc` is an R package for simulating Markov chains using the Barker
Expand Down
12 changes: 10 additions & 2 deletions tests/testthat/helpers.R
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
standard_normal_target_distribution <- function() {
list(
standard_normal_target_distribution <- function(value_and_gradient = FALSE) {
target_distribution <- list(
log_density = function(x) -sum(x^2) / 2,
gradient_log_density = function(x) -x
)
if (value_and_gradient) {
target_distribution[["value_and_gradient_log_density"]] <- function(x) {
list(
value = -sum(x^2) / 2, gradient = -x
)
}
}
target_distribution
}

default_seed <- function() 9821415L
Expand Down
15 changes: 15 additions & 0 deletions tests/testthat/test-adaptation.R
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ for (target_accept_prob in c(0.2, 0.4, 0.6)) {
)
check_adapter(adapter)
adapter$initialize(initial_state = NULL)
adapter_state <- adapter$state()
expect_named(adapter_state, "log_scale")
expect_length(adapter_state$log_scale, 1)
old_scale <- initial_scale
# If accept probability higher than target scale should be increased
for (s in 1:2) {
Expand Down Expand Up @@ -104,6 +107,14 @@ for (dimension in c(1L, 2L, 5L)) {
position <- rnorm(dimension) * target_scales
state <- chain_state(position = position)
adapter$initialize(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) {
Expand Down Expand Up @@ -154,6 +165,10 @@ for (dimension in c(1L, 2L, 3L)) {
check_adapter(adapter)
state <- chain_state(position = rnorm(dimension))
adapter$initialize(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(
Expand Down
46 changes: 27 additions & 19 deletions tests/testthat/test-chain_state.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,33 @@ for (dimension in c(1L, 2L)) {
expect_identical(state$momentum(), momentum)
expect_identical(state$dimension(), dimension)
})
test_that(
sprintf(
"Evaluating log density (gradient) with chain state in dimension %i works",
dimension
),
{
withr::with_seed(default_seed(), position <- rnorm(dimension))
state <- chain_state(position)
target_distribution <- standard_normal_target_distribution()
expect_identical(
state$log_density(target_distribution),
target_distribution$log_density(position)
)
expect_identical(
state$gradient_log_density(target_distribution),
target_distribution$gradient_log_density(position)
)
}
)
for (use_value_and_gradient in c(TRUE, FALSE)) {
test_that(
sprintf(
paste0(
"Evaluating log density (gradient) with chain state and ",
"use_value_and_gradient = %i in dimension %i works"
),
use_value_and_gradient,
dimension
),
{
withr::with_seed(default_seed(), position <- rnorm(dimension))
target_distribution <- standard_normal_target_distribution(
use_value_and_gradient
)
state <- chain_state(position)
expect_identical(
state$log_density(target_distribution),
target_distribution$log_density(position)
)
expect_identical(
state$gradient_log_density(target_distribution),
target_distribution$gradient_log_density(position)
)
}
)
}
test_that(sprintf("Copying chain state in dimension %i works", dimension), {
withr::with_seed(default_seed(), {
position <- rnorm(dimension)
Expand Down
139 changes: 82 additions & 57 deletions tests/testthat/test-chains.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,94 @@ for (n_warm_up_iteration in c(0, 1, 10)) {
for (n_main_iteration in c(0, 1, 10)) {
for (dimension in c(1, 2)) {
for (trace_warm_up in c(TRUE, FALSE)) {
for (wrapped_initial_state in c(TRUE, FALSE)) {
test_that(
sprintf(
paste0(
"Sampling chain with %i warm-up iterations, %i main iterations",
" dimension %i, wrapped_initial_state = %i ",
"and trace_warm_up = %i works"
for (show_progress_bar in c(TRUE, FALSE)) {
for (wrapped_initial_state in c(TRUE, FALSE)) {
test_that(
sprintf(
paste0(
"Sampling chain with %i warm-up iterations, ",
"%i main iterations, dimension %i, ",
"trace_warm_up = %i, show_progress_bar = %i ",
"and wrapped_initial_state = %i works"
),
n_warm_up_iteration,
n_main_iteration,
dimension,
trace_warm_up,
show_progress_bar,
wrapped_initial_state
),
n_warm_up_iteration,
n_main_iteration,
dimension,
wrapped_initial_state,
trace_warm_up
),
{
target_distribution <- standard_normal_target_distribution()
barker_proposal(target_distribution)
proposal <- barker_proposal(target_distribution)
adapters <- list(
scale_adapter(proposal, initial_scale = 1.)
)
withr::with_seed(default_seed(), {
position <- rnorm(dimension)
})
if (wrapped_initial_state) {
initial_state <- chain_state(position)
} else {
initial_state <- position
}
results <- sample_chain(
target_distribution = target_distribution,
proposal = proposal,
initial_state = initial_state,
n_warm_up_iteration = n_warm_up_iteration,
n_main_iteration = n_main_iteration,
adapters = adapters,
trace_warm_up = trace_warm_up
)
expected_results_names <- c("final_state", "traces", "statistics")
if (trace_warm_up) {
{
target_distribution <- standard_normal_target_distribution()
barker_proposal(target_distribution)
proposal <- barker_proposal(target_distribution)
adapters <- list(
scale_adapter(proposal, initial_scale = 1.)
)
withr::with_seed(default_seed(), {
position <- rnorm(dimension)
})
if (wrapped_initial_state) {
initial_state <- chain_state(position)
} else {
initial_state <- position
}
results <- sample_chain(
target_distribution = target_distribution,
proposal = proposal,
initial_state = initial_state,
n_warm_up_iteration = n_warm_up_iteration,
n_main_iteration = n_main_iteration,
adapters = adapters,
trace_warm_up = trace_warm_up,
show_progress_bar = show_progress_bar
)
expected_results_names <- c(
expected_results_names, "warm_up_traces", "warm_up_statistics"
"final_state", "traces", "statistics"
)
if (trace_warm_up) {
expected_results_names <- c(
expected_results_names,
"warm_up_traces",
"warm_up_statistics"
)
}
expect_named(
results,
expected_results_names,
ignore.order = TRUE,
)
expect_nrow(results$traces, n_main_iteration)
expect_ncol(results$traces, dimension + 1)
expect_nrow(results$statistics, n_main_iteration)
expect_ncol(results$statistics, 1)
if (trace_warm_up) {
expect_nrow(results$warm_up_traces, n_warm_up_iteration)
expect_ncol(results$warm_up_traces, dimension + 1)
expect_nrow(results$warm_up_statistics, n_warm_up_iteration)
expect_ncol(results$warm_up_statistics, 2)
}
}
expect_named(
results,
expected_results_names,
ignore.order = TRUE,
)
expect_nrow(results$traces, n_main_iteration)
expect_ncol(results$traces, dimension + 1)
expect_nrow(results$statistics, n_main_iteration)
expect_ncol(results$statistics, 1)
if (trace_warm_up) {
expect_nrow(results$warm_up_traces, n_warm_up_iteration)
expect_ncol(results$warm_up_traces, dimension + 1)
expect_nrow(results$warm_up_statistics, n_warm_up_iteration)
expect_ncol(results$warm_up_statistics, 2)
}
}
)
)
}
}
}
}
}
}

test_that("Sample chains with invalid initial_state raises error", {
target_distribution <- standard_normal_target_distribution()
barker_proposal(target_distribution)
proposal <- barker_proposal(target_distribution)
expect_error(
sample_chain(
target_distribution = target_distribution,
proposal = proposal,
initial_state = list(),
n_warm_up_iteration = 1,
n_main_iteration = 1,
),
"initial_state"
)
})
Loading