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 diff --git a/NEWS.md b/NEWS.md index d9e4d71..4613d80 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,11 @@ `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. * The NEWUOA `ierr = 3` test is skipped when R lacks long double support, since its rounding path depends on `sum()` accumulating in long double (CRAN's noLD check). diff --git a/tests/testthat/test-minqa.R b/tests/testthat/test-minqa.R index c36dede..bbc7474 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)) } }) @@ -170,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", {