Skip to content
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
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
^README\.Rmd$
^cran-comments\.md$
^CRAN-SUBMISSION$
^\.github$
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
20 changes: 20 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
23 changes: 21 additions & 2 deletions R/GetAlgoParams.R
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)){
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
17 changes: 11 additions & 6 deletions R/SQG_DE_bin_1.R
Original file line number Diff line number Diff line change
Expand Up @@ -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, ]
Expand All @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions R/optim_SQGDE.R
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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)

Expand Down
5 changes: 4 additions & 1 deletion man/GetAlgoParams.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion tests/testthat/test-GetAlgoParams.R
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────────────

Expand Down
File renamed without changes.
File renamed without changes.
118 changes: 118 additions & 0 deletions tests/testthat/test-param_block_list.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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))
})
File renamed without changes.
Loading