From 61541c4de89012a40370e03857f3613b80fd3500 Mon Sep 17 00:00:00 2001 From: Brendan Matthew Galdo Date: Mon, 11 May 2026 21:54:40 -0400 Subject: [PATCH 1/6] improve the names of tests --- tests/testthat/{test-adapt_scheme.R => test-adapt_schemes.R} | 0 tests/testthat/{test-optim_SQGDE.R => test-convergence.R} | 0 .../{test-trace_print_freq.R => test-progress_messages.R} | 0 tests/testthat/{test-bug_fixes.R => test-sqg_de_bin_1.R} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/testthat/{test-adapt_scheme.R => test-adapt_schemes.R} (100%) rename tests/testthat/{test-optim_SQGDE.R => test-convergence.R} (100%) rename tests/testthat/{test-trace_print_freq.R => test-progress_messages.R} (100%) rename tests/testthat/{test-bug_fixes.R => test-sqg_de_bin_1.R} (100%) diff --git a/tests/testthat/test-adapt_scheme.R b/tests/testthat/test-adapt_schemes.R similarity index 100% rename from tests/testthat/test-adapt_scheme.R rename to tests/testthat/test-adapt_schemes.R diff --git a/tests/testthat/test-optim_SQGDE.R b/tests/testthat/test-convergence.R similarity index 100% rename from tests/testthat/test-optim_SQGDE.R rename to tests/testthat/test-convergence.R diff --git a/tests/testthat/test-trace_print_freq.R b/tests/testthat/test-progress_messages.R similarity index 100% rename from tests/testthat/test-trace_print_freq.R rename to tests/testthat/test-progress_messages.R diff --git a/tests/testthat/test-bug_fixes.R b/tests/testthat/test-sqg_de_bin_1.R similarity index 100% rename from tests/testthat/test-bug_fixes.R rename to tests/testthat/test-sqg_de_bin_1.R From 67edfb59acd16a774455165cf1d845199e46d604 Mon Sep 17 00:00:00 2001 From: Brendan Matthew Galdo Date: Mon, 11 May 2026 21:56:45 -0400 Subject: [PATCH 2/6] Update .Rbuildignore ignore \.github$ --- .Rbuildignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.Rbuildignore b/.Rbuildignore index d4b4933..b0a15fa 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -6,3 +6,4 @@ ^README\.Rmd$ ^cran-comments\.md$ ^CRAN-SUBMISSION$ +^\.github$ From 6c3855601abef82bf3005d0a78a1d0284748c5a2 Mon Sep 17 00:00:00 2001 From: Brendan Matthew Galdo Date: Fri, 15 May 2026 18:12:17 -0400 Subject: [PATCH 3/6] block coordinate updating --- R/GetAlgoParams.R | 23 ++++- R/SQG_DE_bin_1.R | 17 ++-- R/optim_SQGDE.R | 14 ++- tests/testthat/test-GetAlgoParams.R | 3 +- tests/testthat/test-param_block_list.R | 117 +++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 tests/testthat/test-param_block_list.R diff --git a/R/GetAlgoParams.R b/R/GetAlgoParams.R index b660f22..3c3cb90 100644 --- a/R/GetAlgoParams.R +++ b/R/GetAlgoParams.R @@ -25,6 +25,7 @@ #' @param bounds_type A string specifying how parameter bounds are enforced on proposals. 'clip' truncates proposals to [lower, upper]. 'reflect' mirrors proposals back across the boundary (with clip fallback for very large steps). Default is 'reflect'. #' @param trace_print_freq A positive integer controlling how often iteration progress is printed. A message is printed every \code{trace_print_freq} iterations. Set to \code{Inf} to suppress progress messages entirely. Default is 100. #' @param converge_crit A string denoting the convergence metric used, valid metrics are 'stdev' (standard deviation of population weight in the last stop_check iterations) and 'percent' (percent improvement in median particle weight in the last stop_check iterations). 'stdev' is the default. +#' @param param_block_list An optional list of integer vectors partitioning parameter indices into blocks. When provided, the algorithm cycles through blocks in order across iterations, updating only the parameters in the current block instead of using random crossover selection. Each index in \code{1:n_params} must appear in exactly one block. Set to \code{NULL} (default) to use standard crossover selection. #' @return A list of control parameters for the optim_SQGDE function. #' @export GetAlgoParams = function(n_params, @@ -51,7 +52,8 @@ GetAlgoParams = function(n_params, stop_check = 10, stop_tol = 1e-4, converge_crit = 'stdev', - trace_print_freq = 100){ + trace_print_freq = 100, + param_block_list = NULL){ # n_params ### catch errors if(length(n_params) > 1 || !is.finite(n_params)){ @@ -331,6 +333,22 @@ GetAlgoParams = function(n_params, } } + # param_block_list + if (!is.null(param_block_list)) { + if (!is.list(param_block_list) || length(param_block_list) < 1) { + stop('ERROR: param_block_list must be NULL or a non-empty list of integer vectors') + } + all_indices = unlist(param_block_list) + if (!is.numeric(all_indices) || any(!is.finite(all_indices)) || any(all_indices < 1) || any(all_indices > n_params)) { + stop('ERROR: param_block_list indices must be integers in 1:n_params') + } + all_indices = as.integer(all_indices) + if (length(all_indices) != n_params || length(unique(all_indices)) != n_params) { + stop('ERROR: param_block_list must partition 1:n_params — each index must appear exactly once') + } + param_block_list = lapply(param_block_list, as.integer) + } + out = list('n_params' = n_params, 'n_particles' = n_particles, 'n_iter' = n_iter, @@ -356,7 +374,8 @@ GetAlgoParams = function(n_params, 'stop_tol' = stop_tol, 'stop_check' = stop_check, 'converge_crit' = converge_crit, - 'trace_print_freq' = trace_print_freq) + 'trace_print_freq' = trace_print_freq, + 'param_block_list' = param_block_list) return(out) } diff --git a/R/SQG_DE_bin_1.R b/R/SQG_DE_bin_1.R index 346de08..d477581 100644 --- a/R/SQG_DE_bin_1.R +++ b/R/SQG_DE_bin_1.R @@ -52,7 +52,8 @@ SQG_DE_bin_1 = function(pmem_index, lower = -Inf, upper = Inf, bounds_type = 'reflect', - n_diff, ...) { + n_diff, + param_block = NULL, ...) { weight_use = current_weight[pmem_index] params_use = current_params[pmem_index, ] @@ -73,12 +74,16 @@ SQG_DE_bin_1 = function(pmem_index, base_index = parent_indices[2*n_diff+1] } - # crossover: select which parameters to update - param_idices_bool = stats::rbinom(len_param_use, prob = crossover_rate, size = 1) - if (all(param_idices_bool == 0)) { - param_idices_bool[sample(x = 1:len_param_use, size = 1)] = 1 + # select which parameters to update + if (!is.null(param_block)) { + param_indices = param_block + } else { + param_idices_bool = stats::rbinom(len_param_use, prob = crossover_rate, size = 1) + if (all(param_idices_bool == 0)) { + param_idices_bool[sample(x = 1:len_param_use, size = 1)] = 1 + } + param_indices = seq(1, len_param_use, by = 1)[as.logical(param_idices_bool)] } - param_indices = seq(1, len_param_use, by = 1)[as.logical(param_idices_bool)] # approximate gradient and self-scaling factor ga = grad_approx_fn(param_indices, n_diff, current_params, diff --git a/R/optim_SQGDE.R b/R/optim_SQGDE.R index 53f836a..38c29e7 100644 --- a/R/optim_SQGDE.R +++ b/R/optim_SQGDE.R @@ -145,6 +145,14 @@ optim_SQGDE = function(ObjFun, control_params = GetAlgoParams(), warm_start = NU converge_test_passed=FALSE for(iter in 1:control_params$n_iter){ + # block coordinate: cycle through blocks; NULL means use crossover + if (!is.null(control_params$param_block_list)) { + block_idx = ((iter - 1) %% length(control_params$param_block_list)) + 1 + current_block = control_params$param_block_list[[block_idx]] + } else { + current_block = NULL + } + if(control_params$parallel_type=='none'){ # adapt particles using SQG DE sequentially temp=matrix(unlist(lapply(1:control_params$n_particles, SQG_DE_bin_1, @@ -159,7 +167,8 @@ optim_SQGDE = function(ObjFun, control_params = GetAlgoParams(), warm_start = NU lower = control_params$lower, upper = control_params$upper, bounds_type = control_params$bounds_type, - n_diff = control_params$n_diff, ...)), + n_diff = control_params$n_diff, + param_block = current_block, ...)), control_params$n_particles, control_params$n_params+1, byrow=TRUE) } else { @@ -176,7 +185,8 @@ optim_SQGDE = function(ObjFun, control_params = GetAlgoParams(), warm_start = NU lower = control_params$lower, upper = control_params$upper, bounds_type = control_params$bounds_type, - n_diff = control_params$n_diff, ...)), + n_diff = control_params$n_diff, + param_block = current_block, ...)), control_params$n_particles, control_params$n_params+1, byrow=TRUE) diff --git a/tests/testthat/test-GetAlgoParams.R b/tests/testthat/test-GetAlgoParams.R index 3bb3f38..4e14e23 100644 --- a/tests/testthat/test-GetAlgoParams.R +++ b/tests/testthat/test-GetAlgoParams.R @@ -5,7 +5,8 @@ ALL_NAMES <- c("n_params", "n_particles", "n_iter", "init_sd", "init_center", "crossover_rate", "jitter_size", "parallel_type", "recovery_path", "recovery_freq", "thin", "purify", "n_iters_per_particle", "return_trace", "n_diff", "adapt_scheme", "give_up_init", - "stop_tol", "stop_check", "converge_crit", "trace_print_freq") + "stop_tol", "stop_check", "converge_crit", "trace_print_freq", + "param_block_list") # ── output structure ────────────────────────────────────────────────────────── diff --git a/tests/testthat/test-param_block_list.R b/tests/testthat/test-param_block_list.R new file mode 100644 index 0000000..4d7f9ee --- /dev/null +++ b/tests/testthat/test-param_block_list.R @@ -0,0 +1,117 @@ +library(graDiEnt) + +obj <- function(x) sum((x - 1)^2) + +# ── GetAlgoParams validation ────────────────────────────────────────────────── + +test_that("param_block_list = NULL accepted (default)", { + cp <- GetAlgoParams(n_params = 4) + expect_null(cp$param_block_list) +}) + +test_that("valid partition accepted", { + expect_no_error( + GetAlgoParams(n_params = 4, param_block_list = list(1:2, 3:4)) + ) +}) + +test_that("single-block partition accepted", { + expect_no_error( + GetAlgoParams(n_params = 3, param_block_list = list(1:3)) + ) +}) + +test_that("unequal block sizes accepted", { + expect_no_error( + GetAlgoParams(n_params = 4, param_block_list = list(c(1L, 2L, 3L), 4L)) + ) +}) + +test_that("indices coerced to integer", { + cp <- GetAlgoParams(n_params = 4, param_block_list = list(c(1.0, 2.0), c(3.0, 4.0))) + expect_true(is.integer(cp$param_block_list[[1]])) +}) + +test_that("non-list param_block_list errors", { + expect_error( + GetAlgoParams(n_params = 4, param_block_list = 1:4), + "list" + ) +}) + +test_that("index out of range errors", { + expect_error( + GetAlgoParams(n_params = 4, param_block_list = list(1:3, c(4L, 5L))), + "1:n_params" + ) +}) + +test_that("duplicate index errors", { + expect_error( + GetAlgoParams(n_params = 4, param_block_list = list(c(1L, 2L), c(2L, 3L, 4L))), + "exactly once" + ) +}) + +test_that("missing index errors", { + expect_error( + GetAlgoParams(n_params = 4, param_block_list = list(1L, 2L, 3L)), + "exactly once" + ) +}) + +test_that("empty list errors", { + expect_error( + GetAlgoParams(n_params = 4, param_block_list = list()), + "non-empty" + ) +}) + +# ── optimizer runs with param_block_list ────────────────────────────────────── + +test_that("optimizer runs without error with 2-block partition", { + set.seed(1) + expect_no_error( + suppressMessages( + optim_SQGDE(obj, + GetAlgoParams(n_params = 4, n_iter = 100, n_particles = 12, + n_diff = 2, + param_block_list = list(1:2, 3:4))) + ) + ) +}) + +test_that("optimizer runs without error with single-element blocks", { + set.seed(1) + expect_no_error( + suppressMessages( + optim_SQGDE(obj, + GetAlgoParams(n_params = 3, n_iter = 100, n_particles = 12, + n_diff = 2, + param_block_list = list(1L, 2L, 3L))) + ) + ) +}) + +test_that("optimizer with param_block_list returns finite solution", { + set.seed(1) + out <- suppressMessages( + optim_SQGDE(obj, + GetAlgoParams(n_params = 4, n_iter = 200, n_particles = 12, + n_diff = 2, init_sd = 1, + param_block_list = list(1:2, 3:4))) + ) + expect_true(all(is.finite(out$solution))) + expect_true(is.finite(out$weight)) +}) + +test_that("block cycling: n_params=1 with single block works", { + set.seed(1) + out <- suppressMessages( + optim_SQGDE(obj, + GetAlgoParams(n_params = 1, n_iter = 50, n_particles = 6, + n_diff = 1, + param_block_list = list(1L))) + ) + expect_true(is.finite(out$solution)) +}) From 15d1435d099afc5ded4bf3b6c159c567f4c0cacb Mon Sep 17 00:00:00 2001 From: Brendan Matthew Galdo Date: Fri, 15 May 2026 18:13:01 -0400 Subject: [PATCH 4/6] update news.md and description --- DESCRIPTION | 2 +- NEWS.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 0267e70..5855602 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: graDiEnt Title: Stochastic Quasi-Gradient Differential Evolution Optimization -Version: 1.0.1 +Version: 1.1.0 Authors@R: person(given = "Brendan Matthew", family = "Galdo", diff --git a/NEWS.md b/NEWS.md index 6605b2e..a1434e6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,23 @@ +# graDiEnt 1.1.0 + +## New features + +* **Box constraints**: `GetAlgoParams` gains `lower` and `upper` parameters for per-parameter bounds. Enforced via `bounds_type = "reflect"` (default, mirrors proposals back across boundary) or `"clip"` (truncates to boundary). +* **Warm start**: `optim_SQGDE` gains `warm_start` parameter. Pass the output of a previous run to seed the population and continue optimization without re-initializing. +* **Disk recovery**: `GetAlgoParams` gains `recovery_path` (`.rds` file path) and `recovery_freq` (save interval in iterations). Partial results are written to disk periodically via `saveRDS`, surviving interrupts and OOM crashes. Load with `readRDS(recovery_path)`. +* **Progress message control**: `GetAlgoParams` gains `trace_print_freq`. Set to an integer to print progress every N iterations, or `Inf` to suppress all iteration messages. +* **Unified adaptation schemes**: `rand`, `current`, and `best` DE schemes are now selected via `adapt_scheme` in `GetAlgoParams` rather than separate internal functions. +* **`crossover_rate = 0`**: Now accepted; selects exactly one random parameter to update per iteration. +* **`jitter_size = 0`**: Now accepted; disables jitter noise. +* **Block coordinate updating**: `GetAlgoParams` gains `param_block_list`, an optional list of integer vectors partitioning `1:n_params` into blocks. The optimizer cycles through blocks in order across iterations, updating only the current block's parameters instead of using random crossover selection. Useful for high-dimensional problems where updating parameter subsets per iteration is more efficient. + +## Bug fixes + +* Fixed division-by-zero in `grad_approx_fn` when two particles are identical or have equal weights (zero-diff guard). +* Fixed `rand` scheme incorrectly sampling `2*n_diff` parents instead of `2*n_diff + 1`, causing out-of-bounds index access. +* Fixed `runif` jitter applied over `n_params` elements instead of `length(param_indices)` when `crossover_rate < 1`. +* Fixed `GetAlgoParams` validation for `n_params`, `n_particles`, and `n_iter`: finiteness is now checked before `as.integer()` coercion, preventing silent `NA` production and spurious "condition has length > 1" errors on vector inputs. + # graDiEnt 1.0.1 * Package uses "message()" instead of "print()" to communicate with user. * minor fix to documentation. From a63a4520383fedec1c2205876cfe0ae6ec7feb92 Mon Sep 17 00:00:00 2001 From: Brendan Matthew Galdo Date: Fri, 15 May 2026 18:19:21 -0400 Subject: [PATCH 5/6] devtools document! --- man/GetAlgoParams.Rd | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/man/GetAlgoParams.Rd b/man/GetAlgoParams.Rd index 4ee36a2..38cdbfd 100644 --- a/man/GetAlgoParams.Rd +++ b/man/GetAlgoParams.Rd @@ -29,7 +29,8 @@ GetAlgoParams( stop_check = 10, stop_tol = 1e-04, converge_crit = "stdev", - trace_print_freq = 100 + trace_print_freq = 100, + param_block_list = NULL ) } \arguments{ @@ -82,6 +83,8 @@ GetAlgoParams( \item{converge_crit}{A string denoting the convergence metric used, valid metrics are 'stdev' (standard deviation of population weight in the last stop_check iterations) and 'percent' (percent improvement in median particle weight in the last stop_check iterations). 'stdev' is the default.} \item{trace_print_freq}{A positive integer controlling how often iteration progress is printed. A message is printed every \code{trace_print_freq} iterations. Set to \code{Inf} to suppress progress messages entirely. Default is 100.} + +\item{param_block_list}{An optional list of integer vectors partitioning parameter indices into blocks. When provided, the algorithm cycles through blocks in order across iterations, updating only the parameters in the current block instead of using random crossover selection. Each index in \code{1:n_params} must appear in exactly one block. Set to \code{NULL} (default) to use standard crossover selection.} } \value{ A list of control parameters for the optim_SQGDE function. From 018a3865207e6bcae184520a4313719bbb8d59b4 Mon Sep 17 00:00:00 2001 From: Brendan Matthew Galdo Date: Fri, 15 May 2026 18:24:57 -0400 Subject: [PATCH 6/6] remove non-ascii character --- R/GetAlgoParams.R | 2 +- tests/testthat/test-param_block_list.R | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/R/GetAlgoParams.R b/R/GetAlgoParams.R index 3c3cb90..ca4e72a 100644 --- a/R/GetAlgoParams.R +++ b/R/GetAlgoParams.R @@ -344,7 +344,7 @@ GetAlgoParams = function(n_params, } all_indices = as.integer(all_indices) if (length(all_indices) != n_params || length(unique(all_indices)) != n_params) { - stop('ERROR: param_block_list must partition 1:n_params — each index must appear exactly once') + stop('ERROR: param_block_list must partition 1:n_params - each index must appear exactly once') } param_block_list = lapply(param_block_list, as.integer) } diff --git a/tests/testthat/test-param_block_list.R b/tests/testthat/test-param_block_list.R index 4d7f9ee..e1dcff0 100644 --- a/tests/testthat/test-param_block_list.R +++ b/tests/testthat/test-param_block_list.R @@ -60,6 +60,7 @@ test_that("missing index errors", { ) }) + test_that("empty list errors", { expect_error( GetAlgoParams(n_params = 4, param_block_list = list()),