R/generate_roxygen_docs.R

Defines functions generate_roxygen_docs

#' Generate Roxygen Documentation for a Dynamic Spec Function
#'
#' @description
#' This internal helper constructs a complete Roxygen comment block as a single
#' string. This string is then attached to the dynamically created model
#' specification function, making it self-documenting.
#'
#' @details
#' The function assembles the documentation in a structured way:
#' \itemize{
#'   \item \strong{Title & Description:} A title is generated from the `model_name`,
#'     and the description indicates which `kerasnip` function created it.
#'   \item \strong{Parameters (`@param`):} It documents several groups of parameters:
#'     \itemize{
#'       \item Block-specific hyperparameters (e.g., `dense_units`), introspecting
#'         `layer_blocks` to find default values.
#'       \item Architecture parameters (e.g., `num_dense`).
#'       \item Global training parameters (e.g., `epochs`, `learn_rate`).
#'       \item Compilation override parameters (e.g., `compile_loss`).
#'     }
#'   \item \strong{Sections (`@section`):} It creates dedicated sections for:
#'     \itemize{
#'       \item \strong{Model Architecture:} Explains how the model is built, with
#'         different content for the Sequential vs. Functional API (controlled
#'         by the `functional` flag).
#'       \item \strong{Model Fitting:} Explains how to pass arguments to
#'         `keras3::fit()` using the `fit_` prefix.
#'       \item \strong{Model Compilation:} Explains the default compilation
#'         behavior and how to override it using the `compile_` prefix.
#'     }
#'   \item \strong{Other Tags:} Adds `@seealso` to link to relevant `kerasnip`
#'     functions and `@export` to make the generated function available to users.
#' }
#'
#' @param model_name A character string for the model's name, used to generate the documentation title.
#' @param layer_blocks The named list of user-provided layer block functions. This is
#'   introspected to find default values for block-specific parameters.
#' @param all_args A named list of all arguments for the new function's signature,
#'   used to determine which `@param` tags to generate.
#' @param functional A logical. If `TRUE`, generates documentation specific to
#'   the Functional API. If `FALSE`, generates documentation for the Sequential API.
#' @return A single string containing the full Roxygen documentation, ready to be
#'   attached to a function using `comment()`.
#' @noRd
generate_roxygen_docs <- function(
  model_name,
  layer_blocks,
  all_args,
  functional = FALSE
) {
  # Title and Description
  title <- paste(
    gsub("_", " ", tools::toTitleCase(model_name)),
    "Model Specification"
  )
  desc <- paste0(
    "Defines a `parsnip` model specification for a Keras model built with ",
    "custom layer blocks. This function was generated by `kerasnip::",
    if (isTRUE(functional)) {
      "create_keras_functional_spec"
    } else {
      "create_keras_sequential_spec"
    },
    "()`."
  )

  # Parameters
  param_docs <- c()
  arg_names <- names(all_args)

  # Group args for structured documentation
  num_params <- arg_names[startsWith(arg_names, "num_")]
  fit_params <- arg_names[startsWith(arg_names, "fit_")]
  compile_params <- arg_names[startsWith(arg_names, "compile_")]
  # `learn_rate` is a special top-level convenience argument
  special_params <- "learn_rate"

  block_params <- setdiff(
    arg_names,
    c(num_params, fit_params, compile_params, special_params)
  )

  if (length(block_params) > 0) {
    # Sort block names by length descending to handle overlapping names
    # (e.g., "dense" and "dense_layer")
    sorted_block_names <- names(layer_blocks)[
      order(nchar(names(layer_blocks)), decreasing = TRUE)
    ]

    param_docs <- c(
      param_docs,
      purrr::map_chr(block_params, function(p) {
        # Find the block name that is a prefix for this parameter.
        # The `Find` function returns the first match, and since we sorted
        # block names by length, it will find the longest possible match.
        block_name <- Find(
          function(bn) startsWith(p, paste0(bn, "_")),
          sorted_block_names
        )

        if (is.null(block_name)) {
          # This should not happen if collect_spec_args is correct, but as a
          # fallback, we avoid an error.
          return(paste0("@param ", p, " A model parameter."))
        }

        param_name <- sub(paste0(block_name, "_"), "", p, fixed = TRUE)
        block_fn <- layer_blocks[[block_name]]
        default_val <- rlang::fn_fmls(block_fn)[[param_name]]
        default_str <- if (
          !is.null(default_val) && !rlang::is_missing(default_val)
        ) {
          paste0(
            " Defaults to `",
            deparse(default_val, width.cutoff = 500L),
            "`."
          )
        } else {
          ""
        }
        paste0(
          "@param ",
          p,
          " The `",
          param_name,
          "` for the '",
          block_name,
          "' block.",
          default_str
        )
      })
    )
  }

  # Document architecture params
  if (length(num_params) > 0) {
    param_docs <- c(
      param_docs,
      purrr::map_chr(num_params, function(p) {
        block_name <- sub("num_", "", p)
        paste0(
          "@param ",
          p,
          " The number of times to repeat the '",
          block_name,
          "' block. Defaults to 1."
        )
      })
    )
  }

  # Document special `learn_rate` param
  param_docs <- c(
    param_docs,
    "@param learn_rate The learning rate for the default Adam optimizer. This is ignored if `compile_optimizer` is provided as a pre-built Keras optimizer object."
  )

  # Document compile params
  if (length(compile_params) > 0) {
    param_docs <- c(
      param_docs,
      purrr::map_chr(compile_params, function(p) {
        paste0(
          "@param ",
          p,
          " Argument to `keras3::compile()`. See the 'Model Compilation' section."
        )
      })
    )
  }

  # Document fit params
  if (length(fit_params) > 0) {
    param_docs <- c(
      param_docs,
      purrr::map_chr(fit_params, function(p) {
        paste0(
          "@param ",
          p,
          " Argument to `keras3::fit()`. See the 'Model Fitting' section."
        )
      })
    )
  }

  # Add ... param
  param_docs <- c(
    param_docs,
    paste0(
      "@param ... Additional arguments passed to the Keras engine. Use this for arguments to `keras3::fit()` or `keras3::compile()` ",
      "that are not exposed as top-level arguments."
    )
  )

  # Sections
  if (isTRUE(functional)) {
    architecture_section <- c(
      "#' @section Model Architecture (Functional API):",
      "#' The Keras model is constructed using the Functional API. Each layer block function's arguments",
      "#' determine its inputs. For example, a block `function(input_a, input_b, ...)` will be connected",
      "#' to the outputs of the `input_a` and `input_b` blocks. You can also repeat a block by setting",
      "#' the `num_{block_name}` argument, provided the block has a single input tensor.",
      "#' The first block in `layer_blocks` is assumed to be the input layer and should not have inputs from other layers."
    )
    see_also_fit <- "generic_functional_fit()"
    see_also_create <- "create_keras_functional_spec()"
  } else {
    architecture_section <- c(
      "#' @section Model Architecture (Sequential API):",
      "#' The Keras model is constructed by sequentially applying the layer blocks in the order they were provided to `create_keras_sequential_spec()`.",
      "#' You can control the number of times each block is repeated by setting the `num_{block_name}` argument (e.g., `num_dense = 2`).",
      "#' This allows for dynamically creating deeper or more complex architectures during tuning."
    )
    see_also_fit <- "generic_sequential_fit()"
    see_also_create <- "create_keras_sequential_spec()"
  }

  compilation_section <- c(
    "#' @section Model Compilation:",
    "#' The model is compiled with a default optimizer, loss function, and metric based on the model's mode. You can override these defaults by providing arguments prefixed with `compile_`.",
    "#' \\itemize{",
    "#'   \\item \\strong{Optimizer}: Defaults to `keras3::optimizer_adam()` using the `learn_rate` argument. Override with `compile_optimizer` (e.g., `\"sgd\"` or `keras3::optimizer_sgd(...)`).",
    "#'   \\item \\strong{Loss}: Defaults to `\"mean_squared_error\"` for regression and `\"categorical_crossentropy\"` or `\"binary_crossentropy\"` for classification. Override with `compile_loss`.",
    "#'   \\item \\strong{Metrics}: Defaults to `\"mean_absolute_error\"` for regression and `\"accuracy\"` for classification. Override with `compile_metrics` (e.g., `c(\"mae\", \"mape\")`).",
    "#' }",
    paste0(
      "#' For more details, see the documentation for `kerasnip::",
      see_also_fit,
      "`."
    )
  )

  fitting_section <- c(
    "#' @section Model Fitting:",
    "#' The model is fit using `keras3::fit()`. You can pass any argument to this function by prefixing it with `fit_`.",
    "#' For example, to add Keras callbacks, you can pass `fit_callbacks = list(callback_early_stopping())`.",
    "#' Common arguments include `fit_epochs`, `fit_batch_size`, and `fit_validation_split`."
  )

  # Other tags
  other_tags <- c(
    paste0("#' @seealso [", see_also_create, "], [", see_also_fit, "]"),
    "#' @export"
  )

  # Combine all parts
  paste(
    c(
      paste0("#' ", title),
      "#'",
      paste0("#' ", desc),
      "#'",
      paste0("@", param_docs),
      architecture_section,
      fitting_section,
      compilation_section,
      other_tags
    ),
    collapse = "\n"
  )
}

Try the kerasnip package in your browser

Any scripts or data that you put into this service are public.

kerasnip documentation built on Nov. 5, 2025, 7:32 p.m.