R/expressions.R

Defines functions collect_biogeme_variables biogeme_expression_children collect_biogeme_parameters print.biogeme_expression format.biogeme_expression format_biogeme_expression piecewise validate_piecewise_thresholds boxcox derive panel_likelihood_trajectory monte_carlo integrate_normal distributed_parameter random_variable biogeme_draws draw Elem Summary.biogeme_expression biogeme_max biogeme_min abs.biogeme_expression sqrt.biogeme_expression safe_exp normal_pdf normal_cdf ordered_probit_log_probability ordered_logit_log_probability ordered_response_log_probability logzero biogeme_function exp.biogeme_expression log.biogeme_expression `^.biogeme_expression` `/.biogeme_expression` `*.biogeme_expression` `-.biogeme_expression` `+.biogeme_expression` Ops.biogeme_expression binary_expression biogeme_segmented_beta_definitions segment_beta biogeme_database_segmentation biogeme_segmentation biogeme_segmentation_catalogs biogeme_generic_alt_specific_catalogs biogeme_catalog biogeme_catalog_controller linear_utility linear_term variable biogeme_prior biogeme_beta validate_optional_bound validate_name as_biogeme_expression is_biogeme_expression new_biogeme_expression

Documented in abs.biogeme_expression biogeme_beta biogeme_catalog biogeme_catalog_controller biogeme_database_segmentation biogeme_draws biogeme_function biogeme_generic_alt_specific_catalogs biogeme_max biogeme_min biogeme_prior biogeme_segmentation biogeme_segmentation_catalogs boxcox collect_biogeme_parameters collect_biogeme_variables derive distributed_parameter draw Elem integrate_normal linear_term linear_utility logzero monte_carlo new_biogeme_expression normal_cdf normal_pdf ordered_logit_log_probability ordered_probit_log_probability ordered_response_log_probability panel_likelihood_trajectory piecewise random_variable safe_exp segment_beta sqrt.biogeme_expression variable

#' Create a Biogeme expression node
#'
#' This is an internal constructor. Expressions are represented in R until a
#' complete model is compiled by the Python bridge.
#'
#' @param kind Expression-node kind.
#' @param ... Node attributes.
#' @return An object of class `biogeme_expression`.
#' @keywords internal
new_biogeme_expression <- function(kind, ...) {
  structure(
    c(list(kind = kind), list(...)),
    class = "biogeme_expression"
  )
}

is_biogeme_expression <- function(x) {
  inherits(x, "biogeme_expression")
}

as_biogeme_expression <- function(x) {
  if (is_biogeme_expression(x)) {
    return(x)
  }
  if (inherits(x, "biogeme_draws")) {
    return(draw(x$name, x$draw_type))
  }
  if (is.numeric(x) && length(x) == 1L && !is.na(x)) {
    return(new_biogeme_expression("numeric", value = as.numeric(x)))
  }
  stop(
    "Expressions can only contain scalar numeric values or Biogeme expressions.",
    call. = FALSE
  )
}

validate_name <- function(name, argument) {
  if (!is.character(name) || length(name) != 1L || is.na(name) || !nzchar(name)) {
    stop(argument, " must be a single non-empty character string.", call. = FALSE)
  }
  name
}

validate_optional_bound <- function(value, argument) {
  if (is.null(value)) {
    return(NULL)
  }
  if (!is.numeric(value) || length(value) != 1L || is.na(value)) {
    stop(argument, " must be NULL or one numeric value.", call. = FALSE)
  }
  as.numeric(value)
}

#' Create a Biogeme parameter
#'
#' @param name Parameter name.
#' @param start Initial value.
#' @param lower Optional lower bound.
#' @param upper Optional upper bound.
#' @param fixed Logical value indicating whether the parameter is fixed.
#' @param sigma_prior Standard deviation of the native default normal prior.
#' @param prior Optional declarative prior from [biogeme_prior()]. It is
#'   compiled into a native PyMC prior factory; R callbacks are not accepted.
#' @return A parameter expression.
#' @details
#' Parameter names are preserved exactly by the bridge and are therefore part
#' of the equivalence contract with native Biogeme. Bounds and `fixed` are
#' passed to the native parameter definition.
#' @examples
#' b_cost <- biogeme_beta("b_cost", start = -1, upper = 0)
#' b_cost
#' @export
biogeme_beta <- function(
    name,
    start = 0,
    lower = NULL,
    upper = NULL,
    fixed = FALSE,
    sigma_prior = 5,
    prior = NULL
) {
  name <- validate_name(name, "name")
  if (!is.numeric(start) || length(start) != 1L || is.na(start) || !is.finite(start)) {
    stop("start must be one finite numeric value.", call. = FALSE)
  }
  lower <- validate_optional_bound(lower, "lower")
  upper <- validate_optional_bound(upper, "upper")
  if (!is.null(lower) && !is.null(upper) && lower > upper) {
    stop("lower must not be greater than upper.", call. = FALSE)
  }
  if (!is.null(lower) && start < lower) {
    stop("start must not be smaller than lower.", call. = FALSE)
  }
  if (!is.null(upper) && start > upper) {
    stop("start must not be greater than upper.", call. = FALSE)
  }
  if (!is.logical(fixed) || length(fixed) != 1L || is.na(fixed)) {
    stop("fixed must be one non-missing logical value.", call. = FALSE)
  }
  if (!is.numeric(sigma_prior) || length(sigma_prior) != 1L ||
      is.na(sigma_prior) || !is.finite(sigma_prior) || sigma_prior <= 0) {
    stop("sigma_prior must be one positive finite numeric value.", call. = FALSE)
  }
  if (!is.null(prior) && !inherits(prior, "biogeme_prior")) {
    stop("prior must be NULL or a biogeme_prior() object.", call. = FALSE)
  }

  structure(
    new_biogeme_expression(
      "beta",
      name = name,
      start = as.numeric(start),
      lower = lower,
      upper = upper,
      fixed = isTRUE(fixed),
      sigma_prior = as.numeric(sigma_prior),
      prior = prior
    ),
    class = c("biogeme_beta", "biogeme_expression")
  )
}

#' Declare a native Bayesian prior
#'
#' This descriptor is intentionally data-only. The Python bridge maps it to a
#' PyMC distribution before native Biogeme starts estimation, so no R
#' function or callback is evaluated by the sampler.
#'
#' @param distribution Prior distribution. Currently `"normal"` and
#'   `"student_t"` are supported.
#' @param sigma Positive scale parameter.
#' @param nu Positive degrees of freedom for a Student-t prior.
#' @return A declarative `biogeme_prior` object.
#' @export
biogeme_prior <- function(
    distribution = c("normal", "student_t"),
    sigma = 5,
    nu = 5
) {
  distribution <- match.arg(distribution)
  if (!is.numeric(sigma) || length(sigma) != 1L || is.na(sigma) ||
      !is.finite(sigma) || sigma <= 0) {
    stop("sigma must be one positive finite numeric value.", call. = FALSE)
  }
  if (!is.numeric(nu) || length(nu) != 1L || is.na(nu) ||
      !is.finite(nu) || nu <= 0) {
    stop("nu must be one positive finite numeric value.", call. = FALSE)
  }
  structure(
    list(
      distribution = distribution,
      sigma = as.numeric(sigma),
      nu = as.numeric(nu)
    ),
    class = "biogeme_prior"
  )
}

