diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index cd9685142..bf5ea8fe2 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -30,7 +30,7 @@ jobs: R_REMOTES_NO_ERRORS_FROM_WARNINGS: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-pandoc@v2 diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index d6ffea3a2..02c40ef26 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -18,7 +18,7 @@ jobs: env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-pandoc@v2 diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml index 9b1b5898b..3f9d3a27b 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -13,7 +13,7 @@ jobs: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-r@v2 with: diff --git a/R/engine_functions.R b/R/engine_functions.R index 7d0259810..d0858686a 100644 --- a/R/engine_functions.R +++ b/R/engine_functions.R @@ -816,4 +816,5 @@ #' @aliases reulermultinom #' @aliases round #' @aliases pgamma +#' @aliases safe_power NULL diff --git a/R/enum.R b/R/enum.R index 9b8183609..4bfa5bc6a 100644 --- a/R/enum.R +++ b/R/enum.R @@ -50,6 +50,7 @@ valid_func_sigs = c( , "fwrap,fail: reulermultinom(size, rate, delta_t)" , "fwrap,null: round(x)" , "fwrap,fail: pgamma(q, shape, scale)" + , "fwrap,fail: safe_power(x,y)" ) process_enum = function(x) { RE = "(null|fail|binop|fwrap|bwrap|pwrap)[ ]*,[ ]*(null|fail|binop|fwrap|bwrap|pwrap)[ ]*:[ ]*\\`?([^`]*)\\`?\\((.*)(\\,.*)*\\)" diff --git a/man/engine_functions.Rd b/man/engine_functions.Rd index e574d9a48..dc35d531e 100644 --- a/man/engine_functions.Rd +++ b/man/engine_functions.Rd @@ -52,6 +52,7 @@ \alias{reulermultinom} \alias{round} \alias{pgamma} +\alias{safe_power} \title{Engine Functions} \description{ Functions currently supported by the C++ TMB engine diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index a63d07503..752250bc8 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -111,6 +111,7 @@ enum macpan2_func , MP2_EULER_MULTINOM_SIM = 48 // fwrap,fail: reulermultinom(size, rate, delta_t) , MP2_ROUND = 49 // fwrap,null: round(x) , MP2_PGAMMA = 50 // fwrap,fail: pgamma(q, shape, scale) + , MP2_SAFEPOWER = 51 // fwrap,fail: safe_power(x,y) }; enum macpan2_meth @@ -1286,6 +1287,41 @@ class ExprEvaluator #endif return pow(args[0].array(), args[1].array()).matrix(); // return args[0].pow(args[1].coeff(0,0)); + + case MP2_SAFEPOWER: // SAFE_POWER, equivalent to (ifelse(x==0, 0, x^y)) + if (n != 2) { + SetError(err_code, "safe_power requires exactly two arguments", row, table_x[row] + 1, args.all_rows(), args.all_cols(), args.all_type_ints()); + return m; + } + args = args.recycle_for_bin_op(); + err_code = args.get_error_code(); + switch (err_code) { + case 201: + SetError(err_code, "The two operands do not have the same number of columns", row, table_x[row] + 1, args.all_rows(), args.all_cols(), args.all_type_ints()); + return m; + case 202: + SetError(err_code, "The two operands do not have the same number of rows", row, table_x[row] + 1, args.all_rows(), args.all_cols(), args.all_type_ints()); + return m; + case 203: + SetError(err_code, "The two operands do not have the same number of columns or rows", row, table_x[row] + 1, args.all_rows(), args.all_cols(), args.all_type_ints()); + return m; + } + m1 = args[0]; + m2 = args[1]; + rows = m1.rows(); + cols = m1.cols(); + m = matrix::Zero(rows, cols); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + m.coeffRef(i, j) = CppAD::CondExpEq( + m1.coeff(i, j), + Type(0), + Type(0), + pow(m1.coeff(i, j), m2.coeff(i, j)) + ); + } + } + return m; // #' ## Unary Elementwise Math // #' diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 7060c5c0a..8e13a2344 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -112,6 +112,7 @@ enum macpan2_func , MP2_EULER_MULTINOM_SIM = 48 // fwrap,fail: reulermultinom(size, rate, delta_t) , MP2_ROUND = 49 // fwrap,null: round(x) , MP2_PGAMMA = 50 // fwrap,fail: pgamma(q, shape, scale) + , MP2_SAFEPOWER = 51 // fwrap,fail: safe_power(x,y) }; enum macpan2_meth @@ -1287,6 +1288,41 @@ class ExprEvaluator #endif return pow(args[0].array(), args[1].array()).matrix(); // return args[0].pow(args[1].coeff(0,0)); + + case MP2_SAFEPOWER: // SAFE_POWER, equivalent to (ifelse(x==0, 0, x^y)) + if (n != 2) { + SetError(err_code, "safe_power requires exactly two arguments", row, table_x[row] + 1, args.all_rows(), args.all_cols(), args.all_type_ints()); + return m; + } + args = args.recycle_for_bin_op(); + err_code = args.get_error_code(); + switch (err_code) { + case 201: + SetError(err_code, "The two operands do not have the same number of columns", row, table_x[row] + 1, args.all_rows(), args.all_cols(), args.all_type_ints()); + return m; + case 202: + SetError(err_code, "The two operands do not have the same number of rows", row, table_x[row] + 1, args.all_rows(), args.all_cols(), args.all_type_ints()); + return m; + case 203: + SetError(err_code, "The two operands do not have the same number of columns or rows", row, table_x[row] + 1, args.all_rows(), args.all_cols(), args.all_type_ints()); + return m; + } + m1 = args[0]; + m2 = args[1]; + rows = m1.rows(); + cols = m1.cols(); + m = matrix::Zero(rows, cols); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + m.coeffRef(i, j) = CppAD::CondExpEq( + m1.coeff(i, j), + Type(0), + Type(0), + pow(m1.coeff(i, j), m2.coeff(i, j)) + ); + } + } + return m; // #' ## Unary Elementwise Math // #' diff --git a/tests/testthat/test-binop.R b/tests/testthat/test-binop.R index abb618e4b..44ee112da 100644 --- a/tests/testthat/test-binop.R +++ b/tests/testthat/test-binop.R @@ -1,3 +1,5 @@ +library(macpan2); library(testthat); library(dplyr); library(tidyr); library(ggplot2) + test_that("elementwise binary operator executable specs match spec doc", { ## https://canmod.net/misc/elementwise_binary_operators times = BinaryOperator(`*`) @@ -83,3 +85,30 @@ test_that("elementwise binary operator executable specs match spec doc", { test_that("equivalent unary and binary minus operators give the same answers", { expect_equal(engine_eval(~-4), engine_eval(~0-4)) }) + +test_that("safe_power meets the requirements of #200", { + expect_equal( + matrix(0.1 ^ -5), + engine_eval(~safe_power(0.1, -5)) + ) + expect_equal( + matrix(0 ^ 5), + engine_eval(~safe_power(0, 5)) + ) + expect_equal( + matrix(c(1, 1, 0, 1, 1)), # != matrix((-2:2)^0), + engine_eval(~safe_power(-2:2, 0)) + ) + expect_equal( + matrix(rep(0, 5)), # != matrix(0^(-2:2)) + engine_eval(~safe_power(0, -2:2)) + ) + expect_error( + engine_eval(~safe_power(1)), + regexp = "safe_power requires exactly two arguments" + ) + expect_error( + engine_eval(~safe_power(1:3, 2:3)), + regexp = "The two operands do not have the same number of rows" + ) +})