R/258_reductions_dcp2cone_cone_matrix_stuffing.R

Defines functions .extract_mip_idx .extract_bounds_tensor .bound_expr_for_tensor .has_parametric_bounds .extract_upper_bounds .extract_lower_bounds dims_to_solver_dict

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

## CVXPY SOURCE: reductions/dcp2cone/cone_matrix_stuffing.py
## ConeMatrixStuffing -- convert affine expressions to sparse matrices
## ConeDims -- summary of cone dimensions


# ==================================================================
# ConeDims -- summary of cone dimensions present in constraints
# ==================================================================
## CVXPY SOURCE: cone_matrix_stuffing.py lines 57-141

ConeDims <- new_class("ConeDims", package = "CVXR",
  properties = list(
    zero   = class_integer,
    nonneg = class_integer,
    exp    = class_integer,
    soc    = class_integer,     # vector of SOC cone dims
    psd    = class_integer,     # vector of PSD sizes (n for n x n)
    p3d    = class_numeric,     # vector of PowCone3D alphas
    pnd    = class_list         # list of numeric vectors for PowConeND
  ),
  constructor = function(constr_map) {
    if (FALSE) new_object(S7_object())  ## S7 static-check guard
    ## Zero cone dimension
    zero_constrs <- constr_map[["Zero"]]
    zero <- if (length(zero_constrs) == 0L) 0L
            else as.integer(sum(vapply(zero_constrs, constr_size, numeric(1L))))

    ## NonNeg cone dimension
    nn_constrs <- constr_map[["NonNeg"]]
    nonneg <- if (length(nn_constrs) == 0L) 0L
              else as.integer(sum(vapply(nn_constrs, constr_size, numeric(1L))))

    ## Exponential cone count
    exp_constrs <- constr_map[["ExpCone"]]
    exp_count <- if (length(exp_constrs) == 0L) 0L
                 else as.integer(sum(vapply(exp_constrs, num_cones, numeric(1L))))

    ## SOC dimensions (one entry per individual cone)
    soc_constrs <- constr_map[["SOC"]]
    soc <- if (length(soc_constrs) == 0L) integer(0)
           else as.integer(unlist(lapply(soc_constrs, cone_sizes)))

    ## PSD dimensions (the matrix side length n, whether the constraint is a
    ## full-matrix PSD or the packed SvecPSD the solver actually receives).
    ## CVXPY SOURCE: cone_matrix_stuffing.py:92-93
    psd_constrs <- c(constr_map[["PSD"]], constr_map[["SvecPSD"]])
    psd <- if (length(psd_constrs) == 0L) integer(0)
           else as.integer(unlist(lapply(psd_constrs, cone_sizes)))

    ## PowCone3D alphas
    p3d_constrs <- constr_map[["PowCone3D"]]
    p3d <- if (length(p3d_constrs) == 0L) numeric(0)
           else unlist(lapply(p3d_constrs, function(c) as.numeric(value(c@alpha))))

    ## PowConeND alphas
    pnd_constrs <- constr_map[["PowConeND"]]
    pnd <- if (length(pnd_constrs) == 0L) list()
           else {
             alpha_chunks <- vector("list", length(pnd_constrs))
             for (ci in seq_along(pnd_constrs)) {
               a <- value(pnd_constrs[[ci]]@alpha)
               if (is.matrix(a)) {
                 ## Each column is one cone's alpha vector
                 alpha_chunks[[ci]] <- lapply(seq_len(ncol(a)), function(j) a[, j])
               } else {
                 alpha_chunks[[ci]] <- list(as.numeric(a))
               }
             }
             unlist(alpha_chunks, recursive = FALSE)
           }

    .fast_new(ConeDims, S7_object(),
      zero = zero, nonneg = nonneg, exp = exp_count,
      soc = soc, psd = psd, p3d = p3d, pnd = pnd)
  }
)

method(print, ConeDims) <- function(x, ...) {
  cat(sprintf("%d equalities, %d inequalities, %d exponential cones\n",
    x@zero, x@nonneg, x@exp))
  cat(sprintf("SOC: %s, PSD: %s\n",
    paste(x@soc, collapse = ","),
    paste(x@psd, collapse = ",")))
  invisible(x)
}

