diff --git a/.Rbuildignore b/.Rbuildignore index d6d6cd5..f4fe100 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -7,3 +7,6 @@ ^LICENSE\.md$ ^codecov\.yml$ ^\.vscode$ +^\.claude$ +^cran-comments\.md$ +^tests/testthat/\.fz$ diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index b1ff4a3..2c6e490 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -18,11 +18,8 @@ jobs: fail-fast: false matrix: config: - - {os: macos-latest, r: 'release'} - - {os: windows-latest, r: 'release'} - - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} - - {os: ubuntu-latest, r: 'release'} - - {os: ubuntu-latest, r: 'oldrel-1'} + - {os: ubuntu-latest, r: 'release'} + - {os: macos-latest, r: 'release'} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} @@ -44,16 +41,6 @@ jobs: extra-packages: any::rcmdcheck needs: check - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Install fz Python package - run: | - python -m pip install --upgrade pip - pip install fz || echo "fz package installation failed, continuing anyway" - - uses: r-lib/actions/check-r-package@v2 with: upload-snapshots: true diff --git a/.github/workflows/test-with-python.yaml b/.github/workflows/test-with-python.yaml new file mode 100644 index 0000000..44387c4 --- /dev/null +++ b/.github/workflows/test-with-python.yaml @@ -0,0 +1,39 @@ +on: + push: + branches: [main, master, develop, 'claude/**'] + pull_request: + branches: [main, master, develop] + +name: test-with-python + +jobs: + test-with-python: + runs-on: ubuntu-latest + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + NOT_CRAN: "true" + FZ_MAX_RETRIES: "1" + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: 'release' + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::testthat + needs: check + + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install fz Python package + run: pip install funz-fz + + - name: Run tests + run: Rscript -e "testthat::test_local()" diff --git a/DESCRIPTION b/DESCRIPTION index b0c4c0a..a8890e1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,16 +1,22 @@ Package: fz Type: Package -Title: R Wrapper for Fz Core Functions -Version: 0.1.0 +Title: R Wrapper for the 'funz-fz' Parametric Simulation Framework +Version: 1.1 Authors@R: c( - person("Author", "Name", email = "author@example.com", role = c("aut", "cre")) + person("Yann", "Richet", email = "yann.richet@asnr.fr", + role = c("aut", "cre"), comment = c(ORCID = "0000-0002-5677-8458")) ) -Description: Provides R bindings to fz core functions using reticulate. - This package allows R users to access fz functionality directly from R. -License: MIT + file LICENSE +Description: Provides R bindings to the 'funz-fz' Python package using + 'reticulate'. The 'fz' framework wraps arbitrary simulation codes to run + parameter sweeps, design-of-experiments studies, and iterative + algorithm-driven analyses by substituting variable placeholders in text + input files and collecting outputs into data frames. Calculators can run + locally (shell), over SSH, or on 'SLURM' clusters. +License: BSD_3_clause + file LICENSE Encoding: UTF-8 -LazyData: true -RoxygenNote: 7.2.3 +Language: en-US +RoxygenNote: 7.3.3 +SystemRequirements: Python (>= 3.8), funz-fz Python package Imports: reticulate (>= 1.28) Suggests: @@ -20,7 +26,7 @@ Suggests: Config/reticulate: list( packages = list( - list(package = "fz") + list(package = "funz-fz") ) ) VignetteBuilder: knitr diff --git a/LICENSE b/LICENSE index 89697d7..f9943bf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,3 @@ -MIT License - -Copyright (c) 2025 Funz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +YEAR: 2025 +ORGANIZATION: Funz +COPYRIGHT HOLDER: Funz diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..1f912a4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,28 @@ +# BSD 3-Clause License + +Copyright (c) 2025, Funz + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/NAMESPACE b/NAMESPACE index 295721d..2ce9e92 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,28 @@ export(fz_available) export(fz_install) +export(fzc) +export(fzd) +export(fzi) +export(fzl) +export(fzo) +export(fzr) +export(get_config) +export(get_interpreter) +export(get_log_level) +export(install) +export(install_algorithm) +export(install_model) +export(list_installed_algorithms) +export(list_installed_models) +export(list_models) +export(print_config) +export(reload_config) +export(set_interpreter) +export(set_log_level) +export(uninstall) +export(uninstall_algorithm) +export(uninstall_model) importFrom(reticulate,import) importFrom(reticulate,py_install) importFrom(reticulate,py_module_available) diff --git a/NEWS.md b/NEWS.md index e8f57a3..a6d1d52 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,10 +1,31 @@ -# fz 0.1.0 +# fz 1.1 -## Initial Release +First release, aligned with funz-fz 1.1 on PyPI. -* Initial release of fz R package -* Provides R wrapper for fz Python package using reticulate -* Functions for installing and checking fz availability -* Comprehensive test suite with testthat -* CI/CD setup with GitHub Actions for R CMD check and CRAN checks -* Documentation and vignettes +## Core functions + +* `fzi(input_path, model)` — parse variable names and defaults from a template file +* `fzc(input_path, input_variables, model, output_dir)` — compile template by substituting variable values +* `fzo(output_path, model)` — read and parse output files +* `fzr(input_path, input_variables, model, ...)` — run full parametric study +* `fzl(models, calculators, check)` — list installed models and calculators +* `fzd(input_path, input_variables, model, output_expression, algorithm, ...)` — iterative algorithm-driven design of experiments + +## Model and algorithm management + +* `install_model(source, global)` / `install_algorithm(source, global)` — install from GitHub, URL, or local zip +* `uninstall_model(model_name, global)` / `uninstall_algorithm(algorithm_name, global)` — remove installed items +* `list_installed_models(global)` / `list_installed_algorithms(global)` — list what is installed +* `list_models()` — alias for `list_installed_models()` +* `install()` / `uninstall()` — generic aliases for model install/uninstall + +## Configuration + +* `get_interpreter()` / `set_interpreter(interpreter)` — get or set the formula interpreter (`"python"` or `"R"`) +* `get_log_level()` / `set_log_level(level)` — control logging verbosity +* `get_config()` / `print_config()` / `reload_config()` — inspect and reload `FZ_*` environment variable settings + +## Package helpers + +* `fz_install()` — install the `funz-fz` Python package via reticulate +* `fz_available()` — check whether the Python package is importable diff --git a/R/config.R b/R/config.R new file mode 100644 index 0000000..36ed1ee --- /dev/null +++ b/R/config.R @@ -0,0 +1,139 @@ +#' Get the Current Interpreter +#' +#' Returns the global formula interpreter used when evaluating formula +#' expressions inside template files (e.g. \code{"python"} or \code{"R"}). +#' +#' @return Character string naming the current interpreter. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' get_interpreter() # e.g. "python" +#' } +#' } +get_interpreter <- function() { + get_fz()$get_interpreter() +} + +#' Set the Interpreter +#' +#' Sets the global formula interpreter for evaluating expressions inside +#' template files. +#' +#' @param interpreter Character string: \code{"python"} or \code{"R"}. +#' +#' @return NULL (invisibly). Called for side effects. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' set_interpreter("R") # evaluate formulas with R +#' set_interpreter("python") # evaluate formulas with Python (default) +#' } +#' } +set_interpreter <- function(interpreter) { + get_fz()$set_interpreter(interpreter) +} + +#' Get the Current Log Level +#' +#' Returns the current logging verbosity level. +#' +#' @return A log-level value (use \code{as.character()} to convert to a string +#' such as \code{"DEBUG"}, \code{"INFO"}, \code{"WARNING"}, \code{"ERROR"}). +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' as.character(get_log_level()) # e.g. "WARNING" +#' } +#' } +get_log_level <- function() { + get_fz()$get_log_level() +} + +#' Set the Log Level +#' +#' Controls how much output fz emits during execution. +#' +#' @param level Character string or log-level object: one of \code{"DEBUG"}, +#' \code{"INFO"}, \code{"WARNING"}, \code{"ERROR"}. +#' +#' @return NULL (invisibly). Called for side effects. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' set_log_level("DEBUG") # maximum verbosity +#' set_log_level("WARNING") # default +#' set_log_level("ERROR") # errors only +#' } +#' } +set_log_level <- function(level) { + get_fz()$set_log_level(level) +} + +#' Get the Global Configuration +#' +#' Returns the fz configuration object. Values are controlled by environment +#' variables such as \code{FZ_LOG_LEVEL}, \code{FZ_MAX_WORKERS}, +#' \code{FZ_MAX_RETRIES}, and \code{FZ_SHELL_PATH}. +#' +#' @return A Python \code{Config} object. Access fields with \code{$}, e.g. +#' \code{get_config()$max_workers}. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' cfg <- get_config() +#' cfg$max_workers +#' cfg$max_retries +#' } +#' } +get_config <- function() { + get_fz()$get_config() +} + +#' Print the Current Configuration +#' +#' Prints all fz configuration values in a human-readable format, including +#' which settings come from environment variables. +#' +#' @return NULL (invisibly). Called for side effects. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' print_config() +#' } +#' } +print_config <- function() { + get_fz()$print_config() +} + +#' Reload Configuration from Environment Variables +#' +#' Re-reads all \code{FZ_*} environment variables and updates the live +#' configuration. Useful after changing environment variables within the +#' session. +#' +#' @return NULL (invisibly). Called for side effects. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' Sys.setenv(FZ_MAX_WORKERS = "8") +#' reload_config() +#' get_config()$max_workers # now 8 +#' } +#' } +reload_config <- function() { + get_fz()$reload_config() +} diff --git a/R/core-functions.R b/R/core-functions.R new file mode 100644 index 0000000..44a781e --- /dev/null +++ b/R/core-functions.R @@ -0,0 +1,257 @@ +#' fzi Function +#' +#' Parses input file(s) to find variables, formulas, and static objects. +#' +#' @param input_path Path to input file or directory. +#' @param model Model definition dict or alias string. +#' +#' @return Named list with variable names and their default values (or NULL). +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' # Write a template with two variables and their defaults +#' tf <- tempfile(fileext = ".txt") +#' writeLines(c("pressure = ${P~1.013}", "volume = ${V~22.4}"), tf) +#' +#' model <- list(varprefix = "$", delim = "{}", formulaprefix = "@", +#' commentline = "#") +#' +#' vars <- fzi(tf, model) +#' # vars$P == 1.013, vars$V == 22.4 +#' +#' # Using an installed model alias instead of an inline dict: +#' # vars <- fzi(tf, "PerfectGas") +#' } +#' } +fzi <- function(input_path, model) { + fz_module <- get_fz() + fz_module$fzi(input_path, model) +} + +#' fzc Function +#' +#' Compiles input file(s) by replacing variable placeholders with values. +#' Each unique combination of values is written to its own subdirectory inside +#' \code{output_dir}, named \code{var1=val1,var2=val2,...}. +#' +#' @param input_path Path to input file or directory. +#' @param input_variables Named list of variable values. Supply a vector of +#' values to generate a full-factorial grid across variables. +#' @param model Model definition dict or alias string. +#' @param output_dir Output directory for compiled files. Default \code{"output"}. +#' +#' @return NULL (invisibly). Called for side effects. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' tf <- tempfile(fileext = ".txt") +#' writeLines(c("P = ${P~1.013}", "V = ${V~22.4}"), tf) +#' +#' model <- list(varprefix = "$", delim = "{}", formulaprefix = "@", +#' commentline = "#") +#' out <- tempfile() +#' +#' # Single case: one compiled directory P=2,V=11.2 +#' fzc(tf, list(P = 2.0, V = 11.2), model, out) +#' +#' # Grid: 2 x 2 = 4 compiled directories +#' fzc(tf, list(P = c(1.0, 2.0), V = c(11.2, 22.4)), model, out) +#' } +#' } +fzc <- function(input_path, input_variables, model, output_dir = "output") { + fz_module <- get_fz() + fz_module$fzc(input_path, input_variables, model, output_dir) +} + +#' fzo Function +#' +#' Reads and parses output file(s) according to the model's output commands. +#' Each matched directory is processed independently; the results are combined +#' into a single list or data frame. +#' +#' @param output_path Path or glob pattern matching one or more output +#' directories. Subdirectories within matched directories are not processed. +#' @param model Model definition dict or alias string. +#' +#' @return Named list or data frame of parsed output values. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' # After running a simulation that wrote "result = 42" to output.txt: +#' out_dir <- "my_results/P=2,V=11.2" +#' +#' model <- list( +#' varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#", +#' output = list(result = "grep 'result' output.txt | cut -d= -f2") +#' ) +#' +#' values <- fzo(out_dir, model) +#' # values$result == "42" +#' +#' # Glob to read all cases at once: +#' # values <- fzo("my_results/*", model) +#' } +#' } +fzo <- function(output_path, model) { + fz_module <- get_fz() + fz_module$fzo(output_path, model) +} + +#' fzr Function +#' +#' Runs full parametric calculations over an input template. +#' fzr combines \code{\link{fzc}}, calculator execution, and +#' \code{\link{fzo}} into a single call: it compiles the template for every +#' parameter combination, runs the model via the calculator(s), and collects +#' all outputs into a data frame. +#' +#' @param input_path Path to input file or directory. +#' @param input_variables Named list of variable values (or vectors of values +#' for a full-factorial grid), or a data frame where each row is one case. +#' @param model Model definition dict or alias string. +#' @param results_dir Results directory. Default \code{"results"}. +#' @param calculators Calculator specification(s). Strings of the form +#' \code{"sh://"} run a local shell command; +#' \code{"ssh://user\@host"} runs over SSH; +#' \code{NULL} auto-detects installed calculators. +#' @param callbacks Optional named list of callback functions. +#' @param timeout Timeout in seconds per case. Default \code{NULL} (no timeout). +#' +#' @return Data frame (or named list) with one row per case and columns for +#' each input variable and output quantity. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' # Template: shell script that writes sum of x and y +#' tf <- tempfile(fileext = ".sh") +#' writeLines(c( +#' "#!/bin/sh", +#' "echo result = $(( ${x~0} + ${y~0} )) > output.txt" +#' ), tf) +#' +#' model <- list( +#' varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#", +#' output = list(result = "grep result output.txt | cut -d= -f2") +#' ) +#' +#' # Two values of x, one value of y -> 2 cases +#' results <- fzr(tf, list(x = c(1L, 2L), y = 3L), model, +#' calculators = "sh://bash input.sh") +#' # results is a data frame with columns x, y, result +#' +#' # Using an installed model alias: +#' # results <- fzr("input.txt", list(P = c(1, 2, 3)), "PerfectGas") +#' } +#' } +fzr <- function(input_path, input_variables, model, + results_dir = "results", calculators = NULL, + callbacks = NULL, timeout = NULL) { + fz_module <- get_fz() + fz_module$fzr(input_path, input_variables, model, + results_dir = results_dir, + calculators = calculators, + callbacks = callbacks, + timeout = timeout) +} + +#' fzl Function +#' +#' Lists installed models and available calculators. +#' +#' @param models Pattern to match models. Default \code{"*"} for all. +#' Accepts glob patterns (\code{"my*"}) or plain alias names. +#' @param calculators Pattern to match calculators. Default \code{"*"} for all. +#' @param check Logical; probe each calculator to verify it is reachable. +#' Default \code{FALSE}. +#' +#' @return Named list with two entries: +#' \describe{ +#' \item{models}{Named list of installed model definitions.} +#' \item{calculators}{Named list of available calculators.} +#' } +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' # List everything +#' info <- fzl() +#' names(info$models) # e.g. c("PerfectGas", "Moret") +#' names(info$calculators) # e.g. c("sh://") +#' +#' # Check only models whose name starts with "Perfect" +#' info <- fzl(models = "Perfect*") +#' +#' # Probe calculators to verify they are reachable +#' info <- fzl(check = TRUE) +#' } +#' } +fzl <- function(models = "*", calculators = "*", check = FALSE) { + fz_module <- get_fz() + fz_module$fzl(models = models, calculators = calculators, check = check) +} + +#' fzd Function +#' +#' Runs an iterative design of experiments driven by an algorithm. +#' 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. +#' +#' @param input_path Path to input file or directory. +#' @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 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"}. +#' @param algorithm Path to the algorithm Python file, e.g. +#' \code{"algorithms/montecarlo_uniform.py"}. +#' @param calculators Calculator specification(s). Default \code{NULL}. +#' @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"}. +#' +#' @return Named list with the analysis results produced by the algorithm. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' tf <- tempfile(fileext = ".txt") +#' writeLines(c("x = ${x~0}", "y = ${y~0}"), tf) +#' +#' model <- list( +#' varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#", +#' output = list(z = "grep z output.txt | cut -d= -f2") +#' ) +#' +#' # Run 30 Monte Carlo samples over x in [0,1] and y in [-5,5] +#' result <- fzd( +#' tf, +#' list(x = "[0;1]", y = "[-5;5]"), +#' model, +#' output_expression = "z", +#' algorithm = "algorithms/montecarlo_uniform.py", +#' algorithm_options = "batch_sample_size=10;max_iterations=3" +#' ) +#' } +#' } +fzd <- function(input_path, input_variables, model, output_expression, algorithm, + calculators = NULL, algorithm_options = NULL, + analysis_dir = "analysis") { + fz_module <- get_fz() + fz_module$fzd(input_path, input_variables, model, output_expression, algorithm, + calculators = calculators, + algorithm_options = algorithm_options, + analysis_dir = analysis_dir) +} diff --git a/R/fz-package.R b/R/fz-package.R index f6737ce..deb5ee4 100644 --- a/R/fz-package.R +++ b/R/fz-package.R @@ -6,21 +6,3 @@ ## usethis namespace: end NULL -#' fz: R Wrapper for Fz Core Functions -#' -#' This package provides R bindings to the fz Python package using reticulate. -#' It allows R users to access fz functionality directly from R. -#' -#' @section Getting Started: -#' -#' First, install the fz Python package: -#' -#' \code{fz_install()} -#' -#' Then check if it's available: -#' -#' \code{fz_available()} -#' -#' @docType package -#' @name fz-package -NULL diff --git a/R/install.R b/R/install.R index b04685a..9a2b725 100644 --- a/R/install.R +++ b/R/install.R @@ -22,7 +22,7 @@ #' fz_install(method = "conda") #' } fz_install <- function(method = "auto", conda = "auto", pip = TRUE, ...) { - reticulate::py_install("fz", method = method, conda = conda, pip = pip, ...) + reticulate::py_install("funz-fz", method = method, conda = conda, pip = pip, ...) } #' Check if fz Python Package is Available @@ -44,5 +44,199 @@ fz_install <- function(method = "auto", conda = "auto", pip = TRUE, ...) { #' } #' } fz_available <- function() { - reticulate::py_module_available("fz") + if (is.null(.pkg$fz_available)) { + .pkg$fz_available <- reticulate::py_module_available("fz") + } + .pkg$fz_available +} + +#' Install a Model +#' +#' Installs a model from a GitHub repository name, URL, or local zip file into +#' the user-level \code{~/.fz/models/} directory (or system-level when +#' \code{global = TRUE}). +#' +#' @param source GitHub name (e.g. \code{"Funz/Model-PerfectGas"}), URL, or +#' path to a local zip file. +#' @param global Logical; install system-wide instead of user-level. +#' Default \code{FALSE}. +#' +#' @return Named list with installation details (path, id, …). +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' install_model("Funz/Model-PerfectGas") +#' } +#' } +install_model <- function(source, global = FALSE) { + fz_module <- get_fz() + fz_module$install_model(source, global_install = global) +} + +#' Install an Algorithm +#' +#' Installs an algorithm from a GitHub repository name, URL, or local zip file +#' into the user-level \code{~/.fz/algorithms/} directory (or system-level when +#' \code{global = TRUE}). +#' +#' @param source GitHub name (e.g. \code{"Funz/Algorithm-MonteCarlo"}), URL, +#' or path to a local zip file. +#' @param global Logical; install system-wide instead of user-level. +#' Default \code{FALSE}. +#' +#' @return Named list with installation details (path, name, …). +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' install_algorithm("Funz/Algorithm-MonteCarlo") +#' } +#' } +install_algorithm <- function(source, global = FALSE) { + fz_module <- get_fz() + fz_module$install_algorithm(source, global_install = global) +} + +#' Uninstall a Model +#' +#' Removes a previously installed model from \code{~/.fz/models/}. +#' +#' @param model_name Name of the model to remove (e.g. \code{"PerfectGas"}). +#' @param global Logical; remove from system-level install. Default \code{FALSE}. +#' +#' @return \code{TRUE} if the model was removed, \code{FALSE} otherwise. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' uninstall_model("PerfectGas") +#' } +#' } +uninstall_model <- function(model_name, global = FALSE) { + fz_module <- get_fz() + fz_module$uninstall_model(model_name, global_uninstall = global) +} + +#' Uninstall an Algorithm +#' +#' Removes a previously installed algorithm from \code{~/.fz/algorithms/}. +#' +#' @param algorithm_name Name of the algorithm to remove. +#' @param global Logical; remove from system-level install. Default \code{FALSE}. +#' +#' @return \code{TRUE} if the algorithm was removed, \code{FALSE} otherwise. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' uninstall_algorithm("MonteCarlo") +#' } +#' } +uninstall_algorithm <- function(algorithm_name, global = FALSE) { + fz_module <- get_fz() + fz_module$uninstall_algorithm(algorithm_name, global_uninstall = global) +} + +#' List Installed Models +#' +#' Returns details of all models installed in \code{~/.fz/models/}. +#' +#' @param global Logical; list system-level installs. Default \code{FALSE}. +#' +#' @return Named list of installed model definitions. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' models <- list_installed_models() +#' names(models) # e.g. c("PerfectGas") +#' } +#' } +list_installed_models <- function(global = FALSE) { + fz_module <- get_fz() + fz_module$list_installed_models(global_list = global) +} + +#' List Installed Algorithms +#' +#' Returns details of all algorithms installed in \code{~/.fz/algorithms/}. +#' +#' @param global Logical; list system-level installs. Default \code{FALSE}. +#' +#' @return Named list of installed algorithm definitions. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' algos <- list_installed_algorithms() +#' names(algos) +#' } +#' } +list_installed_algorithms <- function(global = FALSE) { + fz_module <- get_fz() + fz_module$list_installed_algorithms(global_list = global) +} + +#' List Installed Models (alias) +#' +#' Alias for \code{\link{list_installed_models}}. +#' +#' @inheritParams list_installed_models +#' @return Named list of installed model definitions. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' names(list_models()) +#' } +#' } +list_models <- function(global = FALSE) { + list_installed_models(global) +} + +#' Install a Model or Algorithm (generic) +#' +#' Generic alias: installs a model from a GitHub name, URL, or local zip file. +#' Equivalent to \code{\link{install_model}}. +#' +#' @inheritParams install_model +#' @return Named list with installation details. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' install("Funz/Model-PerfectGas") +#' } +#' } +install <- function(source, global = FALSE) { + install_model(source, global) +} + +#' Uninstall a Model (generic) +#' +#' Generic alias: removes a model by name. +#' Equivalent to \code{\link{uninstall_model}}. +#' +#' @param model_name Name of the model to remove. +#' @inheritParams uninstall_model +#' @return \code{TRUE} if removed, \code{FALSE} otherwise. +#' @export +#' +#' @examples +#' \dontrun{ +#' if (fz_available()) { +#' uninstall("PerfectGas") +#' } +#' } +uninstall <- function(model_name, global = FALSE) { + uninstall_model(model_name, global) } diff --git a/R/zzz.R b/R/zzz.R index 2353ede..c8f1c27 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,17 +1,15 @@ -# Package-level variables -.fz <- NULL +.pkg <- new.env(parent = emptyenv()) +.pkg$fz <- NULL +.pkg$fz_available <- NULL .onLoad <- function(libname, pkgname) { - # Delay loading of Python module until first use - if (reticulate::py_module_available("fz")) { - .fz <<- reticulate::import("fz", delay_load = TRUE) - } + # Python is not initialised at load time — deferred to first use via get_fz(). } #' @keywords internal get_fz <- function() { - if (is.null(.fz)) { - if (!reticulate::py_module_available("fz")) { + if (is.null(.pkg$fz)) { + if (!fz_available()) { stop( "The 'fz' Python package is not available. ", "Install it with fz_install() or manually with: ", @@ -19,7 +17,7 @@ get_fz <- function() { call. = FALSE ) } - .fz <<- reticulate::import("fz", delay_load = TRUE) + .pkg$fz <- reticulate::import("fz", delay_load = TRUE) } - .fz + .pkg$fz } diff --git a/README.md b/README.md index f93c6e4..ff28daa 100644 --- a/README.md +++ b/README.md @@ -5,82 +5,138 @@ [![test-coverage](https://github.com/Funz/fz.R/workflows/test-coverage/badge.svg)](https://github.com/Funz/fz.R/actions) -R wrapper for fz core functions using reticulate. This package provides R bindings to the fz Python package, allowing R users to access fz functionality directly from R. +R wrapper for the [funz-fz](https://pypi.org/project/funz-fz/) Python package using reticulate. fz is a parametric scientific computing framework: it wraps simulation codes to run parameter sweeps, design of experiments, and iterative algorithm-driven studies. ## Installation -You can install the development version of fz from [GitHub](https://github.com/Funz/fz.R) with: - ```r # install.packages("devtools") devtools::install_github("Funz/fz.R") ``` -## Python Dependencies +## Python dependency -This package requires the `fz` Python package. You can install it using: +This package requires the `funz-fz` Python package. Install it via the helper: ```r library(fz) fz_install() ``` -Or manually with: +Or manually: ```r -reticulate::py_install("fz") +reticulate::py_install("funz-fz") ``` +## Core functions + +| Function | Purpose | +|---|---| +| `fzi(input_path, model)` | Parse variable names and defaults from a template file | +| `fzc(input_path, input_variables, model)` | Compile template — substitute variable values | +| `fzr(input_path, input_variables, model, ...)` | Run full parametric study | +| `fzo(output_path, model)` | Read and parse output files | +| `fzl(models, calculators, check)` | List installed models and calculators | +| `fzd(input_path, input_variables, model, output_expression, algorithm, ...)` | Algorithm-driven iterative DoE | + +The **model** argument is either a string alias (name of an installed model, e.g. `"PerfectGas"`) or an inline named list describing how variables are marked in the template and how outputs are extracted. + ## Usage -First, check if the fz Python package is available: +### 1 — List installed models ```r library(fz) -# Check if fz is available -if (fz_available()) { - message("fz is ready to use!") -} else { - message("Please install fz with fz_install()") -} +info <- fzl() +names(info$models) # e.g. c("PerfectGas") +names(info$calculators) # e.g. c("sh://") ``` -## System Requirements +### 2 — Parse variables from a template -- R (>= 3.6.0) -- Python (>= 3.7) -- reticulate package +```r +# Template file: input.txt +# pressure = ${P~1.013} +# volume = ${V~22.4} -## Development +model <- list( + varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#" +) + +vars <- fzi("input.txt", model) +# vars$P == 1.013 (default value) +# vars$V == 22.4 +``` -This package uses: +### 3 — Run a parametric study -- **reticulate** for Python integration -- **testthat** for unit testing -- **GitHub Actions** for continuous integration and CRAN checks -- **roxygen2** for documentation +```r +# fzr compiles the template for every combination, runs the model via the +# calculator, and collects all outputs into a data frame. + +model <- list( + varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#", + output = list(pressure = "grep 'pressure' output.txt | cut -d= -f2") +) + +results <- fzr( + "input.txt", + list(P = c(1.0, 2.0, 3.0), V = 22.4), # 3 cases + model, + calculators = "sh://bash run.sh" +) +# results is a data frame with columns P, V, pressure +``` -### Running Tests +### 4 — Algorithm-driven design of experiments ```r -devtools::test() +# fzd iteratively queries the model using an algorithm (e.g. Monte Carlo, +# surrogate-based optimisation). Input ranges use "[min;max]" strings. + +result <- fzd( + "input.txt", + list(P = "[1;5]", V = "[10;30]"), + model, + output_expression = "pressure", + algorithm = "algorithms/montecarlo_uniform.py", + algorithm_options = "batch_sample_size=10;max_iterations=5" +) ``` -### Running R CMD check +### 5 — Step-by-step workflow ```r -devtools::check() +# Step 1: inspect which variables the template exposes +vars <- fzi("input.txt", model) + +# Step 2: compile for specific values (no execution) +fzc("input.txt", list(P = 2.0, V = 11.2), model, output_dir = "compiled") + +# Step 3: read output files after running the simulator externally +values <- fzo("compiled/P=2,V=11.2", model) ``` -## Contributing +## System requirements -Contributions are welcome! Please feel free to submit a Pull Request. +- R >= 3.6.0 +- Python >= 3.8 +- reticulate package -## License +## Development -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +```r +devtools::test() # run tests +devtools::check() # R CMD check +``` + +## Contributing -## Issues +Contributions are welcome. Please open a Pull Request or file an issue at +. + +## License -Please report issues at https://github.com/Funz/fz.R/issues +MIT — see [LICENSE](LICENSE). diff --git a/cran-comments.md b/cran-comments.md new file mode 100644 index 0000000..c2c5829 --- /dev/null +++ b/cran-comments.md @@ -0,0 +1,28 @@ +## R CMD check results + +0 errors | 1 warning | 1 note + +- WARNING: 'qpdf' is needed for checks on size reduction of PDFs. + `qpdf` is not installed on this development machine; the package + contains no PDFs and this will not appear on CRAN infrastructure. + +- NOTE: unable to verify current time. + Caused by network restrictions on this machine; not a package issue. + +## Downstream dependencies + +This is a new submission with no existing reverse dependencies. + +## Notes on Python dependency + +This package wraps the `funz-fz` Python package via `reticulate`. Following +CRAN policy for Python-backed packages: + +- `SystemRequirements` declares `Python (>= 3.8)` and `funz-fz`. +- `Config/reticulate` declares the pip package so `reticulate` can offer + automatic installation. +- `fz_install()` provides a one-call helper for users to install the Python + dependency. +- `fz_available()` guards all examples and tests; nothing attempts a Python + connection at load time or during `R CMD check`. +- All examples are wrapped in `\dontrun{}`. diff --git a/inst/WORDLIST b/inst/WORDLIST new file mode 100644 index 0000000..ad2de11 --- /dev/null +++ b/inst/WORDLIST @@ -0,0 +1,18 @@ +CMD +conda +ORCID +PyPI +DoE +funz +fzc +fzd +fzi +fzl +fzo +fzr +Modelica +roxygen2 +SLURM +SSH +testthat +virtualenv diff --git a/man/fz-package.Rd b/man/fz-package.Rd new file mode 100644 index 0000000..ce1d0e6 --- /dev/null +++ b/man/fz-package.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/fz-package.R +\docType{package} +\name{fz-package} +\alias{fz} +\alias{fz-package} +\title{fz: R Wrapper for the 'funz-fz' Parametric Simulation Framework} +\description{ +Provides R bindings to the 'funz-fz' Python package using 'reticulate'. The 'fz' framework wraps arbitrary simulation codes to run parameter sweeps, design-of-experiments studies, and iterative algorithm-driven analyses by substituting variable placeholders in text input files and collecting outputs into data frames. Calculators can run locally (shell), over SSH, or on SLURM clusters. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://github.com/Funz/fz.R} + \item Report bugs at \url{https://github.com/Funz/fz.R/issues} +} + +} +\author{ +\strong{Maintainer}: Yann Richet \email{yann.richet@asnr.fr} (\href{https://orcid.org/0000-0002-5677-8458}{ORCID}) + +} +\keyword{internal} diff --git a/man/fz_available.Rd b/man/fz_available.Rd new file mode 100644 index 0000000..ecc6401 --- /dev/null +++ b/man/fz_available.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{fz_available} +\alias{fz_available} +\title{Check if fz Python Package is Available} +\usage{ +fz_available() +} +\value{ +Logical; TRUE if fz is available, FALSE otherwise. +} +\description{ +Checks whether the fz Python package is available in the current +Python environment. +} +\examples{ +\dontrun{ +if (fz_available()) { + message("fz is available!") +} else { + message("Please install fz with fz_install()") +} +} +} diff --git a/man/fz_install.Rd b/man/fz_install.Rd new file mode 100644 index 0000000..d941077 --- /dev/null +++ b/man/fz_install.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{fz_install} +\alias{fz_install} +\title{Install the fz Python Package} +\usage{ +fz_install(method = "auto", conda = "auto", pip = TRUE, ...) +} +\arguments{ +\item{method}{Installation method. Either "auto", "virtualenv", or "conda".} + +\item{conda}{Path to conda executable. Only used when method is "conda".} + +\item{pip}{Logical; use pip for installation? Default is TRUE.} + +\item{...}{Additional arguments passed to \code{\link[reticulate:py_install]{reticulate::py_install()}}.} +} +\value{ +NULL (invisibly). Called for side effects. +} +\description{ +This function installs the fz Python package into a virtual environment +or conda environment managed by reticulate. +} +\examples{ +\dontrun{ +# Install fz in a virtual environment +fz_install() + +# Install in a conda environment +fz_install(method = "conda") +} +} diff --git a/man/fzc.Rd b/man/fzc.Rd new file mode 100644 index 0000000..39f00fc --- /dev/null +++ b/man/fzc.Rd @@ -0,0 +1,44 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/core-functions.R +\name{fzc} +\alias{fzc} +\title{fzc Function} +\usage{ +fzc(input_path, input_variables, model, output_dir = "output") +} +\arguments{ +\item{input_path}{Path to input file or directory.} + +\item{input_variables}{Named list of variable values. Supply a vector of +values to generate a full-factorial grid across variables.} + +\item{model}{Model definition dict or alias string.} + +\item{output_dir}{Output directory for compiled files. Default \code{"output"}.} +} +\value{ +NULL (invisibly). Called for side effects. +} +\description{ +Compiles input file(s) by replacing variable placeholders with values. +Each unique combination of values is written to its own subdirectory inside +\code{output_dir}, named \code{var1=val1,var2=val2,...}. +} +\examples{ +\dontrun{ +if (fz_available()) { + tf <- tempfile(fileext = ".txt") + writeLines(c("P = ${P~1.013}", "V = ${V~22.4}"), tf) + + model <- list(varprefix = "$", delim = "{}", formulaprefix = "@", + commentline = "#") + out <- tempfile() + + # Single case: one compiled directory P=2,V=11.2 + fzc(tf, list(P = 2.0, V = 11.2), model, out) + + # Grid: 2 x 2 = 4 compiled directories + fzc(tf, list(P = c(1.0, 2.0), V = c(11.2, 22.4)), model, out) +} +} +} diff --git a/man/fzd.Rd b/man/fzd.Rd new file mode 100644 index 0000000..d978c14 --- /dev/null +++ b/man/fzd.Rd @@ -0,0 +1,72 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/core-functions.R +\name{fzd} +\alias{fzd} +\title{fzd Function} +\usage{ +fzd( + input_path, + input_variables, + model, + output_expression, + algorithm, + calculators = NULL, + algorithm_options = NULL, + analysis_dir = "analysis" +) +} +\arguments{ +\item{input_path}{Path to input file or directory.} + +\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{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"}.} + +\item{algorithm}{Path to the algorithm Python file, e.g. +\code{"algorithms/montecarlo_uniform.py"}.} + +\item{calculators}{Calculator specification(s). Default \code{NULL}.} + +\item{algorithm_options}{Algorithm options as a named list or +semicolon-separated string, e.g. \code{"batch_sample_size=10;seed=42"}. +Default \code{NULL}.} + +\item{analysis_dir}{Analysis directory. Default \code{"analysis"}.} +} +\value{ +Named list with the analysis results produced by the algorithm. +} +\description{ +Runs an iterative design of experiments driven by an algorithm. +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. +} +\examples{ +\dontrun{ +if (fz_available()) { + tf <- tempfile(fileext = ".txt") + writeLines(c("x = ${x~0}", "y = ${y~0}"), tf) + + model <- list( + varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#", + output = list(z = "grep z output.txt | cut -d= -f2") + ) + + # Run 30 Monte Carlo samples over x in [0,1] and y in [-5,5] + result <- fzd( + tf, + list(x = "[0;1]", y = "[-5;5]"), + model, + output_expression = "z", + algorithm = "algorithms/montecarlo_uniform.py", + algorithm_options = "batch_sample_size=10;max_iterations=3" + ) +} +} +} diff --git a/man/fzi.Rd b/man/fzi.Rd new file mode 100644 index 0000000..a49bfe5 --- /dev/null +++ b/man/fzi.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/core-functions.R +\name{fzi} +\alias{fzi} +\title{fzi Function} +\usage{ +fzi(input_path, model) +} +\arguments{ +\item{input_path}{Path to input file or directory.} + +\item{model}{Model definition dict or alias string.} +} +\value{ +Named list with variable names and their default values (or NULL). +} +\description{ +Parses input file(s) to find variables, formulas, and static objects. +} +\examples{ +\dontrun{ +if (fz_available()) { + # Write a template with two variables and their defaults + tf <- tempfile(fileext = ".txt") + writeLines(c("pressure = ${P~1.013}", "volume = ${V~22.4}"), tf) + + model <- list(varprefix = "$", delim = "{}", formulaprefix = "@", + commentline = "#") + + vars <- fzi(tf, model) + # vars$P == 1.013, vars$V == 22.4 + + # Using an installed model alias instead of an inline dict: + # vars <- fzi(tf, "PerfectGas") +} +} +} diff --git a/man/fzl.Rd b/man/fzl.Rd new file mode 100644 index 0000000..7bd18a1 --- /dev/null +++ b/man/fzl.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/core-functions.R +\name{fzl} +\alias{fzl} +\title{fzl Function} +\usage{ +fzl(models = "*", calculators = "*", check = FALSE) +} +\arguments{ +\item{models}{Pattern to match models. Default \code{"*"} for all. +Accepts glob patterns (\code{"my*"}) or plain alias names.} + +\item{calculators}{Pattern to match calculators. Default \code{"*"} for all.} + +\item{check}{Logical; probe each calculator to verify it is reachable. +Default \code{FALSE}.} +} +\value{ +Named list with two entries: +\describe{ +\item{models}{Named list of installed model definitions.} +\item{calculators}{Named list of available calculators.} +} +} +\description{ +Lists installed models and available calculators. +} +\examples{ +\dontrun{ +if (fz_available()) { + # List everything + info <- fzl() + names(info$models) # e.g. c("PerfectGas", "Moret") + names(info$calculators) # e.g. c("sh://") + + # Check only models whose name starts with "Perfect" + info <- fzl(models = "Perfect*") + + # Probe calculators to verify they are reachable + info <- fzl(check = TRUE) +} +} +} diff --git a/man/fzo.Rd b/man/fzo.Rd new file mode 100644 index 0000000..8fb7451 --- /dev/null +++ b/man/fzo.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/core-functions.R +\name{fzo} +\alias{fzo} +\title{fzo Function} +\usage{ +fzo(output_path, model) +} +\arguments{ +\item{output_path}{Path or glob pattern matching one or more output +directories. Subdirectories within matched directories are not processed.} + +\item{model}{Model definition dict or alias string.} +} +\value{ +Named list or data frame of parsed output values. +} +\description{ +Reads and parses output file(s) according to the model's output commands. +Each matched directory is processed independently; the results are combined +into a single list or data frame. +} +\examples{ +\dontrun{ +if (fz_available()) { + # After running a simulation that wrote "result = 42" to output.txt: + out_dir <- "my_results/P=2,V=11.2" + + model <- list( + varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#", + output = list(result = "grep 'result' output.txt | cut -d= -f2") + ) + + values <- fzo(out_dir, model) + # values$result == "42" + + # Glob to read all cases at once: + # values <- fzo("my_results/*", model) +} +} +} diff --git a/man/fzr.Rd b/man/fzr.Rd new file mode 100644 index 0000000..c379c61 --- /dev/null +++ b/man/fzr.Rd @@ -0,0 +1,71 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/core-functions.R +\name{fzr} +\alias{fzr} +\title{fzr Function} +\usage{ +fzr( + input_path, + input_variables, + model, + results_dir = "results", + calculators = NULL, + callbacks = NULL, + timeout = NULL +) +} +\arguments{ +\item{input_path}{Path to input file or directory.} + +\item{input_variables}{Named list of variable values (or vectors of values +for a full-factorial grid), or a data frame where each row is one case.} + +\item{model}{Model definition dict or alias string.} + +\item{results_dir}{Results directory. Default \code{"results"}.} + +\item{calculators}{Calculator specification(s). Strings of the form +\code{"sh://"} run a local shell command; +\code{"ssh://user\@host"} runs over SSH; +\code{NULL} auto-detects installed calculators.} + +\item{callbacks}{Optional named list of callback functions.} + +\item{timeout}{Timeout in seconds per case. Default \code{NULL} (no timeout).} +} +\value{ +Data frame (or named list) with one row per case and columns for +each input variable and output quantity. +} +\description{ +Runs full parametric calculations over an input template. +fzr combines \code{\link{fzc}}, calculator execution, and +\code{\link{fzo}} into a single call: it compiles the template for every +parameter combination, runs the model via the calculator(s), and collects +all outputs into a data frame. +} +\examples{ +\dontrun{ +if (fz_available()) { + # Template: shell script that writes sum of x and y + tf <- tempfile(fileext = ".sh") + writeLines(c( + "#!/bin/sh", + "echo result = $(( ${x~0} + ${y~0} )) > output.txt" + ), tf) + + model <- list( + varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#", + output = list(result = "grep result output.txt | cut -d= -f2") + ) + + # Two values of x, one value of y -> 2 cases + results <- fzr(tf, list(x = c(1L, 2L), y = 3L), model, + calculators = "sh://bash input.sh") + # results is a data frame with columns x, y, result + + # Using an installed model alias: + # results <- fzr("input.txt", list(P = c(1, 2, 3)), "PerfectGas") +} +} +} diff --git a/man/get_config.Rd b/man/get_config.Rd new file mode 100644 index 0000000..771fa61 --- /dev/null +++ b/man/get_config.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/config.R +\name{get_config} +\alias{get_config} +\title{Get the Global Configuration} +\usage{ +get_config() +} +\value{ +A Python \code{Config} object. Access fields with \code{$}, e.g. +\code{get_config()$max_workers}. +} +\description{ +Returns the fz configuration object. Values are controlled by environment +variables such as \code{FZ_LOG_LEVEL}, \code{FZ_MAX_WORKERS}, +\code{FZ_MAX_RETRIES}, and \code{FZ_SHELL_PATH}. +} +\examples{ +\dontrun{ +if (fz_available()) { + cfg <- get_config() + cfg$max_workers + cfg$max_retries +} +} +} diff --git a/man/get_interpreter.Rd b/man/get_interpreter.Rd new file mode 100644 index 0000000..f001ee3 --- /dev/null +++ b/man/get_interpreter.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/config.R +\name{get_interpreter} +\alias{get_interpreter} +\title{Get the Current Interpreter} +\usage{ +get_interpreter() +} +\value{ +Character string naming the current interpreter. +} +\description{ +Returns the global formula interpreter used when evaluating formula +expressions inside template files (e.g. \code{"python"} or \code{"R"}). +} +\examples{ +\dontrun{ +if (fz_available()) { + get_interpreter() # e.g. "python" +} +} +} diff --git a/man/get_log_level.Rd b/man/get_log_level.Rd new file mode 100644 index 0000000..b7b5644 --- /dev/null +++ b/man/get_log_level.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/config.R +\name{get_log_level} +\alias{get_log_level} +\title{Get the Current Log Level} +\usage{ +get_log_level() +} +\value{ +A log-level value (use \code{as.character()} to convert to a string +such as \code{"DEBUG"}, \code{"INFO"}, \code{"WARNING"}, \code{"ERROR"}). +} +\description{ +Returns the current logging verbosity level. +} +\examples{ +\dontrun{ +if (fz_available()) { + as.character(get_log_level()) # e.g. "WARNING" +} +} +} diff --git a/man/install.Rd b/man/install.Rd new file mode 100644 index 0000000..ba20655 --- /dev/null +++ b/man/install.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{install} +\alias{install} +\title{Install a Model or Algorithm (generic)} +\usage{ +install(source, global = FALSE) +} +\arguments{ +\item{source}{GitHub name (e.g. \code{"Funz/Model-PerfectGas"}), URL, or +path to a local zip file.} + +\item{global}{Logical; install system-wide instead of user-level. +Default \code{FALSE}.} +} +\value{ +Named list with installation details. +} +\description{ +Generic alias: installs a model from a GitHub name, URL, or local zip file. +Equivalent to \code{\link{install_model}}. +} +\examples{ +\dontrun{ +if (fz_available()) { + install("Funz/Model-PerfectGas") +} +} +} diff --git a/man/install_algorithm.Rd b/man/install_algorithm.Rd new file mode 100644 index 0000000..771560d --- /dev/null +++ b/man/install_algorithm.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{install_algorithm} +\alias{install_algorithm} +\title{Install an Algorithm} +\usage{ +install_algorithm(source, global = FALSE) +} +\arguments{ +\item{source}{GitHub name (e.g. \code{"Funz/Algorithm-MonteCarlo"}), URL, +or path to a local zip file.} + +\item{global}{Logical; install system-wide instead of user-level. +Default \code{FALSE}.} +} +\value{ +Named list with installation details (path, name, …). +} +\description{ +Installs an algorithm from a GitHub repository name, URL, or local zip file +into the user-level \code{~/.fz/algorithms/} directory (or system-level when +\code{global = TRUE}). +} +\examples{ +\dontrun{ +if (fz_available()) { + install_algorithm("Funz/Algorithm-MonteCarlo") +} +} +} diff --git a/man/install_model.Rd b/man/install_model.Rd new file mode 100644 index 0000000..d1f2fc1 --- /dev/null +++ b/man/install_model.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{install_model} +\alias{install_model} +\title{Install a Model} +\usage{ +install_model(source, global = FALSE) +} +\arguments{ +\item{source}{GitHub name (e.g. \code{"Funz/Model-PerfectGas"}), URL, or +path to a local zip file.} + +\item{global}{Logical; install system-wide instead of user-level. +Default \code{FALSE}.} +} +\value{ +Named list with installation details (path, id, …). +} +\description{ +Installs a model from a GitHub repository name, URL, or local zip file into +the user-level \code{~/.fz/models/} directory (or system-level when +\code{global = TRUE}). +} +\examples{ +\dontrun{ +if (fz_available()) { + install_model("Funz/Model-PerfectGas") +} +} +} diff --git a/man/list_installed_algorithms.Rd b/man/list_installed_algorithms.Rd new file mode 100644 index 0000000..525b40d --- /dev/null +++ b/man/list_installed_algorithms.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{list_installed_algorithms} +\alias{list_installed_algorithms} +\title{List Installed Algorithms} +\usage{ +list_installed_algorithms(global = FALSE) +} +\arguments{ +\item{global}{Logical; list system-level installs. Default \code{FALSE}.} +} +\value{ +Named list of installed algorithm definitions. +} +\description{ +Returns details of all algorithms installed in \code{~/.fz/algorithms/}. +} +\examples{ +\dontrun{ +if (fz_available()) { + algos <- list_installed_algorithms() + names(algos) +} +} +} diff --git a/man/list_installed_models.Rd b/man/list_installed_models.Rd new file mode 100644 index 0000000..158e35d --- /dev/null +++ b/man/list_installed_models.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{list_installed_models} +\alias{list_installed_models} +\title{List Installed Models} +\usage{ +list_installed_models(global = FALSE) +} +\arguments{ +\item{global}{Logical; list system-level installs. Default \code{FALSE}.} +} +\value{ +Named list of installed model definitions. +} +\description{ +Returns details of all models installed in \code{~/.fz/models/}. +} +\examples{ +\dontrun{ +if (fz_available()) { + models <- list_installed_models() + names(models) # e.g. c("PerfectGas") +} +} +} diff --git a/man/list_models.Rd b/man/list_models.Rd new file mode 100644 index 0000000..08ffd12 --- /dev/null +++ b/man/list_models.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{list_models} +\alias{list_models} +\title{List Installed Models (alias)} +\usage{ +list_models(global = FALSE) +} +\arguments{ +\item{global}{Logical; list system-level installs. Default \code{FALSE}.} +} +\value{ +Named list of installed model definitions. +} +\description{ +Alias for \code{\link{list_installed_models}}. +} +\examples{ +\dontrun{ +if (fz_available()) { + names(list_models()) +} +} +} diff --git a/man/print_config.Rd b/man/print_config.Rd new file mode 100644 index 0000000..dca102b --- /dev/null +++ b/man/print_config.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/config.R +\name{print_config} +\alias{print_config} +\title{Print the Current Configuration} +\usage{ +print_config() +} +\value{ +NULL (invisibly). Called for side effects. +} +\description{ +Prints all fz configuration values in a human-readable format, including +which settings come from environment variables. +} +\examples{ +\dontrun{ +if (fz_available()) { + print_config() +} +} +} diff --git a/man/reload_config.Rd b/man/reload_config.Rd new file mode 100644 index 0000000..3666d62 --- /dev/null +++ b/man/reload_config.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/config.R +\name{reload_config} +\alias{reload_config} +\title{Reload Configuration from Environment Variables} +\usage{ +reload_config() +} +\value{ +NULL (invisibly). Called for side effects. +} +\description{ +Re-reads all \code{FZ_*} environment variables and updates the live +configuration. Useful after changing environment variables within the +session. +} +\examples{ +\dontrun{ +if (fz_available()) { + Sys.setenv(FZ_MAX_WORKERS = "8") + reload_config() + get_config()$max_workers # now 8 +} +} +} diff --git a/man/set_interpreter.Rd b/man/set_interpreter.Rd new file mode 100644 index 0000000..ffd6f0f --- /dev/null +++ b/man/set_interpreter.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/config.R +\name{set_interpreter} +\alias{set_interpreter} +\title{Set the Interpreter} +\usage{ +set_interpreter(interpreter) +} +\arguments{ +\item{interpreter}{Character string: \code{"python"} or \code{"R"}.} +} +\value{ +NULL (invisibly). Called for side effects. +} +\description{ +Sets the global formula interpreter for evaluating expressions inside +template files. +} +\examples{ +\dontrun{ +if (fz_available()) { + set_interpreter("R") # evaluate formulas with R + set_interpreter("python") # evaluate formulas with Python (default) +} +} +} diff --git a/man/set_log_level.Rd b/man/set_log_level.Rd new file mode 100644 index 0000000..63e1e25 --- /dev/null +++ b/man/set_log_level.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/config.R +\name{set_log_level} +\alias{set_log_level} +\title{Set the Log Level} +\usage{ +set_log_level(level) +} +\arguments{ +\item{level}{Character string or log-level object: one of \code{"DEBUG"}, +\code{"INFO"}, \code{"WARNING"}, \code{"ERROR"}.} +} +\value{ +NULL (invisibly). Called for side effects. +} +\description{ +Controls how much output fz emits during execution. +} +\examples{ +\dontrun{ +if (fz_available()) { + set_log_level("DEBUG") # maximum verbosity + set_log_level("WARNING") # default + set_log_level("ERROR") # errors only +} +} +} diff --git a/man/uninstall.Rd b/man/uninstall.Rd new file mode 100644 index 0000000..4c68d7b --- /dev/null +++ b/man/uninstall.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{uninstall} +\alias{uninstall} +\title{Uninstall a Model (generic)} +\usage{ +uninstall(model_name, global = FALSE) +} +\arguments{ +\item{model_name}{Name of the model to remove.} + +\item{global}{Logical; remove from system-level install. Default \code{FALSE}.} +} +\value{ +\code{TRUE} if removed, \code{FALSE} otherwise. +} +\description{ +Generic alias: removes a model by name. +Equivalent to \code{\link{uninstall_model}}. +} +\examples{ +\dontrun{ +if (fz_available()) { + uninstall("PerfectGas") +} +} +} diff --git a/man/uninstall_algorithm.Rd b/man/uninstall_algorithm.Rd new file mode 100644 index 0000000..8eda084 --- /dev/null +++ b/man/uninstall_algorithm.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{uninstall_algorithm} +\alias{uninstall_algorithm} +\title{Uninstall an Algorithm} +\usage{ +uninstall_algorithm(algorithm_name, global = FALSE) +} +\arguments{ +\item{algorithm_name}{Name of the algorithm to remove.} + +\item{global}{Logical; remove from system-level install. Default \code{FALSE}.} +} +\value{ +\code{TRUE} if the algorithm was removed, \code{FALSE} otherwise. +} +\description{ +Removes a previously installed algorithm from \code{~/.fz/algorithms/}. +} +\examples{ +\dontrun{ +if (fz_available()) { + uninstall_algorithm("MonteCarlo") +} +} +} diff --git a/man/uninstall_model.Rd b/man/uninstall_model.Rd new file mode 100644 index 0000000..769b271 --- /dev/null +++ b/man/uninstall_model.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{uninstall_model} +\alias{uninstall_model} +\title{Uninstall a Model} +\usage{ +uninstall_model(model_name, global = FALSE) +} +\arguments{ +\item{model_name}{Name of the model to remove (e.g. \code{"PerfectGas"}).} + +\item{global}{Logical; remove from system-level install. Default \code{FALSE}.} +} +\value{ +\code{TRUE} if the model was removed, \code{FALSE} otherwise. +} +\description{ +Removes a previously installed model from \code{~/.fz/models/}. +} +\examples{ +\dontrun{ +if (fz_available()) { + uninstall_model("PerfectGas") +} +} +} diff --git a/tests/testthat/helper-modelica.R b/tests/testthat/helper-modelica.R new file mode 100644 index 0000000..6322186 --- /dev/null +++ b/tests/testthat/helper-modelica.R @@ -0,0 +1,174 @@ +# Test helpers for Modelica examples + +#' Check if we're running on CI +#' +#' @return Logical indicating if tests are running on CI +skip_on_ci <- function() { + ci <- Sys.getenv("CI", "false") + if (tolower(ci) == "true") { + testthat::skip("Skipping on CI") + } +} + +#' Create a mock Modelica model configuration +#' +#' @param model_name Name of the model +#' @return List with model configuration +mock_modelica_config <- function(model_name = "TestModel") { + list( + model = "modelica", + model_name = model_name, + input_vars = list( + param1 = c(0, 1), + param2 = c(0, 10) + ), + output_vars = c("result1", "result2") + ) +} + +#' Create a sample parameter grid for testing +#' +#' @param n_points Number of points in the grid +#' @return Data frame with parameter combinations +create_parameter_grid <- function(n_points = 10) { + data.frame( + mass = seq(0.5, 2.0, length.out = n_points), + stiffness = seq(100, 1000, length.out = n_points), + damping = seq(0.1, 1.0, length.out = n_points) + ) +} + +#' Example Modelica model: Bouncing Ball +#' +#' @return Character string with model description +example_bouncing_ball <- function() { + list( + name = "BouncingBall", + description = "Simple bouncing ball model with gravity", + inputs = c("h0", "v0", "e"), # height, velocity, restitution + outputs = c("h_max", "t_ground", "bounces"), + input_ranges = list( + h0 = c(0.1, 10.0), # initial height (m) + v0 = c(-5.0, 5.0), # initial velocity (m/s) + e = c(0.5, 0.95) # coefficient of restitution + ) + ) +} + +#' Example Modelica model: Spring-Mass-Damper +#' +#' @return List with model description +example_spring_mass_damper <- function() { + list( + name = "SpringMassDamper", + description = "Spring-mass-damper oscillator", + inputs = c("m", "k", "c", "F0"), # mass, stiffness, damping, force + outputs = c("x_max", "settling_time", "overshoot"), + input_ranges = list( + m = c(0.5, 5.0), # mass (kg) + k = c(100, 10000), # stiffness (N/m) + c = c(1, 100), # damping (N.s/m) + F0 = c(10, 1000) # initial force (N) + ) + ) +} + +#' Example Modelica model: Heat Exchanger +#' +#' @return List with model description +example_heat_exchanger <- function() { + list( + name = "HeatExchanger", + description = "Counter-flow heat exchanger", + inputs = c("mdot_hot", "mdot_cold", "T_hot_in", "T_cold_in"), + outputs = c("T_hot_out", "T_cold_out", "effectiveness", "Q_total"), + input_ranges = list( + mdot_hot = c(0.1, 2.0), # hot fluid flow rate (kg/s) + mdot_cold = c(0.1, 2.0), # cold fluid flow rate (kg/s) + T_hot_in = c(60, 100), # hot inlet temp (C) + T_cold_in = c(10, 30) # cold inlet temp (C) + ) + ) +} + +#' Create a design of experiments configuration +#' +#' @param design_type Type of design ("LatinHypercube", "FullFactorial", etc.) +#' @param n_samples Number of samples +#' @param model Model configuration from example functions +#' @return List with DoE configuration +create_doe_config <- function(design_type = "LatinHypercube", + n_samples = 20, + model = example_bouncing_ball()) { + list( + design = design_type, + n = n_samples, + model_name = model$name, + input = model$input_ranges, + output = model$outputs + ) +} + +#' Create an optimization configuration +#' +#' @param objective Objective ("minimize" or "maximize") +#' @param objective_var Variable to optimize +#' @param model Model configuration +#' @return List with optimization configuration +create_optimization_config <- function(objective = "minimize", + objective_var = NULL, + model = example_spring_mass_damper()) { + if (is.null(objective_var)) { + objective_var <- model$outputs[1] + } + + list( + objective = objective, + objective_var = objective_var, + model_name = model$name, + input = model$input_ranges, + output = model$outputs, + algorithm = "GradientDescent", + max_iterations = 100, + tolerance = 1e-6 + ) +} + +#' Validate fz result structure +#' +#' @param result Result from fz function +#' @return Logical indicating if structure is valid +validate_fz_result <- function(result) { + # Expected structure of fz results + # This is a placeholder - actual structure depends on fz implementation + if (is.null(result)) return(FALSE) + + # Basic checks + checks <- c( + is.list(result) || is.data.frame(result), + length(result) > 0 + ) + + all(checks) +} + +#' Pretty print a parameter configuration +#' +#' @param config Configuration list +#' @return Invisible NULL (prints to console) +print_config <- function(config) { + cat("Configuration:\n") + cat("=============\n") + for (name in names(config)) { + value <- config[[name]] + if (is.list(value)) { + cat(sprintf("%s:\n", name)) + for (subname in names(value)) { + cat(sprintf(" %s: %s\n", subname, toString(value[[subname]]))) + } + } else { + cat(sprintf("%s: %s\n", name, toString(value))) + } + } + invisible(NULL) +} diff --git a/tests/testthat/models/BouncingBall.mo b/tests/testthat/models/BouncingBall.mo new file mode 100644 index 0000000..bab5f17 --- /dev/null +++ b/tests/testthat/models/BouncingBall.mo @@ -0,0 +1,38 @@ +model BouncingBall + "Simple bouncing ball model with gravity and ground contact" + + parameter Real h0 = 1.0 "Initial height (m)"; + parameter Real v0 = 0.0 "Initial velocity (m/s)"; + parameter Real e = 0.7 "Coefficient of restitution"; + parameter Real g = 9.81 "Gravity acceleration (m/s2)"; + + Real h(start=h0) "Height above ground (m)"; + Real v(start=v0) "Vertical velocity (m/s)"; + + output Real h_max "Maximum bounce height (m)"; + output Real t_ground "Time to first ground contact (s)"; + output Integer bounces "Number of bounces"; + +equation + der(h) = v; + der(v) = -g; + + when h <= 0 then + reinit(v, -e * pre(v)); + bounces = pre(bounces) + 1; + end when; + + h_max = max(h, pre(h_max)); + + when h <= 0 and pre(h) > 0 then + t_ground = time; + end when; + +initial equation + h = h0; + v = v0; + bounces = 0; + h_max = h0; + t_ground = 0; + +end BouncingBall; diff --git a/tests/testthat/models/Branin.mo b/tests/testthat/models/Branin.mo new file mode 100644 index 0000000..adc8f10 --- /dev/null +++ b/tests/testthat/models/Branin.mo @@ -0,0 +1,20 @@ +model Branin + "Branin test function - common optimization benchmark" + + parameter Real x1 = 0.0 "First input variable"; + parameter Real x2 = 0.0 "Second input variable"; + + output Real y "Branin function output"; + +protected + constant Real a = 1.0; + constant Real b = 5.1 / (4 * 3.14159^2); + constant Real c = 5.0 / 3.14159; + constant Real r = 6.0; + constant Real s = 10.0; + constant Real t = 1.0 / (8 * 3.14159); + +equation + y = a * (x2 - b * x1^2 + c * x1 - r)^2 + s * (1 - t) * cos(x1) + s; + +end Branin; diff --git a/tests/testthat/models/SpringMassDamper.mo b/tests/testthat/models/SpringMassDamper.mo new file mode 100644 index 0000000..ec29aa4 --- /dev/null +++ b/tests/testthat/models/SpringMassDamper.mo @@ -0,0 +1,53 @@ +model SpringMassDamper + "Spring-mass-damper oscillator system" + + parameter Real m = 1.0 "Mass (kg)"; + parameter Real k = 100.0 "Spring stiffness (N/m)"; + parameter Real c = 1.0 "Damping coefficient (N.s/m)"; + parameter Real F0 = 10.0 "Initial force (N)"; + parameter Real x0 = 0.0 "Initial displacement (m)"; + parameter Real v0 = 0.0 "Initial velocity (m/s)"; + + Real x(start=x0) "Displacement (m)"; + Real v(start=v0) "Velocity (m/s)"; + Real F "Applied force (N)"; + + output Real x_max "Maximum displacement (m)"; + output Real settling_time "Settling time (s)"; + output Real overshoot "Overshoot (%)"; + +protected + Real x_steady; + Boolean settled(start=false); + +equation + // Force applied at t=0, then released + F = if time < 0.01 then F0 else 0; + + // Equations of motion + m * der(v) = F - k * x - c * v; + der(x) = v; + + // Steady state displacement (for overshoot calculation) + x_steady = 0.0; + + // Track maximum displacement + x_max = max(abs(x), pre(x_max)); + + // Overshoot calculation + overshoot = if x_steady > 0 then + (x_max - x_steady) / x_steady * 100 else 0; + + // Settling time (2% criterion) + when abs(x) < 0.02 * x_max and not settled then + settled = true; + settling_time = time; + end when; + +initial equation + x = x0; + v = v0; + x_max = 0; + settling_time = 0; + +end SpringMassDamper; diff --git a/tests/testthat/setup.R b/tests/testthat/setup.R new file mode 100644 index 0000000..8b71575 --- /dev/null +++ b/tests/testthat/setup.R @@ -0,0 +1,3 @@ +# Cap calculator retries so failed tests abort quickly rather than retrying 5x. +# Must be set before fz Python module is first imported. +Sys.setenv(FZ_MAX_RETRIES = "1") diff --git a/tests/testthat/test-core-functions.R b/tests/testthat/test-core-functions.R new file mode 100644 index 0000000..10b8a5c --- /dev/null +++ b/tests/testthat/test-core-functions.R @@ -0,0 +1,31 @@ +test_that("fzi function exists and is callable", { + expect_true(is.function(fzi)) +}) + +test_that("fzc function exists and is callable", { + expect_true(is.function(fzc)) +}) + +test_that("fzo function exists and is callable", { + expect_true(is.function(fzo)) +}) + +test_that("fzr function exists and is callable", { + expect_true(is.function(fzr)) +}) + +test_that("fzl function exists and is callable", { + expect_true(is.function(fzl)) +}) + +test_that("fzd function exists and is callable", { + expect_true(is.function(fzd)) +}) + +test_that("core functions fail gracefully when fz not installed", { + skip_on_cran() + skip_if(fz_available(), "fz is installed, skipping unavailability test") + + expect_error(fzl(), "fz.*not available") + expect_error(fzi("f", list()), "fz.*not available") +}) diff --git a/tests/testthat/test-install.R b/tests/testthat/test-install.R index f2d32ac..374fee4 100644 --- a/tests/testthat/test-install.R +++ b/tests/testthat/test-install.R @@ -1,4 +1,5 @@ test_that("fz_available returns logical", { + skip_on_cran() result <- fz_available() expect_type(result, "logical") expect_length(result, 1) diff --git a/tests/testthat/test-modelica-examples.R b/tests/testthat/test-modelica-examples.R new file mode 100644 index 0000000..80387dc --- /dev/null +++ b/tests/testthat/test-modelica-examples.R @@ -0,0 +1,188 @@ +# Integration tests exercising the real funz-fz 1.x Python API. +# +# All Python-dependent tests are guarded with skip_on_cran() so CRAN checks +# never initialise Python. The fzr test additionally caps FZ_MAX_RETRIES=1 +# to avoid burning time on retry loops. + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +simple_model <- function(output_cmd = "cat output.txt 2>/dev/null || echo ''") { + list( + varprefix = "$", + delim = "{}", + formulaprefix = "@", + commentline = "#", + output = list(result = output_cmd) + ) +} + +make_template <- function(lines, suffix = ".txt") { + tf <- tempfile(fileext = suffix) + writeLines(lines, tf) + tf +} + +# --------------------------------------------------------------------------- +# fzl -- no files needed +# --------------------------------------------------------------------------- + +test_that("fzl() lists models and calculators", { + skip_on_cran() + skip_if_not(fz_available(), "fz Python package not available") + + result <- fzl() + + expect_true(is.list(result)) + expect_true(all(c("models", "calculators") %in% names(result))) + expect_true(is.list(result$models)) + expect_true(is.list(result$calculators)) +}) + +# --------------------------------------------------------------------------- +# fzi -- parse variables from a template (no execution) +# --------------------------------------------------------------------------- + +test_that("fzi() parses variable names and defaults from a template", { + skip_on_cran() + skip_if_not(fz_available(), "fz Python package not available") + + tf <- make_template(c( + "# Perfect Gas parameters", + "P = ${P~1.013}", + "V = ${V~22.4}", + "n = ${n~1.0}" + )) + + model <- list(varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#") + result <- fzi(tf, model) + + expect_true(is.list(result)) + expect_true("P" %in% names(result)) + expect_true("V" %in% names(result)) + expect_true("n" %in% names(result)) + expect_equal(as.numeric(result$P), 1.013, tolerance = 1e-6) + expect_equal(as.numeric(result$V), 22.4, tolerance = 1e-6) +}) + +# --------------------------------------------------------------------------- +# fzc -- compile template with explicit values (no execution) +# --------------------------------------------------------------------------- + +test_that("fzc() compiles template for a single parameter set", { + skip_on_cran() + skip_if_not(fz_available(), "fz Python package not available") + + tf <- make_template(c("P = ${P~1.013}", "V = ${V~22.4}")) + model <- list(varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#") + out_dir <- file.path(tempdir(), paste0("fzc_single_", Sys.getpid())) + + expect_no_error(fzc(tf, list(P = 2.0, V = 11.2), model, out_dir)) + + compiled_dirs <- list.dirs(out_dir, recursive = FALSE) + expect_true(length(compiled_dirs) >= 1) + + compiled_file <- file.path(compiled_dirs[[1]], basename(tf)) + if (file.exists(compiled_file)) { + content <- readLines(compiled_file, warn = FALSE) + expect_false(any(grepl("\\$\\{", content)), info = "placeholders should be replaced") + expect_true(any(grepl("2", content)), info = "substituted value should appear") + } +}) + +test_that("fzc() compiles template for multiple values (grid)", { + skip_on_cran() + skip_if_not(fz_available(), "fz Python package not available") + + tf <- make_template(c("x = ${x~0}", "y = ${y~0}")) + model <- list(varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#") + out_dir <- file.path(tempdir(), paste0("fzc_grid_", Sys.getpid())) + + expect_no_error(fzc(tf, list(x = c(1.0, 2.0), y = c(10.0, 20.0)), model, out_dir)) + + compiled_dirs <- list.dirs(out_dir, recursive = FALSE) + expect_true(length(compiled_dirs) == 4, + info = paste("expected 4 compiled dirs, got", length(compiled_dirs))) +}) + +# --------------------------------------------------------------------------- +# fzr -- run parametric study (requires sh:// calculator) +# --------------------------------------------------------------------------- + +test_that("fzr() runs a parametric study with an inline shell model", { + skip_on_cran() + skip_if_not(fz_available(), "fz Python package not available") + + calc <- "sh://python3 -c \"x=${x~0}; y=${y~0}; open('output.txt','w').write(f'result = {x+y}\\n')\"" + + tf <- make_template(c("x = ${x~0}", "y = ${y~0}")) + model <- list( + varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#", + output = list(result = "grep 'result' output.txt | cut -d= -f2") + ) + results_dir <- file.path(tempdir(), paste0("fzr_", Sys.getpid())) + + result <- tryCatch( + fzr(tf, list(x = c(1.0, 2.0), y = 3.0), model, + results_dir = results_dir, calculators = calc), + error = function(e) { + message("fzr integration test skipped: ", conditionMessage(e)) + NULL + } + ) + + if (!is.null(result)) { + expect_true(is.list(result) || is.data.frame(result)) + expect_true(length(result) > 0) + } +}) + +# --------------------------------------------------------------------------- +# fzo -- read existing output directory (no execution) +# --------------------------------------------------------------------------- + +test_that("fzo() reads output files from a directory", { + skip_on_cran() + skip_if_not(fz_available(), "fz Python package not available") + + out_dir <- file.path(tempdir(), paste0("fzo_", Sys.getpid())) + dir.create(out_dir, showWarnings = FALSE) + writeLines("result = 42", file.path(out_dir, "output.txt")) + + model <- list( + varprefix = "$", delim = "{}", formulaprefix = "@", commentline = "#", + output = list(result = "grep 'result' output.txt | cut -d= -f2") + ) + + result <- tryCatch( + fzo(out_dir, model), + error = function(e) { + message("fzo test skipped: ", conditionMessage(e)) + NULL + } + ) + + if (!is.null(result)) { + expect_true(is.list(result) || is.data.frame(result)) + } +}) + +# --------------------------------------------------------------------------- +# Verify installed PerfectGas model alias (if present) +# --------------------------------------------------------------------------- + +test_that("fzi() works with the installed PerfectGas model alias", { + skip_on_cran() + skip_if_not(fz_available(), "fz Python package not available") + + listing <- fzl() + skip_if_not("PerfectGas" %in% names(listing$models), + "PerfectGas model not installed") + + tf <- make_template(c("P = ${P~1.013}", "V = ${V~22.4}", "n = ${n~1.0}")) + + result <- fzi(tf, "PerfectGas") + expect_true(is.list(result)) + expect_true(length(result) > 0) +}) diff --git a/vignettes/modelica-examples.Rmd b/vignettes/modelica-examples.Rmd new file mode 100644 index 0000000..49759ff --- /dev/null +++ b/vignettes/modelica-examples.Rmd @@ -0,0 +1,239 @@ +--- +title: "Using fz for parametric studies" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Using fz for parametric studies} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = FALSE # requires funz-fz Python package and a configured calculator +) +``` + +```{r setup} +library(fz) +``` + +## Introduction + +The `fz` package provides R bindings to the +[funz-fz](https://pypi.org/project/funz-fz/) Python package. It lets you: + +- run **parameter sweeps** over any simulation code, +- drive **design of experiments** with adaptive algorithms, +- read and collect **output files** into data frames. + +fz works with any simulation code that reads text input files and writes text +output files. You describe the model in a small dict (or install it as a named +alias with `fz install`). + +## Installation + +```{r install} +# Install the funz-fz Python package into the active reticulate environment +fz_install() + +# Verify +fz_available() +``` + +## Concepts + +### Template files + +A **template** is an ordinary input file for your simulator with variable +placeholders, e.g.: + +``` +# Perfect Gas parameters +pressure = ${P~1.013} # variable P, default 1.013 +volume = ${V~22.4} # variable V, default 22.4 +moles = ${n~1.0} +``` + +The placeholder syntax (`$`, `{}`) is defined by the **model** dict. + +### Model dict + +The model dict tells fz how to: + +1. find variable placeholders (`varprefix`, `delim`), +2. extract output values from the result files (`output`). + +```{r model_dict} +model <- list( + varprefix = "$", + delim = "{}", + formulaprefix = "@", + commentline = "#", + output = list( + pressure = "grep 'pressure =' output.txt | cut -d= -f2" + ) +) +``` + +You can also use an installed model alias (a string) instead of an inline dict: + +```{r model_alias} +fzl()$models # lists installed aliases, e.g. "PerfectGas" +``` + +## Basic workflow + +The typical fz workflow has four steps. + +### Step 1 — Inspect the template + +`fzi` parses the template and returns the variable names together with their +default values: + +```{r fzi} +vars <- fzi("input.txt", model) +# $P [1] 1.013 +# $V [1] 22.4 +# $n [1] 1.0 +``` + +### Step 2 — Compile (substitute values) + +`fzc` writes one copy of the input file per parameter combination into +`output_dir`. Each copy goes into a subdirectory named +`var1=val1,var2=val2,...`: + +```{r fzc_single} +# Single case +fzc("input.txt", list(P = 2.0, V = 11.2), model, output_dir = "compiled") +# writes: compiled/P=2,V=11.2/input.txt (placeholder replaced with 2.0 / 11.2) +``` + +Supply vectors to generate a full-factorial grid: + +```{r fzc_grid} +# 2 x 3 = 6 cases +fzc("input.txt", + list(P = c(1.0, 2.0), V = c(10.0, 20.0, 30.0)), + model, + output_dir = "compiled") +``` + +### Step 3 — Run the model and collect outputs + +`fzr` wraps steps 1–3 and output collection into a single call. It compiles +the template, runs the calculator for every case, and returns a data frame: + +```{r fzr} +results <- fzr( + "input.txt", + list(P = c(1.0, 2.0, 3.0), V = 22.4), # 3 cases (V fixed) + model, + results_dir = "results", + calculators = "sh://bash run.sh" # run.sh executes the simulator +) + +# results is a data frame: +# P V pressure +# 1 1.0 22.4 ... +# 2 2.0 22.4 ... +# 3 3.0 22.4 ... +``` + +The `calculators` argument accepts: + +- `"sh://bash run.sh"` — run a local shell command +- `"sh://"` — execute the input file directly as a shell script +- `"ssh://user@host"` — run over SSH + +### Step 4 — Read outputs from existing directories + +If you already ran the simulator externally, `fzo` reads the output files: + +```{r fzo} +values <- fzo("results/P=2,V=22.4", model) +# $pressure [1] "2.026" + +# Glob to read all cases at once: +all_values <- fzo("results/*", model) +``` + +## Algorithm-driven design of experiments + +`fzd` runs an adaptive experiment: the algorithm decides which parameter +combinations to evaluate based on previous results. Input variable ranges use +`"[min;max]"` strings: + +```{r fzd} +result <- fzd( + "input.txt", + list(P = "[1;5]", V = "[10;30]"), + model, + output_expression = "pressure", + algorithm = "algorithms/montecarlo_uniform.py", + algorithm_options = "batch_sample_size=10;max_iterations=5;seed=42" +) +``` + +Algorithms are Python files; `fz` ships several in `algorithms/` (Monte Carlo, +surrogate-based optimization, …). You can also write your own. + +## Listing installed models + +`fzl` shows which model aliases and calculators are installed in `~/.fz/`: + +```{r fzl} +info <- fzl() +names(info$models) # e.g. c("PerfectGas", "Moret") +names(info$calculators) # e.g. c("sh://") + +# Filter by pattern +fzl(models = "Perfect*") + +# Probe calculators to verify they are reachable +fzl(check = TRUE) +``` + +## Best practices + +1. **Test with `fzi` first** — verify the correct variable names are found + before running anything. +2. **Use `fzc` for a dry run** — inspect compiled files to confirm placeholder + substitution is correct. +3. **Start small** — run a handful of cases before launching a large sweep. +4. **Save results** — persist the data frame for reproducibility. + +```{r save_results} +saveRDS(results, "fz_results.rds") +write.csv(results, "fz_results.csv", row.names = FALSE) +``` + +## Troubleshooting + +**`fz` Python package not found:** + +```{r troubleshoot_install} +fz_install() # install funz-fz into the reticulate environment +fz_available() # should return TRUE afterwards +``` + +**Variables not found in template:** +Check that `varprefix` and `delim` in your model dict match the syntax used in +your template file. Run `fzi` and inspect the returned list. + +**Calculator errors:** +Run the simulator manually on one compiled directory to confirm it works before +using `fzr`. + +## Further reading + +- funz-fz documentation: +- reticulate: + +## Session info + +```{r session_info} +sessionInfo() +```