#' Create a Biogeme data variable
#'
#' @param name Column name in the Biogeme database.
#' @return A variable expression.
#' @details
#' `variable()` creates a symbolic reference. It does not extract an R column
#' or calculate a vector immediately. Use it in arithmetic and model
#' expressions after the corresponding database column has been defined.
#' @examples
#' x <- variable("income")
#' b <- biogeme_beta("b_income")
#' b * x + 1
#' @export
variable <- function(name) {
  structure(
    new_biogeme_expression("variable", name = validate_name(name, "name")),
    class = c("biogeme_variable", "biogeme_expression")
  )
}

#' Define one coefficient-variable term for a linear utility
#'
#' @param beta A `biogeme_beta` expression.
#' @param x A `biogeme_variable` expression.
#' @return A linear-utility term.
#' @export
linear_term <- function(beta, x) {
  beta <- as_biogeme_expression(beta)
  x <- as_biogeme_expression(x)
  if (!identical(beta$kind, "beta") || !identical(x$kind, "variable")) {
    stop("A linear term must contain one Beta and one data variable.", call. = FALSE)
  }
  structure(list(beta = beta, x = x), class = "biogeme_linear_term")
}

#' @rdname linear_term
#' @export LinearTermTuple
LinearTermTuple <- linear_term

#' Construct a native linear utility expression
#'
#' @param terms A non-empty list of `linear_term()` objects.
#' @return A Biogeme expression compiled to native `LinearUtility`.
#' @export
linear_utility <- function(terms) {
  if (!is.list(terms) || length(terms) == 0L ||
      any(!vapply(terms, inherits, logical(1), what = "biogeme_linear_term"))) {
    stop("terms must be a non-empty list of linear_term() objects.", call. = FALSE)
  }
  new_biogeme_expression("linear_utility", terms = terms)
}

#' @rdname linear_utility
#' @export LinearUtility
LinearUtility <- linear_utility

#' Create a neutral controller for one or more expression catalogs
#'
#' Catalog controllers select the same named specification in every catalog
#' that shares the controller. The object remains an R specification until the
#' complete expression is compiled to native Biogeme.
#'
#' @param name Controller name.
#' @param specification_names Ordered, unique catalog specification names.
#' @param selected_name Optional specification selected before native compilation.
#' @return A neutral catalog controller.
#' @export
biogeme_catalog_controller <- function(
    name,
    specification_names,
    selected_name = NULL
) {
  name <- validate_name(name, "name")
  if (grepl("[;:]", name)) {
    stop("name must not contain ';' or ':'.", call. = FALSE)
  }
  if (!is.character(specification_names) || length(specification_names) == 0L ||
      anyNA(specification_names) || any(!nzchar(specification_names)) ||
      any(grepl("[;:]", specification_names)) ||
      anyDuplicated(specification_names)) {
    stop(
      "specification_names must be a non-empty character vector with unique names.",
      call. = FALSE
    )
  }
  if (!is.null(selected_name)) {
    if (!is.character(selected_name) || length(selected_name) != 1L ||
        is.na(selected_name) || !nzchar(selected_name) ||
        !selected_name %in% specification_names) {
      stop(
        "selected_name must be NULL or one of specification_names.",
        call. = FALSE
      )
    }
  }
  structure(
    list(
      name = name,
      specification_names = as.character(specification_names),
      selected_name = selected_name
    ),
    class = "biogeme_catalog_controller"
  )
}

#' @rdname biogeme_catalog_controller
#' @export
catalog_controller <- biogeme_catalog_controller

#' Create a native-compatible expression catalog
#'
#' @param name Catalog name.
#' @param expressions Non-empty named list of alternative expressions.
#' @param controller Optional [biogeme_catalog_controller()] shared by catalogs.
#' @return A catalog expression compiled to native Biogeme.
#' @export
biogeme_catalog <- function(name, expressions, controller = NULL) {
  name <- validate_name(name, "name")
  if (grepl("[;:]", name)) {
    stop("name must not contain ';' or ':'.", call. = FALSE)
  }
  if (!is.list(expressions) || length(expressions) == 0L ||
      is.null(names(expressions)) || anyNA(names(expressions)) ||
      any(!nzchar(names(expressions))) || any(grepl("[;:]", names(expressions))) ||
      anyDuplicated(names(expressions))) {
    stop(
      "expressions must be a non-empty named list with unique names.",
      call. = FALSE
    )
  }
  expressions <- lapply(expressions, as_biogeme_expression)
  specification_names <- names(expressions)
  if (is.null(controller)) {
    controller <- biogeme_catalog_controller(name, specification_names)
  }
  if (!inherits(controller, "biogeme_catalog_controller")) {
    stop("controller must be a biogeme_catalog_controller object.", call. = FALSE)
  }
  if (!identical(controller$specification_names, specification_names)) {
    stop(
      "controller specification_names must match the catalog expression names.",
      call. = FALSE
    )
  }
  node <- new_biogeme_expression(
    "catalog",
    name = name,
    expressions = expressions,
    controller = controller
  )
  names(node$expressions) <- specification_names
  node
}

#' @rdname biogeme_catalog
#' @export
catalog <- biogeme_catalog

