From bf5ae742743bc111fbc8347d87e12e935d39949e Mon Sep 17 00:00:00 2001 From: Matthew Fidler Date: Tue, 27 Oct 2026 16:10:11 -0500 Subject: [PATCH 1/3] Compare chebyquad minqa results as a set on non-x86 platforms On linux-arm64 the FMA-contracted minqa build reaches the same permutation-invariant chebyquad minimum with two coordinates swapped, failing the positional par comparison in test-minqa.R. --- NEWS.md | 6 ++++++ tests/testthat/test-minqa.R | 24 +++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/NEWS.md b/NEWS.md index 75ce4b6..bca52a3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,12 @@ `OMP_THREAD_LIMIT`, so CRAN no longer sees "OMP: Warning #96: Cannot form a team" (#3). +* The `bobyqa()`/`newuoa()` vs. 'minqa' comparison tests compare the + permutation-invariant chebyquad solutions as a set on platforms without + bitwise agreement, fixing a test failure on CRAN's linux-arm64 check + where the FMA-contracted 'minqa' build reached the same minimum with two + coordinates swapped. + # RcppTrust 0.2.0 * The package license is now GPL-2, because the BOBYQA/NEWUOA ports and diff --git a/tests/testthat/test-minqa.R b/tests/testthat/test-minqa.R index b78daa9..b962741 100644 --- a/tests/testthat/test-minqa.R +++ b/tests/testthat/test-minqa.R @@ -31,8 +31,12 @@ bitwise <- R.version$arch %in% c("x86_64", "amd64") # `wellPosed = FALSE` marks runs (e.g. a bound range of 1e-7) whose end # point is sensitive to rounding, so contracted and uncontracted builds -# legitimately stop at different points. -expectSameAsMinqa <- function(ours, theirs, wellPosed = TRUE) { +# legitimately stop at different points. `permInvariant = TRUE` marks +# objectives (e.g. chebyquad) unchanged by permuting `x`: differently +# rounded runs may reach the same minimum with coordinates swapped (seen +# on aarch64), so the parameters are compared as a set. +expectSameAsMinqa <- function(ours, theirs, wellPosed = TRUE, + permInvariant = FALSE) { expect_identical(class(ours), class(theirs)) if (bitwise) { expect_identical(ours$par, theirs$par) @@ -41,7 +45,12 @@ expectSameAsMinqa <- function(ours, theirs, wellPosed = TRUE) { expect_identical(ours$ierr, theirs$ierr) expect_identical(ours$msg, theirs$msg) } else if (wellPosed) { - expect_equal(ours$par, theirs$par, tolerance = 1e-4) + if (permInvariant) { + expect_equal(sort(ours$par), sort(theirs$par), tolerance = 1e-4) + expect_equal(ours$fval, theirs$fval, tolerance = 1e-6) + } else { + expect_equal(ours$par, theirs$par, tolerance = 1e-4) + } expect_equal(ours$ierr, theirs$ierr) } } @@ -54,7 +63,7 @@ test_that("bobyqa() matches minqa::bobyqa() exactly", { list(par = rep(0.5, 6), fn = extRosen, lower = -2, upper = 2, control = list(npt = 13)), list(par = seq(0.1, 0.9, length.out = 6), fn = chebyquad, lower = 0, - upper = 1), + upper = 1, permInvariant = TRUE), list(par = c(1, 2), fn = fr, lower = c(0, 0), upper = c(4, 4), control = list(maxfun = 50)), list(par = c(4, 4), fn = fr, lower = c(0, 3.9999999), upper = c(4, 4), @@ -65,7 +74,8 @@ test_that("bobyqa() matches minqa::bobyqa() exactly", { ours <- suppressWarnings(bobyqa(cs$par, cs$fn, cs$lower, cs$upper, ctrl)) theirs <- suppressWarnings(minqa::bobyqa(cs$par, cs$fn, cs$lower, cs$upper, ctrl)) - expectSameAsMinqa(ours, theirs, wellPosed = !isFALSE(cs$wellPosed)) + expectSameAsMinqa(ours, theirs, wellPosed = !isFALSE(cs$wellPosed), + permInvariant = isTRUE(cs$permInvariant)) } }) @@ -75,7 +85,7 @@ test_that("newuoa() matches minqa::newuoa() exactly", { list(par = c(-1.2, 1), fn = fr), list(par = rep(0.5, 6), fn = extRosen, control = list(npt = 13)), list(par = seq(0.1, 0.9, length.out = 6), fn = chebyquad, - control = list(npt = 28)), + control = list(npt = 28), permInvariant = TRUE), list(par = rep(pi, 4), fn = function(x) -(10 - sum(x * seq_along(x))^2), control = list(maxfun = 25)) ) @@ -83,7 +93,7 @@ test_that("newuoa() matches minqa::newuoa() exactly", { ctrl <- if (is.null(cs$control)) list() else cs$control ours <- suppressWarnings(newuoa(cs$par, cs$fn, ctrl)) theirs <- suppressWarnings(minqa::newuoa(cs$par, cs$fn, ctrl)) - expectSameAsMinqa(ours, theirs) + expectSameAsMinqa(ours, theirs, permInvariant = isTRUE(cs$permInvariant)) } }) From d71b9ec014052f2a4884da59c381a51703a41a17 Mon Sep 17 00:00:00 2001 From: Matthew Fidler Date: Tue, 27 Oct 2026 16:11:41 -0500 Subject: [PATCH 2/3] Add linux-arm64 CI mirroring CRAN's arm64 special check --- .github/workflows/linux-arm64-check.yaml | 56 ++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/linux-arm64-check.yaml diff --git a/.github/workflows/linux-arm64-check.yaml b/.github/workflows/linux-arm64-check.yaml new file mode 100644 index 0000000..9cebdb7 --- /dev/null +++ b/.github/workflows/linux-arm64-check.yaml @@ -0,0 +1,56 @@ +# Mirrors CRAN's linux-arm64 special check +# (https://github.com/r-devel/linux-arm64-checks): arm64 runner, rcheckserver +# container (CRAN Debian system libraries), Suggests such as 'minqa' built +# from source with that toolchain. +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +name: linux-arm64-check.yaml + +permissions: read-all + +jobs: + linux-arm64: + name: ubuntu-24.04-arm (rcheckserver) + runs-on: ubuntu-24.04-arm + container: + image: ghcr.io/r-devel/rcheckserver/ubuntu:latest + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + timeout-minutes: 90 + run: | + install.packages("remotes") + remotes::install_deps(dependencies = TRUE, upgrade = "never") + shell: Rscript {0} + + - name: Build package + timeout-minutes: 30 + run: R CMD build --no-build-vignettes --no-manual . + shell: bash + + - name: Check package + timeout-minutes: 90 + run: | + R CMD check RcppTrust_*.tar.gz --no-manual --ignore-vignettes || true + status=$(grep "^Status:" RcppTrust.Rcheck/00check.log) + echo "$status" + if [ -z "$status" ] || echo "$status" | grep -qE "WARNING|ERROR"; then + exit 1 + fi + shell: bash + env: + _R_CHECK_FORCE_SUGGESTS_: false + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: linux-arm64-check + path: | + RcppTrust.Rcheck/*.log + RcppTrust.Rcheck/tests From 18e7da3a3426d63a18a82ae7602bce0cd3f3c927 Mon Sep 17 00:00:00 2001 From: Matthew Fidler Date: Wed, 28 Oct 2026 08:21:20 -0500 Subject: [PATCH 3/3] Make BOBYQA RESCUE test BLAS-free so coverage is deterministic The Hilbert objective used %*%, whose CPU-dependent BLAS kernels changed which exit the ill-conditioned run took between CI machines, so the cancellation exit (ierr 320/5) was covered on some runs and not others and codecov/project flipped. Use outer()/sum() instead and add an n = 8 case so the RESCUE re-entry paths are also covered on every run. --- tests/testthat/test-minqa.R | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/testthat/test-minqa.R b/tests/testthat/test-minqa.R index b962741..a918cab 100644 --- a/tests/testthat/test-minqa.R +++ b/tests/testthat/test-minqa.R @@ -180,17 +180,24 @@ test_that("NEWUOA's BIGDEN safeguard path matches minqa", { test_that("BOBYQA's RESCUE path runs deterministically", { # A Hilbert-matrix quadratic with a tiny rhoend enters RESCUE, where minqa # 1.2.8 itself evaluates uninitialized memory, so there is no minqa - # reference: the port must converge and repeat exactly. + # reference: the port must converge and repeat exactly. The objective + # avoids BLAS (`%*%`), whose CPU-dependent kernels changed which exit the + # run took between CI machines (and so the coverage of the + # cancellation exit). On x86_64, n = 7 ends at the "too much + # cancellation" exit and n = 8 takes the other RESCUE re-entries. hilb <- function(x) { n <- length(x) - drop(x %*% (1 / (outer(0:(n - 1), 0:(n - 1), "+") + 1)) %*% x) + sum(outer(x, x) / (outer(0:(n - 1), 0:(n - 1), "+") + 1)) + } + for (n in 7:8) { + ctrl <- list(npt = (n + 1) * (n + 2) / 2, rhobeg = 0.5, rhoend = 1e-10, + maxfun = 20000) + a <- suppressWarnings(bobyqa(rep(1, n), hilb, -2, 2, control = ctrl)) + b <- suppressWarnings(bobyqa(rep(1, n), hilb, -2, 2, control = ctrl)) + expect_identical(a, b) + expect_true(a$ierr %in% c(0, 3, 5)) + expect_lt(a$fval, 1e-4) } - ctrl <- list(npt = 36, rhobeg = 0.5, rhoend = 1e-10, maxfun = 20000) - a <- suppressWarnings(bobyqa(rep(1, 7), hilb, -2, 2, control = ctrl)) - b <- suppressWarnings(bobyqa(rep(1, 7), hilb, -2, 2, control = ctrl)) - expect_identical(a, b) - expect_true(a$ierr %in% c(0, 3, 5)) - expect_lt(a$fval, 1e-4) }) test_that("minqa R wrapper argument handling", {