R/057_atoms_affine_kron.R

Defines functions kron .kron_cst_loc

Documented in kron

#####
## DO NOT EDIT THIS FILE!! EDIT THE SOURCE INSTEAD: rsrc_tree/atoms/affine/kron.R
#####

## CVXPY SOURCE: atoms/affine/kron.py
## Kron -- Kronecker product (one arg must be constant)


Kron <- new_class("Kron", parent = AffAtom, package = "CVXR",
  constructor = function(a, b, id = NULL) {
    if (FALSE) new_object(S7_object())  ## S7 static-check guard
    if (is.null(id)) id <- next_expr_id()
    a <- as_expr(a)
    b <- as_expr(b)
    ## At least one must be constant
    if (!is_constant(a) && !is_constant(b)) {
      cli_abort("At least one argument to {.fn kron} must be constant.")
    }
    shape <- c(.shape(a)[1L] * .shape(b)[1L], .shape(a)[2L] * .shape(b)[2L])

    obj <- .fast_new(Kron, S7_object(),
      id    = as.integer(id),
      .cache = new.env(parent = emptyenv()),
      args  = list(a, b),
      shape = shape
    )
    obj
  }
)

method(shape_from_args, Kron) <- function(x) {
  a <- .args(x)[[1L]]; b <- .args(x)[[2L]]
  c(.shape(a)[1L] * .shape(b)[1L], .shape(a)[2L] * .shape(b)[2L])
}

# -- sign: same as multiplication (CVXPY kron.py line 71-74) ------
method(sign_from_args, Kron) <- function(x) {
  mul_sign(.args(x)[[1L]], .args(x)[[2L]])
}

# -- the data operand ----------------------------------------------
## The operand the constructor accepted as constant and graph_implementation()
## hands to the C++ handler as data (upstream: `cst_loc = 0 if
## self.args[0].is_constant() else 1`, kron.py:79). Located by "has no
## variables" rather than is_constant() so the answer is the same inside the
## DPP scope, where a Parameter is not constant; is_atom_convex below relies on
## that.
.kron_cst_loc <- function(x) {
  if (length(variables(.args(x)[[1L]])) == 0L) 1L else 2L
}

# -- monotonicity (CVXPY kron.py lines 76-86) --------------------
method(is_incr, Kron) <- function(x, idx, ...) {
  is_nonneg(.args(x)[[.kron_cst_loc(x)]])
}
method(is_decr, Kron) <- function(x, idx, ...) {
  is_nonpos(.args(x)[[.kron_cst_loc(x)]])
}

# -- is_nsd: PSD (x) NSD is NSD (CVXPY kron.py lines 96-102) ------
## A *sufficient* condition, exactly as upstream states it: one argument PSD and
## the other NSD makes the Kronecker product NSD.
##
## There is deliberately NO is_psd method here, and that is not an oversight.
## Upstream's is_psd (kron.py:88-94, both-PSD or both-NSD) is already covered by
## `AffAtom`'s monotonicity-based propagation -- verified, `is_psd(kron(I, I))`
## and `is_psd(kron(-I, -I))` are both TRUE without it -- so an override could
## only narrow what CVXR deduces.  is_nsd was the one case AffAtom loses: before
## this, `is_nsd` returned FALSE for EVERY Kron (checked across a 27-cell grid of
## PSD/NSD/indefinite/plain operands), so this method is purely additive and
## cannot turn an existing TRUE into FALSE.
method(is_nsd, Kron) <- function(x) {
  a <- .args(x)[[1L]]
  b <- .args(x)[[2L]]
  (is_psd(a) && is_nsd(b)) || (is_nsd(a) && is_psd(b))
}

## No log-log hooks: kron.py defines none, so Atom's FALSE applies
## (atom.py:195-202), although kron with a positive constant is entrywise a
## monomial; parity over a claim upstream does not make (sweep item 5, 5.1).

method(numeric_value, Kron) <- function(x, values, ...) {
  kronecker(values[[1L]], values[[2L]])
}

# -- curvature: affine, and under the DPP scope the DPP rule ----
## CVXPY SOURCE: atoms/affine/kron.py lines 54-69
##     def is_atom_convex(self) -> bool:
##         if u.scopes.dpp_scope_active():
##             # kron is not DPP if any parameters are present.
##             x = self.args[0]
##             y = self.args[1]
##             return ((x.is_constant() or y.is_constant()) and
##                     (is_param_free(x) and is_param_free(y)))
##         else:
##             return self.args[0].is_constant() or self.args[1].is_constant()
## RECORDED DIVERGENCE (ADR D_PORT.5, notes/audit/dpp_rules_2b.md): one
## predicate differs. The C++ handlers get_kron_mat / get_kron_l_mat read only
## the DATA operand through get_constant_data(), which cannot take a PARAM
## LinOp; the other operand's tensor, parameter slices included, flows through
## build_tensor like any affine argument. So CVXR requires the data operand
## alone to be parameter-free, where upstream rejects a parameter anywhere
## (test_kron_canon.py:71-77 calls that "the price of eliminating the speed
## benefit of using Parameter objects"). Measured against CVXPY 1.9.3: the
## only difference is the is_dpp() answer for kron(C, x + p); every solve
## agrees. Outside the scope the constructor (kron.R:11-14) has already
## enforced upstream's `else` branch. Until 1.9.2.9163 this rule sat in a
## method(is_dpp, Kron) reached by an argument recursion (decision 29).
method(is_atom_convex, Kron) <- function(x) {
  if (dpp_scope_active()) {
    return(is_param_free(.args(x)[[.kron_cst_loc(x)]]))
  }
  TRUE
}
method(is_atom_concave, Kron) <- function(x) is_atom_convex(x)

method(graph_implementation, Kron) <- function(x, arg_objs, shape, data = NULL, ...) {
  ## Determine which arg is constant
  if (is_constant(.args(x)[[1L]])) {
    list(kron_r_linop(arg_objs[[1L]], arg_objs[[2L]], shape), list())
  } else {
    list(kron_l_linop(arg_objs[[1L]], arg_objs[[2L]], shape), list())
  }
}

#' Kronecker product of two expressions
#'
#' @param a An Expression (one must be constant)
#' @param b An Expression
#' @returns A Kron atom
#' @export
kron <- function(a, b) {
  Kron(a, b)
}

Try the CVXR package in your browser

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

CVXR documentation built on Oct. 19, 2026, 5:07 p.m.