#' Build synchronized generic/alternative-specific catalogs
#'
#' This is the neutral R counterpart of native Biogeme's
#' `generic_alt_specific_catalogs()`.  Each returned element is a named list
#' of catalogs, one for every alternative.  The catalogs share one native
#' controller with the exact specifications `generic` and `altspec`; when
#' segmentations are supplied, the segmentation catalogs are nested below
#' that controller in the same order as native Biogeme.
#'
#' @param generic_name Shared name used for the segmentation and
#'   generic/alternative-specific controllers.
#' @param beta_parameters Non-empty list of `biogeme_beta()` expressions.
#' @param alternatives At least two unique alternative names.
#' @param potential_segmentations Optional non-empty list of segmentation
#'   objects.
#' @param maximum_number Maximum number of potential segmentations in one
#'   option.
#' @return A list of named lists of catalog expressions, one list per Beta.
#' @export
biogeme_generic_alt_specific_catalogs <- function(
    generic_name,
    beta_parameters,
    alternatives,
    potential_segmentations = NULL,
    maximum_number = 5L
) {
  generic_name <- validate_name(generic_name, "generic_name")
  if (grepl("[;:]", generic_name)) {
    stop("generic_name must not contain ';' or ':'.", call. = FALSE)
  }
  if (!is.list(beta_parameters) || length(beta_parameters) == 0L ||
      any(!vapply(beta_parameters, function(x) identical(x$kind, "beta"), logical(1)))) {
    stop("beta_parameters must be a non-empty list of Beta expressions.", call. = FALSE)
  }
  if (!is.character(alternatives) || length(alternatives) < 2L ||
      anyNA(alternatives) || any(!nzchar(alternatives)) ||
      any(grepl("[;:]", alternatives)) || anyDuplicated(alternatives)) {
    stop(
      "alternatives must contain at least two unique non-empty names.",
      call. = FALSE
    )
  }
  if (!is.null(potential_segmentations)) {
    if (!is.list(potential_segmentations) || length(potential_segmentations) == 0L ||
        any(!vapply(
          potential_segmentations,
          inherits,
          logical(1),
          what = "biogeme_segmentation"
        ))) {
      stop(
        "potential_segmentations must be NULL or a non-empty list of segmentation objects.",
        call. = FALSE
      )
    }
  }
  if (!is.numeric(maximum_number) || length(maximum_number) != 1L ||
      is.na(maximum_number) || maximum_number < 0 ||
      maximum_number != floor(maximum_number)) {
    stop("maximum_number must be one non-negative integer.", call. = FALSE)
  }
  maximum_number <- as.integer(maximum_number)

  # Native SegmentedParameters creates the generic Beta first, followed by
  # one alternative-specific Beta block per alternative.  Keeping that order
  # is important because the generated parameter names are part of the
  # equivalence contract.
  all_beta_parameters <- beta_parameters
  for (alternative in alternatives) {
    all_beta_parameters <- c(
      all_beta_parameters,
      lapply(beta_parameters, function(beta) {
        # Native SegmentedParameters copies the value, bounds, and fixed
        # status, while newly created alternative-specific Betas use native
        # default prior metadata.
        biogeme_beta(
          name = paste0(beta$name, "_", alternative),
          start = beta$start,
          lower = beta$lower,
          upper = beta$upper,
          fixed = beta$fixed
        )
      })
    )
  }

  if (is.null(potential_segmentations)) {
    segmented_catalogs <- all_beta_parameters
  } else {
    segmented_catalogs <- biogeme_segmentation_catalogs(
      generic_name = generic_name,
      beta_parameters = all_beta_parameters,
      potential_segmentations = potential_segmentations,
      maximum_number = maximum_number
    )
  }

  generic_specific_controller <- biogeme_catalog_controller(
    paste0(generic_name, "_gen_altspec"),
    c("generic", "altspec")
  )
  number_of_parameters <- length(beta_parameters)
  result <- vector("list", number_of_parameters)
  names(result) <- vapply(beta_parameters, function(beta) beta$name, character(1))

  for (parameter_index in seq_len(number_of_parameters)) {
    generic_expression <- segmented_catalogs[[parameter_index]]
    alternative_expressions <- lapply(seq_along(alternatives), function(index) {
      segmented_catalogs[[number_of_parameters * index + parameter_index]]
    })
    catalogs_for_parameter <- lapply(seq_along(alternatives), function(index) {
      alternative <- alternatives[[index]]
      biogeme_catalog(
        name = paste0(
          beta_parameters[[parameter_index]]$name,
          "_",
          alternative,
          "_gen_altspec"
        ),
        expressions = list(
          generic = generic_expression,
          altspec = alternative_expressions[[index]]
        ),
        controller = generic_specific_controller
      )
    })
    names(catalogs_for_parameter) <- alternatives
    result[[parameter_index]] <- catalogs_for_parameter
  }
  result
}

#' @rdname biogeme_generic_alt_specific_catalogs
#' @export
generic_alt_specific_catalogs <- biogeme_generic_alt_specific_catalogs

#' Build synchronized catalogs for possible parameter segmentations
#'
#' This is the structural counterpart of native Biogeme's
#' `segmentation_catalogs()`. It only enumerates catalog names and builds
#' neutral expression nodes; native Biogeme still creates the segmented
#' parameters and evaluates the selected specification.
#'
#' @param generic_name Shared controller name.
#' @param beta_parameters Non-empty list of `biogeme_beta()` expressions.
#' @param potential_segmentations Non-empty list of segmentation objects.
#' @param maximum_number Maximum number of segmentations in one option.
#' @param selected_name Optional segmentation specification selected before
#'   native compilation.
#' @return A list of synchronized catalog expressions, one per beta.
#' @export
biogeme_segmentation_catalogs <- function(
    generic_name,
    beta_parameters,
    potential_segmentations,
    maximum_number,
    selected_name = NULL
) {
  generic_name <- validate_name(generic_name, "generic_name")
  if (!is.list(beta_parameters) || length(beta_parameters) == 0L ||
      any(!vapply(beta_parameters, function(x) identical(x$kind, "beta"), logical(1)))) {
    stop("beta_parameters must be a non-empty list of Beta expressions.", call. = FALSE)
  }
  if (!is.list(potential_segmentations) || length(potential_segmentations) == 0L ||
      any(!vapply(potential_segmentations, inherits, logical(1), what = "biogeme_segmentation"))) {
    stop(
      "potential_segmentations must be a non-empty list of segmentation objects.",
      call. = FALSE
    )
  }
  if (!is.numeric(maximum_number) || length(maximum_number) != 1L ||
      is.na(maximum_number) || maximum_number < 0 ||
      maximum_number != floor(maximum_number)) {
    stop("maximum_number must be one non-negative integer.", call. = FALSE)
  }
  maximum_number <- as.integer(maximum_number)
  if (!is.null(selected_name)) {
    if (!is.character(selected_name) || length(selected_name) != 1L ||
        is.na(selected_name) || !nzchar(selected_name)) {
      stop("selected_name must be NULL or one non-empty string.", call. = FALSE)
    }
  }

  # expand.grid varies its first column fastest. Reversing the columns gives
  # the same False/True product order as itertools.product in native Python.
  n_segmentations <- length(potential_segmentations)
  combinations <- expand.grid(
    rep(list(c(FALSE, TRUE)), n_segmentations),
    KEEP.OUT.ATTRS = FALSE,
    stringsAsFactors = FALSE
  )
  combinations <- combinations[n_segmentations:1L]
  keep <- rowSums(combinations) <= maximum_number
  combinations <- combinations[keep, , drop = FALSE]
  specification_names <- vapply(seq_len(nrow(combinations)), function(row) {
    selected <- as.logical(combinations[row, ])
    if (!any(selected)) return("no_seg")
    paste(
      vapply(potential_segmentations[selected], function(segmentation) segmentation$variable, character(1)),
      collapse = "-"
    )
  }, character(1))
  controller <- biogeme_catalog_controller(
    generic_name,
    specification_names,
    selected_name = selected_name
  )

  lapply(beta_parameters, function(beta) {
    options <- lapply(seq_len(nrow(combinations)), function(row) {
      selected <- as.logical(combinations[row, ])
      if (!any(selected)) {
        beta
      } else {
        segment_beta(beta, potential_segmentations[selected])
      }
    })
    names(options) <- specification_names
    biogeme_catalog(
      name = paste0("segmented_", beta$name),
      expressions = options,
      controller = controller
    )
  })
}