# -- dims_to_solver_dict -------------------------------------------
## CVXPY SOURCE: conic_solver.py lines 87-97

dims_to_solver_dict <- function(cone_dims) {
  list(
    f   = cone_dims@zero,
    l   = cone_dims@nonneg,
    q   = cone_dims@soc,
    ep  = cone_dims@exp,
    s   = cone_dims@psd,
    p   = cone_dims@p3d,
    pnd = cone_dims@pnd
  )
}


# -- extract_mip_idx -----------------------------------------------
## CVXPY SOURCE: matrix_stuffing.py lines 79-96
## Maps per-variable boolean/integer flags to global flattened variable indices.

.extract_lower_bounds <- function(vars, x_length) {
  has_bounds <- any(vapply(vars, function(v) {
    b <- .attributes(v)$bounds
    (!is.null(b) && is.list(b) && !all(is.infinite(as.numeric(b[[1L]])))) ||
      is_nonneg(v)
  }, logical(1L)))
  if (!has_bounds) return(NULL)

  lower <- rep(-Inf, x_length)
  offset <- 0L
  for (v in vars) {
    sz <- expr_size(v)
    idx <- (offset + 1L):(offset + sz)
    b <- .attributes(v)$bounds
    if (!is.null(b) && is.list(b)) {
      if (.s7_is(b[[1L]], Expression) || inherits(b[[1L]], "Matrix")) {
        cli_abort("Sparse or parametric bounds should not reach matrix stuffing.")
      }
      lower[idx] <- as.numeric(array(b[[1L]], dim = .shape(v)))
    }
    if (is_nonneg(v)) lower[idx] <- pmax(lower[idx], 0)
    offset <- offset + sz
  }
  lower
}

.extract_upper_bounds <- function(vars, x_length) {
  has_bounds <- any(vapply(vars, function(v) {
    b <- .attributes(v)$bounds
    (!is.null(b) && is.list(b) && !all(is.infinite(as.numeric(b[[2L]])))) ||
      is_nonpos(v)
  }, logical(1L)))
  if (!has_bounds) return(NULL)

  upper <- rep(Inf, x_length)
  offset <- 0L
  for (v in vars) {
    sz <- expr_size(v)
    idx <- (offset + 1L):(offset + sz)
    b <- .attributes(v)$bounds
    if (!is.null(b) && is.list(b)) {
      if (.s7_is(b[[2L]], Expression) || inherits(b[[2L]], "Matrix")) {
        cli_abort("Sparse or parametric bounds should not reach matrix stuffing.")
      }
      upper[idx] <- as.numeric(array(b[[2L]], dim = .shape(v)))
    }
    if (is_nonpos(v)) upper[idx] <- pmin(upper[idx], 0)
    offset <- offset + sz
  }
  upper
}

.has_parametric_bounds <- function(vars) {
  any(vapply(vars, function(v) {
    b <- .attributes(v)$bounds
    !is.null(b) && is.list(b) &&
      any(vapply(b, function(x) .s7_is(x, Expression), logical(1L)))
  }, logical(1L)))
}

.bound_expr_for_tensor <- function(v, which) {
  bound_idx <- if (identical(which, "lower")) 1L else 2L
  default_val <- if (identical(which, "lower")) -Inf else Inf
  sz <- expr_size(v)
  b <- .attributes(v)$bounds

  if (identical(which, "lower") && is_nonneg(v)) {
    return(Constant(matrix(0, sz, 1L)))
  }
  if (identical(which, "upper") && is_nonpos(v)) {
    return(Constant(matrix(0, sz, 1L)))
  }

  if (!is.null(b) && is.list(b)) {
    slot <- b[[bound_idx]]
    if (.s7_is(slot, Expression)) {
      if (expr_is_scalar(slot) && sz > 1L) {
        slot <- cvxr_promote(slot, .shape(v))
      }
      if (!identical(as.integer(.shape(slot)), as.integer(c(sz, 1L)))) {
        slot <- reshape_expr(slot, c(sz, 1L), order = "F")
      }
      return(slot)
    }
    if (inherits(slot, "Matrix")) {
      cli_abort("Sparse bounds should not reach matrix stuffing.")
    }
    return(Constant(matrix(as.numeric(array(slot, dim = .shape(v))), sz, 1L)))
  }

  Constant(matrix(default_val, sz, 1L))
}

