R/220_reductions_dgp2dcp_dgp2dcp.R

Defines functions .make_dgp_methods .dgp2dcp_expr .dgp2dcp_tree

#####
## DO NOT EDIT THIS FILE!! EDIT THE SOURCE INSTEAD: rsrc_tree/reductions/dgp2dcp/dgp2dcp.R
#####

## CVXPY SOURCE: reductions/dgp2dcp/dgp2dcp.py
## Dgp2Dcp -- reduces DGP problems to DCP problems via log-space transformation
##
## Key design:
## - Inherits from Canonicalization but uses its own tree walk (G1)
## - Per-problem DGP methods closure with variable/parameter caches (G2)
## - Stores dgp_methods on instance .cache for DPP reuse (G8)
## - Stores original problem in inverse_data@.extra (G3)


## dgp_canonicalize generic is defined in dgp_canonicalizers.R (loads before this file)

Dgp2Dcp <- new_class("Dgp2Dcp", parent = Canonicalization, package = "CVXR",
  constructor = function() {
    new_object(S7_object(),
      .cache = new.env(parent = emptyenv())
    )
  }
)

## -- reduction_accepts ---------------------------------------------
## CVXPY SOURCE: dgp2dcp.py line 62-65
method(reduction_accepts, Dgp2Dcp) <- function(x, problem, ...) {
  is_dgp(problem)
}

## -- reduction_apply -----------------------------------------------
## CVXPY SOURCE: dgp2dcp.py lines 119-128
## Creates per-problem DGP methods, then walks tree via own tree walk.
method(reduction_apply, Dgp2Dcp) <- function(x, problem, ...) {
  if (!reduction_accepts(x, problem)) {
    cli_abort("The supplied problem is not DGP.")
  }

  ## Create per-problem DGP methods (G2)
  dgp_methods <- .make_dgp_methods()

  ## Store on instance for DPP reuse (G8)
  x@.cache$dgp_methods <- dgp_methods

  ## Create inverse data
  inverse_data <- InverseData(problem)
  ## Store original problem (G3)
  inverse_data@.extra$problem <- problem

  ## Canonicalize objective via OWN tree walk (G1)
  obj_result <- .dgp2dcp_tree(dgp_methods, problem@objective)
  canon_objective <- obj_result[[1L]]

  ## Canonicalize each constraint -- collect chunks, flatten once
  n_cons <- length(problem@constraints)
  all_chunks <- vector("list", n_cons + 1L)
  all_chunks[[1L]] <- obj_result[[2L]]
  for (i in seq_len(n_cons)) {
    con <- problem@constraints[[i]]
    con_result <- .dgp2dcp_tree(dgp_methods, con)
    all_chunks[[i + 1L]] <- c(con_result[[2L]], list(con_result[[1L]]))
    ## Store constraint ID mapping
    assign(as.character(con@id), con_result[[1L]]@id,
           envir = inverse_data@cons_id_map)
  }
  canon_constraints <- unlist(all_chunks, recursive = FALSE)
  if (is.null(canon_constraints)) canon_constraints <- list()

  new_problem <- Problem(canon_objective, canon_constraints)
  list(new_problem, inverse_data)
}

## -- reduction_invert ----------------------------------------------
## CVXPY SOURCE: dgp2dcp.py lines 152-160
## Transform solution back from log-space: exp(value) for all primals.
method(reduction_invert, Dgp2Dcp) <- function(x, solution, inverse_data, ...) {
  ## First apply parent invert (handles cons_id_map remapping)
  ## S7: call Canonicalization method directly (no callNextMethod in S7)
  solution <- method(reduction_invert, Canonicalization)(x, solution, inverse_data, ...)

  if (solution@status == SOLVER_ERROR) return(solution)

  ## Transform primal vars back: exp(log_x) = x
  for (vid in names(solution@primal_vars)) {
    solution@primal_vars[[vid]] <- exp(solution@primal_vars[[vid]])
  }

  ## Transform objective: f(x) = exp(F(u))
  solution@opt_val <- exp(solution@opt_val)

  solution
}