#' @rdname biogeme_segmentation_catalogs
#' @export
segmentation_catalogs <- biogeme_segmentation_catalogs

#' Define a discrete parameter segmentation
#'
#' @param variable A data-variable name or expression.
#' @param mapping Named mapping from integer values to segment names.
#' @param reference Optional reference segment name.
#' @return A `biogeme_segmentation` specification.
#' @export
biogeme_segmentation <- function(variable, mapping, reference = NULL) {
  variable_expression <- if (is.character(variable)) {
    structure(
      new_biogeme_expression("variable", name = validate_name(variable[[1L]], "variable")),
      class = c("biogeme_variable", "biogeme_expression")
    )
  } else {
    as_biogeme_expression(variable)
  }
  if (!identical(variable_expression$kind, "variable")) {
    stop("variable must be a database variable name or expression.", call. = FALSE)
  }
  if (!is.atomic(mapping) && !is.list(mapping)) {
    stop("mapping must be a named vector or list.", call. = FALSE)
  }
  mapping <- unlist(mapping, use.names = TRUE)
  if (length(mapping) == 0L || is.null(names(mapping)) ||
      anyNA(names(mapping)) || any(!nzchar(names(mapping)))) {
    stop("mapping must be non-empty and named by integer values.", call. = FALSE)
  }
  keys <- suppressWarnings(as.integer(names(mapping)))
  labels <- as.character(unname(mapping))
  if (anyNA(keys) || anyDuplicated(keys) || anyNA(labels) || any(!nzchar(labels)) ||
      anyDuplicated(labels)) {
    stop("mapping keys must be unique integers and labels must be unique non-empty strings.", call. = FALSE)
  }
  if (is.null(reference)) reference <- labels[[1L]]
  reference <- validate_name(reference, "reference")
  if (!reference %in% labels) {
    stop("reference must be one of the mapping labels.", call. = FALSE)
  }
  structure(
    list(
      variable = variable_expression$name,
      mapping = stats::setNames(labels, as.character(keys)),
      reference = reference
    ),
    class = "biogeme_segmentation"
  )
}

#' Generate a native-compatible segmentation specification from a database
#'
#' @param database A `biogeme_database` object.
#' @param variable A database variable name or expression.
#' @param mapping Named mapping from integer values to segment names.
#' @param reference Optional reference segment name.
#' @return A `biogeme_segmentation` specification.
#' @export
biogeme_database_segmentation <- function(database, variable, mapping, reference = NULL) {
  validate_biogeme_database(database)
  variable_expression <- if (is.character(variable)) {
    structure(
      new_biogeme_expression("variable", name = validate_name(variable[[1L]], "variable")),
      class = c("biogeme_variable", "biogeme_expression")
    )
  } else {
    as_biogeme_expression(variable)
  }
  if (!biogeme_database_has_column(database, variable_expression$name)) {
    stop("Segmentation variable is not present in the database.", call. = FALSE)
  }
  biogeme_segmentation(variable_expression, mapping, reference)
}

#' @rdname biogeme_database_segmentation
#' @export
database_generate_segmentation <- biogeme_database_segmentation

#' Create a segmented parameter expression
#'
#' The generated native parameters follow Biogeme's public naming contract:
#' `<base>_ref` for the reference and `<base>_diff_<segment>` for differences.
#'
#' @param beta A `biogeme_beta` expression.
#' @param segmentations A non-empty list of `biogeme_segmentation` objects.
#' @param prefix Native segmentation expression prefix.
#' @return A Biogeme expression compiled through native `Segmentation`.
#' @export
segment_beta <- function(beta, segmentations, prefix = "segmented") {
  beta <- as_biogeme_expression(beta)
  if (!identical(beta$kind, "beta")) {
    stop("beta must be a biogeme_beta expression.", call. = FALSE)
  }
  if (!is.list(segmentations) || length(segmentations) == 0L ||
      any(!vapply(segmentations, inherits, logical(1), what = "biogeme_segmentation"))) {
    stop("segmentations must be a non-empty list of biogeme_segmentation objects.", call. = FALSE)
  }
  structure(
    new_biogeme_expression(
      "segmented_beta",
      beta = beta,
      segmentations = segmentations,
      prefix = validate_name(prefix, "prefix")
    ),
    class = c("biogeme_segmented_beta", "biogeme_expression")
  )
}

#' @rdname segment_beta
#' @export
segmented_beta <- segment_beta

biogeme_segmented_beta_definitions <- function(expression) {
  if (!is_biogeme_expression(expression) || !identical(expression$kind, "segmented_beta")) {
    stop("expression must be a segmented_beta expression.", call. = FALSE)
  }
  base <- expression$beta
  definitions <- list(
    biogeme_beta(
      paste0(base$name, "_ref"),
      start = base$start,
      lower = base$lower,
      upper = base$upper,
      fixed = base$fixed
    )
  )
  for (segmentation in expression$segmentations) {
    labels <- unname(segmentation$mapping)
    labels <- labels[labels != segmentation$reference]
    for (label in labels) {
      definitions[[length(definitions) + 1L]] <- biogeme_beta(
        paste0(base$name, "_diff_", label),
        start = base$start,
        fixed = base$fixed
      )
    }
  }
  definitions
}

binary_expression <- function(operator, left, right) {
  if (!operator %in% c("+", "-", "*", "/", "^", "==", "!=", "<", "<=", ">", ">=", "&", "|")) {
    stop("Unsupported Biogeme binary operator: ", operator, call. = FALSE)
  }
  new_biogeme_expression(
    "binary",
    operator = operator,
    left = as_biogeme_expression(left),
    right = as_biogeme_expression(right)
  )
}

#' @export
#' @method Ops biogeme_expression
Ops.biogeme_expression <- function(e1, e2 = NULL) {
  operator <- .Generic
  if (operator == "!") {
    return(new_biogeme_expression("unary", operator = "!", operand = e1))
  }
  if (is.null(e2)) {
    stop("A second operand is required for Biogeme operator '", operator, "'.", call. = FALSE)
  }
  binary_expression(operator, e1, e2)
}

#' @export
#' @method + biogeme_expression
`+.biogeme_expression` <- function(e1, e2) {
  binary_expression("+", e1, e2)
}

#' @export
#' @method - biogeme_expression
`-.biogeme_expression` <- function(e1, e2 = NULL) {
  if (is.null(e2)) {
    return(new_biogeme_expression("unary", operator = "-", operand = e1))
  }
  binary_expression("-", e1, e2)
}

#' @export
#' @method * biogeme_expression
`*.biogeme_expression` <- function(e1, e2) {
  binary_expression("*", e1, e2)
}

#' @export
#' @method / biogeme_expression
`/.biogeme_expression` <- function(e1, e2) {
  binary_expression("/", e1, e2)
}