.extract_bounds_tensor <- function(vars, x_length, param_to_size, param_id_map,
                                   which = c("lower", "upper")) {
  which <- match.arg(which)
  op_list <- lapply(vars, function(v) {
    canonical_form(.bound_expr_for_tensor(v, which))[[1L]]
  })
  get_problem_matrix_tensor(
    op_list,
    id_to_col = integer(0),
    var_length = 0L,
    param_to_size = param_to_size,
    param_id_map = param_id_map
  )
}

.extract_mip_idx <- function(problem, inverse_data) {
  vars <- variables(problem)
  bool_chunks <- vector("list", length(vars))
  int_chunks  <- vector("list", length(vars))

  for (i in seq_along(vars)) {
    var <- vars[[i]]
    vid <- as.character(.id(var))
    offset <- inverse_data@var_offsets[[vid]]
    sz <- expr_size(var)
    ## CVXPY SOURCE: matrix_stuffing.py:107-119 -- shift each variable's OWN
    ## index set by its offset in the stuffed vector. Upstream ravels a numpy
    ## multi-index; CVXR's canonical form is already a flat 1-based vector
    ## (`.mip_idx`, expressions/leaf.R), so the shift is all that is left.
    ##
    ## Two INDEPENDENT ifs, matching upstream: a variable may carry a boolean
    ## index list AND an integer one (upstream's own test_bool_int_variable
    ## does exactly that). The `else if` this replaces silently dropped the
    ## integer set in that case.
    if (length(var@.boolean_idx) > 0L) {
      bool_chunks[[i]] <- offset + var@.boolean_idx
    }
    if (length(var@.integer_idx) > 0L) {
      int_chunks[[i]] <- offset + var@.integer_idx
    }
  }

  bool_idx <- unlist(bool_chunks)
  int_idx  <- unlist(int_chunks)

  ## INVARIANT: the two sets are disjoint, boolean winning.
  ##
  ## Boolean is a SUBSET of integer -- {0,1} is the integers intersected with
  ## [0,1] -- so an entry named by both is not a conflict, and applying both
  ## constraints yields the boolean one. Upstream never resolves this: each
  ## solver marks integrality from the union and then applies the [0,1]
  ## restriction for the boolean list ON TOP (scipy_conif.py:132-141,
  ## cbc_conif.py:154-160), so boolean wins there by write order.
  ##
  ## CVXR's eight MIP interfaces happen to apply boolean FIRST and integer
  ## second, so inheriting upstream's overlap would silently give the opposite
  ## answer -- and would leave the semantics resting on the statement order in
  ## six separate files, which is the same duplicated-knowledge shape that
  ## produced the transposed PSD duals. Enforcing disjointness once here is
  ## order-independent and behaviourally identical to upstream.
  if (!is.null(bool_idx) && !is.null(int_idx)) {
    int_idx <- setdiff(int_idx, bool_idx)
  }
  list(bool_idx = if (is.null(bool_idx)) integer(0) else bool_idx,
       int_idx  = if (is.null(int_idx))  integer(0) else int_idx)
}

# ==================================================================
# ConeMatrixStuffing -- reduction from affine expressions to matrices
# ==================================================================
## CVXPY SOURCE: cone_matrix_stuffing.py lines 321-470

ConeMatrixStuffing <- new_class("ConeMatrixStuffing", parent = Reduction,
  package = "CVXR",
  properties = list(
    quad_obj = class_logical
  ),
  constructor = function(quad_obj = FALSE) {
    if (FALSE) new_object(S7_object())  ## S7 static-check guard
    .fast_new(ConeMatrixStuffing, S7_object(),
      .cache = new.env(parent = emptyenv()),
      quad_obj = quad_obj
    )
  }
)

