R/138_reductions_dcp2cone_dcp2cone.R

Defines functions .dcp2cone_expr .dcp2cone_tree .cvxr_vec

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

## CVXPY SOURCE: reductions/dcp2cone/dcp2cone.py
## Dcp2Cone -- reduce DCP problems to conic form
##
## Uses S7 generic dispatch (dcp_canonicalize, quad_canonicalize, has_dcp_canon)
## for expression-level canonicalization. When quad_obj=TRUE, also tries
## quad_canonicalize for quadratic atoms in the objective's affine subtree (QP path).
## This is NOT the same as graph_implementation (which is LinOp-level).


## -- S7 generics for canonicalization dispatch ----------------------
## These replace the old environment-based CANON_METHODS / QUAD_CANON_METHODS
## registries with proper S7 method dispatch.
##
## INHERITANCE SAFETY INVARIANT: Every atom subclass of a canon atom MUST have
## its own explicit method() registration. Without it, S7 would silently dispatch
## to the parent's canonicalizer -- potentially producing incorrect results.
## Verified safe pairs: Log/Log1p, Pnorm/PnormApprox, Power/PowerApprox,
## GeoMean/GeoMeanApprox -- all have explicit separate registrations.

#' DCP cone canonicalization dispatch
#'
#' Replaces CANON_METHODS environment lookup. Default: identity copy.
#' Each DCP atom registers its own method returning list(canon_expr, constraints).
#' @noRd
dcp_canonicalize <- new_generic("dcp_canonicalize", "expr",
  function(expr, args, ...) {
    S7_dispatch()
  }
)

method(dcp_canonicalize, S7_object) <- function(expr, args, ...) {
  list(expr_copy(expr, args), list())
}

#' Predicate: does this expression class have a DCP cone canonicalizer?
#'
#' Used by .dcp2cone_tree() for affine_above tracking. Returns TRUE only for
#' atoms with a registered dcp_canonicalize method (NOT FiniteSet).
#' @noRd
has_dcp_canon <- new_generic("has_dcp_canon", "expr",
  function(expr) {
    S7_dispatch()
  }
)

method(has_dcp_canon, S7_object) <- function(expr) FALSE

#' Quadratic canonicalization dispatch
#'
#' Replaces QUAD_CANON_METHODS environment lookup. Default: NULL sentinel
#' (meaning "no quad canon for this class" -- fall through to dcp_canonicalize).
#' Guard logic (.quadratic_power(), is_qpwa()) is inside each method.
#' @noRd
quad_canonicalize <- new_generic("quad_canonicalize", "expr",
  function(expr, args, ...) {
    S7_dispatch()
  }
)

method(quad_canonicalize, S7_object) <- function(expr, args, ...) NULL

# -- Helper: vectorize an expression (column-major) -----------------
## Equivalent to CVXPY vec(x, order='F')
.cvxr_vec <- function(x) {
  Reshape(x, c(expr_size(x), 1L), order = "F")
}

# -- Dcp2Cone class -------------------------------------------------
## CVXPY SOURCE: dcp2cone.py lines 31-145

Dcp2Cone <- new_class("Dcp2Cone", parent = Canonicalization,
  package = "CVXR",
  properties = list(
    quad_obj = class_logical
  ),
  constructor = function(quad_obj = FALSE) {
    new_object(S7_object(),
      .cache = new.env(parent = emptyenv()),
      quad_obj = quad_obj
    )
  }
)

## accepts: problem must be Minimize and DCP
## CVXPY SOURCE: dcp2cone.py lines 47-50
method(reduction_accepts, Dcp2Cone) <- function(x, problem, ...) {
  S7_inherits(problem@objective, Minimize) && is_dcp(problem)
}

## apply: override to pass affine_above flag
## CVXPY SOURCE: dcp2cone.py lines 52-76
method(reduction_apply, Dcp2Cone) <- function(x, problem, ...) {
  if (!reduction_accepts(x, problem)) {
    cli_abort("Cannot reduce problem to cone program: must be a minimization DCP problem.")
  }

  inverse_data <- InverseData(problem)

  ## Canonicalize objective (affine_above = TRUE for objective)
  obj_result <- .dcp2cone_tree(x@quad_obj, problem@objective, TRUE)
  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 <- .dcp2cone_tree(x@quad_obj, con, FALSE)
    all_chunks[[i + 1L]] <- c(con_result[[2L]], list(con_result[[1L]]))
    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)
}

# -- Dcp2Cone-specific tree walk ------------------------------------
## CVXPY SOURCE: dcp2cone.py lines 78-107
## The key difference from base Canonicalization is the affine_above tracking:
## if the path from root to current node is all affine, we may skip cone canon.

.dcp2cone_tree <- function(quad_obj, expr, affine_above) {
  ## Determine if this atom is affine (no DCP cone canonicalizer registered)
  affine_atom <- !has_dcp_canon(expr)

  ## Recurse into each argument -- pre-allocate, flatten once
  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 <- .dcp2cone_tree(quad_obj,
                                  expr@args[[i]],
                                  affine_atom && affine_above)
    canon_args[[i]] <- arg_result[[1L]]
    constr_chunks[[i]] <- arg_result[[2L]]
  }

  ## Canonicalize this node
  ## CVXPY passes affine_above (NOT affine_atom && affine_above) here.
  ## affine_above means "is the path ABOVE this node all affine?"
  ## This allows QuadForm (non-affine) at the top of an affine path
  ## to be dispatched to quad canon methods.
  node_result <- .dcp2cone_expr(quad_obj, expr, canon_args,
                                 affine_above = affine_above)
  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)
}

## .dcp2cone_expr: canonicalize a single node (Dcp2Cone version)
## CVXPY SOURCE: dcp2cone.py lines 109-145
.dcp2cone_expr <- function(quad_obj, expr, args, affine_above = FALSE) {
  ## Skip constants with no parameters
  if (S7_inherits(expr, Expression) &&
      is_constant(expr) && length(parameters(expr)) == 0L) {
    return(list(expr, list()))
  }

  ## QP path: try quad_canonicalize first when quad_obj=TRUE
  ## and the path above is all affine.
  ## Guard logic (.quadratic_power, is_qpwa) is inside each quad method.
  ## NULL return = guard failed or no quad method -> fall through to DCP.
  ## CVXPY SOURCE: dcp2cone.py lines 128-139
  if (quad_obj && affine_above) {
    quad_result <- quad_canonicalize(expr, args)
    if (!is.null(quad_result)) return(quad_result)
  }

  ## DCP cone canonicalization (S7 dispatch -- default returns identity copy)
  dcp_canonicalize(expr, args)
}

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.