#' @export
#' @method ^ biogeme_expression
`^.biogeme_expression` <- function(e1, e2) {
  binary_expression("^", e1, e2)
}

#' @export
#' @method log biogeme_expression
log.biogeme_expression <- function(x, base = exp(1)) {
  if (!is.numeric(base) || length(base) != 1L || is.na(base) || base != exp(1)) {
    stop("Only the natural logarithm is supported.", call. = FALSE)
  }
  new_biogeme_expression("unary", operator = "log", operand = as_biogeme_expression(x))
}

#' @export
#' @method exp biogeme_expression
exp.biogeme_expression <- function(x) {
  new_biogeme_expression("unary", operator = "exp", operand = as_biogeme_expression(x))
}

#' Create a Biogeme function node
#'
#' This constructor is intentionally generic so that new public Biogeme
#' functions can be added without evaluating the expression in R.
#' @param operator Native Biogeme function/operator name.
#' @param args A list of scalar values or Biogeme expressions.
#' @return A Biogeme expression.
#' @keywords internal
biogeme_function <- function(operator, args = list(), ...) {
  operator <- validate_name(operator, "operator")
  if (!is.list(args)) {
    stop("args must be a list.", call. = FALSE)
  }
  new_biogeme_expression(
    "function",
    operator = operator,
    args = lapply(args, as_biogeme_expression),
    attributes = list(...)
  )
}

#' A numerically safe logarithm that returns zero at zero
#' @param x A scalar or Biogeme expression.
#' @return A Biogeme expression.
#' @export
logzero <- function(x) {
  biogeme_function("logzero", list(x))
}

#' Construct a native ordered-response log-likelihood expression
#'
#' The observed response is matched to `categories`; the intervals are
#' defined by the ordered `cutpoints`.  This internal helper selects the
#' native logistic or normal cumulative distribution.  No probability is
#' evaluated in R.
#'
#' @param eta Latent-index expression.
#' @param cutpoints Non-empty list of cutpoint expressions.
#' @param alternative Observed response expression.
#' @param categories Optional ordered numeric labels. If omitted, native
#'   Biogeme uses `0:(length(cutpoints))`.
#' @param neutral_labels Optional numeric labels whose contribution is one.
#' @param enforce_order Whether native Biogeme should enforce ordered
#'   cutpoints during evaluation.
#' @param eps Positive numerical lower bound used by native Biogeme.
#' @param distribution Native ordered-response distribution.
#' @return A Biogeme expression compiled to native Biogeme.
#' @keywords internal
ordered_response_log_probability <- function(
    distribution = c("logit", "probit"),
    eta,
    cutpoints,
    alternative,
    categories = NULL,
    neutral_labels = numeric(),
    enforce_order = TRUE,
    eps = 1e-12
) {
  distribution <- match.arg(distribution)
  eta <- as_biogeme_expression(eta)
  alternative <- as_biogeme_expression(alternative)
  if (!is.list(cutpoints) || length(cutpoints) == 0L) {
    stop("cutpoints must be a non-empty list of expressions.", call. = FALSE)
  }
  cutpoints <- lapply(cutpoints, as_biogeme_expression)
  n_categories <- length(cutpoints) + 1L
  if (!is.null(categories)) {
    if (!is.numeric(categories) || length(categories) != n_categories ||
        anyNA(categories) || any(!is.finite(categories))) {
      stop(
        "categories must be NULL or a finite numeric vector of length ",
        n_categories,
        ".",
        call. = FALSE
      )
    }
    if (anyDuplicated(categories)) {
      stop("categories must contain unique labels.", call. = FALSE)
    }
    categories <- as.numeric(categories)
  }
  if (!is.numeric(neutral_labels) || anyNA(neutral_labels) ||
      any(!is.finite(neutral_labels))) {
    stop("neutral_labels must be a finite numeric vector.", call. = FALSE)
  }
  neutral_labels <- as.numeric(neutral_labels)
  if (!is.logical(enforce_order) || length(enforce_order) != 1L || is.na(enforce_order)) {
    stop("enforce_order must be one non-missing logical value.", call. = FALSE)
  }
  if (!is.numeric(eps) || length(eps) != 1L || is.na(eps) ||
      !is.finite(eps) || eps <= 0) {
    stop("eps must be one positive finite numeric value.", call. = FALSE)
  }
  new_biogeme_expression(
    paste0("ordered_", distribution),
    eta = eta,
    cutpoints = cutpoints,
    alternative = alternative,
    categories = categories,
    neutral_labels = neutral_labels,
    enforce_order = isTRUE(enforce_order),
    eps = as.numeric(eps)
  )
}

#' Construct a native ordered-logit log-likelihood expression
#'
#' @param eta Latent-index expression.
#' @param cutpoints Non-empty list of cutpoint expressions.
#' @param alternative Observed response expression.
#' @param categories Optional ordered numeric labels.
#' @param neutral_labels Optional numeric labels whose contribution is one.
#' @param enforce_order Whether native Biogeme should enforce ordered cutpoints.
#' @param eps Positive numerical lower bound used by native Biogeme.
#' @return A Biogeme expression compiled to native `OrderedLogLogit`.
#' @export
ordered_logit_log_probability <- function(
    eta,
    cutpoints,
    alternative,
    categories = NULL,
    neutral_labels = numeric(),
    enforce_order = TRUE,
    eps = 1e-12
) {
  ordered_response_log_probability(
    distribution = "logit",
    eta = eta,
    cutpoints = cutpoints,
    alternative = alternative,
    categories = categories,
    neutral_labels = neutral_labels,
    enforce_order = enforce_order,
    eps = eps
  )
}

#' Construct a native ordered-probit log-likelihood expression
#'
#' Ordered probit uses the native normal CDF with the same cutpoint and
#' category semantics as [ordered_logit_log_probability()].
#'
#' @param eta Latent-index expression.
#' @param cutpoints Non-empty list of cutpoint expressions.
#' @param alternative Observed response expression.
#' @param categories Optional ordered numeric labels.
#' @param neutral_labels Optional numeric labels whose contribution is one.
#' @param enforce_order Whether native Biogeme should enforce ordered cutpoints.
#' @param eps Positive numerical lower bound used by native Biogeme.
#' @return A Biogeme expression compiled to native `OrderedLogProbit`.
#' @export
ordered_probit_log_probability <- function(
    eta,
    cutpoints,
    alternative,
    categories = NULL,
    neutral_labels = numeric(),
    enforce_order = TRUE,
    eps = 1e-12
) {
  ordered_response_log_probability(
    distribution = "probit",
    eta = eta,
    cutpoints = cutpoints,
    alternative = alternative,
    categories = categories,
    neutral_labels = neutral_labels,
    enforce_order = enforce_order,
    eps = eps
  )
}

#' Normal cumulative distribution function
#' @param x A scalar or Biogeme expression.
#' @return A Biogeme expression.
#' @export
normal_cdf <- function(x) {
  biogeme_function("normal_cdf", list(x))
}

