Nothing
#####
## DO NOT EDIT THIS FILE!! EDIT THE SOURCE INSTEAD: rsrc_tree/reductions/cvx_attr2constr.R
#####
## CVXPY SOURCE: reductions/cvx_attr2constr.py
## CVXPY 1.9 parity notes:
## - `reduce_bounds` mirrors cvx_attr2constr.py:168-179,206-211,269-271.
## When FALSE, bound-generating attributes are preserved for solvers that
## consume native variable bounds.
## - Dimension-reducing variables and parameters mirror
## cvx_attr2constr.py:77-164,213-290 for CVXR's 2D symmetric/PSD/NSD/diag
## attributes.
## - R deviation: the default is TRUE here (CVXPY default is FALSE) so direct
## calls remain conservative; solver chains opt into native bounds when safe.
## - Deferred/N/A: CVXPY's batched N-D symmetric branch and sparse-leaf
## reduction branch are outside CVXR's current 2D/sparsity surface; var_*
## gradient transforms remain deferred.
##
## CvxAttr2Constr -- expand convex variable attributes into constraints
##
## Handles: nonneg, nonpos, PSD, NSD, symmetric, bounds, diag.
## Deferred: sparsity.
## `upper_tri_to_full()` is used below (lines ~137, ~155, ~378) but is NOT
## defined here. Its isomorphic home is atoms/affine/upper_tri.R (CVXPY defines
## it in atoms/affine/upper_tri.py), and it is implemented natively in
## src/RcppConv.cpp. An R copy lived in this file until 2026-08-14 and silently
## shadowed that native routine -- see the note in atoms/affine/upper_tri.R.
# -- Dimension-reducing leaf helpers --------------------------------
.cvxattr_has_symmetric_attr <- function(attrs) {
isTRUE(attrs$symmetric) || isTRUE(attrs$PSD) || isTRUE(attrs$NSD)
}
## CVXPY SOURCE: cvx_attr2constr.py:127-164
## CVXR is 2D-only, so this ports the symmetric/PSD/NSD and diagonal branches.
## The CVXPY sparsity branch and batched symmetric branch remain deferred.
.cvxattr_has_dim_reducing_attr <- function(leaf) {
attrs <- .attributes(leaf)
## CVXPY SOURCE: leaf.py:714-718 -- `self.sparse_idx is not None or
## self.attributes['diag'] or attributes_present([self], SYMMETRIC_ATTRIBUTES)`.
## `sparsity` was missing from this test, which is why nothing lowered it.
.cvxattr_has_symmetric_attr(attrs) || isTRUE(attrs$diag) ||
length(leaf@.sparse_idx) > 0L
}
.cvxattr_reduced_size <- function(leaf) {
attrs <- .attributes(leaf)
if (.cvxattr_has_symmetric_attr(attrs)) {
n <- .shape(leaf)[1L]
return(as.integer((n * (n + 1L)) %/% 2L))
}
## CVXPY SOURCE: leaf.py:722-723 -- `len(self.sparse_idx[0])`, i.e. one
## variable entry per stored position.
if (length(leaf@.sparse_idx) > 0L) return(length(leaf@.sparse_idx))
if (isTRUE(attrs$diag)) return(as.integer(.shape(leaf)[1L]))
as.integer(expr_size(leaf))
}
## The scatter matrix that lifts an nnz-vector back to the leaf's full shape:
## a (prod(shape) x nnz) 0/1 matrix with a single 1 per column, at the stored
## flat position. CVXPY SOURCE: cvx_attr2constr.py:155-161, which builds the
## same thing from `np.ravel_multi_index(leaf.sparse_idx, leaf.shape, order='F')`
## -- CVXR's `.sparse_idx` is ALREADY that flat column-major index, so there is
## nothing to ravel.
.cvxattr_sparse_coeff <- function(idx, n_full) {
Matrix::sparseMatrix(i = as.integer(idx), j = seq_along(idx),
x = rep(1, length(idx)),
dims = c(as.integer(n_full), length(idx)))
}
## Backstop for the construction-time check in expressions/leaf.R
## (`.reject_partial_mip_idx`): a leaf whose replacement has a different shape
## must not carry a partial boolean/integer index list. Unreachable in normal
## use -- the leaf could not have been constructed -- and kept because this is
## the point where the shape actually changes.
.cvxattr_reject_partial_mip_idx <- function(attrs, reducing) {
.reject_partial_mip_idx(attrs$boolean, attrs$integer, reducing)
}
## CVXPY SOURCE: cvx_attr2constr.py:102-125
.cvxattr_lower_value <- function(leaf, val = NULL) {
if (is.null(val)) val <- value(leaf)
if (is.null(val)) return(NULL)
attrs <- .attributes(leaf)
if (.cvxattr_has_symmetric_attr(attrs)) {
n <- .shape(leaf)[1L]
idx <- which(upper.tri(matrix(0, n, n), diag = TRUE), arr.ind = TRUE)
idx <- idx[order(idx[, 1L], idx[, 2L]), , drop = FALSE]
return(as.numeric(val[idx]))
}
## CVXPY SOURCE: cvx_attr2constr.py:128-132 -- the sparse branch of
## lower_value. CVXR stores leaf values DENSELY (no `_value`-holds-only-nnz
## split, and no `value_sparse`), so the full-size / stored-size distinction
## upstream draws does not arise: extract at the stored positions either way.
if (length(leaf@.sparse_idx) > 0L) {
return(as.numeric(val)[leaf@.sparse_idx])
}
if (isTRUE(attrs$diag)) {
return(as.numeric(diag(as.matrix(val))))
}
val
}
## CVXPY SOURCE: cvx_attr2constr.py:77-100
.cvxattr_build_full_value <- function(leaf, lowered_value) {
attrs <- .attributes(leaf)
if (.cvxattr_has_symmetric_attr(attrs)) {
n <- .shape(leaf)[1L]
idx <- which(upper.tri(matrix(0, n, n), diag = TRUE), arr.ind = TRUE)
idx <- idx[order(idx[, 1L], idx[, 2L]), , drop = FALSE]
full <- matrix(0, n, n)
flat_val <- as.numeric(lowered_value)
for (k in seq_len(nrow(idx))) {
i <- idx[k, 1L]
j <- idx[k, 2L]
full[i, j] <- flat_val[k]
full[j, i] <- flat_val[k]
}
return(full)
}
## CVXPY SOURCE: cvx_attr2constr.py:93-97 -- scatter the reduced values back
## into a full-shape array of zeros.
if (length(leaf@.sparse_idx) > 0L) {
full <- numeric(prod(.shape(leaf)))
full[leaf@.sparse_idx] <- as.numeric(lowered_value)
dim(full) <- .shape(leaf)
return(full)
}
if (isTRUE(attrs$diag)) {
return(Matrix::Diagonal(x = as.numeric(lowered_value)))
}
lowered_value
}
## Adjoint of the full <- reduced reconstruction, for the dict-diff VARIABLE
## chain rule (#3147 part A). Maps a FULL n x n gradient to the reduced
## representation's gradient. Distinct from .cvxattr_lower_value (which merely
## extracts the upper triangle of a VALUE): the gradient adjoint of the
## symmetric fill `full = A %*% tri` is `t(A) %*% vec(full)`, which DOUBLES the
## off-diagonal contributions. For diag the adjoint is just the diagonal.
## CVXPY SOURCE: cvx_attr2constr.py:313-330 (var_backward, fill_mat.T @ ...).
.cvxattr_reduce_grad <- function(leaf, full_grad) {
attrs <- .attributes(leaf)
if (.cvxattr_has_symmetric_attr(attrs)) {
n <- .shape(leaf)[1L]
A <- upper_tri_to_full(n) # (n^2) x (n(n+1)/2)
## crossprod enforces length(vec) == n^2 (non-conformable error otherwise).
tri <- as.numeric(Matrix::crossprod(A, as.numeric(full_grad)))
return(array(tri, dim = c(length(tri), 1L)))
}
if (isTRUE(attrs$diag)) {
n <- .shape(leaf)[1L]
dg <- diag(matrix(as.numeric(full_grad), n, n))
return(array(dg, dim = c(n, 1L)))
}
full_grad # pass-through attrs (nonneg/nonpos/bounds): no reshape
}
## CVXPY SOURCE: cvx_attr2constr.py:127-164
.cvxattr_build_dim_reduced_expression <- function(leaf, reduced_leaf) {
attrs <- .attributes(leaf)
if (.cvxattr_has_symmetric_attr(attrs)) {
n <- .shape(leaf)[1L]
fill_coeff <- Constant(upper_tri_to_full(n))
full_mat <- fill_coeff %*% reduced_leaf
expr <- reshape_expr(full_mat, c(n, n))
if (isTRUE(attrs$PSD)) {
return(psd_wrap(expr))
} else if (isTRUE(attrs$NSD)) {
## CVXPY SOURCE: cvx_attr2constr.py:151 -- `return nsd_wrap(expr)`.
## Was `-psd_wrap(-expr)` while nsd_wrap did not exist: curvature- and
## sign-equivalent, but two extra nodes and not the upstream tree.
return(nsd_wrap(expr))
} else {
return(symmetric_wrap(expr))
}
}
## CVXPY SOURCE: cvx_attr2constr.py:155-161 -- the `leaf.sparse_idx is not
## None` branch, checked BEFORE diag (the two are mutually exclusive anyway,
## enforced at construction since leaf.py:155-160).
if (length(leaf@.sparse_idx) > 0L) {
n_full <- as.integer(prod(.shape(leaf)))
coeff <- Constant(.cvxattr_sparse_coeff(leaf@.sparse_idx, n_full))
return(reshape_expr(coeff %*% reduced_leaf, .shape(leaf)))
}
if (isTRUE(attrs$diag)) {
return(DiagVec(reduced_leaf))
}
reduced_leaf
}
.cvxattr_pass_through_attrs <- function(attrs, reduction_attributes) {
args <- list()
## CVXPY SOURCE: cvx_attr2constr.py:196-219 -- upstream copies the attribute
## dict verbatim (`new_attr = var.attributes.copy()`) and rebuilds the leaf
## with `**new_attr`, so an INDEX LIST survives unchanged. Coercing to TRUE
## here promoted a partial boolean/integer constraint to a full one whenever
## a leaf was rebuilt -- i.e. for variables that are also symmetric / PSD /
## NSD / diag / sparse -- silently solving a MORE constrained problem than
## the user asked for.
for (key in c("nonneg", "nonpos", "pos", "neg", "boolean", "integer")) {
val <- attrs[[key]]
keep <- !(key %in% reduction_attributes) &&
!is.null(val) && !identical(val, FALSE) && length(val) > 0L
if (keep) {
args[[key]] <- val
}
}
if (!("bounds" %in% reduction_attributes) && !is.null(attrs$bounds)) {
args$bounds <- attrs$bounds
}
args
}
# -- Helper: preserve un-reduced bound attributes ------------------
## CVXPY SOURCE: cvx_attr2constr.py:144-148 -- the `**new_attr` pass-through
## that carries un-reduced attributes onto the replacement Variable.
## When reduce_bounds is FALSE the bound-generating attributes survive the
## reduction; copy them onto the replacement variable's constructor args so
## that get_bounds() (leaf.R) reads them on the reduced problem.
.cvxattr_preserve_bound_attrs <- function(args, attrs, reduce_bounds) {
if (reduce_bounds) return(args)
for (k in c("nonneg", "nonpos", "pos", "neg")) {
if (isTRUE(attrs[[k]])) args[[k]] <- TRUE
}
if (!is.null(attrs$bounds) && is.list(attrs$bounds)) args$bounds <- attrs$bounds
args
}
## `.cvxattr_add_bound_constraints()` used to live here, holding the bounds half
## of leaf.py's `_bound_domain` while the sign half was inlined in
## reduction_apply below. Both halves now live in ONE function in the file that
## owns them upstream -- `.leaf_bound_domain()` in expressions/leaf.R -- and
## this reduction calls it, exactly as cvx_attr2constr.py:271 calls
## `var._bound_domain(obj, constr)`. See the comment there for why the split
## was not merely untidy.
# -- CvxAttr2Constr reduction class -------------------------------
## CVXPY SOURCE: cvx_attr2constr.py lines 105-215
CvxAttr2Constr <- new_class("CvxAttr2Constr", parent = Reduction,
package = "CVXR",
properties = list(
reduce_bounds = class_logical
),
constructor = function(reduce_bounds = TRUE) {
if (FALSE) new_object(S7_object()) ## S7 static-check guard
.fast_new(CvxAttr2Constr, S7_object(),
.cache = new.env(parent = emptyenv()),
reduce_bounds = isTRUE(reduce_bounds)
)
}
)
method(reduction_accepts, CvxAttr2Constr) <- function(x, problem, ...) TRUE
method(reduction_apply, CvxAttr2Constr) <- function(x, problem, ...) {
reduce_bounds <- isTRUE(x@reduce_bounds)
vars_ <- variables(problem)
params_ <- parameters(problem)
has_var_attrs <- length(convex_attributes(vars_)) > 0L
has_param_attrs <- any(vapply(params_, .cvxattr_has_dim_reducing_attr, logical(1)))
if (!has_var_attrs && !has_param_attrs) {
return(list(problem, list()))
}
reduction_attributes <- c("symmetric", "PSD", "NSD", "diag", "sparsity")
if (reduce_bounds) {
reduction_attributes <- c(reduction_attributes, "nonneg", "nonpos", "pos", "neg", "bounds")
}
x@.cache$parameters <- new.env(hash = TRUE, parent = emptyenv())
## Original parameter objects keyed by id, for the dict-diff param hooks
## (#3147 part A); they need the original leaf's attributes/shape to lower
## or rebuild a value. reduction_invert uses inverse_data instead.
x@.cache$id2old_param <- new.env(hash = TRUE, parent = emptyenv())
## R-specific DPP-cache correctness: the reduced Parameter objects must keep
## STABLE identity across re-applications of this reduction. The DPP fast
## path caches a `param_prog` that holds direct references to these reduced
## Parameters and re-syncs them via update_parameters(); CVXR's problem_data()
## always re-runs the full chain (unlike CVXPY's cache-aware get_problem_data),
## so re-creating a fresh reduced Parameter here would orphan the one the
## cached param_prog points at -- a later solve would then read a stale value
## (e.g. problem_data() followed by changing a diag/symmetric Parameter and
## re-solving). Persist the reduced Parameters in a store that survives
## re-application and reuse them below.
if (is.null(x@.cache$reduced_param_store)) {
x@.cache$reduced_param_store <- new.env(hash = TRUE, parent = emptyenv())
}
## For each unique variable, create replacement and constraints
id2new_var <- new.env(hash = TRUE, parent = emptyenv())
id2new_obj <- new.env(hash = TRUE, parent = emptyenv())
id2old_var <- new.env(hash = TRUE, parent = emptyenv())
## Collect attribute constraint chunks per variable (up to ~4 each: PSD/NSD + nonneg + nonpos + bounds)
attr_constr_chunks <- vector("list", length(vars_))
n_attr_chunks <- 0L
for (var in vars_) {
vid <- as.character(.id(var))
if (exists(vid, envir = id2new_var, inherits = FALSE)) next
assign(vid, var, envir = id2old_var)
attrs <- .attributes(var)
has_sym <- !is.null(attrs$symmetric) && attrs$symmetric
has_psd <- !is.null(attrs$PSD) && attrs$PSD
has_nsd <- !is.null(attrs$NSD) && attrs$NSD
has_diag <- !is.null(attrs$diag) && attrs$diag
## Read the CANONICAL index, never the raw attribute -- it is an index set,
## so `isTRUE(attrs$sparsity)` is FALSE for every real pattern.
has_sparsity <- length(var@.sparse_idx) > 0L
has_nonneg <- !is.null(attrs$nonneg) && attrs$nonneg
has_nonpos <- !is.null(attrs$nonpos) && attrs$nonpos
## `pos` and `neg` generate the SAME constraints as `nonneg` / `nonpos`.
## CVXPY SOURCE: leaf.py:358-361 (_bound_domain)
## if self.attributes['nonneg'] or self.attributes['pos']:
## constraints.append(term >= 0)
## if self.attributes['nonpos'] or self.attributes['neg']:
## constraints.append(term <= 0)
## and cvx_attr2constr.py:32-52, where CONVEX_ATTRIBUTES and
## BOUND_ATTRIBUTES both list all FOUR sign attributes.
##
## CVXR handled only nonneg/nonpos here, so `Variable(pos = TRUE)` and
## `Variable(neg = TRUE)` produced NO constraint: the attribute reached
## get_bounds() (leaf.R:590 already treats pos as a lower bound of 0) but
## only solvers with BOUNDED_VARIABLES = TRUE ever read those bounds, so on
## CLARABEL/SCS/ECOS/OSQP/MOSEK the sign was silently dropped and a bounded
## problem was reported UNBOUNDED. Present in every release since 1.8.2.
has_pos <- !is.null(attrs$pos) && isTRUE(attrs$pos)
has_neg <- !is.null(attrs$neg) && isTRUE(attrs$neg)
has_bounds <- !is.null(attrs$bounds) && is.list(attrs$bounds)
## CVXPY approach: copy all attributes, then clear only the
## "reduction_attributes". Preserve boolean, integer, nonneg, nonpos,
## bounds on the new variable.
##
## CVXPY SOURCE: cvx_attr2constr.py:207-211, which iterates
## `self.reduction_attributes()` -- derived from CONVEX_ATTRIBUTES, so
## `sparsity` is in it. This loop hardcoded four keys and OMITTED
## "sparsity", while `reduction_attributes` twenty lines up listed five.
## Two lists for one concept, disagreeing: the attribute was declared
## reducible and then never cleared, so the replacement variable kept it and
## the reduction had no effect.
new_attrs <- attrs
needs_new_var <- FALSE
for (key in c("symmetric", "PSD", "NSD", "diag", "sparsity")) {
if (.attr_set(new_attrs[[key]])) {
needs_new_var <- TRUE
new_attrs[[key]] <- FALSE
}
}
## Also mark as needing new var if nonneg/nonpos/bounds present -- but only
## when we are lowering bounds. When reduce_bounds = FALSE the attributes
## stay on the variable, so a pure-bounds variable is kept untouched
## (CVXPY cvx_attr2constr.py:114-119 -- bound attrs are absent from
## reduction_attributes, so `new_var` is never set on their account).
if (reduce_bounds && (has_nonneg || has_nonpos || has_pos || has_neg ||
has_bounds)) needs_new_var <- TRUE
if (has_sym || has_psd || has_nsd) {
## Create upper-triangular variable and fill to full matrix
n <- .shape(var)[1L]
tri_size <- (n * (n + 1L)) %/% 2L
## New variable with same id but stripped symmetric/PSD/NSD attrs
## Preserve boolean/integer on the upper-tri variable
upper_tri_args <- list(c(tri_size, 1L), var_id = .id(var))
## Shape changes here (n x n -> tri_size x 1), so only a WHOLE-variable
## boolean/integer attribute survives; an index list is rejected.
.cvxattr_reject_partial_mip_idx(new_attrs, "symmetric/PSD/NSD")
if (isTRUE(new_attrs$boolean)) upper_tri_args$boolean <- TRUE
if (isTRUE(new_attrs$integer)) upper_tri_args$integer <- TRUE
upper_tri_args <- .cvxattr_preserve_bound_attrs(upper_tri_args, attrs, reduce_bounds)
upper_tri_var <- do.call(Variable, upper_tri_args)
assign(vid, upper_tri_var, envir = id2new_var)
## Fill coefficient: n^2 x tri_size sparse matrix
fill_coeff <- Constant(upper_tri_to_full(n))
full_mat <- fill_coeff %*% upper_tri_var
obj <- reshape_expr(full_mat, c(n, n))
## Map original var's python id to the new expression
assign(as.character(.id(var)), obj, envir = id2new_obj)
## Add PSD/NSD constraint on the full matrix
var_constrs <- list()
if (has_psd) {
var_constrs <- list(PSD(obj))
} else if (has_nsd) {
var_constrs <- list(PSD(-obj))
}
} else if (has_sparsity) {
## Sparse variable: replace with an nnz-vector and scatter it back into
## the full shape, so every off-pattern entry is a STRUCTURAL ZERO rather
## than a free variable the solver may set.
## CVXPY SOURCE: cvx_attr2constr.py:236-252 (the _has_dim_reducing_attr
## branch) + build_dim_reduced_expression's sparse arm (:155-161).
idx <- var@.sparse_idx
sp_args <- list(c(length(idx), 1L), var_id = .id(var))
## Shape changes here (n_full -> nnz); see the upper-tri branch above.
.cvxattr_reject_partial_mip_idx(new_attrs, "sparsity")
if (isTRUE(new_attrs$boolean)) sp_args$boolean <- TRUE
if (isTRUE(new_attrs$integer)) sp_args$integer <- TRUE
## Bound attributes are NOT carried over: they are indexed by the FULL
## shape and would be the W2 truncation bug in another guise. Upstream
## transforms sparse bounds explicitly (cvx_attr2constr.py:241-256) and
## rejects a dense array bound on a sparse leaf; CVXR rejects dense
## bounds on a sparse leaf at construction (leaf.R, has_structural_zeros),
## so only a scalar bound can reach here, and a scalar needs no transform.
if (!reduce_bounds) {
for (k in c("nonneg", "nonpos", "pos", "neg")) {
if (isTRUE(attrs[[k]])) sp_args[[k]] <- TRUE
}
if (!is.null(attrs$bounds) && is.list(attrs$bounds) &&
all(vapply(attrs$bounds, function(b) length(b) <= 1L, logical(1)))) {
sp_args$bounds <- attrs$bounds
}
}
sparse_var <- do.call(Variable, sp_args)
if (!is.null(value(var))) value(sparse_var) <- .cvxattr_lower_value(var)
assign(vid, sparse_var, envir = id2new_var)
obj <- .cvxattr_build_dim_reduced_expression(var, sparse_var)
assign(as.character(.id(var)), obj, envir = id2new_obj)
var_constrs <- list()
} else if (has_diag) {
## Diagonal n×n variable: replace with n-vector + DiagMat() lift.
## CVXPY SOURCE: cvx_attr2constr.py:167-171
n <- .shape(var)[1L]
diag_var_args <- list(c(n, 1L), var_id = .id(var))
## Shape changes here (n x n -> n x 1); see the upper-tri branch above.
.cvxattr_reject_partial_mip_idx(new_attrs, "diag")
if (isTRUE(new_attrs$boolean)) diag_var_args$boolean <- TRUE
if (isTRUE(new_attrs$integer)) diag_var_args$integer <- TRUE
diag_var_args <- .cvxattr_preserve_bound_attrs(diag_var_args, attrs, reduce_bounds)
diag_var <- do.call(Variable, diag_var_args)
## Propagate the original variable's value to the lowered n-vector so the
## NLP initial point survives the reduction. CVXPY SOURCE:
## cvx_attr2constr.py:251-252 (the _has_dim_reducing_attr branch).
if (!is.null(value(var))) value(diag_var) <- .cvxattr_lower_value(var)
assign(vid, diag_var, envir = id2new_var)
## DiagVec lifts an n-vector to an n×n diagonal matrix.
## (DiagMat goes the other way: square matrix → diagonal vector.)
obj <- DiagVec(diag_var) # n×n diagonal expression
assign(as.character(.id(var)), obj, envir = id2new_obj)
var_constrs <- list()
} else if (needs_new_var) {
## Create replacement variable preserving boolean/integer
new_var_args <- list(.shape(var), var_id = .id(var))
## SAME shape as the original (this branch only lowers bounds), so an
## index list still denotes the same entries and is carried over as-is.
## `&& new_attrs$boolean` used to coerce it: a length-1 list was silently
## promoted to TRUE (constraining the WHOLE variable) and a longer one
## raised "'length = 2' in coercion to 'logical(1)'".
for (key in c("boolean", "integer")) {
val <- new_attrs[[key]]
if (!is.null(val) && !identical(val, FALSE) && length(val) > 0L) {
new_var_args[[key]] <- val
}
}
new_var_args <- .cvxattr_preserve_bound_attrs(new_var_args, attrs, reduce_bounds)
obj <- do.call(Variable, new_var_args)
assign(vid, obj, envir = id2new_var)
assign(as.character(.id(var)), obj, envir = id2new_obj)
var_constrs <- list()
} else {
## No attribute to reduce -- keep the variable as-is
obj <- var
assign(vid, var, envir = id2new_var)
assign(as.character(.id(var)), var, envir = id2new_obj)
var_constrs <- list()
}
## Add nonneg/nonpos/bounds constraints independently, but only when we are
## lowering bounds. When reduce_bounds = FALSE the attributes stay on the
## variable (see .cvxattr_preserve_bound_attrs) for the solver to enforce.
## CVXPY SOURCE: cvx_attr2constr.py:168-171 (only calls var._bound_domain when
## self.reduce_bounds); leaf.py _bound_domain() -- each is a separate `if`.
if (reduce_bounds) {
obj <- get(as.character(.id(var)), envir = id2new_obj)
## CVXPY SOURCE: cvx_attr2constr.py:270-271 --
## if self.reduce_bounds:
## var._bound_domain(obj, constr)
## The sign pair, the bounds pair and their finite-entry masking all live
## in `.leaf_bound_domain()` (expressions/leaf.R), whose other caller is
## `domain(Leaf)`. `cone = TRUE` keeps the NonNeg/NonPos cone form this
## reduction has always emitted; see that function's header.
var_constrs <- .leaf_bound_domain(var, obj, var_constrs, cone = TRUE)
}
if (length(var_constrs) > 0L) {
n_attr_chunks <- n_attr_chunks + 1L
attr_constr_chunks[[n_attr_chunks]] <- var_constrs
}
}
## For each unique parameter with 2D dimension-reducing attributes, create a
## reduced parameter and a reconstruction expression.
## CVXPY SOURCE: cvx_attr2constr.py:278-290
store <- x@.cache$reduced_param_store
for (param in params_) {
pid <- as.character(.id(param))
if (!.cvxattr_has_dim_reducing_attr(param)) next
if (exists(pid, envir = id2new_obj, inherits = FALSE)) next
lowered <- .cvxattr_lower_value(param)
if (exists(pid, envir = store, inherits = FALSE)) {
## Reuse the reduced Parameter from a prior application (stable identity)
## and re-lower the current value into it.
reduced_param <- get(pid, envir = store, inherits = FALSE)
if (!is.null(lowered)) value(reduced_param) <- lowered
} else {
n <- .cvxattr_reduced_size(param)
if (n != expr_size(param)) {
.cvxattr_reject_partial_mip_idx(
.attributes(param),
if (.cvxattr_has_symmetric_attr(.attributes(param))) "symmetric/PSD/NSD" else "diag")
}
param_args <- c(
list(shape = c(n, 1L), name = expr_name(param)),
.cvxattr_pass_through_attrs(.attributes(param), reduction_attributes)
)
reduced_param <- do.call(Parameter, param_args)
if (!is.null(lowered)) value(reduced_param) <- lowered
assign(pid, reduced_param, envir = store)
}
obj <- .cvxattr_build_dim_reduced_expression(param, reduced_param)
assign(pid, reduced_param, envir = x@.cache$parameters)
assign(pid, param, envir = x@.cache$id2old_param)
assign(pid, obj, envir = id2new_obj)
}
## Substitute variables in the objective and constraints via tree_copy
new_obj <- tree_copy(problem@objective, id_objects = id2new_obj)
n_pcons <- length(problem@constraints)
tree_copy_chunks <- vector("list", n_pcons)
cons_id_map <- list()
for (i in seq_len(n_pcons)) {
con <- problem@constraints[[i]]
new_con <- tree_copy(con, id_objects = id2new_obj)
tree_copy_chunks[[i]] <- list(new_con)
cons_id_map[[as.character(con@id)]] <- .id(new_con)
}
## Combine all constraint chunks: attribute constraints + tree-copied constraints
all_constr_chunks <- c(attr_constr_chunks[seq_len(n_attr_chunks)], tree_copy_chunks)
new_constrs <- unlist(all_constr_chunks, recursive = FALSE)
if (is.null(new_constrs)) new_constrs <- list()
## Stash the variable maps on the instance so the dict-diff hooks
## (var_backward/var_forward) can reach them (#3147 part A); reduction_invert
## reads its own copy from inverse_data.
x@.cache$id2old_var <- id2old_var
x@.cache$id2new_var <- id2new_var
new_problem <- Problem(new_obj, new_constrs)
inverse_data <- list(id2new_var = id2new_var,
id2old_var = id2old_var,
cons_id_map = cons_id_map)
list(new_problem, inverse_data)
}
method(update_parameters, CvxAttr2Constr) <- function(x, problem, ...) {
params_map <- x@.cache$parameters
if (is.null(params_map)) return(invisible(NULL))
for (param in parameters(problem)) {
pid <- as.character(.id(param))
if (!exists(pid, envir = params_map, inherits = FALSE)) next
reduced_param <- get(pid, envir = params_map, inherits = FALSE)
lowered <- .cvxattr_lower_value(param)
if (!is.null(lowered)) value(reduced_param) <- lowered
}
invisible(NULL)
}
## Dict-in/dict-out param hooks (#3147 part A). For each lowered parameter
## (dim-reducing attribute), map its delta/gradient between the full and reduced
## (upper-tri / diagonal) representations. Pass-through params are untouched.
## Reduced-leaf values are kept dimensioned to the reduced param's shape.
method(param_forward, CvxAttr2Constr) <- function(x, param_deltas) {
params_map <- x@.cache$parameters
id2old <- x@.cache$id2old_param
if (is.null(params_map)) return(param_deltas)
for (pid in ls(params_map, all.names = TRUE)) {
if (is.null(param_deltas[[pid]])) next
reduced_param <- get(pid, envir = params_map, inherits = FALSE)
orig_param <- get(pid, envir = id2old, inherits = FALSE)
lowered <- .cvxattr_lower_value(orig_param, param_deltas[[pid]])
param_deltas[[as.character(reduced_param@id)]] <-
array(as.numeric(lowered), dim = .shape(reduced_param))
}
param_deltas
}
method(param_backward, CvxAttr2Constr) <- function(x, dparams) {
params_map <- x@.cache$parameters
id2old <- x@.cache$id2old_param
if (is.null(params_map)) return(dparams)
for (pid in ls(params_map, all.names = TRUE)) {
reduced_param <- get(pid, envir = params_map, inherits = FALSE)
rpid <- as.character(.id(reduced_param))
if (is.null(dparams[[rpid]])) next
orig_param <- get(pid, envir = id2old, inherits = FALSE)
full <- .cvxattr_build_full_value(orig_param, dparams[[rpid]])
dparams[[rpid]] <- NULL # pop reduced id
dparams[[pid]] <- full # full value at original id
}
dparams
}
## Dict-in/dict-out VARIABLE chain rule (#3147 part A). For a symmetric/PSD/NSD
## or diag Variable, lowered to an upper-tri (resp. diagonal) reduced variable
## that SHARES the original id, reshape gradients/deltas between the full and
## reduced representations. Pass-through variables (nonneg/nonpos/bounds) keep
## their full shape and are untouched. CVXPY SOURCE: cvx_attr2constr.py:313-346.
## (This closes the symmetric-Variable derivative gap flagged in ADR D_19.1.)
method(var_backward, CvxAttr2Constr) <- function(x, del_vars) {
id2old <- x@.cache$id2old_var
if (is.null(id2old)) return(del_vars)
for (vid in ls(id2old, all.names = TRUE)) {
if (is.null(del_vars[[vid]])) next
ov <- get(vid, envir = id2old, inherits = FALSE)
if (!.cvxattr_has_dim_reducing_attr(ov)) next # pass-through: unchanged
del_vars[[vid]] <- .cvxattr_reduce_grad(ov, del_vars[[vid]]) # full -> reduced
}
del_vars
}
method(var_forward, CvxAttr2Constr) <- function(x, dvars) {
id2old <- x@.cache$id2old_var
if (is.null(id2old)) return(dvars)
for (vid in ls(id2old, all.names = TRUE)) {
if (is.null(dvars[[vid]])) next
ov <- get(vid, envir = id2old, inherits = FALSE)
if (!.cvxattr_has_dim_reducing_attr(ov)) next # pass-through: unchanged
full <- .cvxattr_build_full_value(ov, dvars[[vid]]) # reduced -> full
if (inherits(full, "Matrix")) full <- as.matrix(full)
dvars[[vid]] <- full
}
dvars
}
method(reduction_invert, CvxAttr2Constr) <- function(x, solution, inverse_data, ...) {
if (length(inverse_data) == 0L) return(solution)
id2new_var <- inverse_data$id2new_var
id2old_var <- inverse_data$id2old_var
cons_id_map <- inverse_data$cons_id_map
pvars <- list()
old_ids <- ls(id2old_var, all.names = TRUE)
for (vid in old_ids) {
old_var <- get(vid, envir = id2old_var)
new_var <- get(vid, envir = id2new_var)
new_vid <- as.character(.id(new_var))
if (!is.null(solution@primal_vars[[new_vid]])) {
raw_val <- solution@primal_vars[[new_vid]]
## Recover full matrix for symmetric/PSD/NSD
attrs <- .attributes(old_var)
has_sym <- !is.null(attrs$symmetric) && attrs$symmetric
has_psd <- !is.null(attrs$PSD) && attrs$PSD
has_nsd <- !is.null(attrs$NSD) && attrs$NSD
has_diag <- !is.null(attrs$diag) && attrs$diag
if (has_sym || has_psd || has_nsd) {
n <- .shape(old_var)[1L]
val <- numeric(n * n)
## Indices for upper triangle (row-major, 0-based)
idx <- which(upper.tri(matrix(0, n, n), diag = TRUE), arr.ind = TRUE)
idx <- idx[order(idx[, 1L], idx[, 2L]), , drop = FALSE]
flat_val <- as.numeric(raw_val)
## Fill upper triangle and mirror
full <- matrix(0, n, n)
for (k in seq_len(nrow(idx))) {
i <- idx[k, 1L]
j <- idx[k, 2L]
full[i, j] <- flat_val[k]
full[j, i] <- flat_val[k]
}
pvars[[vid]] <- full
} else if (length(old_var@.sparse_idx) > 0L) {
## Scatter the nnz-vector solution back into the full shape, zeros
## everywhere off the pattern.
## CVXPY SOURCE: cvx_attr2constr.py:93-97 (recover_value_for_leaf).
pvars[[vid]] <- .cvxattr_build_full_value(old_var, raw_val)
} else if (has_diag) {
## Lift n-vector solution back to a sparse n×n diagonal matrix.
## Mirrors CVXPY recover_value_for_variable (cvx_attr2constr.py:77-80).
pvars[[vid]] <- Matrix::Diagonal(x = as.numeric(raw_val))
} else {
pvars[[vid]] <- raw_val
}
}
}
## Remap dual variables. `cons_id_map` is a NAMED LIST here, so the loop this
## replaces paid a linear scan on BOTH sides -- three O(n) terms per iteration.
## O(n) overall via one vectorized match; see `.remap_by_id_map`.
dvars <- .remap_by_id_map(solution@dual_vars, cons_id_map)
Solution(solution@status, solution@opt_val, pvars, dvars, solution@attr)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.