Nothing
#####
## DO NOT EDIT THIS FILE!! EDIT THE SOURCE INSTEAD: rsrc_tree/expressions/leaf.R
#####
## CVXPY SOURCE: expressions/leaf.py
## PARTIAL PORT: in: the whole Leaf attribute surface for CVXR's 2D model --
## construction and validation (incl. the dimension-reducing cardinality rule,
## leaf.py:155-160), bounds validation and get_bounds() (leaf.py:836-1032),
## _bound_domain and domain (leaf.py:350-430), project() including the
## boolean/integer/sparsity index branches (leaf.py:460-514), and the
## canonical index sets .boolean_idx / .integer_idx / .sparse_idx.
## out: the SPARSE VALUE API -- `value_sparse` getter/setter, the
## `sparse_path` argument of save_value/project, `_sparse_high_fill_in`, and
## the two RuntimeWarnings about reaching a sparse leaf through its dense
## representation (leaf.py:506-509, 536-545). CVXR stores leaf values
## DENSELY, so `_value`-holds-only-the-nonzeros -- the thing all of that
## machinery exists to manage -- has no counterpart, and the warnings would
## fire on CVXR's ordinary path. The `sparsity` ATTRIBUTE itself is fully
## enforced: CvxAttr2Constr lowers a sparse leaf to an nnz-vector plus a
## scatter, so off-pattern entries are structural zeros in the cone program.
## out: batched N-D leaves (CVXR is 2D-only).
## Leaf -- base class for Variable, Constant, Parameter
## CVXPY 1.9 parity notes:
## - bounds validation and get_bounds() mirror leaf.py:836-927,929-1032 for
## CVXR's 2D model, including numeric/sparse bounds, symbolic Expression
## bounds, sign attributes, and boolean bounds.
## - dimension-reducing leaf handling is implemented in CvxAttr2Constr for
## CVXR's 2D symmetric/PSD/NSD/diag/sparsity attributes.
# -- Is an attribute set? ---------------------------------------------
## Python truthiness for a CVXPY leaf attribute, which is either a bool or an
## index set: `if self.attributes[k]` is TRUE for `True` and for any NON-EMPTY
## index sequence, FALSE for `False` and for an empty one.
##
## `isTRUE()` is NOT that test -- it is FALSE for every index spelling -- and
## using it is how `sparsity` came to be accepted, advertised by
## `convex_attributes()`, and enforced nowhere: each site that should have
## noticed the attribute asked `isTRUE()` and was told no.
#' @keywords internal
.attr_set <- function(v) !is.null(v) && !identical(v, FALSE) && length(v) > 0L
# -- Helper: build leaf attributes list with validation ----------------
# Called by Leaf, Variable, and Parameter constructors to avoid
# duplicating validation logic while each calling new_object() directly.
#' @keywords internal
.build_leaf_attrs <- function(shape, nonneg = FALSE, nonpos = FALSE,
complex = FALSE, imag = FALSE,
symmetric = FALSE, diag = FALSE,
PSD = FALSE, NSD = FALSE, hermitian = FALSE,
boolean = FALSE, integer = FALSE,
sparsity = FALSE, pos = FALSE, neg = FALSE,
bounds = NULL, allow_expr_bounds = TRUE) {
## Validate square constraint for matrix attributes
## CVXPY SOURCE: leaf.py line 118-121
if ((PSD || NSD || symmetric || diag || hermitian) &&
(length(shape) != 2L || shape[1L] != shape[2L])) {
cli_abort("Invalid dimensions ({paste(shape, collapse = ', ')}). Must be a square matrix.")
}
## At most ONE dimension-reducing attribute.
## CVXPY SOURCE: leaf.py:155-160
## dim_reducing_attr = ['diag','symmetric','PSD','NSD','hermitian','sparsity']
## if sum(1 for k in dim_reducing_attr if self.attributes[k]) > 1:
## raise ValueError("A CVXPY Variable cannot have more than one of ...")
##
## This single check is what licenses every downstream if/elif chain to treat
## these as mutually exclusive -- `_has_dim_reducing_attr` (leaf.py:714-718),
## `_reduced_size` (:720-728), `build_dim_reduced_expression`
## (cvx_attr2constr.py:143-161), and on the CVXR side
## `cvx_attr2constr.R:358` (`has_sym || has_psd || has_nsd`) followed by
## `else if (has_diag)`. CVXR had no such check, so the second attribute was
## SILENTLY DROPPED and the solve answered a different question:
## Variable(c(2,2), symmetric=TRUE, diag=TRUE); min sum(X) s.t. X >= -1
## CVXR -4 (the full-matrix answer) correct diagonal answer -2
## Variable(c(2,2), PSD=TRUE, NSD=TRUE); min tr(X) s.t. X[1,1] >= 1
## CVXR 1 (NSD dropped) honoring both is infeasible
## CVXPY 1.9.2 raises ValueError for both, and for PSD+diag and
## symmetric+hermitian.
dim_reducing <- c("diag", "symmetric", "PSD", "NSD", "hermitian", "sparsity")[
c(.attr_set(diag), .attr_set(symmetric), .attr_set(PSD), .attr_set(NSD),
.attr_set(hermitian), .attr_set(sparsity))]
if (length(dim_reducing) > 1L) {
cli_abort(c(
"A CVXR Variable cannot have more than one of these attributes: {.val {dim_reducing}}.",
"i" = "Each of {.val {c('diag', 'symmetric', 'PSD', 'NSD', 'hermitian', 'sparsity')}} reduces the leaf to a different smaller representation, so they cannot be combined."
))
}
## Validate bounds (CVXPY SOURCE: leaf.py:836-927 _ensure_valid_bounds)
if (!is.null(bounds)) {
if (!is.list(bounds) || length(bounds) != 2L) {
cli_abort("Bounds should be a list of two items.")
}
has_expr_bound <- any(vapply(bounds, function(v) .s7_is(v, Expression), logical(1)))
## `.attr_set`, not `isTRUE`: `sparsity` is an INDEX SET, so every spelling
## except the degenerate TRUE is a vector and `isTRUE` is FALSE for all of
## them -- which silently skipped the structural-zero bound rules below.
has_structural_zeros <- .attr_set(sparsity) || isTRUE(diag)
n_elem <- prod(shape)
if (has_structural_zeros) {
if (!is.null(bounds[[1L]]) && !.s7_is(bounds[[1L]], Expression) &&
!inherits(bounds[[1L]], "Matrix")) {
if (length(bounds[[1L]]) == 1L) {
if (as.numeric(bounds[[1L]]) > 0) {
cli_abort("Scalar lower bound must be <= 0 for sparse or diagonal variables.")
}
} else {
cli_abort("Dense array bounds are not supported for sparse or diagonal variables.")
}
}
if (!is.null(bounds[[2L]]) && !.s7_is(bounds[[2L]], Expression) &&
!inherits(bounds[[2L]], "Matrix")) {
if (length(bounds[[2L]]) == 1L) {
if (as.numeric(bounds[[2L]]) < 0) {
cli_abort("Scalar upper bound must be >= 0 for sparse or diagonal variables.")
}
} else {
cli_abort("Dense array bounds are not supported for sparse or diagonal variables.")
}
}
}
if (has_expr_bound) {
if (!isTRUE(allow_expr_bounds)) {
cli_abort("Parametric bounds are only supported on Variables.")
}
if (has_structural_zeros) {
cli_abort(c(
"Expression bounds are not yet supported for sparse or diagonal variables.",
"i" = "Use numeric scalar bounds or dense variables."
))
}
none_defaults <- list(-Inf, Inf)
for (i in 1:2) {
b <- bounds[[i]]
if (.s7_is(b, Expression)) {
if (length(variables(b)) > 0L) {
cli_abort("Parametric bounds must not depend on Variables. Use Parameters or numeric values instead.")
}
if (!expr_is_scalar(b) && !identical(as.integer(.shape(b)), as.integer(shape))) {
cli_abort("Expression bounds must be scalar or have the same dimensions as the variable.")
}
} else if (is.null(b)) {
bounds[[i]] <- none_defaults[[i]]
} else if (length(b) == 1L) {
bounds[[i]] <- as.numeric(b)
} else if (inherits(b, "Matrix")) {
if (!identical(as.integer(dim(b)), as.integer(shape))) {
cli_abort("Bounds should be NULL, scalars, arrays, or CVXR Expressions with matching dimensions.")
}
} else if (!identical(as.integer(dim(as.array(b))), as.integer(shape)) &&
length(b) != n_elem) {
cli_abort("Bounds should be NULL, scalars, arrays, or CVXR Expressions with matching dimensions.")
}
}
} else {
## Promote NULL to -Inf/Inf, scalars to arrays matching shape. Sparse
## Matrix bounds stay sparse, matching CVXPY's COO-preserving path.
##
## The DENSE non-scalar arm below is the one that was missing. Upstream's
## numeric path (leaf.py:909-921) computes
## valid_array = isinstance(val, np.ndarray) and val.shape == self.shape
## and raises unless the bound is None, scalar-like, or exactly that
## shape. CVXR shape-checked only `Matrix`-class bounds, so a plain R
## matrix of the wrong size was accepted and silently mis-used later:
## `CvxAttr2Constr` rebuilds a symmetric/diag variable at a SMALLER shape
## and copies the attributes across (.cvxattr_preserve_bound_attrs,
## cvx_attr2constr.R:186 -- upstream's `**new_attr` at
## cvx_attr2constr.py:213, where this same validator re-runs and rejects),
## then `.extract_lower_bounds` (cone_matrix_stuffing.R:127) reshapes with
## `array(b, dim = .shape(v))`, TRUNCATING n^2 bounds to n(n+1)/2.
## Measured on a symmetric 3x3 with per-entry bounds, `min sum(X)`,
## sum(LB) = -31: CVXR returned -26 on HIGHS (feasible but suboptimal, so
## nothing downstream complains) while CLARABEL/SCS/OSQP gave -31. CVXPY
## raises here, and only on the HIGHS path, because the bound-consuming
## solvers are the ones that keep the attribute (reduce_bounds = FALSE).
##
## R deviation, deliberate: a dim-less numeric of the right total length
## is accepted, since `Variable(3, bounds = list(rep(-1, 3), rep(1, 3)))`
## is ordinary R and CVXR shapes vectors as c(n, 1). A bound that CARRIES
## a dim must match `shape` exactly, as upstream requires.
none_defaults <- list(-Inf, Inf)
bad_shape <- "Bounds should be NULL, scalars, or arrays with the same dimensions as the variable/parameter."
for (i in 1:2) {
b <- bounds[[i]]
if (is.null(b)) {
bounds[[i]] <- rep(none_defaults[[i]], n_elem)
} else if (inherits(b, "Matrix")) {
if (!identical(as.integer(dim(b)), as.integer(shape))) cli_abort(bad_shape)
} else if (length(b) == 1L) {
bounds[[i]] <- rep(b, n_elem)
} else if (!is.null(dim(b))) {
if (!identical(as.integer(dim(b)), as.integer(shape))) cli_abort(bad_shape)
} else if (length(b) != n_elem) {
cli_abort(bad_shape)
}
}
}
lb <- bounds[[1L]]
ub <- bounds[[2L]]
lb_data <- if (inherits(lb, "sparseMatrix")) lb@x else lb
ub_data <- if (inherits(ub, "sparseMatrix")) ub@x else ub
if (!.s7_is(lb, Expression) && !.s7_is(ub, Expression) &&
(any(is.nan(lb_data)) || any(is.nan(ub_data)))) {
cli_abort("NaN is not feasible as lower or upper bound.")
}
if (!.s7_is(lb, Expression) && any(lb_data == Inf)) {
cli_abort("Inf is not feasible as a lower bound.")
}
if (!.s7_is(ub, Expression) && any(ub_data == -Inf)) {
cli_abort("-Inf is not feasible as an upper bound.")
}
if (!.s7_is(lb, Expression) && !.s7_is(ub, Expression)) {
lb_check <- if (inherits(lb, "sparseMatrix")) as.matrix(lb) else lb_data
ub_check <- if (inherits(ub, "sparseMatrix")) as.matrix(ub) else ub_data
}
if (!.s7_is(lb, Expression) && !.s7_is(ub, Expression) && any(lb_check > ub_check)) {
cli_abort("Invalid bounds: some upper bounds are less than corresponding lower bounds.")
}
}
## CVXPY v1.8.2 fix: reject combining sign attributes (pos/neg) with
## sparsity attributes (sparsity/diag). Sparsity forces zeros, which
## contradicts strict positivity/negativity.
sign_attrs <- c(if (pos) "pos", if (neg) "neg")
## `if (sparsity)` on an index vector raised R's bare
## "the condition has length > 1" -- so EVERY index spelling of `sparsity`
## died here, before reaching any of the code that was supposed to use it.
sparse_attrs <- c(if (.attr_set(sparsity)) "sparsity", if (isTRUE(diag)) "diag")
if (length(sign_attrs) > 0L && length(sparse_attrs) > 0L) {
cli_abort(c(
"Cannot combine {.val {sign_attrs}} with {.val {sparse_attrs}}.",
"i" = "Sparsity and diag attributes force zeros, which contradicts strict positivity/negativity."
))
}
## A partial boolean/integer index list is incompatible with the attributes
## that rebuild the leaf at a smaller shape -- checked here, at construction,
## because it is invalid regardless of the problem the leaf ends up in.
reducing <- c("symmetric", "PSD", "NSD", "diag", "sparsity")[
c(isTRUE(symmetric), isTRUE(PSD), isTRUE(NSD), isTRUE(diag), .attr_set(sparsity))]
if (length(reducing) > 0L) {
.reject_partial_mip_idx(boolean, integer, paste(reducing, collapse = "/"))
}
## Build attributes list (mirrors CVXPY leaf.py line 124-130)
list(
nonneg = nonneg, nonpos = nonpos,
pos = pos, neg = neg,
complex = complex, imag = imag,
symmetric = symmetric, diag = diag,
PSD = PSD, NSD = NSD,
hermitian = hermitian, boolean = boolean,
integer = integer, sparsity = sparsity,
bounds = bounds
)
}
# -- .mip_idx: canonical index set for a boolean/integer attribute -----
## CVXPY SOURCE: leaf.py lines 133-146 -- upstream keeps `attributes['boolean']`
## exactly as the user wrote it and derives a separate `boolean_idx` for every
## consumer. Same split here: the raw attribute stays in `attributes`, this
## returns the canonical form stored in the `.boolean_idx` / `.integer_idx`
## properties.
##
## Canonical form: a 1-BASED FLAT (column-major) integer vector, which is plain
## R `x[i]` indexing. `TRUE` expands to every position, matching upstream's
## `np.unravel_index(arange(prod(shape)), ...)`.
##
## DEVIATION (deliberate, documented): upstream's index form is a numpy
## multi-index -- a sequence of PER-DIMENSION arrays, so `boolean=[(1,1),(0,1)]`
## means entries (1,0) and (1,1), not the coordinate pairs it resembles. That
## idiom has no R counterpart, and its edges are sharp even in Python
## (`boolean=[0]` raises TypeError on a 1-D variable; `boolean=[(0,0)]` raises
## ValueError on a 2-D one). CVXR accepts the R spellings instead, all four
## unambiguous, and CVXR is 2-D only so a coordinate is always (row, col):
##
## TRUE / FALSE whole variable / none
## c(1, 4) flat column-major positions
## cbind(c(1, 2), c(1, 2)) entries (1,1) and (2,2) [2-column matrix]
## matrix(c(TRUE, FALSE, ...)) logical mask, same shape as the variable
##
## Validation is stricter than upstream's, on purpose: numpy raises where R
## would recycle or silently mis-index, and silent mis-indexing is the failure
## mode this whole bug class keeps producing.
.mip_idx <- function(attr, shape, what) {
if (is.null(attr) || identical(attr, FALSE)) return(integer(0))
n <- as.integer(prod(shape))
if (isTRUE(attr)) return(seq_len(n))
nr <- shape[1L]
nc <- if (length(shape) >= 2L) shape[2L] else 1L
## Logical mask, same shape as the variable.
if (is.logical(attr)) {
if (length(attr) != n) {
cli_abort(c(
"{.arg {what}} logical mask has {length(attr)} entries but the variable has {n}.",
"i" = "A mask must have exactly the shape of the variable."
))
}
if (anyNA(attr)) cli_abort("{.arg {what}} logical mask must not contain {.val NA}.")
return(which(attr))
}
if (!is.numeric(attr)) {
cli_abort(c(
"{.arg {what}} must be {.code TRUE}/{.code FALSE}, a vector of flat indices, \\
a two-column matrix of (row, column) pairs, or a logical mask.",
"x" = "Got {.cls {class(attr)[[1L]]}}."
))
}
if (anyNA(attr)) cli_abort("{.arg {what}} indices must not contain {.val NA}.")
if (any(attr != trunc(attr))) {
cli_abort("{.arg {what}} indices must be whole numbers; got {.val {attr[attr != trunc(attr)][1L]}}.")
}
if (is.matrix(attr) && ncol(attr) == 2L) {
## (row, column) pairs -- CVXR is 2-D, so this is the whole story.
r <- as.integer(attr[, 1L]); cc <- as.integer(attr[, 2L])
bad <- r < 1L | r > nr | cc < 1L | cc > nc
if (any(bad)) {
i <- which(bad)[1L]
cli_abort(c(
"{.arg {what}} index ({r[i]}, {cc[i]}) is outside the variable's {nr}x{nc} shape.",
"i" = "Indices are 1-based, as everywhere else in R."
))
}
idx <- (cc - 1L) * nr + r # column-major flat position
} else {
idx <- as.integer(attr)
bad <- idx < 1L | idx > n
if (any(bad)) {
cli_abort(c(
"{.arg {what}} index {.val {idx[which(bad)[1L]]}} is outside 1:{n}.",
"i" = "Flat indices are 1-based and column-major."
))
}
}
if (anyDuplicated(idx)) {
cli_abort("{.arg {what}} indices must be unique; {.val {idx[anyDuplicated(idx)]}} is repeated.")
}
sort(idx)
}
# -- .reject_partial_mip_idx ------------------------------------------
## A PARTIAL boolean/integer index list cannot coexist with an attribute that
## makes the reduction rebuild the variable at a SMALLER shape (symmetric /
## PSD / NSD -> upper-triangle vector, diag -> diagonal vector, sparsity ->
## stored entries): the indices are positions in the ORIGINAL variable and
## nothing carries them across the rebuild.
##
## On a 3x3 symmetric, `boolean = c(5)` means entry (2,2), but position 5 of the
## 6-element upper-triangle vector is entries (3,2) & (2,3) -- in range, wrong
## entries, no error. On a 2x2 the same request is out of range instead.
##
## PARITY: CVXPY refuses this combination too, incidentally rather than by
## design -- it carries the raw attribute onto the reduced variable and numpy
## raises `invalid entry in coordinates array` inside `ravel_multi_index`
## (matrix_stuffing.py:113). Measured on 1.9.2, the requests that survive there
## are exactly the entries of the matrix's FIRST COLUMN, where the untranslated
## row index coincides with the correct reduced position and the column index 0
## is the only one in range for the (k, 1) reduced shape. That is an artifact of
## numpy indexing, not documented behavior, and CVXR cannot reproduce it: its
## canonical index form is a flat 1-based vector with no column component.
## Rejecting uniformly is both the parity answer and the consistent one.
.reject_partial_mip_idx <- function(boolean, integer, reducing) {
for (nm in c("boolean", "integer")) {
val <- if (identical(nm, "boolean")) boolean else integer
if (is.null(val) || identical(val, FALSE) || isTRUE(val)) next
cli_abort(c(
"A partial {.arg {nm}} index list cannot be combined with {.arg {reducing}}.",
"x" = "{.arg {reducing}} replaces the variable with a smaller one, so the \\
indices would refer to different entries.",
"i" = "Use {.code {nm} = TRUE} to constrain the whole variable, or drop \\
{.arg {reducing}} and state the structure with explicit constraints.",
"i" = "CVXPY rejects this combination as well."
))
}
invisible(NULL)
}
Leaf <- new_class("Leaf", parent = Expression, package = "CVXR",
properties = list(
.value = new_property(class = class_any, default = NULL),
attributes = new_property(class = class_list, default = list()),
## Canonical 1-based flat index sets derived from the raw attributes; see
## `.mip_idx`. Constraint 17: `.fast_new` skips defaults, so EVERY Leaf
## subclass constructor passes both explicitly.
.boolean_idx = new_property(class = class_integer, default = integer(0)),
.integer_idx = new_property(class = class_integer, default = integer(0)),
## CVXPY SOURCE: leaf.py:148-152 -- `self.sparse_idx =
## self._validate_indices(sparsity)`, the canonical form of the `sparsity`
## attribute. Same split as boolean/integer: the raw attribute stays in
## `attributes`, the canonical index lives here, and every consumer reads
## THIS rather than re-deriving (see the project() comment for what
## re-deriving cost boolean).
.sparse_idx = new_property(class = class_integer, default = integer(0))
),
constructor = function(shape = c(1L, 1L), value = NULL,
nonneg = FALSE, nonpos = FALSE,
complex = FALSE, imag = FALSE,
symmetric = FALSE, diag = FALSE,
PSD = FALSE, NSD = FALSE, hermitian = FALSE,
boolean = FALSE, integer = FALSE,
sparsity = FALSE, pos = FALSE, neg = FALSE,
bounds = NULL, id = NULL) {
if (FALSE) new_object(S7_object()) ## S7 static-check guard
shape <- validate_shape(shape)
attrs <- .build_leaf_attrs(shape,
nonneg = nonneg, nonpos = nonpos,
complex = complex, imag = imag,
symmetric = symmetric, diag = diag,
PSD = PSD, NSD = NSD, hermitian = hermitian,
boolean = boolean, integer = integer,
sparsity = sparsity, pos = pos, neg = neg,
bounds = bounds)
if (is.null(id)) id <- next_expr_id()
obj <- .fast_new(Leaf, S7_object(),
id = as.integer(id),
.cache = new.env(parent = emptyenv()),
shape = shape,
.value = NULL,
attributes = attrs,
.boolean_idx = .mip_idx(boolean, shape, "boolean"),
.integer_idx = .mip_idx(integer, shape, "integer"),
.sparse_idx = .mip_idx(sparsity, shape, "sparsity"),
args = list()
)
## Assign value if provided (goes through validation)
if (!is.null(value)) {
value(obj) <- value
}
obj
}
)
# -- is_pos: strictly positive (from attributes) -----------------------
## CVXPY SOURCE: leaf.py lines 271-273
method(is_pos, Leaf) <- function(x) isTRUE(.attributes(x)$pos)
# -- Log-log DGP: Leaf is log-log convex/concave iff positive ---------
## CVXPY SOURCE: leaf.py lines 254-260
method(is_log_log_convex, Leaf) <- function(x) is_pos(x)
method(is_log_log_concave, Leaf) <- function(x) is_pos(x)
# -- Quadratic / PWL: leaves are always quadratic and piecewise-linear --
## CVXPY SOURCE: leaf.py lines 613-623
method(is_quadratic, Leaf) <- function(x) TRUE
method(has_quadratic_term, Leaf) <- function(x) FALSE
method(is_pwl, Leaf) <- function(x) TRUE
# -- Curvature: Leaves are always convex and concave (affine) ----------
## CVXPY SOURCE: leaf.py lines 246-252
method(is_convex, Leaf) <- function(x) TRUE
method(is_concave, Leaf) <- function(x) TRUE
# -- get_bounds: effective (lower, upper) from bounds + sign + boolean --
## CVXPY SOURCE: leaf.py lines 929-1032 (CVXPY v1.9.0)
## Numeric dense bounds are length-prod(shape) vectors already broadcast at
## construction; sparse Matrix bounds are preserved; symbolic
## Expression/Parameter bounds are skipped here and enforced when
## CvxAttr2Constr lowers bound attributes.
method(get_bounds, Leaf) <- function(x) {
## Cached (CVXPY Atom.get_bounds is @compute_once; bounds depend only on the
## leaf's immutable attributes, so caching is safe).
cached <- cache_get(x, "get_bounds")
if (!cache_miss(cached)) return(cached)
a <- .attributes(x)
n_elem <- prod(.shape(x))
lb <- rep(-Inf, n_elem)
ub <- rep(Inf, n_elem)
## bounds attribute (already length-n_elem vectors, or NULL)
if (!is.null(a$bounds) && is.list(a$bounds)) {
if (!.s7_is(a$bounds[[1L]], Expression)) {
if (inherits(a$bounds[[1L]], "Matrix")) {
lb <- a$bounds[[1L]]
} else {
lb <- pmax(lb, a$bounds[[1L]])
}
}
if (!.s7_is(a$bounds[[2L]], Expression)) {
if (inherits(a$bounds[[2L]], "Matrix")) {
ub <- a$bounds[[2L]]
} else {
ub <- pmin(ub, a$bounds[[2L]])
}
}
}
## sign attributes (CVXPY leaf.py:984-1000)
if (isTRUE(a$nonneg) || isTRUE(a$pos)) {
lb <- if (inherits(lb, "sparseMatrix")) pmax(as.matrix(lb), 0) else pmax(lb, 0)
}
if (isTRUE(a$nonpos) || isTRUE(a$neg)) {
ub <- if (inherits(ub, "sparseMatrix")) pmin(as.matrix(ub), 0) else pmin(ub, 0)
}
## boolean -> [0, 1] (CVXPY leaf.py:1002-1017)
if (isTRUE(a$boolean)) {
lb <- if (inherits(lb, "sparseMatrix")) pmax(as.matrix(lb), 0) else pmax(lb, 0)
ub <- if (inherits(ub, "sparseMatrix")) pmin(as.matrix(ub), 1) else pmin(ub, 1)
}
## Shape to the leaf's dims (matching CVXPY's shape-carrying bounds) via a
## dim-attribute re-tag -- O(1), no data copy. Composite-expression
## propagation (#3080) needs shaped bounds for matmul/transpose/reshape/index.
if (!inherits(lb, "Matrix")) dim(lb) <- .shape(x)
if (!inherits(ub, "Matrix")) dim(ub) <- .shape(x)
result <- list(lb, ub)
cache_set(x, "get_bounds", result)
result
}
# -- Sign queries from attributes --------------------------------------
## CVXPY SOURCE: leaf.py lines 262-269
method(is_nonneg, Leaf) <- function(x) {
a <- .attributes(x)
isTRUE(a$nonneg) || isTRUE(a$pos) || isTRUE(a$boolean)
}
method(is_nonpos, Leaf) <- function(x) {
a <- .attributes(x)
isTRUE(a$nonpos) || isTRUE(a$neg)
}
# -- DNLP curvature: a leaf is trivially linearizable ------------------
## CVXPY SOURCE: leaf.py lines 267-273 (CVXPY v1.9.0)
method(is_linearizable_convex, Leaf) <- function(x) TRUE
method(is_linearizable_concave, Leaf) <- function(x) TRUE
# -- Matrix property queries -------------------------------------------
## is_symmetric: scalar or relevant attributes
## CVXPY SOURCE: leaf.py lines 284-287
method(is_symmetric, Leaf) <- function(x) {
expr_is_scalar(x) ||
any(vapply(c("diag", "symmetric", "PSD", "NSD"),
function(k) isTRUE(.attributes(x)[[k]]), logical(1)))
}
## is_psd / is_nsd: directly from attributes
## CVXPY SOURCE: leaf.py lines 601-607
method(is_psd, Leaf) <- function(x) isTRUE(.attributes(x)$PSD)
method(is_nsd, Leaf) <- function(x) isTRUE(.attributes(x)$NSD)
## is_complex / is_imag: from attributes
## CVXPY SOURCE: leaf.py lines 289-295
method(is_complex, Leaf) <- function(x) {
a <- .attributes(x)
isTRUE(a$complex) || isTRUE(a$imag) || isTRUE(a$hermitian)
}
method(is_imag, Leaf) <- function(x) isTRUE(.attributes(x)$imag)
## is_hermitian: from attributes
## CVXPY SOURCE: leaf.py lines 279-282
method(is_hermitian, Leaf) <- function(x) {
(is_real(x) && is_symmetric(x)) ||
isTRUE(.attributes(x)$hermitian) ||
is_psd(x) || is_nsd(x)
}
# -- value / value<- --------------------------------------------------
## Value getter: check .cache first (reference semantics for constraints),
## then fall back to @.value (copy semantics).
## .cache is an environment (R reference type), so when constraint objects
## hold references to Variables, value updates propagate through the shared
## .cache environment even though R has copy-on-modify semantics for S7 props.
method(value, Leaf) <- function(x) {
if (exists("leaf_value", envir = x@.cache, inherits = FALSE))
return(x@.cache$leaf_value)
x@.value
}
# -- Shared value validation (used by Leaf and Parameter value<-) ------
## Validates shape, projects onto attribute domain, checks tolerance.
## Returns the validated (converted) value or NULL.
.validate_leaf_value <- function(x, value) {
if (is.null(value)) return(NULL)
value <- intf_convert(value)
val_shape <- intf_shape(value)
if (!identical(as.integer(val_shape), as.integer(.shape(x)))) {
cli_abort("Invalid dimensions ({paste(val_shape, collapse = ', ')}) for {.cls {class(x)[[1L]]}} value.")
}
## Project and validate
projected <- project(x, value)
delta <- suppressWarnings(abs(value - projected))
if (inherits(delta, "sparseMatrix")) {
if (length(delta@x) > 0L) {
nan_mask <- is.nan(delta@x)
if (any(nan_mask)) {
val_dense <- as.matrix(value)
proj_dense <- as.matrix(projected)
nz <- summary(delta)
equal_inf <- val_dense[cbind(nz$i, nz$j)] == proj_dense[cbind(nz$i, nz$j)]
## `%in% TRUE` rather than the bare comparison: `NaN == NaN` is NA in R,
## and an NA in a subscript is an error, not a no-op. Only genuinely
## equal entries (the Inf - Inf case this exists for) are zeroed.
delta@x[nan_mask & (equal_inf %in% TRUE)] <- 0
}
}
close_enough <- all(abs(delta@x) < SPARSE_PROJECTION_TOL)
} else {
nan_mask <- is.nan(delta)
if (any(nan_mask)) {
equal_inf <- as.array(value) == as.array(projected)
## See the sparse branch: `NaN == NaN` is NA, and NA is not a legal
## subscript. This is the Inf - Inf case only.
delta[nan_mask & (equal_inf %in% TRUE)] <- 0
}
if (isTRUE(.attributes(x)$PSD) || isTRUE(.attributes(x)$NSD)) {
close_enough <- norm(delta, type = "2") <= PSD_NSD_PROJECTION_TOL
} else {
close_enough <- all(abs(delta) < GENERAL_PROJECTION_TOL)
}
}
## `!isTRUE`, not `!`: a NaN in the value makes every comparison above NA, and
## `if (NA)` raises R's internal "missing value where TRUE/FALSE needed"
## instead of the message this cascade exists to produce. Upstream has no
## such case -- `np.allclose` returns False for NaN (leaf.py:640-651) -- so
## treating a non-TRUE result as "not close" IS the upstream behavior.
## Measured: `value(Parameter(2)) <- c(NaN, 1)` gave the raw R error in CVXR
## and `ValueError: Parameter value must be real.` in CVXPY 1.9.2.
if (!isTRUE(close_enough)) {
attr_str <- .leaf_attr_str(x)
cli_abort("{.cls {class(x)[[1L]]}} value must be {attr_str}.")
}
value
}
method(`value<-`, Leaf) <- function(x, value) {
validated <- .validate_leaf_value(x, value)
x@.cache$leaf_value <- validated # Reference-semantic store (shared with constraint refs)
x@.value <- validated # Copy-semantic store (for direct access)
x
}
# -- save_leaf_value --------------------------------------------------
## CVXPY SOURCE: leaf.py lines 454-462
## Stores solver output WITHOUT validation/projection.
## Used by problem_unpack() after solving.
save_leaf_value <- function(x, val) {
x@.cache$leaf_value <- val
invisible(x)
}
# -- project ----------------------------------------------------------
## CVXPY SOURCE: leaf.py lines 373-451
method(project, Leaf) <- function(x, val, ...) {
a <- .attributes(x)
## Real projection (skip if complex).
## Bridge a base-R / Matrix-package gap: CVXPY uses np.real() which
## handles numpy sparse arrays transparently, but base R's Re() errors
## on sparse Matrix objects ("non-numeric argument to function").
## When val is a real numeric (or sparse real Matrix), Re() is a no-op
## by design, so guard with is.complex(val) — only invoke Re() when
## the input is actually complex, in which case Re() works (numpy and
## base R agree on complex semantics).
##
## The predicate must be is_complex(), NOT a hand-rolled complex/imag test:
## leaf.py:318 defines is_complex() as complex || is_imag() || hermitian, so a
## `hermitian = TRUE` leaf IS complex. Testing only complex/imag stripped the
## imaginary part of a Hermitian value BEFORE the hermitian branch below could
## run, which made `value(Parameter(c(2, 2), hermitian = TRUE)) <- H` abort with
## "value must be real" for every genuinely complex Hermitian H, and made
## project() silently return Re(H).
if (!is_complex(x) && is.complex(val)) {
val <- Re(val)
}
## Count active attributes (skip projection for >1 attribute)
n_attr <- sum(vapply(a, function(v) !identical(v, FALSE) && !is.null(v), logical(1)))
if (n_attr > 1L) return(val)
if (isTRUE(a$nonpos) && isTRUE(a$nonneg)) {
return(0 * val)
} else if (isTRUE(a$nonpos) || isTRUE(a$neg)) {
return(pmin(val, 0))
} else if (isTRUE(a$nonneg) || isTRUE(a$pos)) {
return(pmax(val, 0))
} else if (length(x@.boolean_idx) > 0L) {
## CVXPY SOURCE: leaf.py:465-470 -- upstream indexes with `self.boolean_idx`,
## the CANONICAL index set computed once in __init__ (leaf.py:131-139), not
## with the raw `attributes['boolean']` the user wrote.
##
## CVXR has that canonical form too -- `.mip_idx()` -> `@.boolean_idx` -- and
## the SOLVER path already reads it (cone_matrix_stuffing.R:234-238,
## problems/problem.R:247). This method re-derived from the raw attribute
## instead, so two enumerations of "which entries are boolean" disagreed
## inside CVXR and two of the four documented spellings broke, in opposite
## directions (all on a 2x2 with the DIAGONAL boolean, i.e. entries 1 and 4):
##
## boolean = cbind(c(1,2), c(1,2)) as.integer() of the coordinate matrix
## is 1,2,1,2, so project() rounded entries 1 and 2 -- the WRONG ones --
## and `value(v) <- matrix(c(1, 0.7, 0.7, 0), 2, 2)` was REJECTED even
## though that value is valid.
## boolean = matrix(c(TRUE,FALSE,FALSE,TRUE), 2, 2) a logical mask is
## neither `isTRUE` nor `is.numeric`, so every branch fell through and
## project() was a NO-OP: `value(v) <- matrix(c(0.7,0,0,0.7), 2, 2)` was
## SILENTLY ACCEPTED with non-integral entries on the boolean diagonal.
##
## `_validate_value` routes every `.value =` through project (leaf.py:608),
## which is why a projection bug is a validation bug.
##
## `boolean = TRUE` needs no separate arm: .mip_idx expands it to every
## position, so this branch reproduces the old whole-variable behavior.
idx <- x@.boolean_idx
new_val <- as.numeric(val)
new_val[idx] <- round(pmin(pmax(new_val[idx], 0), 1))
if (!is.null(dim(val))) dim(new_val) <- dim(val)
return(new_val)
} else if (length(x@.integer_idx) > 0L) {
## CVXPY SOURCE: leaf.py:471-475. Mirror of the boolean branch above.
idx <- x@.integer_idx
new_val <- as.numeric(val)
new_val[idx] <- round(new_val[idx])
if (!is.null(dim(val))) dim(new_val) <- dim(val)
return(new_val)
} else if (length(x@.sparse_idx) > 0L) {
## CVXPY SOURCE: leaf.py:506-512 -- project onto the sparsity pattern by
## zeroing every entry outside it.
##
## Upstream additionally emits a RuntimeWarning here, because a sparse leaf
## stores only its nonzero data and reaching this branch means someone came
## in through the dense representation. CVXR stores leaf values densely, so
## that warning would fire on the ordinary path and is deliberately not
## ported; `value_sparse` has no CVXR counterpart either. See the
## `## PARTIAL PORT:` sentinel at the top of this file.
new_val <- as.numeric(val)
keep <- logical(length(new_val))
keep[x@.sparse_idx] <- TRUE
new_val[!keep] <- 0
if (!is.null(dim(val))) dim(new_val) <- dim(val)
return(new_val)
} else if (isTRUE(a$symmetric) || isTRUE(a$PSD) || isTRUE(a$NSD)) {
if (is.integer(val)) val <- as.double(val)
val <- (val + t(val)) / 2
if (isTRUE(a$symmetric)) return(val)
ev <- .eigvalsh(val, only_values = FALSE)
w <- ev$values
V <- ev$vectors
if (isTRUE(a$PSD)) {
bad <- w < 0
if (!any(bad)) return(val)
w[bad] <- 0
} else {
## NSD
bad <- w > 0
if (!any(bad)) return(val)
w[bad] <- 0
}
return((V %*% diag(w, nrow = length(w))) %*% t(V))
} else if (isTRUE(a$diag)) {
d <- diag(val)
return(Matrix::Diagonal(x = d))
} else if (isTRUE(a$hermitian)) {
return((val + t(Conj(val))) / 2)
} else if (isTRUE(a$imag)) {
## CVXPY: np.imag(val) * 1j -- project onto purely imaginary
return(Im(val) * 1i)
} else if (isTRUE(a$complex)) {
## CVXPY: val.astype(complex) -- ensure complex type
if (!is.complex(val)) val <- as.complex(val)
return(val)
}
## Bounds clamping
## CVXPY SOURCE: leaf.py lines 440-445 (project -> np.clip)
if (!is.null(a$bounds) && is.list(a$bounds)) {
if (any(vapply(a$bounds, function(b) .s7_is(b, Expression), logical(1)))) {
return(val)
}
lb <- a$bounds[[1L]]
ub <- a$bounds[[2L]]
val <- pmax(val, lb)
val <- pmin(val, ub)
}
val
}
# -- Leaf collections default to empty --------------------------------
## CVXPY SOURCE: leaf.py lines 234-244
method(variables, Leaf) <- function(x) list()
method(parameters, Leaf) <- function(x) {
b <- .attributes(x)$bounds
if (is.null(b) || !is.list(b)) return(list())
unique_params <- list()
seen <- character(0)
for (bound in b) {
if (.s7_is(bound, Expression)) {
for (p in parameters(bound)) {
pid <- as.character(.id(p))
if (!(pid %in% seen)) {
unique_params[[length(unique_params) + 1L]] <- p
seen <- c(seen, pid)
}
}
}
}
unique_params
}
method(constants, Leaf) <- function(x) list()
method(atoms, Leaf) <- function(x) list()
# -- _bound_domain: the ONE place a leaf's attributes become constraints
## CVXPY SOURCE: leaf.py:350-414 (Leaf._bound_domain), with its two callers at
## cvx_attr2constr.py:271 (`var._bound_domain(obj, constr)`) and leaf.py:424
## (`Leaf.domain`).
##
## Why this function exists in CVXR now: it did not, and the rule it encodes had
## been transcribed THREE times, none of them complete -- the sign pair inlined
## in CvxAttr2Constr, the bounds pair in a private helper beside it, and the
## PSD/NSD pair in a third branch -- while the fourth caller, `domain(Leaf)`,
## was a stub returning `list()`. That split is not a style question: it is the
## mechanism that let `Variable(pos = TRUE)` emit no constraint for six
## releases, because "add the sign constraint" existed in a copy that nobody
## updated when `pos`/`neg` were added to the attribute list. One function, two
## callers, as upstream.
##
## DELIBERATE DEVIATION, single-site and documented: upstream emits the ordinary
## inequalities `term >= 0` / `term <= 0`; CVXR's reduction has always emitted
## the CONE constraints NonNeg(term) / NonPos(term) at this point, which is
## equivalent here (term is affine) and skips a canonicalization step. `cone`
## selects the form so that consolidating these copies changes no solve. The
## `domain()` caller uses upstream's inequality form, which is what a user
## reading `domain(x)` expects to see.
.leaf_bound_domain <- function(leaf, term, constraints = list(), cone = FALSE) {
a <- .attributes(leaf)
## CVXPY SOURCE: leaf.py:358-361 -- each sign PAIR shares one constraint.
if (isTRUE(a$nonneg) || isTRUE(a$pos)) {
constraints <- c(constraints, list(if (cone) NonNeg(term) else term >= 0))
}
if (isTRUE(a$nonpos) || isTRUE(a$neg)) {
constraints <- c(constraints, list(if (cone) NonPos(term) else term <= 0))
}
## CVXPY SOURCE: leaf.py:362-414 -- bounds. Build only the FINITE numeric
## bound constraints; symbolic Expression bounds are appended whole.
if (is.null(a$bounds) || !is.list(a$bounds)) return(constraints)
add_lower <- function(constrs, bound) {
if (.s7_is(bound, Expression)) {
return(c(constrs, list(term >= bound)))
}
if (inherits(bound, "sparseMatrix")) {
sb <- Matrix::summary(bound)
keep <- sb$x != -Inf
if (any(keep)) {
idx <- cbind(sb$i[keep], sb$j[keep])
constrs <- c(constrs, list(term[idx] >= sb$x[keep]))
}
return(constrs)
}
if (!any(bound != -Inf)) return(constrs)
if (length(bound) == 1L) {
c(constrs, list(term >= as.numeric(bound)))
} else {
b <- bound
dim(b) <- .shape(term)
mask <- b != -Inf
c(constrs, list(term[mask] >= b[mask]))
}
}
add_upper <- function(constrs, bound) {
if (.s7_is(bound, Expression)) {
return(c(constrs, list(term <= bound)))
}
if (inherits(bound, "sparseMatrix")) {
sb <- Matrix::summary(bound)
keep <- sb$x != Inf
if (any(keep)) {
idx <- cbind(sb$i[keep], sb$j[keep])
constrs <- c(constrs, list(term[idx] <= sb$x[keep]))
}
return(constrs)
}
if (!any(bound != Inf)) return(constrs)
if (length(bound) == 1L) {
c(constrs, list(term <= as.numeric(bound)))
} else {
b <- bound
dim(b) <- .shape(term)
mask <- b != Inf
c(constrs, list(term[mask] <= b[mask]))
}
}
constraints <- add_lower(constraints, a$bounds[[1L]])
add_upper(constraints, a$bounds[[2L]])
}
# -- domain -----------------------------------------------------------
## CVXPY SOURCE: leaf.py:416-430
##
## This was `function(x) list()`. `method(domain, Atom)` (atoms/atom.R:541-547)
## recurses into arguments correctly, so the loss was confined to leaves -- but
## it meant `domain(Variable(2, nonneg = TRUE))` returned nothing where CVXPY
## returns `[x >= 0]`, and `domain()` of a partial_optimize
## (transforms/partial_optimize.R:290, the one live consumer in CVXR) came back
## with 1 constraint where CVXPY gives 3.
method(domain, Leaf) <- function(x) {
a <- .attributes(x)
domain <- .leaf_bound_domain(x, x, list())
## CVXPY SOURCE: leaf.py:426-429 -- semidefiniteness, `if`/`elif`.
if (isTRUE(a$PSD)) {
domain <- c(domain, list(PSD(x)))
} else if (isTRUE(a$NSD)) {
domain <- c(domain, list(PSD(-x)))
}
domain
}
# -- grad: default empty list -----------------------------------------
method(grad, Leaf) <- function(x) list()
# -- Helper to construct attribute error string ------------------------
.leaf_attr_str <- function(x) {
a <- .attributes(x)
if (isTRUE(a$nonneg)) "nonnegative"
else if (isTRUE(a$pos)) "positive"
else if (isTRUE(a$nonpos)) "nonpositive"
else if (isTRUE(a$neg)) "negative"
## CVXPY SOURCE: leaf.py:661-662 -- the sparsity arm of the same cascade.
else if (length(x@.sparse_idx) > 0L) "zero outside of sparsity pattern"
else if (isTRUE(a$diag)) "diagonal"
else if (isTRUE(a$PSD)) "positive semidefinite"
else if (isTRUE(a$NSD)) "negative semidefinite"
else if (isTRUE(a$imag)) "imaginary"
## CVXPY SOURCE: leaf.py:670-671 -- `elif self.attributes['bounds']:
## attr_str = 'in bounds'`. Without this arm the cascade fell through to
## "real", so rejecting an out-of-bounds assignment reported
## "value must be real." -- true of the number, and nothing to do with why it
## was rejected. Same defect, same cascade, as the boolean arm below, which
## was fixed separately; this is its neighbour.
else if (!is.null(a$bounds) && is.list(a$bounds)) "in bounds"
else if (isTRUE(a$symmetric)) "symmetric"
## `isTRUE` alone reported "real" for a PARTIAL index list, so rejecting 0.7
## on a `boolean = c(1)` leaf said "value must be real" -- true of 0.7, and
## nothing to do with why it was rejected. CVXPY says "value must be boolean"
## for the same assignment (measured, 1.9.2), which is what these two lines
## now produce for both spellings of the attribute.
else if (length(x@.boolean_idx) > 0L) "boolean"
else if (length(x@.integer_idx) > 0L) "integer"
else "real"
}
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.