#' Normal probability density function
#' @param x A scalar or Biogeme expression.
#' @return A Biogeme expression.
#' @export
normal_pdf <- function(x) {
  biogeme_function("normal_pdf", list(x))
}

#' Numerically safe exponential
#' @param x A scalar or Biogeme expression.
#' @return A Biogeme expression.
#' @export
safe_exp <- function(x) {
  biogeme_function("safe_exp", list(x))
}

#' @rdname logzero
#' @export
safe_log <- logzero

#' Square root of a Biogeme expression
#' @param x A scalar or Biogeme expression.
#' @return A Biogeme expression.
#' @export
sqrt.biogeme_expression <- function(x) {
  biogeme_function("sqrt", list(x))
}

#' Absolute value of a Biogeme expression
#' @param x A scalar or Biogeme expression.
#' @return A Biogeme expression.
#' @export
abs.biogeme_expression <- function(x) {
  biogeme_function("abs", list(x))
}

#' Minimum of Biogeme expressions
#' @param ... Scalar values or Biogeme expressions.
#' @return A Biogeme expression.
#' @export
biogeme_min <- function(...) {
  args <- list(...)
  if (length(args) == 0L) {
    stop("biogeme_min requires at least one argument.", call. = FALSE)
  }
  biogeme_function("min", args)
}

#' Maximum of Biogeme expressions
#' @param ... Scalar values or Biogeme expressions.
#' @return A Biogeme expression.
#' @export
biogeme_max <- function(...) {
  args <- list(...)
  if (length(args) == 0L) {
    stop("biogeme_max requires at least one argument.", call. = FALSE)
  }
  biogeme_function("max", args)
}

#' @export
#' @method Summary biogeme_expression
Summary.biogeme_expression <- function(..., na.rm = FALSE) {
  if (!.Generic %in% c("min", "max")) {
    stop("Unsupported Summary operation for a Biogeme expression: ", .Generic, call. = FALSE)
  }
  if (isTRUE(na.rm)) stop("na.rm is not meaningful for symbolic expressions.", call. = FALSE)
  if (.Generic == "min") biogeme_min(...) else biogeme_max(...)
}

#' Select an expression from a numeric mapping
#' @param mapping Named list whose names are integer keys.
#' @param index Numeric or Biogeme expression used as the key.
#' @return A Biogeme expression.
#' @export
Elem <- function(mapping, index) {
  if (!is.list(mapping) || is.null(names(mapping)) || length(mapping) == 0L ||
      anyNA(names(mapping)) || any(!nzchar(names(mapping)))) {
    stop("mapping must be a non-empty named list.", call. = FALSE)
  }
  keys <- suppressWarnings(as.integer(names(mapping)))
  if (anyNA(keys) || anyDuplicated(keys)) {
    stop("mapping names must be unique integer keys.", call. = FALSE)
  }
  node <- new_biogeme_expression(
    "elem",
    mapping = lapply(mapping, as_biogeme_expression),
    keys = keys,
    index = as_biogeme_expression(index)
  )
  names(node$mapping) <- as.character(keys)
  node
}

#' @rdname Elem
#' @export
elem <- Elem

#' Create a named Biogeme draw node
#' @param name Draw variable name.
#' @param draw_type Biogeme draw type, such as `NORMAL` or `UNIFORM_HALTON2`.
#' @return A Biogeme expression.
#' @export
draw <- function(name, draw_type = "NORMAL") {
  new_biogeme_expression(
    "draw",
    name = validate_name(name, "name"),
    draw_type = validate_name(draw_type, "draw_type")
  )
}

#' Declare first-class draw metadata for a model
#' @param name Draw variable name.
#' @param draw_type Native draw type.
#' @param number_of_draws Optional draw count.
#' @param seed Optional draw seed.
#' @param matrix Optional user-supplied numeric draw matrix.
#' @param generator Optional bridge generator name. Supported bridge-owned
#'   generators include `"TRIANGULAR"`, `"HALTON13"`, and
#'   `"HALTON13_ANTI"`. The latter two match the custom base-13, skip-10
#'   generators used by the Monte Carlo examples.
#' @return A `biogeme_draws` object.
#' @export
biogeme_draws <- function(
    name,
    draw_type = "NORMAL",
    number_of_draws = NULL,
    seed = NULL,
    matrix = NULL,
    generator = NULL
) {
  name <- validate_name(name, "name")
  draw_type <- validate_name(draw_type, "draw_type")
  if (!is.null(number_of_draws) &&
      (!is.numeric(number_of_draws) || length(number_of_draws) != 1L ||
       is.na(number_of_draws) || number_of_draws < 1 || number_of_draws != floor(number_of_draws))) {
    stop("number_of_draws must be NULL or one positive integer.", call. = FALSE)
  }
  if (!is.null(seed) && (!is.numeric(seed) || length(seed) != 1L || is.na(seed) || !is.finite(seed))) {
    stop("seed must be NULL or one finite numeric value.", call. = FALSE)
  }
  if (!is.null(matrix) && (!is.numeric(matrix) || is.null(dim(matrix)) || length(dim(matrix)) != 2L)) {
    stop("matrix must be NULL or a numeric matrix.", call. = FALSE)
  }
  if (!is.null(generator) &&
      (!is.character(generator) || length(generator) != 1L ||
       is.na(generator) || !nzchar(generator))) {
    stop("generator must be NULL or one non-empty character string.", call. = FALSE)
  }
  structure(
    list(
      name = name,
      draw_type = draw_type,
      number_of_draws = if (is.null(number_of_draws)) NULL else as.integer(number_of_draws),
      seed = if (is.null(seed)) NULL else as.numeric(seed),
      matrix = matrix,
      generator = generator
    ),
    class = "biogeme_draws"
  )
}

#' Create a random variable for native numerical integration
#' @param name Random variable name.
#' @return A Biogeme expression.
#' @export
random_variable <- function(name) {
  new_biogeme_expression("random_variable", name = validate_name(name, "name"))
}

#' Store a simulated individual-level parameter in Bayesian results
#'
#' `distributed_parameter()` is a symbolic wrapper around native Biogeme's
#' `DistributedParameter` expression. It preserves the named variable in the
#' Bayesian output while its child expression supplies the value used by the
#' likelihood. The wrapper is compiled once with the rest of the expression
#' tree; it is not evaluated by R during sampling.
#' @param name Name used for the stored native variable.
#' @param expression Child expression, usually a location parameter plus a
#'   scale parameter multiplied by [draw()].
#' @return A Biogeme expression.
#' @export
distributed_parameter <- function(name, expression) {
  new_biogeme_expression(
    "distributed_parameter",
    name = validate_name(name, "name"),
    operand = as_biogeme_expression(expression)
  )
}