## accepts: affine (or quadratic when quad_obj) objective (Minimize),
## no convex attributes, all constraint args affine
## CVXPY SOURCE: cone_matrix_stuffing.py lines 335-342
method(reduction_accepts, ConeMatrixStuffing) <- function(x, problem, ...) {
  is_min <- .s7_is(problem@objective, Minimize)
  obj_expr <- .args(problem@objective)[[1L]]
  valid_obj <- if (x@quad_obj) {
    is_affine(obj_expr) || is_quadratic(obj_expr)
  } else {
    is_affine(obj_expr)
  }
  no_cvx_attr <- length(convex_attributes(variables(problem))) == 0L
  aff_con <- are_args_affine(problem@constraints)
  is_min && valid_obj && no_cvx_attr && aff_con
}

## apply: lower constraints and extract A, b, c matrices
## CVXPY SOURCE: cone_matrix_stuffing.py lines 360-430
method(reduction_apply, ConeMatrixStuffing) <- function(x, problem, ...) {
  inverse_data <- InverseData(problem)

  ## Step 1: Lower constraints -- pre-allocate
  n_cons_raw <- length(problem@constraints)
  cons <- vector("list", n_cons_raw)
  for (i in seq_len(n_cons_raw)) {
    con <- problem@constraints[[i]]
    if (.s7_is(con, Equality)) {
      con <- lower_equality(con)
    } else if (.s7_is(con, Inequality)) {
      con <- lower_ineq_to_nonneg(con)
    } else if (.s7_is(con, NonPos)) {
      con <- nonpos2nonneg(con)
    } else if (.s7_is(con, SOC) && con@axis == 1L) {
      ## Transpose X for axis=1 -> axis=2
      con <- SOC(.args(con)[[1L]], t(.args(con)[[2L]]), axis = 2L,
                 constr_id = .id(con))
    } else if (.s7_is(con, PowConeND) && con@axis == 1L) {
      ## CVXPY SOURCE: cone_matrix_stuffing.py lines 382-388
      ## Transpose W and alpha for axis=1 -> axis=2
      con <- PowConeND(t(con@.W), con@.z,
                        t(con@alpha), axis = 2L,
                        constr_id = .id(con))
    }
    ## ExpCone/PowCone3D flattening: only needed if multidimensional
    ## For Phase 5b, our canonicalizers produce 1D args; skip flattening for now
    cons[[i]] <- con
  }

  ## Step 2: Group and order constraints
  ## CVXPY order: Zero, NonNeg, SOC, PSD, ExpCone, PowCone3D, PowConeND
  constr_map <- group_constraints(cons)
  ## CVXPY SOURCE: cone_matrix_stuffing.py:408-412 -- SvecPSD occupies the same
  ## slot in the cone ordering as PSD, immediately after it.
  ordered_cons <- c(constr_map[["Zero"]], constr_map[["NonNeg"]],
                    constr_map[["SOC"]], constr_map[["PSD"]],
                    constr_map[["SvecPSD"]],
                    constr_map[["ExpCone"]], constr_map[["PowCone3D"]],
                    constr_map[["PowConeND"]])

  ## Step 3: Store constraint ID mapping (identity -- already lowered)
  ## CVXPY SOURCE: cone_matrix_stuffing.py line 406
  id2cons <- new.env(hash = TRUE, parent = emptyenv())
  for (con in ordered_cons) {
    assign(as.character(.id(con)), .id(con), envir = inverse_data@cons_id_map)
    assign(as.character(.id(con)), con, envir = id2cons)
  }

  ## Step 4: Create CoeffExtractor and extract objective
  extractor <- CoeffExtractor(inverse_data)

  ## Create the flattened variable
  x_var <- Variable(c(extractor@x_length, 1L))

  ## Detect DPP path: parameters exist and no EvalParams was applied
  ## (i.e., parameters are still present in the expressions).
  has_params <- length(parameters(problem)) > 0L

  ## Extract objective
  P_mat <- NULL
  c_tensor <- NULL
  P_tensor <- NULL
  pv <- NULL  ## parameter vector, lazily computed when has_params
  if (x@quad_obj) {
    if (has_params) {
      ## CVXPY v1.9.0 #3142: DPP tensor path for a PARAMETRIC quadratic
      ## objective. Extract P and the linear term as parameter tensors so a
      ## re-solve rebuilds them from new parameter values (no baking, no
      ## staleness). The current numeric P/q/d for THIS solve come from
      ## contracting the tensors with the current parameter vector.
      qt <- coeff_quad_form_tensor(extractor, .args(problem@objective)[[1L]])
      P_tensor <- qt$P_tensor
      c_tensor <- qt$c_tensor
      pv <- get_parameter_vector(extractor@param_to_size,
                                  extractor@param_id_map,
                                  parameters(problem))
      pv_sparse <- Matrix::Matrix(pv, ncol = 1L, sparse = TRUE)
      ## Reshape the contracted vec(P) to (x_length, x_length) without
      ## densifying (-> general dgCMatrix; see .dpp_contract_reshape).
      P_mat <- .dpp_contract_reshape(P_tensor, pv_sparse,
                                     extractor@x_length, extractor@x_length)
      c_flat <- as.numeric(c_tensor %*% pv)
      c_vec <- c_flat[seq_len(extractor@x_length)]
      d_offset <- c_flat[extractor@x_length + 1L]
    } else {
      ## QP path: extract 0.5*x'*P*x + q'*x + d (numeric, P baked)
      quad_result <- coeff_quad_form(extractor, .args(problem@objective)[[1L]])
      P_mat <- quad_result$P          # already 2x scaled for solver
      c_vec <- quad_result$q          # linear term
      d_offset <- quad_result$offset  # constant
    }
  } else {
    if (has_params) {
      ## DPP tensor path: extract c_tensor (x_length+1, param_size)
      c_tensor <- coeff_affine_tensor(extractor, list(.args(problem@objective)[[1L]]))
      ## Also get current numeric values via apply
      pv <- get_parameter_vector(extractor@param_to_size,
                                  extractor@param_id_map,
                                  parameters(problem))
      c_flat <- as.numeric(c_tensor %*% pv)
      c_vec <- c_flat[seq_len(extractor@x_length)]
      d_offset <- c_flat[extractor@x_length + 1L]
    } else {
      ## LP/conic path: extract c'*x + d
      obj_result <- coeff_affine(extractor, list(.args(problem@objective)[[1L]]))
      c_vec <- as.numeric(obj_result$A)  # x_length vector
      d_offset <- as.numeric(obj_result$b)  # scalar offset
    }
  }

  ## Step 5: Batch constraint args and extract A, b
  expr_chunks <- vector("list", length(ordered_cons))
  for (i in seq_along(ordered_cons)) {
    expr_chunks[[i]] <- .args(ordered_cons[[i]])
  }
  expr_list <- unlist(expr_chunks, recursive = FALSE)
  if (is.null(expr_list)) expr_list <- list()

  A_tensor <- NULL
  if (length(expr_list) > 0L) {
    if (has_params) {
      ## DPP tensor path: extract A_tensor
      A_tensor <- coeff_affine_tensor(extractor, expr_list)
      ## Also get current numeric values via apply
      if (is.null(pv)) {
        pv <- get_parameter_vector(extractor@param_to_size,
                                    extractor@param_id_map,
                                    parameters(problem))
      }
      ## Reshape the contracted column-major vec to (n_rows, x_length + 1)
      ## without densifying; first x_length columns = A, last column = b.
      pv_sparse <- Matrix::Matrix(pv, ncol = 1L, sparse = TRUE)
      n_cols <- extractor@x_length + 1L
      n_rows <- nrow(A_tensor) %/% n_cols
      Ab <- .dpp_contract_reshape(A_tensor, pv_sparse, n_rows, n_cols)
      A <- Ab[, seq_len(extractor@x_length), drop = FALSE]
      b <- as.numeric(Ab[, n_cols])
    } else {
      con_result <- coeff_affine(extractor, expr_list)
      A <- con_result$A
      b <- con_result$b
    }
  } else {
    A <- Matrix::sparseMatrix(i = integer(0), j = integer(0), x = numeric(0),
                              dims = c(0L, extractor@x_length))
    b <- numeric(0)
  }

  ## Step 6: Build ConeDims
  cone_dims <- ConeDims(constr_map)

  ## Step 7: Store extra data for inversion
  inverse_data@.extra <- new.env(hash = TRUE, parent = emptyenv())
  inverse_data@.extra$minimize <- TRUE
  inverse_data@.extra$constraints <- ordered_cons
  inverse_data@.extra$id2cons <- id2cons

  ## Native variable bounds. CVXPY builds bound tensors when bounds depend on
  ## Parameters; CVXR mirrors that for 2D scalar-or-exact-shape bounds.
  vars_ <- variables(problem)
  if (.has_parametric_bounds(vars_)) {
    if (is.null(pv)) {
      pv <- get_parameter_vector(extractor@param_to_size,
                                  extractor@param_id_map,
                                  parameters(problem))
    }
    lb_tensor <- .extract_bounds_tensor(
      vars_, extractor@x_length, extractor@param_to_size,
      extractor@param_id_map, which = "lower"
    )
    ub_tensor <- .extract_bounds_tensor(
      vars_, extractor@x_length, extractor@param_to_size,
      extractor@param_id_map, which = "upper"
    )
    lower_bounds <- as.numeric(lb_tensor %*% pv)
    upper_bounds <- as.numeric(ub_tensor %*% pv)
  } else {
    lb_tensor <- NULL
    ub_tensor <- NULL
    lower_bounds <- .extract_lower_bounds(vars_, extractor@x_length)
    upper_bounds <- .extract_upper_bounds(vars_, extractor@x_length)
  }

  ## Return data as named list + inverse data
  data <- list()
  data[[SD_C]]      <- c_vec
  data[[SD_OFFSET]]  <- d_offset
  data[[SD_A]]      <- A
  data[[SD_B]]      <- b
  data[[SD_DIMS]]   <- cone_dims
  data[[LOWER_BOUNDS]] <- lower_bounds
  data[[UPPER_BOUNDS]] <- upper_bounds
  data[["x_id"]]    <- .id(x_var)
  data[["constraints"]] <- ordered_cons
  if (!is.null(P_mat)) data[[SD_P]] <- P_mat

  ## Step 8: Build ParamConeProg for DPP caching
  if (has_params && !is.null(c_tensor)) {
    if (is.null(A_tensor)) {
      A_tensor <- Matrix::sparseMatrix(
        i = integer(0), j = integer(0), x = numeric(0),
        dims = c(0L, sum(as.integer(unlist(extractor@param_to_size))))
      )
    }
    data[[SD_PARAM_PROB]] <- ParamConeProg(
      c_tensor = c_tensor,
      A_tensor = A_tensor,
      x_length = extractor@x_length,
      x_id = .id(x_var),
      parameters = parameters(problem),
      param_id_to_col = extractor@param_id_map,
      param_to_size = extractor@param_to_size,
      variables = variables(problem),
      var_id_to_col = inverse_data@var_offsets,
      constraints = ordered_cons,
      cone_dims = cone_dims,
      P_tensor = P_tensor,
      lower_bounds = data[[LOWER_BOUNDS]],
      upper_bounds = data[[UPPER_BOUNDS]],
      lb_tensor = lb_tensor,
      ub_tensor = ub_tensor
    )
  }

  ## Extract MIP indices for boolean/integer variables
  mip_idx <- .extract_mip_idx(problem, inverse_data)
  data[[SD_BOOL_IDX]] <- mip_idx$bool_idx
  data[[SD_INT_IDX]]  <- mip_idx$int_idx

  list(data, inverse_data)
}

