# Copyright (C) 2023 Avraham Adler. All Rights Reserved. # SPDX-License-Identifier: LGPL-3.0-or-later # # File: test-nloptr # Author: Avraham Adler # Date: 7 February 2023 # # Test code in nloptr.R and nloptr.c which is not tested elsewhere. # # Changelog: # 2023-08-23: Converted snapshots to testing portions of outputs and messages. # # It is possible for NLOPT to go slightly beyond maxtime or maxeval, especially # for the global algorithms, which is why the stopping criterion has a # weird-looking test. See # https://nlopt.readthedocs.io/en/latest/NLopt_Reference/#stopping-criteria library(nloptr) options(digits=7) tol <- sqrt(.Machine$double.eps) ########################## Tests for nloptr.R ################################## ctlNM <- list(algorithm = "NLOPT_LN_NELDERMEAD", xtol_rel = 1e-8, check_derivatives = TRUE) ctlSQP <- list(algorithm = "NLOPT_LD_SLSQP", xtol_rel = 1e-8, check_derivatives = TRUE) # internal function to check the arguments of the functions expect_error(nloptr(3, "Zed"), "must be a function", fixed = TRUE) fn <- function(x, b = NULL, c = NULL) x expect_error(nloptr(3, fn, c = "Q"), "but this has not been passed to the 'nloptr' function", fixed = TRUE) expect_error(nloptr(3, fn, b = "X", c = "Y", d = "Q"), "passed to (...) in 'nloptr' but this is not required in the", fixed = TRUE) expect_warning(nloptr(3, fn, b = 3, c = 4, opts = ctlNM), "Skipping derivative checker because algorithm", fixed = TRUE) ########################## Tests for nloptr.c ################################## ctl <- list(xtol_rel = 1e-8, maxeval = 1000L) fn <- function(x) x ^ 2 - 4 * x + 4 lb <- 0 ub <- 6 optSol <- 2 optVal <- 0 ## NLOPT_GN_DIRECT_L_NOSCAL alg <- list(algorithm = "NLOPT_GN_DIRECT_L_NOSCAL") testRun <- nloptr(5, fn, lb = lb, ub = ub, opts = c(alg, ctl)) expect_equal(testRun$solution, optSol, tolerance = tol) expect_equal(testRun$objective, optVal, tolerance = tol) expect_true(testRun$iterations <= ctl$maxeval + 5) expect_true(testRun$status > 0) ## NLOPT_GN_DIRECT_L_RAND_NOSCAL alg <- list(algorithm = "NLOPT_GN_DIRECT_L_RAND_NOSCAL") testRun <- nloptr(5, fn, lb = lb, ub = ub, opts = c(alg, ctl)) expect_equal(testRun$solution, optSol, tolerance = tol) expect_equal(testRun$objective, optVal, tolerance = tol) expect_true(testRun$iterations <= ctl$maxeval + 5) expect_true(testRun$status > 0) ## NLOPT_LN_PRAXIS alg <- list(algorithm = "NLOPT_LN_PRAXIS") testRun <- nloptr(5, fn, lb = lb, ub = ub, opts = c(alg, ctl)) expect_equal(testRun$solution, optSol, tolerance = tol) expect_equal(testRun$objective, optVal, tolerance = tol) expect_true(testRun$iterations <= ctl$maxeval + 5) expect_true(testRun$status > 0) ## NLOPT_GN_MLSL alg <- list(algorithm = "NLOPT_GN_MLSL") lopts <- list(local_opts = list(algorithm = "NLOPT_LN_COBYLA", xtol_rel = 1e-8)) testRun <- nloptr(5, fn, lb = lb, ub = ub, opts = c(alg, ctl, lopts)) expect_equal(testRun$solution, optSol, tolerance = tol) expect_equal(testRun$objective, optVal, tolerance = tol) expect_true(testRun$iterations <= ctl$maxeval + 5) expect_true(testRun$status > 0) ## NLOPT_GN_MLSL_LDS alg <- list(algorithm = "NLOPT_GN_MLSL_LDS") lopts <- list(local_opts = list(algorithm = "NLOPT_LN_COBYLA", xtol_rel = 1e-8)) testRun <- nloptr(5, fn, lb = lb, ub = ub, opts = c(alg, ctl, lopts)) expect_equal(testRun$solution, optSol, tolerance = tol) expect_equal(testRun$objective, optVal, tolerance = tol) expect_true(testRun$iterations <= ctl$maxeval + 5) expect_true(testRun$status > 0) ## NLOPT_LN_AUGLAG_EQ x0 <- c(-2, 2, 2, -1, -1) fn1 <- function(x) exp(x[1] * x[2] * x[3] * x[4] * x[5]) eqn1 <- function(x) { c(x[1] * x[1] + x[2] * x[2] + x[3] * x[3] + x[4] * x[4] + x[5] * x[5], x[2] * x[3] - 5 * x[4] * x[5], x[1] * x[1] * x[1] + x[2] * x[2] * x[2]) } optSol <- rep(0, 5) optVal <- 1 testRun <- nloptr(x0, fn1, eval_g_eq = eqn1, opts = list(algorithm = "NLOPT_LN_AUGLAG_EQ", xtol_rel = 1e-6, maxeval = 10000L, local_opts = list(algorithm = "NLOPT_LN_COBYLA", xtol_rel = 1e-6))) expect_equal(testRun$solution, optSol, tolerance = tol) expect_equal(testRun$objective, optVal, tolerance = tol) expect_true(testRun$iterations <= 10005L) expect_true(testRun$status > 0) ## NLOPT_LD_AUGLAG_EQ gr1 <- function(x) { c(x[2] * x[3] * x[4] * x[5], x[1] * x[3] * x[4] * x[5], x[1] * x[2] * x[4] * x[5], x[1] * x[2] * x[3] * x[5], x[1] * x[2] * x[3] * x[4]) * exp(prod(x)) } heqjac <- function(x) nl.jacobian(x0, eqn1) testRun <- nloptr(x0, fn1, gr1, eval_g_eq = eqn1, eval_jac_g_eq = heqjac, opts = list(algorithm = "NLOPT_LD_AUGLAG_EQ", xtol_rel = 1e-6, maxeval = 10000L, local_opts = list(algorithm = "NLOPT_LN_COBYLA", xtol_rel = 1e-6))) expect_equal(testRun$solution, optSol, tolerance = tol) expect_equal(testRun$objective, optVal, tolerance = tol) expect_true(testRun$iterations <= 10005L) expect_true(testRun$status > 0) ## NLOPT_LN_NEWUOA_BOUND fn <- function(x) x[1L] ^ 4 + x[2L] ^ 2 - 5 * x[1L] * x[2L] + 5 gr <- function(x) c(4 * x[1L] ^ 3 - 5 * x[2L], 2 * x[2L] - 5 * x[1L]) lb <- c(0, 0) ub <- c(5, 5) # https://www.wolframalpha.com/input?i=minimum+of+x+%5E+4+%2B+y+%5E+2+-+5+*+x+*+y++%2B+5+ # nolint optSol <- c(5 / (2 * sqrt(2)), 25 / (4 * sqrt(2))) optVal <- -305 / 64 alg <- list(algorithm = "NLOPT_LN_NEWUOA_BOUND") testRun <- nloptr(c(1, 1), fn, lb = lb, ub = ub, opts = c(alg, ctl)) expect_equal(testRun$solution, optSol, tolerance = 1e-5) expect_equal(testRun$objective, optVal, tolerance = tol) expect_true(testRun$iterations <= ctl$maxeval + 5) expect_true(testRun$status > 0) ## NLOPT_GN_ESCH alg <- list(algorithm = "NLOPT_GN_ESCH") ctl <- list(xtol_rel = 1e-8, maxeval = 50000L) testRun <- nloptr(c(1, 1), fn, lb = lb, ub = ub, opts = c(alg, ctl)) expect_equal(testRun$solution, optSol, tolerance = 1e-2) expect_equal(testRun$objective, optVal, tolerance = 1e-2) expect_true(testRun$iterations <= ctl$maxeval + 5) expect_true(testRun$status > 0) ## NLOPT_LD_LBFGS_NOCEDAL # NLOPT_LD_LBFGS_NOCEDAL as this algorithm has not been included as of NLOPT # 2.7.1 per https://github.com/stevengj/nlopt/issues/40 so the expected outcome # is NLOPT_INVALID_ARGS. Perhaps we should ring-fence it for now? # (AA: 2023-02-08) alg <- list(algorithm = "NLOPT_LD_LBFGS_NOCEDAL") testRun <- nloptr(c(1, 1), fn, gr, lb = lb, ub = ub, opts = c(alg, ctl)) minus2mess <- paste("NLOPT_INVALID_ARGS: Invalid arguments (e.g. lower bounds", "are bigger than upper bounds, an unknown algorithm was", "specified, etcetera).") expect_identical(testRun$status, -2L) expect_identical(testRun$message, minus2mess) ## case NLOPT_FAILURE fnl <- function(x) { list("objective" = (x[1] - 1) ^ 2 + (x[2] - 1) ^ 2, "gradient" = c(4 * (x[1] - 1), 3 - (x[2] - 1))) } x0 <- c(3, 3) testRun <- nloptr(x0, fnl, opts = list(algorithm = "NLOPT_LD_LBFGS", xtol_rel = 1e-8)) expect_identical(testRun$status, -1L) expect_identical(testRun$message, "NLOPT_FAILURE: Generic failure code.") # Tinytest has no snapshot functionality. Instead, this will test the output # and message against portions of the expected, instead of the totality. ## MULTIVARIATE FUNCTION x0 <- c(2, 2) ub <- c(5, 5) lb <- c(-1, -1) fn <- function(x) (x[1L] - 1) ^ 2 + (x[2L] - 1) ^ 2 + 1 gr <- function(x) c(2 * (x[1L] - 1), 2 * (x[2L] - 1)) hin <- function(x) c(1.44 - x[1L] ^ 2, 2.197 - x[2L] ^ 3) hinjac <- function(x) matrix(c(-2 * x[1L], 0, 0, -3 * x[2L] ^ 2), 2L, 2L) heq <- function(x) c(x[1L] * x[2L] - 2.55, x[1L] - x[2L] - 0.2) heqjac <- function(x) matrix(c(x[2L], 1, x[1L], -1), 2L) optSol <- c(1.7, 1.5) optVal <- 1.74 expect_stdout( suppressMessages( nloptr(x0, fn, gr, lb, ub, hin, hinjac, heq, heqjac, opts = list(algorithm = "NLOPT_LD_SLSQP", xtol_rel = 1e-8, print_level = 3, check_derivatives = TRUE)) ), "g(x) = (-1.450000, -1.178000)", fixed = TRUE ) # Wrap in capture.output to prevent wall of text on screen when running. This is # to test the message; expect_stdout tests the output. expect_message( capture.output( nloptr(x0, fn, gr, lb, ub, hin, hinjac, heq, heqjac, opts = list(algorithm = "NLOPT_LD_SLSQP", xtol_rel = 1e-8, print_level = 3, check_derivatives = TRUE)), type = "output" ), "eval_jac_g_ineq[1, 1] = -4.0e+00 ~ -4.0e+00 [7.450581e-09]", fixed = TRUE ) ## UNIVARIATE FUNCTION x0 <- 5 fn <- function(x) x ^ 2 - 4 * x + 4 gr <- function(x) 2 * x - 4 hin <- function(x) 5.29 - x ^ 2 hinjac <- function(x) -2 * x heq <- function(x) 10 * x - 27 heqjac <- function(x) 10 lb <- 0 ub <- 6 optSol <- 2.7 optVal <- 0.49 ## UNIVARIATE expect_stdout( suppressMessages( nloptr(x0, fn, gr, lb, ub, hin, hinjac, heq, heqjac, opts = list(algorithm = "NLOPT_LD_SLSQP", xtol_rel = 1e-8, print_level = 3, check_derivatives = TRUE)) ), "g(x) = -2.000000", fixed = TRUE ) expect_message( capture.output( nloptr(x0, fn, gr, lb, ub, hin, hinjac, heq, heqjac, opts = list(algorithm = "NLOPT_LD_SLSQP", xtol_rel = 1e-8, print_level = 3, check_derivatives = TRUE)), type = "output" ), "eval_jac_g_ineq[1] = -1e+01 ~ -1e+01 [9.536743e-09]", fixed = TRUE ) # Test NLOPT_ROUNDOFF_LIMITED expect_true( grepl("Roundoff errors led to a breakdown", nloptr(x0, fn, gr, opts = list(algorithm = "NLOPT_LD_SLSQP", xtol_rel = -Inf))$message, fixed = TRUE) ) # Test triggering stopval expect_true( grepl("Optimization stopped because stopval", nloptr(c(4, 4), fn, opts = list(algorithm = "NLOPT_LN_SBPLX", stopval = 20))$message, fixed = TRUE) )