From eb1655cc5b38e35f36242c5c119fa1a5912eade4 Mon Sep 17 00:00:00 2001 From: yannrichet Date: Sun, 12 Jul 2026 14:59:54 +0200 Subject: [PATCH] feat: support direct R function models in fzd() fzd() now accepts an R function as model, letting adaptive design-of-experiments algorithms drive an R function directly instead of a file-based model. In this mode input_path must be NULL, output_expression is optional (defaults to the function's first output), and calculators is accepted for API compatibility but has no effect: fz always evaluates function models sequentially, one call at a time in the calling thread, since R closures bridged in via reticulate are only safe to call from the main thread. Passing calculators != 1L now warns that it has no effect. Requires the fz main branch on GitHub (fz_install(packages = "git+https://github.com/Funz/fz.git")) and Funz/fz#73, which makes fz's function-model support call sequentially instead of via a Python thread pool (even a single-worker pool always dispatches to a worker thread, never the calling thread, which crashed the R session). fz_install() gains a packages argument (default "funz-fz") to select the install source, enabling the GitHub main branch as an alternative to PyPI. Co-Authored-By: Claude Sonnet 5 --- DESCRIPTION | 2 +- NEWS.md | 17 +++++++++ R/core-functions.R | 86 +++++++++++++++++++++++++++++++++++++++++++--- R/install.R | 11 ++++-- man/fz_install.Rd | 16 ++++++++- man/fzd.Rd | 68 +++++++++++++++++++++++++++++++++--- 6 files changed, 186 insertions(+), 14 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index ed87e57..e2578cf 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: fz Type: Package Title: R Wrapper for the 'funz-fz' Parametric Simulation Framework -Version: 1.1 +Version: 1.1.9000 Authors@R: c( person("Yann", "Richet", email = "yann.richet@asnr.fr", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-5677-8458")) diff --git a/NEWS.md b/NEWS.md index a6d1d52..d2cae55 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,20 @@ +# fz (development version) + +* `fzd()` now accepts an R function as `model`, letting adaptive + design-of-experiments algorithms drive an R function directly instead of + a file-based model (`input_path = NULL`, `output_expression` optional, + `calculators` accepted as any single integer but currently always run + sequentially, one call at a time — see below). This requires the `main` + branch of `fz` on GitHub — install it with + `fz_install(packages = "git+https://github.com/Funz/fz.git")`, and needs + [Funz/fz#73](https://github.com/Funz/fz/pull/73) merged: R closures are + bridged in via `reticulate`, which is only safe to call from the main + thread, so `fz`'s function-model support was changed to always call the + model sequentially (like `lapply`) instead of via a Python thread pool, + regardless of `calculators`. +* `fz_install()` gains a `packages` argument (default `"funz-fz"`) so the + latest `main` branch can be installed instead of the PyPI release. + # fz 1.1 First release, aligned with funz-fz 1.1 on PyPI. diff --git a/R/core-functions.R b/R/core-functions.R index ac919fc..f5aeea4 100644 --- a/R/core-functions.R +++ b/R/core-functions.R @@ -185,21 +185,60 @@ fzl <- function(models = "*", calculators = "*", check = FALSE) { #' algorithm adaptively choose which parameter combinations to evaluate, which #' is useful for sensitivity analysis, surrogate-model fitting, or optimization. #' -#' @param input_path Path to input file or directory. +#' @param input_path Path to input file or directory. Must be \code{NULL} when +#' \code{model} is an R function (see "Direct function model" below). #' @param input_variables Named list of variable range strings of the form #' \code{"[min;max]"}, e.g. \code{list(x = "[0;1]", y = "[-5;5]")}. -#' @param model Model definition dict or alias string. +#' @param model Model definition dict or alias string, or an R function (see +#' "Direct function model" below). #' @param output_expression Expression evaluated on the model outputs to #' produce the scalar quantity the algorithm optimizes or analyses, -#' e.g. \code{"result"} or \code{"out1 + 2 * out2"}. +#' e.g. \code{"result"} or \code{"out1 + 2 * out2"}. May be \code{NULL} +#' only when \code{model} is a function, in which case the first output +#' value is used. #' @param algorithm Path to the algorithm Python file, e.g. #' \code{"algorithms/montecarlo_uniform.py"}. -#' @param calculators Calculator specification(s). Default \code{NULL}. +#' @param calculators Calculator specification(s). Default \code{NULL}. When +#' \code{model} is a function, this must be a single integer (default +#' \code{1L}), accepted for API compatibility (see "Direct function model" +#' below) — calls are always run sequentially regardless of its value. #' @param algorithm_options Algorithm options as a named list or #' semicolon-separated string, e.g. \code{"batch_sample_size=10;seed=42"}. #' Default \code{NULL}. #' @param analysis_dir Analysis directory. Default \code{"analysis"}. #' +#' @section Direct function model: +#' Instead of a file-based model, \code{model} can be an R function (this +#' requires the \code{main} branch of \code{fz} from GitHub, installed with +#' \code{fz_install(packages = "git+https://github.com/Funz/fz.git")} — this +#' mode is not available in released PyPI versions of \code{funz-fz} yet). In +#' this mode: +#' \itemize{ +#' \item \code{input_path} must be \code{NULL} — there are no input files. +#' \item \code{input_variables} names must match the function's arguments. +#' \item \code{output_expression} may be \code{NULL}; the value used is then +#' the first element of the function's return value (its return value +#' directly if scalar, the first element if a vector/list, or the first +#' entry's value if a named list). +#' \item \code{calculators} must be a single integer, accepted for API +#' compatibility but currently without effect: on the \code{fz} side, +#' function-model calls always run sequentially, one at a time in the +#' calling thread — never through a thread pool. This is required +#' because R functions are called back into the R session via +#' \code{reticulate}, which is only safe from the main thread; running +#' a Python-side thread pool (which always dispatches to a worker thread, +#' even with a single worker) would call the function from a thread +#' other than the main one and crash the R session. This safety fix +#' requires \href{https://github.com/Funz/fz/pull/73}{Funz/fz#73} on the +#' \code{fz} \code{main} branch (not yet in a PyPI release as of +#' 2026-07-12); without it, direct function models crash regardless of +#' \code{calculators}. A value other than \code{1} emits a warning +#' noting that it has no effect. +#' \item each iteration's directory (\code{iterNNN/}) only contains a +#' \code{values.csv} of that iteration's function inputs/outputs, since +#' there is no file-based execution. +#' } +#' #' @return Named list with the analysis results produced by the algorithm. #' @export #' @@ -224,9 +263,46 @@ fzl <- function(models = "*", calculators = "*", check = FALSE) { #' ) #' } #' } -fzd <- function(input_path, input_variables, model, output_expression, algorithm, +#' +#' \dontrun{ +#' # Direct function model (requires fz main branch from GitHub) +#' rosenbrock <- function(x, y) { +#' list(result = (1 - x)^2 + 100 * (y - x^2)^2) +#' } +#' +#' result <- fzd( +#' input_path = NULL, +#' input_variables = list(x = "[-2;2]", y = "[-2;2]"), +#' model = rosenbrock, +#' output_expression = "result", +#' algorithm = "examples/algorithms/bfgs.py", +#' calculators = 4L, +#' algorithm_options = list(max_iter = 20, tol = 1e-4) +#' ) +#' } +fzd <- function(input_path, input_variables, model, output_expression = NULL, algorithm, calculators = NULL, algorithm_options = NULL, analysis_dir = "analysis") { + if (is.function(model)) { + if (is.null(calculators)) { + calculators <- 1L + } else if (!is.numeric(calculators) || length(calculators) != 1) { + stop( + "When 'model' is an R function, 'calculators' must be a single integer.", + call. = FALSE + ) + } + calculators <- as.integer(calculators) + if (calculators != 1L) { + warning( + "calculators = ", calculators, " has no effect when 'model' is an R ", + "function: evaluations always run sequentially (one call at a time), ", + "since R functions bridged in via reticulate are only safe to call ", + "from the main thread.", + call. = FALSE + ) + } + } fz_module <- get_fz() fz_module$fzd(input_path, input_variables, model, output_expression, algorithm, calculators = calculators, diff --git a/R/install.R b/R/install.R index 249b56c..a7e6f74 100644 --- a/R/install.R +++ b/R/install.R @@ -3,6 +3,10 @@ #' This function installs the fz Python package into a virtual environment #' or conda environment managed by reticulate. #' +#' @param packages Package specification passed to [reticulate::py_install()]. +#' Default \code{"funz-fz"} installs the latest release from PyPI. To track +#' unreleased features, install the latest \code{main} branch directly from +#' GitHub with \code{"git+https://github.com/Funz/fz.git"}. #' @param method Installation method. Either "auto", "virtualenv", or "conda". #' @param conda Path to conda executable. Only used when method is "conda". #' @param pip Logical; use pip for installation? Default is TRUE. @@ -20,9 +24,12 @@ #' #' # Install in a conda environment #' fz_install(method = "conda") +#' +#' # Track the latest main branch on GitHub (unreleased features) +#' fz_install(packages = "git+https://github.com/Funz/fz.git") #' } -fz_install <- function(method = "auto", conda = "auto", pip = TRUE, ...) { - reticulate::py_install("funz-fz", method = method, conda = conda, pip = pip, ...) +fz_install <- function(packages = "funz-fz", method = "auto", conda = "auto", pip = TRUE, ...) { + reticulate::py_install(packages, method = method, conda = conda, pip = pip, ...) } #' Check if fz Python Package is Available diff --git a/man/fz_install.Rd b/man/fz_install.Rd index d941077..73d31f2 100644 --- a/man/fz_install.Rd +++ b/man/fz_install.Rd @@ -4,9 +4,20 @@ \alias{fz_install} \title{Install the fz Python Package} \usage{ -fz_install(method = "auto", conda = "auto", pip = TRUE, ...) +fz_install( + packages = "funz-fz", + method = "auto", + conda = "auto", + pip = TRUE, + ... +) } \arguments{ +\item{packages}{Package specification passed to \code{\link[reticulate:py_install]{reticulate::py_install()}}. +Default \code{"funz-fz"} installs the latest release from PyPI. To track +unreleased features, install the latest \code{main} branch directly from +GitHub with \code{"git+https://github.com/Funz/fz.git"}.} + \item{method}{Installation method. Either "auto", "virtualenv", or "conda".} \item{conda}{Path to conda executable. Only used when method is "conda".} @@ -29,5 +40,8 @@ fz_install() # Install in a conda environment fz_install(method = "conda") + +# Track the latest main branch on GitHub (unreleased features) +fz_install(packages = "git+https://github.com/Funz/fz.git") } } diff --git a/man/fzd.Rd b/man/fzd.Rd index 95cb7f1..52581d1 100644 --- a/man/fzd.Rd +++ b/man/fzd.Rd @@ -8,7 +8,7 @@ fzd( input_path, input_variables, model, - output_expression, + output_expression = NULL, algorithm, calculators = NULL, algorithm_options = NULL, @@ -16,21 +16,28 @@ fzd( ) } \arguments{ -\item{input_path}{Path to input file or directory.} +\item{input_path}{Path to input file or directory. Must be \code{NULL} when +\code{model} is an R function (see "Direct function model" below).} \item{input_variables}{Named list of variable range strings of the form \code{"[min;max]"}, e.g. \code{list(x = "[0;1]", y = "[-5;5]")}.} -\item{model}{Model definition dict or alias string.} +\item{model}{Model definition dict or alias string, or an R function (see +"Direct function model" below).} \item{output_expression}{Expression evaluated on the model outputs to produce the scalar quantity the algorithm optimizes or analyses, -e.g. \code{"result"} or \code{"out1 + 2 * out2"}.} +e.g. \code{"result"} or \code{"out1 + 2 * out2"}. May be \code{NULL} +only when \code{model} is a function, in which case the first output +value is used.} \item{algorithm}{Path to the algorithm Python file, e.g. \code{"algorithms/montecarlo_uniform.py"}.} -\item{calculators}{Calculator specification(s). Default \code{NULL}.} +\item{calculators}{Calculator specification(s). Default \code{NULL}. When +\code{model} is a function, this must be a single integer (default +\code{1L}), accepted for API compatibility (see "Direct function model" +below) — calls are always run sequentially regardless of its value.} \item{algorithm_options}{Algorithm options as a named list or semicolon-separated string, e.g. \code{"batch_sample_size=10;seed=42"}. @@ -47,6 +54,40 @@ Unlike \code{\link{fzr}} (which evaluates a fixed grid), \code{fzd} lets an algorithm adaptively choose which parameter combinations to evaluate, which is useful for sensitivity analysis, surrogate-model fitting, or optimization. } +\section{Direct function model}{ + +Instead of a file-based model, \code{model} can be an R function (this +requires the \code{main} branch of \code{fz} from GitHub, installed with +\code{fz_install(packages = "git+https://github.com/Funz/fz.git")} — this +mode is not available in released PyPI versions of \code{funz-fz} yet). In +this mode: +\itemize{ +\item \code{input_path} must be \code{NULL} — there are no input files. +\item \code{input_variables} names must match the function's arguments. +\item \code{output_expression} may be \code{NULL}; the value used is then +the first element of the function's return value (its return value +directly if scalar, the first element if a vector/list, or the first +entry's value if a named list). +\item \code{calculators} must be a single integer, accepted for API +compatibility but currently without effect: on the \code{fz} side, +function-model calls always run sequentially, one at a time in the +calling thread — never through a thread pool. This is required +because R functions are called back into the R session via +\code{reticulate}, which is only safe from the main thread; running +a Python-side thread pool (which always dispatches to a worker thread, +even with a single worker) would call the function from a thread +other than the main one and crash the R session. This safety fix +requires \href{https://github.com/Funz/fz/pull/73}{Funz/fz#73} on the +\code{fz} \code{main} branch (not yet in a PyPI release as of +2026-07-12); without it, direct function models crash regardless of +\code{calculators}. A value other than \code{1} emits a warning +noting that it has no effect. +\item each iteration's directory (\code{iterNNN/}) only contains a +\code{values.csv} of that iteration's function inputs/outputs, since +there is no file-based execution. +} +} + \examples{ \donttest{ if (fz_available()) { @@ -68,4 +109,21 @@ if (fz_available()) { ) } } + +\dontrun{ +# Direct function model (requires fz main branch from GitHub) +rosenbrock <- function(x, y) { + list(result = (1 - x)^2 + 100 * (y - x^2)^2) +} + +result <- fzd( + input_path = NULL, + input_variables = list(x = "[-2;2]", y = "[-2;2]"), + model = rosenbrock, + output_expression = "result", + algorithm = "examples/algorithms/bfgs.py", + calculators = 4L, + algorithm_options = list(max_iter = 20, tol = 1e-4) +) +} }