## -- update_parameters ----------------------------------------------
## CVXPY SOURCE: dgp2dcp.py lines 67-78
## Called in DPP fast path: transforms original parameter values to log-space.
method(update_parameters, Dgp2Dcp) <- function(x, problem, ...) {
  dgp_methods <- x@.cache$dgp_methods
  if (is.null(dgp_methods)) return(invisible(NULL))
  params_cache <- dgp_methods$params_cache
  for (param in parameters(problem)) {
    pid <- as.character(param@id)
    if (exists(pid, envir = params_cache, inherits = FALSE)) {
      log_param <- get(pid, envir = params_cache, inherits = FALSE)
      value(log_param) <- log(value(param))
    }
  }
  invisible(NULL)
}


## ==================================================================
## Own tree walk functions (G1)
## ==================================================================

## .dgp2dcp_tree: recursive bottom-up walk (same structure as .canonicalize_tree
## but calls .dgp2dcp_expr which does NOT skip constants)
.dgp2dcp_tree <- function(dgp_methods, expr) {
  n_args <- length(expr@args)
  canon_args <- vector("list", n_args)
  constr_chunks <- vector("list", n_args + 1L)
  for (i in seq_len(n_args)) {
    arg_result <- .dgp2dcp_tree(dgp_methods, expr@args[[i]])
    canon_args[[i]] <- arg_result[[1L]]
    constr_chunks[[i]] <- arg_result[[2L]]
  }
  node_result <- .dgp2dcp_expr(dgp_methods, expr, canon_args)
  constr_chunks[[n_args + 1L]] <- node_result[[2L]]
  constrs <- unlist(constr_chunks, recursive = FALSE)
  if (is.null(constrs)) constrs <- list()
  list(node_result[[1L]], constrs)
}

## .dgp2dcp_expr: canonicalize a single node
## NO constant-skipping (G1). Variable/Parameter dispatch via dgp_methods (G2).
.dgp2dcp_expr <- function(dgp_methods, expr, args) {
  ## Variable -> stateful variable_canon
  if (S7_inherits(expr, Variable)) {
    return(dgp_methods$variable_canon(expr, args))
  }
  ## Parameter -> stateful parameter_canon
  if (S7_inherits(expr, Parameter)) {
    return(dgp_methods$parameter_canon(expr, args))
  }
  ## S7 dispatch via dgp_canonicalize -- NULL means no method registered
  result <- dgp_canonicalize(expr, args)
  if (!is.null(result)) return(result)
  ## Default: copy with canonicalized args
  list(expr_copy(expr, args), list())
}


## ==================================================================
## Per-problem DGP methods closure (G2)
## ==================================================================

.make_dgp_methods <- function() {
  ## Per-problem caches for dedup
  vars_cache <- new.env(hash = TRUE, parent = emptyenv())
  params_cache <- new.env(hash = TRUE, parent = emptyenv())

  variable_canon <- function(variable, args) {
    vid <- as.character(variable@id)
    if (exists(vid, envir = vars_cache, inherits = FALSE)) {
      return(list(get(vid, envir = vars_cache, inherits = FALSE), list()))
    }
    ## Create unconstrained log-space variable (same shape, preserves ID)
    log_var <- Variable(variable@shape, var_id = variable@id)
    assign(vid, log_var, envir = vars_cache)
    list(log_var, list())
  }

  parameter_canon <- function(parameter, args) {
    pid <- as.character(parameter@id)
    if (exists(pid, envir = params_cache, inherits = FALSE)) {
      return(list(get(pid, envir = params_cache, inherits = FALSE), list()))
    }
    ## Create log-space parameter (DPP: may not have value yet)
    log_param <- Parameter(parameter@shape, name = expr_name(parameter))
    if (!is.null(value(parameter))) {
      value(log_param) <- log(value(parameter))
    }
    assign(pid, log_param, envir = params_cache)
    list(log_param, list())
  }

  list(
    variable_canon = variable_canon,
    parameter_canon = parameter_canon,
    vars_cache = vars_cache,
    params_cache = params_cache
  )
}

## DEFERRED: Derivative chain-rule overrides (dgp2dcp.py lines 80-112)
## Dgp2Dcp transforms parameters via log, so the chain rule for derivatives
## requires log/exp Jacobian correction:
##
## param_backward(x, param, dparams) -- multiply gradient by param.value (exp(log_param))
## param_forward(x, param, delta) -- divide delta by param.value
## var_backward(x, var, value) -- multiply by exp(log_var.value)
## var_forward(x, var, value) -- divide by exp(log_var.value)
##
## Deferred: derivative API depends on diffcp. See notes/derivative_api_deferred.md.

Try the CVXR package in your browser

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

CVXR documentation built on March 6, 2026, 9:10 a.m.