## invert: split solution vector back into per-variable values
## CVXPY SOURCE: cone_matrix_stuffing.py lines 432-470
method(reduction_invert, ConeMatrixStuffing) <- function(x, solution, inverse_data, ...) {
  var_map <- inverse_data@var_offsets

  ## Flip sign of opt val if maximize
  opt_val <- solution@opt_val
  if (!(solution@status %in% ERROR_STATUS) &&
      !is.null(inverse_data@.extra$minimize) &&
      !inverse_data@.extra$minimize) {
    opt_val <- -opt_val
  }

  primal_vars <- list()
  dual_vars <- list()

  ## Remap dual variables. CVXPY v1.9.0 fix: #3197 -- hoisted above the
  ## failure short-circuit so an infeasibility-certificate dual (the dual ray)
  ## is remapped and returned for INF_OR_UNB solutions, not just optimal ones.
  if (length(solution@dual_vars) > 0L) {
    con_map_env <- inverse_data@cons_id_map
    id2cons <- inverse_data@.extra$id2cons
    old_ids <- ls(con_map_env, all.names = TRUE)
    ## One vectorized lookup instead of n linear scans of `names(dual_vars)`,
    ## and a preallocated result instead of one grown by name: O(n^2) -> O(n),
    ## measured 9.1 ms -> 3.4 ms at n = 1000. This is `.remap_by_id_map`
    ## (zzz_R_specific/utility.R) with the per-constraint reshape below kept
    ## inline, since it is what makes the dual's SHAPE contract true.
    ## See notes/string_key_hashing_sweep_2026-08-13.md and ADR D_PERF.7.
    new_ids <- as.character(unlist(mget(old_ids, envir = con_map_env),
                                   use.names = FALSE))
    dv_idx <- match(new_ids, names(solution@dual_vars))
    n_old <- length(old_ids)
    out_dv <- vector("list", n_old)
    for (i in seq_len(n_old)) {
      if (is.na(dv_idx[i])) next
      old_id <- old_ids[[i]]
      dv <- solution@dual_vars[[dv_idx[i]]]
      if (is.null(dv)) next

      ## CVXPY SOURCE: cone_matrix_stuffing.py:467-478 -- give the dual the
      ## SHAPE OF ITS CONSTRAINT, in F (column-major) order, which R's `matrix`
      ## is natively.  The exemption list transliterates verbatim; the
      ## `PSD && num_cones() > 1` clause is written out even though
      ## `num_cones(PSD)` is 1L (psd.R:67) so it can never fire, because the
      ## point is to read as CVXPY's does.
      ##
      ## This is what makes the roxygen at constraints/constraint.R:89-91 ("a
      ## numeric matrix ... or a list of numeric matrices") true.  Its absence
      ## was the root cause of both dual bugs fixed earlier on this branch:
      ## with no reshape here, every per-cone `save_dual_value` had to know the
      ## raw stuffed layout itself, and two of them got it wrong.
      con_obj <- get0(old_id, envir = id2cons, ifnotfound = NULL)
      shape <- if (is.null(con_obj)) NULL else .shape(con_obj)
      exempt <- is.null(shape) || prod(shape) == 1L ||
        .s7_is(con_obj, ExpCone) || .s7_is(con_obj, SOC) ||
        (.s7_is(con_obj, PSD) && num_cones(con_obj) > 1L)

      if (exempt) {
        out_dv[i] <- list(dv)
      } else if (length(dv) != prod(shape)) {
        ## numpy raises here; R would RECYCLE silently whenever one length
        ## divides the other, which is exactly the class of corruption this
        ## whole reshape exists to prevent.
        cli_abort(c(
          "Dual for constraint {old_id} has length {length(dv)}, but its shape is {.val {shape}}.",
          "i" = "This is an internal inconsistency between the solver's dual layout and the constraint."
        ))
      } else {
        out_dv[i] <- list(matrix(dv, nrow = shape[1L], ncol = shape[2L]))
      }
    }
    names(out_dv) <- old_ids
    filled <- !vapply(out_dv, is.null, logical(1))
    dual_vars <- out_dv[filled]
  }

  if (!(solution@status %in% SOLUTION_PRESENT)) {
    return(Solution(solution@status, opt_val, primal_vars, dual_vars,
                    solution@attr))
  }

  ## Split vectorized variable into components
  x_opt <- solution@primal_vars[[1L]]
  if (is.null(x_opt)) {
    ## Try to get the single primal var
    if (length(solution@primal_vars) > 0L) {
      x_opt <- solution@primal_vars[[1L]]
    }
  }

  if (!is.null(x_opt)) {
    for (var_id in names(var_map)) {
      offset <- var_map[[var_id]]
      shape <- inverse_data@var_shapes[[var_id]]
      sz <- prod(shape)
      val <- x_opt[(offset + 1L):(offset + sz)]
      primal_vars[[var_id]] <- matrix(val, nrow = shape[1L], ncol = shape[2L])
    }
  }

  Solution(solution@status, opt_val, primal_vars, dual_vars, solution@attr)
}

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.