#' Integrate an expression over a standard normal random variable
#' @param expression Integrand expression.
#' @param name Random variable name.
#' @param number_of_quadrature_points Number of quadrature points.
#' @return A Biogeme expression.
#' @export
integrate_normal <- function(expression, name, number_of_quadrature_points = 30L) {
  if (!is.numeric(number_of_quadrature_points) || length(number_of_quadrature_points) != 1L ||
      is.na(number_of_quadrature_points) || number_of_quadrature_points < 1 ||
      number_of_quadrature_points != floor(number_of_quadrature_points)) {
    stop("number_of_quadrature_points must be one positive integer.", call. = FALSE)
  }
  new_biogeme_expression(
    "integrate_normal",
    operand = as_biogeme_expression(expression),
    name = validate_name(name, "name"),
    number_of_quadrature_points = as.integer(number_of_quadrature_points)
  )
}

#' Monte Carlo average of an expression
#' @param expression Expression to integrate.
#' @return A Biogeme expression.
#' @export
monte_carlo <- function(expression) {
  new_biogeme_expression("monte_carlo", operand = as_biogeme_expression(expression))
}

#' Aggregate an observation-level likelihood over a panel trajectory
#' @param expression Observation-level likelihood or probability expression.
#' @return A Biogeme expression.
#' @export
panel_likelihood_trajectory <- function(expression) {
  new_biogeme_expression(
    "panel_likelihood_trajectory",
    operand = as_biogeme_expression(expression)
  )
}

#' Symbolically differentiate an expression with respect to a named variable
#' @param expression Expression to differentiate.
#' @param name Variable or parameter name.
#' @return A Biogeme expression.
#' @export
derive <- function(expression, name) {
  new_biogeme_expression(
    "derive",
    operand = as_biogeme_expression(expression),
    name = validate_name(name, "name")
  )
}

#' @rdname derive
#' @export
Derive <- derive

#' Box--Cox transformation
#' @param x Expression to transform.
#' @param lambda Box--Cox exponent expression.
#' @return A Biogeme expression.
#' @export
boxcox <- function(x, lambda) {
  new_biogeme_expression(
    "boxcox",
    left = as_biogeme_expression(x),
    right = as_biogeme_expression(lambda)
  )
}

validate_piecewise_thresholds <- function(thresholds) {
  if (!is.numeric(thresholds) && !is.list(thresholds)) {
    stop("thresholds must be numeric, with optional NULL endpoints.", call. = FALSE)
  }
  thresholds <- as.list(thresholds)
  if (length(thresholds) < 2L || all(vapply(thresholds, is.null, logical(1)))) {
    stop("thresholds must contain at least two values and not be all NULL.", call. = FALSE)
  }
  if (any(vapply(thresholds[-c(1L, length(thresholds))], is.null, logical(1)))) {
    stop("Only the first and last piecewise thresholds may be NULL.", call. = FALSE)
  }
  values <- vapply(thresholds, function(value) {
    if (is.null(value)) return(NA_real_)
    if (!is.numeric(value) || length(value) != 1L || is.na(value) || !is.finite(value)) {
      stop("Piecewise thresholds must be finite numeric values or NULL.", call. = FALSE)
    }
    as.numeric(value)
  }, numeric(1))
  finite <- values[is.finite(values)]
  if (length(finite) > 1L && any(diff(finite) <= 0)) {
    stop("Finite piecewise thresholds must be strictly increasing.", call. = FALSE)
  }
  thresholds
}

#' Piecewise-linear expression using native Biogeme naming rules
#' @param expression Variable expression.
#' @param thresholds Piecewise thresholds; only endpoints may be NULL.
#' @param betas Optional list of parameter expressions.
#' @param transform Whether to return the full formula or the transformed
#'   variable form.
#' @return A Biogeme expression.
#' @export
piecewise <- function(expression, thresholds, betas = NULL, transform = c("formula", "variable")) {
  transform <- match.arg(transform)
  thresholds <- validate_piecewise_thresholds(thresholds)
  expected_betas <- if (transform == "variable") length(thresholds) - 2L else length(thresholds) - 1L
  if (!is.null(betas)) {
    if (!is.list(betas) || length(betas) != expected_betas) {
      stop("betas must contain one expression per piecewise interval.", call. = FALSE)
    }
    betas <- lapply(betas, as_biogeme_expression)
  }
  new_biogeme_expression(
    "piecewise",
    operand = as_biogeme_expression(expression),
    thresholds = thresholds,
    betas = betas,
    transform = transform
  )
}

format_biogeme_expression <- function(x) {
  if (!is_biogeme_expression(x)) {
    return(format(x))
  }
  if (identical(x$kind, "numeric")) {
    return(format(x$value))
  }
  if (identical(x$kind, "variable")) {
    return(x$name)
  }
  if (identical(x$kind, "beta")) {
    return(x$name)
  }
  if (identical(x$kind, "unary")) {
    operand <- format_biogeme_expression(x$operand)
    if (identical(x$operator, "-")) {
      return(paste0("(-", operand, ")"))
    }
    return(paste0(x$operator, "(", operand, ")"))
  }
  if (identical(x$kind, "function")) {
    args <- vapply(x$args, format_biogeme_expression, character(1))
    return(paste0(x$operator, "(", paste(args, collapse = ", "), ")"))
  }
  if (identical(x$kind, "elem")) {
    entries <- vapply(seq_along(x$mapping), function(i) {
      paste0(x$keys[[i]], ": ", format_biogeme_expression(x$mapping[[i]]))
    }, character(1))
    return(paste0("Elem({", paste(entries, collapse = ", "), "}, ",
      format_biogeme_expression(x$index), ")"))
  }
  if (identical(x$kind, "catalog")) {
    return(paste0(
      "Catalog(", x$name, ": ",
      paste(names(x$expressions), collapse = " | "), ")"
    ))
  }
  if (identical(x$kind, "draw")) {
    return(paste0("Draws(\"", x$name, "\", \"", x$draw_type, "\")"))
  }
  if (identical(x$kind, "random_variable")) {
    return(paste0("RandomVariable(\"", x$name, "\")"))
  }
  if (identical(x$kind, "distributed_parameter")) {
    return(paste0(
      "DistributedParameter(\"", x$name, "\", ",
      format_biogeme_expression(x$operand), ")"
    ))
  }
  if (identical(x$kind, "integrate_normal")) {
    return(paste0("IntegrateNormal(", format_biogeme_expression(x$operand), ", \"",
      x$name, "\")"))
  }
  if (identical(x$kind, "monte_carlo")) {
    return(paste0("MonteCarlo(", format_biogeme_expression(x$operand), ")"))
  }
  if (identical(x$kind, "panel_likelihood_trajectory")) {
    return(paste0("PanelLikelihoodTrajectory(", format_biogeme_expression(x$operand), ")"))
  }
  if (identical(x$kind, "derive")) {
    return(paste0("Derive(", format_biogeme_expression(x$operand), ", \"", x$name, "\")"))
  }
  if (identical(x$kind, "boxcox")) {
    return(paste0("BoxCox(", format_biogeme_expression(x$left), ", ",
      format_biogeme_expression(x$right), ")"))
  }
  if (identical(x$kind, "piecewise")) {
    thresholds <- vapply(x$thresholds, function(value) {
      if (is.null(value)) return("NULL")
      format(value)
    }, character(1))
    return(paste0("piecewise(", format_biogeme_expression(x$operand), ", c(",
      paste(thresholds, collapse = ", "), "))"))
  }
  if (identical(x$kind, "binary")) {
    left <- format_biogeme_expression(x$left)
    right <- format_biogeme_expression(x$right)
    return(paste0("(", left, " ", x$operator, " ", right, ")"))
  }
  if (identical(x$kind, "linear_utility")) {
    terms <- vapply(x$terms, function(term) {
      paste0(
        format_biogeme_expression(term$beta), " * ",
        format_biogeme_expression(term$x)
      )
    }, character(1))
    return(paste(terms, collapse = " + "))
  }
  if (identical(x$kind, "segmented_beta")) {
    return(paste0(x$beta$name, " segmented by ", length(x$segmentations), " variable(s)"))
  }
  if (x$kind %in% c("logit_probability", "logit_log_probability")) {
    alternative <- if (is_biogeme_expression(x$alternative)) {
      format_biogeme_expression(x$alternative)
    } else {
      as.character(x$alternative)
    }
    return(paste0(
      if (x$kind == "logit_log_probability") "logit log probability" else "logit probability",
      " for alternative ", alternative
    ))
  }
  if (x$kind %in% c("ordered_logit", "ordered_probit")) {
    return(paste0(
      "ordered ",
      if (x$kind == "ordered_logit") "logit" else "probit",
      " log probability for response ",
      format_biogeme_expression(x$alternative)
    ))
  }
  if (x$kind %in% c(
      "nested_logit",
      "nested_logit_mev_mu",
      "nested_probability",
      "nested_endogenous_sampling",
      "cross_nested_logit",
      "cross_nested_logit_mu",
      "cross_nested_probability"
  )) {
    alternative <- if (is_biogeme_expression(x$alternative)) {
      format_biogeme_expression(x$alternative)
    } else {
      as.character(x$alternative)
    }
    return(paste0("nested logit probability for alternative ", alternative))
  }
  "<unknown Biogeme expression>"
}

