R/230_reductions_dgp2dcp_dgp2dcp.R

Defines functions .make_dgp_methods .dgp_log_transform_bound .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() {
    if (FALSE) new_object(S7_object())  ## S7 static-check guard
    .fast_new(Dgp2Dcp, 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

  ## Original leaf objects keyed by id, for the dict-diff chain rule
  ## (#3147 part A): var_backward/var_forward multiply by value(orig_var) =
  ## exp(log_var) = GP-space value; param_*  by 1/value(orig_param).
  ## CVXPY SOURCE: dgp2dcp.py:178 (self._id_to_var).
  id_to_var <- list()
  for (v in variables(problem)) id_to_var[[as.character(v@id)]] <- v
  x@.cache$id_to_var <- id_to_var
  id_to_param <- list()
  for (p in parameters(problem)) id_to_param[[as.character(p@id)]] <- p
  x@.cache$id_to_param <- id_to_param

  ## 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(.id(con)), .id(con_result[[1L]]),
           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(.id(param))
    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(.args(expr))
  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, .args(expr)[[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_is(expr, Variable)) {
    return(dgp_methods$variable_canon(expr, args))
  }
  ## Parameter -> stateful parameter_canon
  if (.s7_is(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)
## ==================================================================

## Transform a DGP bound to log space. CVXPY SOURCE:
## dgp2dcp/canonicalizers/__init__.py:126-162 (_log_transform_bound).
## A bound `b` on a positive-domain variable maps to log(b) in the log domain.
## Returns list(log_bound, aux_constraints) where aux_constraints is non-empty
## only for parametric Expression bounds (canonicalized through the DGP tree).
.dgp_log_transform_bound <- function(bound, methods) {
  if (.s7_is(bound, Expression)) {
    if (length(parameters(bound)) > 0L) {
      ## Parametric bound: canonicalize through the DGP tree to log space.
      return(.dgp2dcp_tree(methods, bound))
    }
    ## Parameter-free Expression: evaluate numerically.
    return(list(Constant(log(value(bound))), list()))
  }
  ## Numeric vector (already broadcast to n_elem at leaf construction).
  ## Preserve sentinels: -Inf (no lower bound) stays -Inf; log(Inf) = Inf is fine.
  log_b <- bound
  neg_inf <- bound == -Inf
  log_b[!neg_inf] <- log(bound[!neg_inf])
  log_b[neg_inf] <- -Inf
  list(log_b, list())
}

.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())
  ## Self-reference so variable_canon can canonicalize parametric bounds via
  ## the DGP tree walk (.dgp2dcp_tree).
  methods <- NULL

  variable_canon <- function(variable, args) {
    vid <- as.character(.id(variable))
    if (exists(vid, envir = vars_cache, inherits = FALSE)) {
      return(list(get(vid, envir = vars_cache, inherits = FALSE), list()))
    }
    ## Swap the positive variable for an unconstrained log-space variable,
    ## transforming any bounds to log space so the downstream CvxAttr2Constr
    ## lowers them to constraints. CVXPY SOURCE:
    ## dgp2dcp/canonicalizers/__init__.py:186-208 (variable_canon).
    bounds <- .attributes(variable)$bounds
    constrs <- list()
    if (!is.null(bounds)) {
      lb_res <- .dgp_log_transform_bound(bounds[[1L]], methods)
      ub_res <- .dgp_log_transform_bound(bounds[[2L]], methods)
      constrs <- c(lb_res[[2L]], ub_res[[2L]])
      log_var <- Variable(.shape(variable), var_id = .id(variable),
                          bounds = list(lb_res[[1L]], ub_res[[1L]]))
    } else {
      log_var <- Variable(.shape(variable), var_id = .id(variable))
    }
    assign(vid, log_var, envir = vars_cache)
    list(log_var, constrs)
  }

  parameter_canon <- function(parameter, args) {
    pid <- as.character(.id(parameter))
    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(.shape(parameter), 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())
  }

  ## Populate the self-reference captured by variable_canon (parametric bounds).
  methods <- list(
    variable_canon = variable_canon,
    parameter_canon = parameter_canon,
    vars_cache = vars_cache,
    params_cache = params_cache
  )
  methods
}

# -- Dgp2Dcp derivative chain rule (Phase 4.4) --------------------
## CVXPY SOURCE: dgp2dcp.py:79-118.
##
## DGP rewrites every variable as `x = exp(log_var)` and every
## parameter as `p = exp(log_param)`.  The chain rule then becomes:
##   backward (gradient): d/d(param)   = (1 / param) * d/d(log_param)
##                        d/d(var)     = var          * d/d(log_var)
##   forward  (delta):    d(log_param) = (1 / param) * d(param)
##                        d(var)       = var          * d(log_var)

## Dict-in/dict-out chain rule (#3147 part A). All ops keep operands dimensioned
## (`dim(pv) <- dim(g)` errors on a length mismatch instead of recycling --
## ADR D_19.5 addendum 2). Log leaves share the original variable id (var_id =
## variable@id) but get a FRESH parameter id; we key by those accordingly.

## CVXPY SOURCE: dgp2dcp.py:137-159 (param_backward). inner -> outer, ADDING the
## log-chain gradient to any pre-existing direct gradient (a parameter used both
## as a base log(p) and directly as a power exponent).
method(param_backward, Dgp2Dcp) <- function(x, dparams) {
  dgp_methods <- x@.cache$dgp_methods
  id_to_param <- x@.cache$id_to_param
  if (is.null(dgp_methods)) return(dparams)
  params_cache <- dgp_methods$params_cache
  for (opid in ls(params_cache, all.names = TRUE)) {
    log_param <- get(opid, envir = params_cache, inherits = FALSE)
    log_pid <- as.character(.id(log_param))
    if (is.null(dparams[[log_pid]])) next
    lg <- dparams[[log_pid]]
    pv <- value(id_to_param[[opid]]); dim(pv) <- dim(lg)   # keep-dims guard
    log_grad <- (1 / pv) * lg
    dparams[[log_pid]] <- NULL                              # pop the log id
    if (!is.null(dparams[[opid]])) {
      base <- dparams[[opid]]; dim(base) <- dim(log_grad)   # guard
      dparams[[opid]] <- base + log_grad                    # ADD (dual use)
    } else {
      dparams[[opid]] <- log_grad
    }
  }
  dparams
}

## CVXPY SOURCE: dgp2dcp.py:161-178 (param_forward). outer -> inner; keep the
## direct delta in place (do not pop param.id) and add the log-space delta.
method(param_forward, Dgp2Dcp) <- function(x, param_deltas) {
  dgp_methods <- x@.cache$dgp_methods
  id_to_param <- x@.cache$id_to_param
  if (is.null(dgp_methods)) return(param_deltas)
  params_cache <- dgp_methods$params_cache
  for (opid in ls(params_cache, all.names = TRUE)) {
    if (is.null(param_deltas[[opid]])) next
    log_param <- get(opid, envir = params_cache, inherits = FALSE)
    log_pid <- as.character(.id(log_param))
    d <- param_deltas[[opid]]
    pv <- value(id_to_param[[opid]]); dim(pv) <- dim(d)     # keep-dims guard
    param_deltas[[log_pid]] <- (1 / pv) * d
  }
  param_deltas
}

## x = exp(log_var) => dx/d(log_var) = x = value(orig_var). The log var shares
## the original id, so the dict key is unchanged; we multiply in place.
## CVXPY SOURCE: dgp2dcp.py:97-135 (var_backward / var_forward).
method(var_backward, Dgp2Dcp) <- function(x, del_vars) {
  id_to_var <- x@.cache$id_to_var
  if (is.null(id_to_var)) return(del_vars)
  for (vid in names(del_vars)) {
    ov <- id_to_var[[vid]]
    if (is.null(ov)) next
    g <- del_vars[[vid]]
    vv <- value(ov); dim(vv) <- dim(g)                       # keep-dims guard
    del_vars[[vid]] <- g * vv
  }
  del_vars
}

method(var_forward, Dgp2Dcp) <- function(x, dvars) {
  id_to_var <- x@.cache$id_to_var
  if (is.null(id_to_var)) return(dvars)
  for (vid in names(dvars)) {
    ov <- id_to_var[[vid]]
    if (is.null(ov)) next
    d <- dvars[[vid]]
    vv <- value(ov); dim(vv) <- dim(d)                       # keep-dims guard
    dvars[[vid]] <- d * vv
  }
  dvars
}

Try the CVXR package in your browser

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

CVXR documentation built on Aug. 24, 2026, 9:10 a.m.