#' @export
format.biogeme_expression <- function(x, ...) {
  format_biogeme_expression(x)
}

#' @export
print.biogeme_expression <- function(x, ...) {
  cat(format_biogeme_expression(x), "\n")
  invisible(x)
}

#' Collect parameter names from an expression
#'
#' @param expression A Biogeme expression.
#' @return A character vector of parameter names.
#' @keywords internal
collect_biogeme_parameters <- function(expression) {
  if (!is_biogeme_expression(expression)) {
    stop("expression must be a Biogeme expression.", call. = FALSE)
  }
  if (identical(expression$kind, "beta")) {
    return(expression$name)
  }
  if (identical(expression$kind, "segmented_beta")) {
    names <- paste0(expression$beta$name, "_ref")
    for (segmentation in expression$segmentations) {
      labels <- unname(segmentation$mapping)
      labels <- labels[labels != segmentation$reference]
      names <- c(names, paste0(expression$beta$name, "_diff_", labels))
    }
    return(names)
  }
  children <- biogeme_expression_children(expression)
  if (length(children) == 0L) return(character())
  unique(unlist(lapply(children, collect_biogeme_parameters), use.names = FALSE))
}

biogeme_expression_children <- function(expression) {
  if (!is_biogeme_expression(expression)) {
    stop("expression must be a Biogeme expression.", call. = FALSE)
  }
  kind <- expression$kind
  if (kind == "binary") return(list(expression$left, expression$right))
  if (kind == "linear_utility") {
    return(unlist(lapply(expression$terms, function(term) list(term$beta, term$x)), recursive = FALSE))
  }
  if (kind == "catalog") return(unname(expression$expressions))
  if (kind == "segmented_beta") return(list())
  if (kind %in% c("logit_probability", "logit_log_probability")) {
    return(c(
      unname(expression$utilities),
      unname(expression$availability),
      if (is_biogeme_expression(expression$alternative)) list(expression$alternative) else list()
    ))
  }
  if (kind %in% c("ordered_logit", "ordered_probit")) {
    return(c(
      list(expression$eta),
      expression$cutpoints,
      list(expression$alternative)
    ))
  }
  if (kind %in% c("nested_probability", "nested_logit", "nested_logit_mev_mu")) {
    return(c(
      unname(expression$utilities),
      unname(expression$availability),
      if (is_biogeme_expression(expression$alternative)) list(expression$alternative) else list(),
      lapply(expression$nests$nests, function(nest) nest$nest_parameter),
      if (kind == "nested_logit_mev_mu") list(expression$scale_parameter) else list()
    ))
  }
  if (kind == "nested_endogenous_sampling") {
    return(c(
      unname(expression$utilities),
      unname(expression$availability),
      unname(expression$correction),
      if (is_biogeme_expression(expression$alternative)) list(expression$alternative) else list(),
      lapply(expression$nests$nests, function(nest) nest$nest_parameter)
    ))
  }
  if (kind %in% c(
      "cross_nested_logit",
      "cross_nested_logit_mu",
      "cross_nested_probability"
  )) {
    return(c(
      unname(expression$utilities),
      unname(expression$availability),
      if (is_biogeme_expression(expression$alternative)) list(expression$alternative) else list(),
      unlist(lapply(expression$nests$nests, function(nest) {
        c(list(nest$nest_parameter), unname(nest$allocation))
      }), recursive = FALSE),
      if (kind == "cross_nested_logit_mu") list(expression$scale_parameter) else list()
    ))
  }
  if (kind %in% c("unary", "derive", "integrate_normal", "monte_carlo",
                  "panel_likelihood_trajectory", "distributed_parameter")) {
    return(list(expression$operand))
  }
  if (kind == "function") return(expression$args)
  if (kind == "elem") return(c(list(expression$index), unname(expression$mapping)))
  if (kind == "boxcox") return(list(expression$left, expression$right))
  if (kind == "piecewise") {
    return(c(list(expression$operand), if (is.null(expression$betas)) list() else expression$betas))
  }
  list()
}

#' Collect data-variable names from an expression
#' @param expression A Biogeme expression.
#' @return A character vector.
#' @keywords internal
collect_biogeme_variables <- function(expression) {
  if (!is_biogeme_expression(expression)) {
    stop("expression must be a Biogeme expression.", call. = FALSE)
  }
  result <- if (identical(expression$kind, "variable")) expression$name else character()
  if (identical(expression$kind, "segmented_beta")) {
    return(vapply(expression$segmentations, function(segmentation) segmentation$variable, character(1)))
  }
  children <- biogeme_expression_children(expression)
  if (length(children) > 0L) {
    result <- c(result, unlist(lapply(children, collect_biogeme_variables), use.names = FALSE))
  }
  unique(result)
}

Try the rbiogeme package in your browser

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

rbiogeme documentation built on Sept. 29, 2026, 5:09 p.m.