Nothing
## CVXPY Atoms Parity Tests
## ========================
## These tests mirror CVXPY's test_atoms.py (TestAtoms class) to verify
## that CVXR atom shape/sign/curvature/numeric behavior matches CVXPY exactly.
##
## Source: /Users/naras/GitHub/cvxpy/cvxpy/tests/test_atoms.py
## CVXPY setUp: a=Variable(), x=Variable(2), y=Variable(2),
## A=Variable((2,2)), B=Variable((2,2)), C=Variable((3,2))
##
## Shape mapping: CVXPY tuple()→c(1,1), (n,)→c(n,1), (n,m)→c(n,m)
## Sign mapping: CVXPY s.NONNEG→NONNEG_SIGN, s.NONPOS→NONPOS_SIGN, etc.
## Curvature mapping: CVXPY s.CONVEX→CONVEX, s.CONCAVE→CONCAVE, etc.
library(testthat)
library(CVXR)
# ═══════════════════════════════════════════════════════════════════════
# test_norm_inf (test_atoms.py line 72)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_norm_inf
test_that("norm_inf: shape, curvature, sign (CVXPY test_norm_inf)", {
x <- Variable(2)
y <- Variable(2)
expr <- x + y
atom <- norm_inf(expr)
## CVXPY: shape == tuple() → R: c(1L, 1L) scalar
expect_equal(atom@shape, c(1L, 1L))
## CVXPY: curvature == s.CONVEX
expect_equal(expr_curvature(atom), CONVEX)
expect_true(is_convex(atom))
expect_true(is_concave(-atom))
## CVXPY: norm_inf(atom).curvature == s.CONVEX (composition: convex nondecr of convex)
expect_equal(expr_curvature(norm_inf(atom)), CONVEX)
## CVXPY: norm_inf(-atom).curvature == s.CONVEX
expect_equal(expr_curvature(norm_inf(-atom)), CONVEX)
})
# ═══════════════════════════════════════════════════════════════════════
# test_norm1 (test_atoms.py line 85)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_norm1
test_that("norm1: shape, curvature (CVXPY test_norm1)", {
x <- Variable(2)
y <- Variable(2)
expr <- x + y
atom <- norm1(expr)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONVEX)
## Composition: norm1 of convex nonneg → CONVEX
expect_equal(expr_curvature(norm1(atom)), CONVEX)
## norm1(-atom) → CONVEX (norm is always convex)
expect_equal(expr_curvature(norm1(-atom)), CONVEX)
})
# ═══════════════════════════════════════════════════════════════════════
# test_power (test_atoms.py line 135)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_power
test_that("power: shape preserved for various p values (CVXPY test_power)", {
for (shape in list(c(1L, 1L), c(3L, 1L), c(2L, 3L))) {
x <- Variable(shape)
y <- Variable(shape)
expr <- x + y
for (p in c(0, 1, 2, 3, 2.7, 0.67, -1, -2.3)) {
atom <- power(expr, p)
expect_equal(atom@shape, shape, info = paste("p =", p, "shape =", paste(shape, collapse = ",")))
}
}
})
## @cvxpy test_atoms.py::TestAtoms::test_power
test_that("power: curvature varies by p (CVXPY test_power)", {
x <- Variable(c(2L, 1L))
y <- Variable(c(2L, 1L))
expr <- x + y
## p > 1 → CONVEX
expect_equal(expr_curvature(power(expr, 2)), CONVEX)
expect_equal(expr_curvature(power(expr, 3)), CONVEX)
expect_equal(expr_curvature(power(expr, 2.7)), CONVEX)
## p < 0 → CONVEX
expect_equal(expr_curvature(power(expr, -1)), CONVEX)
expect_equal(expr_curvature(power(expr, -2.3)), CONVEX)
## p == 1 → AFFINE
expect_equal(expr_curvature(power(expr, 1)), AFFINE)
## p == 0 → CONSTANT
expect_true(is_constant(power(expr, 0)))
## 0 < p < 1 → CONCAVE
expect_equal(expr_curvature(power(expr, 0.67)), CONCAVE)
})
## @cvxpy test_atoms.py::TestAtoms::test_power
test_that("power: sign is NONNEG for p != 1 (CVXPY test_power)", {
x <- Variable(c(2L, 1L))
y <- Variable(c(2L, 1L))
expr <- x + y
for (p in c(0, 2, 3, 2.7, 0.67, -1, -2.3)) {
atom <- power(expr, p)
expect_true(is_nonneg(atom), info = paste("p =", p))
}
})
## @cvxpy test_atoms.py::TestAtoms::test_power
test_that("power: numeric value for constant (CVXPY test_power)", {
## assert cp.power(-1, 2).value == 1
expect_equal(as.numeric(value(power(Constant(-1), 2))), 1)
})
# ═══════════════════════════════════════════════════════════════════════
# test_pnorm (test_atoms.py line 227)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_pnorm
test_that("pnorm: shape and curvature for various p (CVXPY test_pnorm)", {
x <- Variable(2)
## p = 1.5 → scalar, CONVEX, NONNEG
atom <- p_norm(x, p = 1.5)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONVEX)
expect_true(is_nonneg(atom))
## p = 1 → scalar, CONVEX, NONNEG (dispatches to norm1)
atom <- p_norm(x, p = 1)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONVEX)
expect_true(is_nonneg(atom))
## p = 2 → scalar, CONVEX, NONNEG
atom <- p_norm(x, p = 2)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONVEX)
expect_true(is_nonneg(atom))
## p = Inf → scalar, CONVEX, NONNEG (dispatches to norm_inf)
atom <- p_norm(x, p = Inf)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONVEX)
expect_true(is_nonneg(atom))
## p = 0.5 → scalar, CONCAVE, NONNEG
atom <- p_norm(x, p = 0.5)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONCAVE)
expect_true(is_nonneg(atom))
## p = 0.7 → scalar, CONCAVE, NONNEG
atom <- p_norm(x, p = 0.7)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONCAVE)
expect_true(is_nonneg(atom))
## p = -0.1 → scalar, CONCAVE, NONNEG
atom <- p_norm(x, p = -0.1)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONCAVE)
expect_true(is_nonneg(atom))
## p = -1 → scalar, CONCAVE, NONNEG
atom <- p_norm(x, p = -1)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONCAVE)
expect_true(is_nonneg(atom))
## p = -1.3 → scalar, CONCAVE, NONNEG
atom <- p_norm(x, p = -1.3)
expect_equal(atom@shape, c(1L, 1L))
expect_equal(expr_curvature(atom), CONCAVE)
expect_true(is_nonneg(atom))
})
## @cvxpy test_atoms.py::TestAtoms::test_pnorm
test_that("pnorm: axis/keepdims shapes (CVXPY test_pnorm)", {
A <- Variable(c(2L, 2L))
## norm(A, 2, axis=2) → R c(1L, 2L)
expr <- p_norm(A, p = 2, axis = 2L)
expect_equal(expr@shape, c(1L, 2L))
## norm(A, 2, axis=0, keepdims=TRUE) → CVXPY shape (1, 2) → R c(1L, 2L)
expr <- p_norm(A, p = 2, axis = 2L, keepdims = TRUE)
expect_equal(expr@shape, c(1L, 2L))
## norm(A, 2, axis=1, keepdims=TRUE) → CVXPY shape (2, 1)
expr <- p_norm(A, p = 2, axis = 1L, keepdims = TRUE)
expect_equal(expr@shape, c(2L, 1L))
})
# ═══════════════════════════════════════════════════════════════════════
# test_quad_over_lin (test_atoms.py line 318)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_quad_over_lin
test_that("quad_over_lin: DCP curvature (CVXPY test_quad_over_lin)", {
x <- Variable(2)
a <- Variable(1)
## quad_over_lin(square(x), a) → CONVEX
atom <- quad_over_lin(x^2, a)
expect_equal(expr_curvature(atom), CONVEX)
## quad_over_lin(-square(x), a) → CONVEX
atom <- quad_over_lin(-(x^2), a)
expect_equal(expr_curvature(atom), CONVEX)
## quad_over_lin(sqrt(x), a) → UNKNOWN (not DCP)
atom <- quad_over_lin(sqrt(x), a)
expect_equal(expr_curvature(atom), UNKNOWN_CURVATURE)
expect_false(is_dcp(atom))
})
## @cvxpy test_atoms.py::TestAtoms::test_quad_over_lin
test_that("quad_over_lin: shape validation (CVXPY test_quad_over_lin)", {
x <- Variable(2)
## Second arg must be scalar
expect_error(quad_over_lin(x, x), "scalar")
})
## @cvxpy test_atoms.py::TestAtoms::test_quad_over_lin
test_that("quad_over_lin: numeric value (CVXPY test_quad_over_lin)", {
x_val <- matrix(c(3.0, 4.0), ncol = 1)
y_val <- 2.0
atom <- quad_over_lin(Constant(x_val), Constant(y_val))
expected <- (3.0^2 + 4.0^2) / 2.0 # = 12.5
expect_equal(as.numeric(value(atom)), expected, tolerance = 1e-10)
})
# ═══════════════════════════════════════════════════════════════════════
# test_max (test_atoms.py line 371)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_max
test_that("max: sign logic (CVXPY test_max)", {
expect_equal(expr_sign_str(max_entries(Constant(1))), NONNEG_SIGN)
expect_equal(expr_sign_str(max_entries(Constant(-2))), NONPOS_SIGN)
expect_equal(expr_sign_str(MaxEntries(Variable(1))), UNKNOWN_SIGN)
expect_equal(expr_sign_str(MaxEntries(Constant(0))), ZERO_SIGN)
})
## @cvxpy test_atoms.py::TestAtoms::test_max
test_that("max: axis/keepdims shapes (CVXPY test_max)", {
## CVXPY: max(Variable(2), axis=0, keepdims=True).shape == (1,)
## R: c(1L, 1L)
expr <- MaxEntries(Variable(2), axis = 2L, keepdims = TRUE)
expect_equal(expr@shape, c(1L, 1L))
## CVXPY: max(Variable((2,3)), axis=0, keepdims=True).shape == (1, 3)
expr <- MaxEntries(Variable(c(2L, 3L)), axis = 2L, keepdims = TRUE)
expect_equal(expr@shape, c(1L, 3L))
## CVXPY: max(Variable((2,3)), axis=1).shape == (2,) → R c(2L, 1L)
expr <- MaxEntries(Variable(c(2L, 3L)), axis = 1L)
expect_equal(expr@shape, c(2L, 1L))
})
# ═══════════════════════════════════════════════════════════════════════
# test_min (test_atoms.py line 396)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_min
test_that("min: sign logic (CVXPY test_min)", {
expect_equal(expr_sign_str(MinEntries(Constant(1))), NONNEG_SIGN)
expect_equal(expr_sign_str(MinEntries(Constant(-2))), NONPOS_SIGN)
expect_equal(expr_sign_str(MinEntries(Variable(1))), UNKNOWN_SIGN)
expect_equal(expr_sign_str(MinEntries(Constant(0))), ZERO_SIGN)
})
## @cvxpy test_atoms.py::TestAtoms::test_min
test_that("min: axis/keepdims shapes (CVXPY test_min)", {
## CVXPY: min(Variable(2), axis=0).shape == tuple() → R c(1L, 1L)
expr <- MinEntries(Variable(2), axis = 2L)
expect_equal(expr@shape, c(1L, 1L))
## CVXPY: min(Variable((2,3)), axis=0).shape == (3,) → R c(1L, 3L)
expr <- MinEntries(Variable(c(2L, 3L)), axis = 2L)
expect_equal(expr@shape, c(1L, 3L))
## CVXPY: min(Variable((2,3)), axis=1).shape == (2,) → R c(2L, 1L)
expr <- MinEntries(Variable(c(2L, 3L)), axis = 1L)
expect_equal(expr@shape, c(2L, 1L))
})
# ═══════════════════════════════════════════════════════════════════════
# test_maximum_sign (test_atoms.py line 432)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_maximum_sign
test_that("maximum: sign logic two args (CVXPY test_maximum_sign)", {
expect_equal(expr_sign_str(Maximum(Constant(1), Constant(2))), NONNEG_SIGN)
expect_equal(expr_sign_str(Maximum(Constant(1), Variable(1))), NONNEG_SIGN)
expect_equal(expr_sign_str(Maximum(Constant(1), Constant(-2))), NONNEG_SIGN)
expect_equal(expr_sign_str(Maximum(Constant(1), Constant(0))), NONNEG_SIGN)
expect_equal(expr_sign_str(Maximum(Variable(1), Constant(0))), NONNEG_SIGN)
expect_equal(expr_sign_str(Maximum(Variable(1), Variable(1))), UNKNOWN_SIGN)
expect_equal(expr_sign_str(Maximum(Variable(1), Constant(-2))), UNKNOWN_SIGN)
expect_equal(expr_sign_str(Maximum(Constant(0), Constant(0))), ZERO_SIGN)
expect_equal(expr_sign_str(Maximum(Constant(0), Constant(-2))), ZERO_SIGN)
expect_equal(expr_sign_str(Maximum(Constant(-3), Constant(-2))), NONPOS_SIGN)
})
## @cvxpy test_atoms.py::TestAtoms::test_maximum_sign
test_that("maximum: promotion shape (CVXPY test_maximum_sign)", {
## CVXPY: maximum(1, Variable(2)).shape == (2,) → R c(2L, 1L)
expr <- Maximum(Constant(1), Variable(2))
expect_true(is_nonneg(expr))
expect_equal(expr@shape, c(2L, 1L))
})
# ═══════════════════════════════════════════════════════════════════════
# test_minimum_sign (test_atoms.py line 459)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_minimum_sign
test_that("minimum: sign logic two args (CVXPY test_minimum_sign)", {
expect_equal(expr_sign_str(Minimum(Constant(1), Constant(2))), NONNEG_SIGN)
expect_equal(expr_sign_str(Minimum(Constant(1), Variable(1))), UNKNOWN_SIGN)
expect_equal(expr_sign_str(Minimum(Constant(1), Constant(-2))), NONPOS_SIGN)
expect_equal(expr_sign_str(Minimum(Constant(1), Constant(0))), ZERO_SIGN)
expect_equal(expr_sign_str(Minimum(Variable(1), Constant(0))), NONPOS_SIGN)
expect_equal(expr_sign_str(Minimum(Variable(1), Variable(1))), UNKNOWN_SIGN)
expect_equal(expr_sign_str(Minimum(Variable(1), Constant(-2))), NONPOS_SIGN)
expect_equal(expr_sign_str(Minimum(Constant(0), Constant(0))), ZERO_SIGN)
expect_equal(expr_sign_str(Minimum(Constant(0), Constant(-2))), NONPOS_SIGN)
expect_equal(expr_sign_str(Minimum(Constant(-3), Constant(-2))), NONPOS_SIGN)
})
## @cvxpy test_atoms.py::TestAtoms::test_minimum_sign
test_that("minimum: promotion shape (CVXPY test_minimum_sign)", {
## CVXPY: minimum(-1, Variable(2)).shape == (2,) → R c(2L, 1L)
expr <- Minimum(Constant(-1), Variable(2))
expect_true(is_nonpos(expr))
expect_equal(expr@shape, c(2L, 1L))
})
# ═══════════════════════════════════════════════════════════════════════
# test_sum (test_atoms.py line 485)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_sum
test_that("sum: sign and curvature (CVXPY test_sum)", {
## sum(1).sign == NONNEG
expect_true(is_nonneg(SumEntries(Constant(1))))
## sum(Constant([1, -1])).sign == UNKNOWN
expect_equal(expr_sign_str(SumEntries(Constant(matrix(c(1, -1), ncol = 1)))), UNKNOWN_SIGN)
## sum(Constant([1, -1])).curvature == CONSTANT
expect_true(is_constant(SumEntries(Constant(matrix(c(1, -1), ncol = 1)))))
## sum(Variable(2)).sign == UNKNOWN
expect_equal(expr_sign_str(SumEntries(Variable(2))), UNKNOWN_SIGN)
## sum(Variable(2)).shape == tuple() → R c(1L, 1L)
expect_equal(SumEntries(Variable(2))@shape, c(1L, 1L))
## sum(Variable(2)).curvature == AFFINE
expect_equal(expr_curvature(SumEntries(Variable(2))), AFFINE)
})
## @cvxpy test_atoms.py::TestAtoms::test_sum
test_that("sum: keepdims shape (CVXPY test_sum)", {
## sum(Variable((2,1)), keepdims=True).shape == (1, 1)
expect_equal(SumEntries(Variable(c(2L, 1L)), keepdims = TRUE)@shape, c(1L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_sum
test_that("sum: axis shapes (CVXPY test_sum)", {
## sum(Variable(2), axis=0).shape == tuple() → c(1L, 1L)
expect_equal(SumEntries(Variable(2), axis = 2L)@shape, c(1L, 1L))
## sum(Variable((2,3)), axis=0, keepdims=True).shape == (1, 3)
expect_equal(SumEntries(Variable(c(2L, 3L)), axis = 2L, keepdims = TRUE)@shape, c(1L, 3L))
## sum(Variable((2,3)), axis=2, keepdims=False).shape == (3,) → c(1L, 3L)
expect_equal(SumEntries(Variable(c(2L, 3L)), axis = 2L)@shape, c(1L, 3L))
## sum(Variable((2,3)), axis=1).shape == (2,) → c(2L, 1L)
expect_equal(SumEntries(Variable(c(2L, 3L)), axis = 1L)@shape, c(2L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_sum
test_that("sum: mixed curvature (CVXPY test_sum)", {
## mat = [[1, -1]]; sum(mat @ square(Variable(2))).curvature == UNKNOWN
mat <- Constant(matrix(c(1, -1), nrow = 1))
expr <- SumEntries(mat %*% (Variable(2)^2))
expect_equal(expr_curvature(expr), UNKNOWN_CURVATURE)
})
## @cvxpy test_atoms.py::TestAtoms::test_sum
test_that("sum: invalid axis (CVXPY test_sum)", {
x <- Variable(2)
expect_error(SumEntries(x, axis = 4L))
## In CVXPY, Variable(2) is 1D shape (2,), so axis=1 is invalid.
## In R, Variable(2) is (2,1) which is 2D, so axis=1 (columns) is valid.
## Test with a truly invalid axis instead:
expect_error(SumEntries(Variable(2), axis = 3L))
})
## @cvxpy test_atoms.py::TestAtoms::test_sum
test_that("sum: sparse matrix input (CVXPY test_sum)", {
A <- Matrix::Diagonal(3) # eye(3)
expect_equal(as.numeric(value(SumEntries(Constant(A)))), 3)
})
# ═══════════════════════════════════════════════════════════════════════
# test_multiply (test_atoms.py line 525)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_multiply
test_that("multiply: sign, curvature, shape (CVXPY test_multiply)", {
x <- Variable(2)
## multiply([1, -1], x).sign == UNKNOWN
expr <- Constant(matrix(c(1, -1), ncol = 1)) * x
expect_equal(expr_sign_str(expr), UNKNOWN_SIGN)
## multiply([1, -1], x).curvature == AFFINE
expect_equal(expr_curvature(expr), AFFINE)
## multiply([1, -1], x).shape == (2,) → c(2L, 1L)
expect_equal(expr@shape, c(2L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_multiply
test_that("multiply: sign with nonneg/nonpos constants (CVXPY test_multiply)", {
## CVXPY uses Parameters; we use Constants (since no Parameter support yet)
pos <- Constant(matrix(c(1, 2), ncol = 1)) # nonneg
neg <- Constant(matrix(c(-1, -2), ncol = 1)) # nonpos
## pos * pos → NONNEG
expect_true(is_nonneg(pos * pos))
## pos * neg → NONPOS
expect_true(is_nonpos(pos * neg))
## neg * neg → NONNEG
expect_true(is_nonneg(neg * neg))
})
## @cvxpy test_atoms.py::TestAtoms::test_multiply
test_that("multiply: concave curvature (CVXPY test_multiply)", {
x <- Variable(2)
neg <- Constant(matrix(c(-1, -2), ncol = 1))
## neg * square(x) → CONCAVE (nonpos * convex nonneg → concave)
expr <- neg * (x^2)
expect_equal(expr_curvature(expr), CONCAVE)
})
## @cvxpy test_atoms.py::TestAtoms::test_multiply
test_that("multiply: promotion (CVXPY test_multiply)", {
C <- Variable(c(3L, 2L))
## multiply(1, C).shape == C.shape
expect_equal((Constant(1) * C)@shape, C@shape)
})
# ═══════════════════════════════════════════════════════════════════════
# test_vstack (test_atoms.py line 548)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_vstack
test_that("vstack: shapes (CVXPY test_vstack)", {
x <- Variable(2)
y <- Variable(2)
A <- Variable(c(2L, 2L))
C <- Variable(c(3L, 2L))
B <- Variable(c(2L, 2L))
## CVXPY: vstack([x, y, x]).shape == (3, 2) for 1D inputs
## In R, x is (2,1) so VStack of three (2,1) → (6, 1)
atom <- VStack(x, y, x)
expect_equal(atom@shape, c(6L, 1L))
## CVXPY: vstack([A, C, B]).shape == (7, 2)
atom <- VStack(A, C, B)
expect_equal(atom@shape, c(7L, 2L))
})
## @cvxpy test_atoms.py::TestAtoms::test_vstack
test_that("vstack: entries from indexing (CVXPY test_vstack)", {
x <- Variable(2)
## Extracting each element and vstacking
entries <- list(x[1L, ], x[2L, ])
atom <- do.call(VStack, entries)
expect_equal(atom@shape, c(2L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_vstack
test_that("vstack: validation (CVXPY test_vstack)", {
C <- Variable(c(3L, 2L))
## Incompatible column dimensions: (3,2) vs scalar promoted to (1,1)
expect_error(VStack(C, Constant(1)))
## In R, Variable(2) is (2,1) and Variable(3) is (3,1) — both 1-column, so VStack is valid
## Instead test: (3,2) vs (2,1) — incompatible columns (2 vs 1)
expect_error(VStack(C, Variable(2)))
})
# ═══════════════════════════════════════════════════════════════════════
# test_hstack (test_atoms.py line 576)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_hstack
test_that("hstack: shapes (CVXPY test_hstack)", {
A <- Variable(c(2L, 2L))
B <- Variable(c(2L, 2L))
## CVXPY: hstack([A, B]).shape == (2, 4)
atom <- HStack(A, B)
expect_equal(atom@shape, c(2L, 4L))
})
## @cvxpy test_atoms.py::TestAtoms::test_hstack
test_that("hstack: validation (CVXPY test_hstack)", {
C <- Variable(c(3L, 2L))
A <- Variable(c(2L, 2L))
## Incompatible row dimensions
expect_error(HStack(C, A))
})
# ═══════════════════════════════════════════════════════════════════════
# test_reshape (test_atoms.py line 644)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_reshape
test_that("reshape: sign, curvature, shape (CVXPY test_reshape)", {
A <- Variable(c(2L, 2L))
## reshape(A, (4,1), order='F')
expr <- reshape_expr(A, c(4L, 1L), order = "F")
expect_equal(expr_sign_str(expr), UNKNOWN_SIGN)
expect_equal(expr_curvature(expr), AFFINE)
expect_equal(expr@shape, c(4L, 1L))
## reshape back to (2,2)
expr2 <- reshape_expr(expr, c(2L, 2L), order = "F")
expect_equal(expr2@shape, c(2L, 2L))
})
## @cvxpy test_atoms.py::TestAtoms::test_reshape
test_that("reshape: convex nonneg (CVXPY test_reshape)", {
x <- Variable(2)
## reshape(square(x), (1, 2), order='F')
expr <- reshape_expr(x^2, c(1L, 2L), order = "F")
expect_true(is_nonneg(expr))
expect_equal(expr_curvature(expr), CONVEX)
expect_equal(expr@shape, c(1L, 2L))
})
## @cvxpy test_atoms.py::TestAtoms::test_reshape
test_that("reshape: invalid dimensions (CVXPY test_reshape)", {
C <- Variable(c(3L, 2L))
expect_error(reshape_expr(C, c(5L, 4L), order = "F"), "reshape")
})
## @cvxpy test_atoms.py::TestAtoms::test_reshape
test_that("reshape: C-order numeric (CVXPY test_reshape)", {
## a = np.arange(10); reshape(a, (5,2), order='C')
a <- matrix(0:9, ncol = 1)
A_np <- matrix(0:9, nrow = 5, ncol = 2, byrow = TRUE)
A_cp <- reshape_expr(Constant(a), c(5L, 2L), order = "C")
expect_equal(as.matrix(value(A_cp)), A_np, tolerance = 1e-10)
})
# ═══════════════════════════════════════════════════════════════════════
# test_vec (test_atoms.py line 754)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_vec
test_that("vec: sign, curvature, shape (CVXPY test_vec)", {
C <- Variable(c(3L, 2L))
## vec(C, order='F')
expr <- vec(C)
expect_equal(expr_sign_str(expr), UNKNOWN_SIGN)
expect_equal(expr_curvature(expr), AFFINE)
## CVXPY: shape == (6,) → R c(6L, 1L)
expect_equal(expr@shape, c(6L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_vec
test_that("vec: scalar (CVXPY test_vec)", {
x <- Variable(2)
## vec(x) → c(2L, 1L)
expr <- vec(x)
expect_equal(expr@shape, c(2L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_vec
test_that("vec: convex nonneg preserves (CVXPY test_vec)", {
a <- Variable(1)
## vec(square(a))
expr <- vec(a^2)
expect_true(is_nonneg(expr))
expect_equal(expr_curvature(expr), CONVEX)
## CVXPY: shape == (1,) → R c(1L, 1L)
expect_equal(expr@shape, c(1L, 1L))
})
# ═══════════════════════════════════════════════════════════════════════
# test_diag (test_atoms.py line 770)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_diag
test_that("diag: vector to matrix (CVXPY test_diag)", {
x <- Variable(2)
## diag(x) → (2, 2)
expr <- DiagVec(x)
expect_equal(expr_sign_str(expr), UNKNOWN_SIGN)
expect_equal(expr_curvature(expr), AFFINE)
expect_equal(expr@shape, c(2L, 2L))
})
## @cvxpy test_atoms.py::TestAtoms::test_diag
test_that("diag: matrix to vector (CVXPY test_diag)", {
A <- Variable(c(2L, 2L))
## diag(A) → (2,) → c(2L, 1L)
expr <- DiagMat(A)
expect_equal(expr_sign_str(expr), UNKNOWN_SIGN)
expect_equal(expr_curvature(expr), AFFINE)
expect_equal(expr@shape, c(2L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_diag
test_that("diag: transpose of matrix (CVXPY test_diag)", {
## In CVXPY, x.T of 1D (2,) stays (2,) so diag works.
## In R, Variable(2) is (2,1) and Transpose gives (1,2) row vector.
## DiagVec requires column vector → use matrix diagonal extraction instead.
## Test: DiagMat of transposed 2x2 matrix
A <- Variable(c(2L, 2L))
expr <- DiagMat(Transpose(A))
expect_equal(expr_sign_str(expr), UNKNOWN_SIGN)
expect_equal(expr_curvature(expr), AFFINE)
expect_equal(expr@shape, c(2L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_diag
test_that("diag: constant matrix to vector (CVXPY test_diag)", {
psd_matrix <- matrix(c(1, -1, -1, 1), 2, 2)
expr <- DiagMat(Constant(psd_matrix))
expect_true(is_nonneg(expr))
expect_true(is_constant(expr))
expect_equal(expr@shape, c(2L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_diag
test_that("diag: non-square matrix rejected (CVXPY test_diag)", {
C <- Variable(c(3L, 2L))
expect_error(DiagMat(C), "square")
})
## @cvxpy test_atoms.py::TestAtoms::test_diag
test_that("diag: PSD property (CVXPY test_diag)", {
## diag(w) is PSD when w >= 0
w <- Constant(matrix(c(1.0, 2.0), ncol = 1))
expr <- DiagVec(w)
expect_true(is_psd(expr))
expr_neg <- DiagVec(-w)
expect_true(is_nsd(expr_neg))
expr_mixed <- DiagVec(Constant(matrix(c(1, -1), ncol = 1)))
expect_false(is_psd(expr_mixed))
expect_false(is_nsd(expr_mixed))
})
# ═══════════════════════════════════════════════════════════════════════
# test_trace (test_atoms.py line 825)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_trace
test_that("trace: sign, curvature, shape (CVXPY test_trace)", {
A <- Variable(c(2L, 2L))
expr <- Trace(A)
expect_equal(expr_sign_str(expr), UNKNOWN_SIGN)
expect_equal(expr_curvature(expr), AFFINE)
## CVXPY: shape == tuple() → R c(1L, 1L)
expect_equal(expr@shape, c(1L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_trace
test_that("trace: non-square rejected (CVXPY test_trace)", {
C <- Variable(c(3L, 2L))
expect_error(Trace(C), "square")
})
## @cvxpy test_atoms.py::TestAtoms::test_trace_sign_psd
test_that("trace: PSD/NSD sign (CVXPY test_trace_sign_psd)", {
X_psd <- Variable(c(2L, 2L), PSD = TRUE)
X_nsd <- Variable(c(2L, 2L), NSD = TRUE)
expect_true(is_nonneg(Trace(X_psd)))
expect_true(is_nonpos(Trace(X_nsd)))
})
# ═══════════════════════════════════════════════════════════════════════
# test_upper_tri (test_atoms.py line 899)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_upper_tri
test_that("upper_tri: non-square rejected (CVXPY test_upper_tri)", {
C <- Variable(c(3L, 2L))
expect_error(UpperTri(C), "square")
})
## @cvxpy test_atoms.py::TestAtoms::test_upper_tri
test_that("upper_tri: shape for square matrix", {
A <- Variable(c(4L, 4L))
ut <- UpperTri(A)
## n*(n-1)/2 = 4*3/2 = 6
expect_equal(ut@shape, c(6L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_upper_tri
test_that("upper_tri: numeric value row-major order", {
A <- Variable(c(3L, 3L))
value(A) <- matrix(1:9, 3, 3)
ut <- UpperTri(A)
## Row-major order of strict upper triangle:
## (1,2)=4, (1,3)=7, (2,3)=8
expect_equal(as.numeric(value(ut)), c(4, 7, 8))
})
# ═══════════════════════════════════════════════════════════════════════
# test_vec_to_upper_tri (test_atoms.py line 905)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_vec_to_upper_tri
test_that("vec_to_upper_tri: default (non-strict) (CVXPY test_vec_to_upper_tri)", {
## x = Variable((3,)); vec_to_upper_tri(x)
## x.value = [1, 2, 3] → [[1, 2], [0, 3]]
x <- Variable(c(3L, 1L))
X <- vec_to_upper_tri(x, strict = FALSE)
value(x) <- matrix(c(1, 2, 3), 3, 1)
actual <- as.matrix(value(X))
expected <- matrix(c(1, 0, 2, 3), 2, 2)
expect_equal(actual, expected, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_vec_to_upper_tri
test_that("vec_to_upper_tri: strict=TRUE (CVXPY test_vec_to_upper_tri)", {
## y = Variable((1,)); vec_to_upper_tri(y, strict=True)
## y.value = [4] → [[0, 4], [0, 0]]
y <- Variable(c(1L, 1L))
Y <- vec_to_upper_tri(y, strict = TRUE)
value(y) <- matrix(4, 1, 1)
actual <- as.matrix(value(Y))
expected <- matrix(c(0, 0, 4, 0), 2, 2)
expect_equal(actual, expected, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_vec_to_upper_tri
test_that("vec_to_upper_tri: strict 4x4 (CVXPY test_vec_to_upper_tri)", {
## a = [11, 12, 13, 16, 17, 21] → 4x4 strict upper tri
a <- Constant(matrix(c(11, 12, 13, 16, 17, 21), 6, 1))
A_actual <- as.matrix(value(vec_to_upper_tri(a, strict = TRUE)))
A_expect <- matrix(c(0, 0, 0, 0, 11, 0, 0, 0, 12, 16, 0, 0, 13, 17, 21, 0), 4, 4)
expect_equal(A_actual, A_expect, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_vec_to_upper_tri
test_that("vec_to_upper_tri: validation (CVXPY test_vec_to_upper_tri)", {
## Not a triangular number
expect_error(vec_to_upper_tri(Variable(c(4L, 1L)), strict = FALSE), "triangular")
expect_error(vec_to_upper_tri(Variable(c(4L, 1L)), strict = TRUE), "triangular")
})
# ═══════════════════════════════════════════════════════════════════════
# test_huber (test_atoms.py line 951)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_huber
test_that("huber: M validation (CVXPY test_huber)", {
x <- Variable(2)
## Valid
h <- Huber(x, M = 1)
expect_true(is_convex(h))
## Negative M rejected
expect_error(Huber(x, M = -1), "[Nn]on-negative|[Mm]ust be")
## Vector M rejected (must be scalar)
expect_error(Huber(x, M = c(1, 1)), "[Ss]calar|[Mm]ust be")
})
## @cvxpy test_atoms.py::TestAtoms::test_huber
test_that("huber: numeric value (CVXPY test_huber)", {
## huber(2, M=1).value == 3
## huber loss: |x| <= M → x^2; |x| > M → 2M|x| - M^2
## huber(2, 1) = 2*1*2 - 1 = 3
expect_equal(as.numeric(value(Huber(Constant(2), M = 1))), 3, tolerance = 1e-10)
})
# ═══════════════════════════════════════════════════════════════════════
# test_sum_largest (test_atoms.py line 1034)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_sum_largest
test_that("sum_largest: k validation (CVXPY test_sum_largest)", {
x <- Variable(2)
## Negative k rejected
expect_error(SumLargest(x, k = -1), "positive|[Ss]econd")
})
## @cvxpy test_atoms.py::TestAtoms::test_sum_largest
test_that("sum_largest: PWL property (CVXPY test_sum_largest)", {
x <- Variable(2)
atom <- SumLargest(x, k = 2)
expect_true(is_pwl(atom))
})
## @cvxpy test_atoms.py::TestAtoms::test_sum_largest
test_that("sum_largest: numeric value (CVXPY test_sum_largest)", {
## 10000 random values
set.seed(42)
v <- rnorm(100)
x <- Constant(matrix(v, ncol = 1))
for (i in c(5, 10, 25, 50)) {
expr <- SumLargest(x, k = i)
prev_idx <- order(-v)[1:i]
expect_equal(as.numeric(value(expr)), sum(v[prev_idx]), tolerance = 1e-6,
info = paste("k =", i))
}
})
# ═══════════════════════════════════════════════════════════════════════
# test_sum_smallest (test_atoms.py line 1112)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_sum_smallest
test_that("sum_smallest: k validation (CVXPY test_sum_smallest)", {
x <- Variable(2)
expect_error(sum_smallest(x, -1), "positive|[Ss]econd")
})
## @cvxpy test_atoms.py::TestAtoms::test_sum_smallest
test_that("sum_smallest: PWL property (CVXPY test_sum_smallest)", {
x <- Variable(2)
atom <- sum_smallest(x, 2)
expect_true(is_pwl(atom))
})
# ═══════════════════════════════════════════════════════════════════════
# test_kron_expr (test_atoms.py line 1292)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_kron_expr
test_that("kron: sign and shape (CVXPY test_kron_expr)", {
a <- Constant(matrix(1, 3, 2))
## kron(a, nonneg) → NONNEG, shape (6, 2)
b_nonneg <- Constant(matrix(c(1, 2), 2, 1))
expr <- Kron(a, b_nonneg)
expect_true(is_nonneg(expr))
expect_equal(expr@shape, c(6L, 2L))
## kron(a, nonpos) → NONPOS
b_nonpos <- Constant(matrix(c(-1, -2), 2, 1))
expr <- Kron(a, b_nonpos)
expect_true(is_nonpos(expr))
})
## @cvxpy test_atoms.py::TestAtoms::test_kron_expr
test_that("kron: validation (CVXPY test_kron_expr)", {
x <- Variable(2)
## Both arguments variable → error
expect_error(Kron(x, x), "constant")
})
# ═══════════════════════════════════════════════════════════════════════
# test_conv (test_atoms.py line 1232)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_conv
test_that("conv: sign and shape (CVXPY test_conv)", {
a <- Constant(matrix(1, 3, 1)) # ones (3, 1)
## conv(a, nonneg) → NONNEG, shape (4, 1)
b_nonneg <- Constant(matrix(c(1, 2), 2, 1))
expr <- Convolve(a, b_nonneg)
expect_true(is_nonneg(expr))
## CVXPY: (4, 1)
expect_equal(expr@shape, c(4L, 1L))
## conv(a, nonpos) → NONPOS
b_nonpos <- Constant(matrix(c(-1, -2), 2, 1))
expr <- Convolve(a, b_nonpos)
expect_true(is_nonpos(expr))
})
## @cvxpy test_atoms.py::TestAtoms::test_conv
test_that("conv: at least one arg must be constant (CVXPY test_conv)", {
x <- Variable(2)
y <- Variable(2)
## Both variable → error
expect_error(Convolve(x, y), "constant")
## Variable + Constant → OK (CVXR is more permissive than CVXPY here)
expect_no_error(Convolve(x, Constant(-1)))
})
# ═══════════════════════════════════════════════════════════════════════
# test_cumsum (test_atoms.py line 1267)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_cumsum
test_that("cumsum: numeric value matches R cumsum (CVXPY test_cumsum)", {
for (axis in c(2L, 1L)) {
x <- Variable(c(4L, 3L))
x_val <- matrix(0:11, 4, 3)
value(x) <- x_val
expr <- Cumsum(x, axis = axis)
## R cumsum along axis
if (axis == 2L) {
target <- apply(x_val, 2, cumsum)
} else {
target <- t(apply(x_val, 1, cumsum))
}
expect_equal(as.matrix(value(expr)), target, tolerance = 1e-10,
info = paste("axis =", axis))
}
})
# ═══════════════════════════════════════════════════════════════════════
# test_diff (test_atoms.py line 1663)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_diff
test_that("diff: shape for 2D matrix (CVXPY test_diff)", {
A <- Variable(c(20L, 10L))
## diff(A, axis=0).shape == np.diff(zeros(20,10), axis=0).shape == (19, 10)
expect_equal(cvxr_diff(A, axis = 2L)@shape, c(19L, 10L))
## diff(A, axis=1).shape == np.diff(zeros(20,10), axis=1).shape == (20, 9)
expect_equal(cvxr_diff(A, axis = 1L)@shape, c(20L, 9L))
})
## @cvxpy test_atoms.py::TestAtoms::test_diff
test_that("diff: numeric value axis=1 (CVXPY test_diff issue #1834)", {
x1 <- matrix(c(1, 2, 3, 4, 5), nrow = 1) # (1, 5)
x2 <- Variable(c(1L, 5L))
value(x2) <- x1
## diff(x2, axis=1)
expr <- cvxr_diff(x2, axis = 1L)
expected <- diff(x1[1, ]) # c(1, 1, 1, 1)
expect_equal(as.numeric(value(expr)), expected, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_diff
test_that("diff: higher-order diff (CVXPY test_diff)", {
A <- Variable(c(20L, 10L))
## k=2, axis=0 → (18, 10)
expect_equal(cvxr_diff(A, k = 2L, axis = 2L)@shape, c(18L, 10L))
## k=2, axis=1 → (20, 8)
expect_equal(cvxr_diff(A, k = 2L, axis = 1L)@shape, c(20L, 8L))
})
# ═══════════════════════════════════════════════════════════════════════
# test_log1p (test_atoms.py line 889)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_log1p
test_that("log1p: curvature and numeric (CVXPY test_log1p)", {
## CVXPY has a dedicated Log1p atom with sign tracking.
## In CVXR, log1p(x) is implemented as log(x + 1), so sign is UNKNOWN.
## Test curvature and numeric value instead.
expr <- log(Constant(1) + 1) # log(2)
expect_true(is_constant(expr))
expect_equal(as.numeric(value(expr)), log(2), tolerance = 1e-10)
## log1p(-0.5) = log(0.5)
expr_neg <- log(Constant(1) + Constant(-0.5))
expect_true(is_constant(expr_neg))
expect_equal(as.numeric(value(expr_neg)), log(0.5), tolerance = 1e-10)
})
# ═══════════════════════════════════════════════════════════════════════
# Comprehensive atom sign/curvature snapshot (additional coverage)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy NONE
test_that("abs: convex, nonneg (CVXPY atom properties)", {
x <- Variable(2)
a <- Abs(x)
expect_equal(expr_curvature(a), CONVEX)
expect_true(is_nonneg(a))
expect_equal(a@shape, c(2L, 1L))
})
## @cvxpy test_nonlinear_atoms.py::TestNonlinearAtoms::test_exp
test_that("exp: convex, nonneg (CVXPY atom properties)", {
x <- Variable(1)
e <- Exp(x)
expect_equal(expr_curvature(e), CONVEX)
expect_true(is_nonneg(e))
expect_equal(e@shape, c(1L, 1L))
})
## @cvxpy test_nonlinear_atoms.py::TestNonlinearAtoms::test_log
test_that("log: concave, unknown sign (CVXPY atom properties)", {
x <- Variable(1)
l <- Log(x)
expect_equal(expr_curvature(l), CONCAVE)
expect_equal(l@shape, c(1L, 1L))
})
## @cvxpy test_nonlinear_atoms.py::TestNonlinearAtoms::test_entr
test_that("entr: concave, unknown sign (CVXPY atom properties)", {
x <- Variable(1)
e <- Entr(x)
expect_equal(expr_curvature(e), CONCAVE)
expect_equal(e@shape, c(1L, 1L))
})
## @cvxpy NONE
test_that("sum_squares: convex, nonneg (CVXPY atom properties)", {
x <- Variable(3)
ss <- sum_squares(x)
expect_true(is_convex(ss))
expect_true(is_nonneg(ss))
expect_equal(ss@shape, c(1L, 1L))
})
## @cvxpy NONE
test_that("pos/neg: convex, nonneg (CVXPY atom properties)", {
x <- Variable(2)
p <- pos(x)
n <- neg(x)
expect_true(is_convex(p))
expect_true(is_nonneg(p))
expect_true(is_convex(n))
expect_true(is_nonneg(n))
})
## @cvxpy NONE
test_that("square: convex, nonneg (CVXPY atom properties)", {
x <- Variable(2)
sq <- x^2
expect_true(is_convex(sq))
expect_true(is_nonneg(sq))
})
## @cvxpy NONE
test_that("sqrt: concave, nonneg (CVXPY atom properties)", {
x <- Variable(1)
s <- sqrt(x)
expect_true(is_concave(s))
expect_true(is_nonneg(s))
})
## @cvxpy NONE
test_that("inv_pos: convex, nonneg (CVXPY atom properties)", {
x <- Variable(1)
ip <- inv_pos(x)
expect_true(is_convex(ip))
expect_true(is_nonneg(ip))
})
# ═══════════════════════════════════════════════════════════════════════
# Numeric evaluation parity
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_norm1
test_that("norm1: numeric value matches R norm (CVXPY numeric)", {
x <- Variable(3)
value(x) <- matrix(c(-2, 3, -1), 3, 1)
expect_equal(as.numeric(value(norm1(x))), 6, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_norm_inf
test_that("norm_inf: numeric value matches R max(abs()) (CVXPY numeric)", {
x <- Variable(3)
value(x) <- matrix(c(-2, 3, -1), 3, 1)
expect_equal(as.numeric(value(norm_inf(x))), 3, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_pnorm
test_that("pnorm p=2: numeric value matches Euclidean norm (CVXPY numeric)", {
x <- Variable(3)
value(x) <- matrix(c(3, 4, 0), 3, 1)
expect_equal(as.numeric(value(p_norm(x, p = 2))), 5, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_trace
test_that("trace: numeric value (CVXPY numeric)", {
A <- Variable(c(3L, 3L))
value(A) <- diag(c(1, 2, 3))
expect_equal(as.numeric(value(Trace(A))), 6, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_diag
test_that("diag: vector to matrix numeric (CVXPY numeric)", {
x <- Variable(3)
value(x) <- matrix(c(1, 2, 3), 3, 1)
D <- DiagVec(x)
expected <- diag(c(1, 2, 3))
expect_equal(as.matrix(value(D)), expected, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_diag
test_that("diag: matrix to vector numeric (CVXPY numeric)", {
A <- Variable(c(3L, 3L))
value(A) <- matrix(1:9, 3, 3)
d <- DiagMat(A)
expect_equal(as.numeric(value(d)), c(1, 5, 9), tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_sum_largest
test_that("sum_largest: numeric value (CVXPY numeric)", {
v <- c(10, 1, 5, 3, 7)
x <- Constant(matrix(v, ncol = 1))
## sum_largest(x, 2) → 10 + 7 = 17
expect_equal(as.numeric(value(SumLargest(x, k = 2))), 17, tolerance = 1e-10)
## sum_largest(x, 1) → 10
expect_equal(as.numeric(value(SumLargest(x, k = 1))), 10, tolerance = 1e-10)
## sum_largest(x, 5) → sum(v) = 26
expect_equal(as.numeric(value(SumLargest(x, k = 5))), 26, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_sum_smallest
test_that("sum_smallest: numeric value (CVXPY numeric)", {
v <- c(10, 1, 5, 3, 7)
x <- Variable(c(5L, 1L))
value(x) <- matrix(v, 5, 1)
## sum_smallest(x, 2) → 1 + 3 = 4
expect_equal(as.numeric(value(sum_smallest(x, 2))), 4, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_huber
test_that("huber: numeric values for various inputs (CVXPY numeric)", {
## huber(x, M): |x| <= M → x^2; |x| > M → 2M|x| - M^2
## huber(0.5, 1) → 0.25 (inside)
expect_equal(as.numeric(value(Huber(Constant(0.5), M = 1))), 0.25, tolerance = 1e-10)
## huber(2, 1) → 2*1*2 - 1 = 3 (outside)
expect_equal(as.numeric(value(Huber(Constant(2), M = 1))), 3, tolerance = 1e-10)
## huber(-1.5, 1) → 2*1*1.5 - 1 = 2 (outside, negative input)
expect_equal(as.numeric(value(Huber(Constant(-1.5), M = 1))), 2, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_reshape
test_that("reshape: F-order and C-order numeric parity (CVXPY numeric)", {
## F-order: column-major (R default)
m <- matrix(1:6, 2, 3) # [[1,3,5],[2,4,6]]
expr_f <- reshape_expr(Constant(m), c(3L, 2L), order = "F")
## F-order reshape: read column-major, fill column-major
## As a vector (col-major): 1, 2, 3, 4, 5, 6 → fill (3,2) col-major: [[1,4],[2,5],[3,6]]
expected_f <- matrix(c(1, 2, 3, 4, 5, 6), 3, 2)
expect_equal(as.matrix(value(expr_f)), expected_f, tolerance = 1e-10)
## C-order: row-major
expr_c <- reshape_expr(Constant(m), c(3L, 2L), order = "C")
## C-order reshape: read row-major, fill row-major
## m row-major: 1, 3, 5, 2, 4, 6 → fill (3,2) row-major: [[1,3],[5,2],[4,6]]
expected_c <- matrix(c(1, 5, 4, 3, 2, 6), 3, 2)
expect_equal(as.matrix(value(expr_c)), expected_c, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_kron_expr
test_that("kron: numeric value (CVXPY numeric)", {
a <- Constant(matrix(c(1, 0, 0, 1), 2, 2)) # I2
b <- Constant(matrix(c(1, 2, 3, 4), 2, 2)) # [[1,3],[2,4]]
k <- Kron(a, b)
## kronecker(I2, b) = block diagonal of b
expected <- as.matrix(kronecker(diag(2), matrix(c(1, 2, 3, 4), 2, 2)))
expect_equal(as.matrix(value(k)), expected, tolerance = 1e-10)
})
## @cvxpy test_atoms.py::TestAtoms::test_conv
test_that("conv: numeric value (CVXPY numeric)", {
a <- Constant(matrix(c(1, 1, 1), 3, 1))
b <- Variable(c(2L, 1L))
value(b) <- matrix(c(1, 2), 2, 1)
expr <- Convolve(a, b)
## conv([1,1,1], [1,2]) = [1, 3, 3, 2]
expect_equal(as.numeric(value(expr)), c(1, 3, 3, 2), tolerance = 1e-10)
})
# ═══════════════════════════════════════════════════════════════════════
# test_upper_tri_to_full (test_atoms.py line 899)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_upper_tri_to_full
test_that("upper_tri_to_full: produces symmetric matrix (CVXPY test_upper_tri_to_full)", {
for (n in 3:7) {
A <- upper_tri_to_full(n)
ell <- (n * (n + 1L)) %/% 2L
v <- seq_len(ell) - 1L # 0-based like np.arange
## A @ v reshaped to (n, n) F-order should be symmetric
Mv <- as.numeric(A %*% v)
M <- matrix(Mv, n, n) # F-order (column-major)
expect_equal(M, t(M), tolerance = 1e-10, info = paste("n =", n))
}
})
# ═══════════════════════════════════════════════════════════════════════
# test_index: shape, sign, curvature (test_atoms.py line 1169)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_index
test_that("index: shape for various slices (CVXPY test_index)", {
A <- Variable(c(5L, 4L))
## A[0:2, 0:1] → (2, 1)
expr <- A[1:2, 1L]
expect_equal(expr@shape, c(2L, 1L))
## A[0:2, :] → (2, 4)
expr2 <- A[1:2, ]
expect_equal(expr2@shape, c(2L, 4L))
## A[:, 1:3] → (5, 2)
expr3 <- A[, 2:3]
expect_equal(expr3@shape, c(5L, 2L))
## Single element: A[0, 0] → scalar (1, 1)
expr4 <- A[1L, 1L]
expect_equal(expr4@shape, c(1L, 1L))
})
## @cvxpy test_atoms.py::TestAtoms::test_index
test_that("index: curvature is AFFINE (CVXPY test_index)", {
A <- Variable(c(3L, 3L))
expr <- A[1:2, 1:2]
expect_equal(expr_curvature(expr), AFFINE)
})
## @cvxpy test_atoms.py::TestAtoms::test_index
test_that("index: sign preserved for nonneg variable (CVXPY test_index)", {
x <- Variable(5, nonneg = TRUE)
expr <- x[1:3, ]
expect_true(is_nonneg(expr))
})
# ═══════════════════════════════════════════════════════════════════════
# test_nonnegative_variable (test_atoms.py line 1580)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_nonnegative_variable
test_that("nonneg variable: sign propagates through atoms (CVXPY test_nonnegative_variable)", {
x <- Variable(c(4L, 1L), nonneg = TRUE)
## sum(x) → NONNEG
expect_true(is_nonneg(sum(x)))
## norm1(x) → NONNEG
expect_true(is_nonneg(norm1(x)))
## max(x) → NONNEG
expect_true(is_nonneg(max(x)))
})
# ═══════════════════════════════════════════════════════════════════════
# test_log_problem (test_nonlinear_atoms.py)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_nonlinear_atoms.py::TestNonlinearAtoms::test_log_problem
test_that("test_log_problem: log in objective (CVXPY test_log_problem)", {
skip_if_not_installed("clarabel")
x <- Variable(2, name = "x")
## Log in objective: max sum(log(x)) s.t. x <= [1, e]
## Optimal: x = [1, e], log(1) + log(e) = 0 + 1 = 1
obj <- Maximize(sum(log(x)))
constr <- list(x <= c(1, exp(1)))
prob <- Problem(obj, constr)
result <- psolve(prob, solver = "CLARABEL")
expect_equal(status(prob), OPTIMAL)
expect_equal(result, 1, tolerance = 1e-3)
expect_equal(as.numeric(value(x)), c(1, exp(1)), tolerance = 1e-3)
})
## @cvxpy test_nonlinear_atoms.py::TestNonlinearAtoms::test_log_problem
test_that("test_log_problem: log in constraint (CVXPY test_log_problem)", {
skip_if_not_installed("clarabel")
x <- Variable(2, name = "x")
## Log in constraint: min sum(x) s.t. log(x) >= 0, x <= [1, 1]
## log(x) >= 0 => x >= 1, combined with x <= 1 => x = [1, 1], sum = 2
obj <- Minimize(sum(x))
constr <- list(log(x) >= 0, x <= c(1, 1))
prob <- Problem(obj, constr)
result <- psolve(prob, solver = "CLARABEL")
expect_equal(status(prob), OPTIMAL)
expect_equal(result, 2, tolerance = 1e-3)
})
## @cvxpy test_nonlinear_atoms.py::TestNonlinearAtoms::test_log_problem
test_that("test_log_problem: index into log (CVXPY test_log_problem)", {
skip_if_not_installed("clarabel")
x <- Variable(2, name = "x")
## Index into log: max log(x)[2] s.t. x <= [1, e]
## CVXPY: max log(x)[1] (0-based) = log(x_2) => x_2 = e, log(e) = 1
## R: log(x)[2] (1-based)
obj <- Maximize(log(x)[2])
constr <- list(x <= c(1, exp(1)))
prob <- Problem(obj, constr)
result <- psolve(prob, solver = "CLARABEL")
expect_equal(status(prob), OPTIMAL)
expect_equal(result, 1, tolerance = 1e-3)
})
## @cvxpy test_nonlinear_atoms.py::TestNonlinearAtoms::test_log_problem
test_that("test_log_problem: scalar log (CVXPY test_log_problem)", {
skip_if_not_installed("clarabel")
x <- Variable(2, name = "x")
## Scalar log: max log(x[2]) s.t. x <= [1, e]
## CVXPY: max log(x[1]) (0-based) = max log(x_2) => 1
## R: x[2] (1-based)
obj <- Maximize(log(x[2]))
constr <- list(x <= c(1, exp(1)))
prob <- Problem(obj, constr)
result <- psolve(prob, solver = "CLARABEL")
expect_equal(status(prob), OPTIMAL)
expect_equal(result, 1, tolerance = 1e-3)
})
# ═══════════════════════════════════════════════════════════════════════
# test_conv_prob (test_convolution.py)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_convolution.py::TestConvolution::test_conv_prob
test_that("test_conv_prob: conv(h, x) with random data is unbounded (CVXPY test_conv_prob)", {
skip_if_not_installed("clarabel")
N <- 5L
set.seed(42)
y <- matrix(rnorm(N), ncol = 1)
h <- matrix(rnorm(2), ncol = 1)
x <- Variable(c(N, 1))
v <- conv(h, x)
## v has length N + 2 - 1 = N + 1. Slice v[1:N] (1-based).
obj <- Minimize(sum_entries(multiply(y, v[1:N, ])))
prob <- Problem(obj, list())
result <- psolve(prob, solver = "CLARABEL")
## Unconstrained: should be unbounded
expect_true(status(prob) %in% c("unbounded", "unbounded_inaccurate"))
})
## @cvxpy test_convolution.py::TestConvolution::test_conv_prob
test_that("test_conv_prob: conv with 1-D vectors is unbounded (CVXPY test_conv_prob)", {
skip_if_not_installed("clarabel")
N <- 5L
set.seed(42)
y <- rnorm(N)
h <- rnorm(2)
x <- Variable(N)
v <- conv(h, x)
## v has length N + 2 - 1 = N + 1. Slice v[1:N] (1-based).
obj <- Minimize(sum_entries(multiply(y, v[1:N, ])))
prob <- Problem(obj, list())
result <- psolve(prob, solver = "CLARABEL")
## Unconstrained: should be unbounded
expect_true(status(prob) %in% c("unbounded", "unbounded_inaccurate"))
})
# ═══════════════════════════════════════════════════════════════════════
# test_is_psd / test_is_nsd (test_constant.py)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_constant.py::test_is_psd
test_that("Constant: is_psd / is_nsd for identity matrices", {
## CVXPY: n=50, psd=eye(n), nsd=-eye(n)
## Verified via CVXPY:
## Constant(psd).is_psd() = True, .is_nsd() = False
## Constant(nsd).is_nsd() = True, .is_psd() = False
n <- 50L
psd_c <- Constant(diag(n))
nsd_c <- Constant(-diag(n))
expect_true(is_psd(psd_c))
expect_false(is_nsd(psd_c))
expect_true(is_nsd(nsd_c))
expect_false(is_psd(nsd_c))
})
## @cvxpy test_constant.py::test_is_psd
test_that("Constant: psd_wrap forces is_psd = TRUE", {
## CVXPY: psd_wrap(Constant(P)).is_psd() == True
## psd_wrap wraps a constant and asserts PSD regardless of eigenvalues
n <- 50L
P <- diag(n)
wrapped <- psd_wrap(Constant(P))
expect_true(is_psd(wrapped))
expect_false(is_nsd(wrapped))
})
# ═══════════════════════════════════════════════════════════════════════
# test_prod (test_constant.py) — Prod atom with sparse matrices
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_constant.py::test_prod
test_that("Prod: sparse cross matrix product (axis=NULL, axis=1, axis=2)", {
## Build a 100x100 sparse cross matrix:
## rows 0:99 in col 0 (col of ones), plus row 0 in cols 1:99 (row of ones)
## Diagonal element (0,0) gets value 2 (double-counted), rest of row 0 and col 0 are 1.
## All other entries are 0.
## CVXPY axis=0 → R axis=2, CVXPY axis=1 → R axis=1
## Verified via CVXPY:
## prod(A).value = 0.0
## prod(A, axis=0)[:5] = [1, 0, 0, 0, 0], shape=(100,) → R c(100,1)
## prod(A, axis=1)[:5] = [1, 0, 0, 0, 0], shape=(100,) → R c(100,1)
## Build the sparse matrix using 1-based indices for sparseMatrix
rows_part1 <- 1:100 # 1-based: arange(100) + 1
cols_part1 <- rep(1L, 100) # all in column 1
rows_part2 <- rep(1L, 99) # all in row 1
cols_part2 <- 2:100 # columns 2:100
A <- Matrix::sparseMatrix(
i = c(rows_part1, rows_part2),
j = c(cols_part1, cols_part2),
x = rep(1, 199),
dims = c(100, 100)
)
## Prod over all entries: any row/col with zeros → product = 0
prod_all <- Prod(Constant(A))
expect_equal(as.numeric(value(prod_all)), 0.0, tolerance = 1e-10)
## Prod along axis=2 (CVXPY axis=0): column-wise product
## Column 1 has all 1s → product = 1. Columns 2-100 have zeros → product = 0.
prod_ax2 <- Prod(Constant(A), axis = 2L)
val_ax2 <- as.numeric(value(prod_ax2))
expect_equal(val_ax2[1], 1.0, tolerance = 1e-10)
expect_equal(val_ax2[2:100], rep(0, 99), tolerance = 1e-10)
## Prod along axis=1 (CVXPY axis=1): row-wise product
## Row 1 has all 1s (or 2 in position (1,1)) → product != 0.
## Rows 2-100 only have one nonzero in col 1, rest are zero → product = 0.
prod_ax1 <- Prod(Constant(A), axis = 1L)
val_ax1 <- as.numeric(value(prod_ax1))
expect_equal(val_ax1[2:100], rep(0, 99), tolerance = 1e-10)
})
## @cvxpy test_constant.py::test_prod
test_that("Prod: dense small matrix (overall product)", {
## CVXPY: B = arange(4).reshape(2,2) + 1 = [[1,3],[2,4]] (col-major in R: [[1,2],[3,4]])
## Note: NumPy reshape is row-major, R is column-major
## B (NumPy row-major) = [[1,2],[3,4]], prod = 24
## Verified via CVXPY: prod(sparse(B)).value = 24
B <- matrix(1:4, 2, 2) # R col-major: [[1,3],[2,4]] — product = 24
B_sparse <- Matrix::Matrix(B, sparse = TRUE)
prod_B <- Prod(Constant(B_sparse))
expect_equal(as.numeric(value(prod_B)), 24.0, tolerance = 1e-10)
})
# ═══════════════════════════════════════════════════════════════════════
# test_add_expr_copy (test_atoms.py line 54)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_add_expr_copy
test_that("AddExpression copy: same type, same args, new object", {
x <- Variable(2, name = "x")
y <- Variable(2, name = "y")
A <- Variable(c(2, 2), name = "A")
B <- Variable(c(2, 2), name = "B")
atom <- x + y
copy <- CVXR:::expr_copy(atom)
## A new object is constructed, so copy@args has same content but is not identical
expect_equal(class(copy)[1], class(atom)[1])
expect_equal(copy@args[[1]]@id, atom@args[[1]]@id)
expect_equal(copy@args[[2]]@id, atom@args[[2]]@id)
## Test copy with new args
copy2 <- CVXR:::expr_copy(atom, args = list(A, B))
expect_equal(class(copy2)[1], class(atom)[1])
expect_true(copy2@args[[1]]@id == A@id)
expect_true(copy2@args[[2]]@id == B@id)
})
# ═══════════════════════════════════════════════════════════════════════
# test_list_input (test_atoms.py line 96)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_list_input
test_that("list input rejected by atoms", {
## CVXPY: cp.max([cp.Variable(), 1]) raises Exception about list input
## In CVXR, as_expr rejects list inputs
expect_error(max_entries(list(Variable(), 1)), regexp = ".*Cannot convert.*list.*")
expect_error(cvxr_norm(list(1, Variable())), regexp = ".*Cannot convert.*list.*")
x <- Variable()
y <- Variable()
expect_error(cvxr_norm(list(x, y)), regexp = ".*Cannot convert.*list.*")
})
# ═══════════════════════════════════════════════════════════════════════
# test_norm_exceptions (test_atoms.py line 119)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_norm_exceptions
test_that("norm(x, 'nuc') on non-matrix raises error", {
## CVXPY: cp.norm(x, 'nuc') on vector -> "Unsupported norm option nuc for non-matrix."
## In CVXR, cvxr_norm passes "nuc" to as.numeric which gives NA, causing an error.
## We just check that an error is raised for nuclear norm on a vector.
x <- Variable(2)
expect_error(suppressWarnings(cvxr_norm(x, "nuc")))
})
# ═══════════════════════════════════════════════════════════════════════
# test_harmonic_mean (test_atoms.py line 220)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_harmonic_mean
test_that("harmonic_mean: shape, curvature, sign", {
x <- Variable(2)
atom <- harmonic_mean(x)
## CVXPY: shape == tuple() -> scalar c(1,1)
expect_equal(atom@shape, c(1L, 1L))
## CVXPY: curvature == s.CONCAVE
expect_equal(curvature(atom), "CONCAVE")
## CVXPY: sign == s.NONNEG
expect_true(is_nonneg(atom))
})
# ═══════════════════════════════════════════════════════════════════════
# test_matrix_frac (test_atoms.py line 354)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_matrix_frac
test_that("matrix_frac: shape, curvature, validation", {
x <- Variable(2)
A <- Variable(c(2, 2))
C <- Variable(c(3, 2))
atom <- matrix_frac(x, A)
## CVXPY: shape == tuple() -> scalar c(1,1)
expect_equal(atom@shape, c(1L, 1L))
## CVXPY: curvature == s.CONVEX
expect_equal(curvature(atom), "CONVEX")
## Non-square matrix: "The second argument to matrix_frac must be a square matrix."
expect_error(matrix_frac(x, C), regexp = ".*square matrix.*")
## Incompatible dimensions
expect_error(matrix_frac(Variable(3), A), regexp = ".*incompatible.*dimensions.*")
})
# ═══════════════════════════════════════════════════════════════════════
# test_concatenate (test_atoms.py line 604)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_concatenate
test_that("Concatenate atom (stub — not in CVXR)", {
skip("Concatenate atom not implemented in CVXR; use HStack/VStack instead")
})
# ═══════════════════════════════════════════════════════════════════════
# test_squeeze (test_atoms.py line 736)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_squeeze
test_that("Squeeze atom (N/A in CVXR's 2D shape system)", {
## CVXPY's squeeze removes size-1 dimensions from any-rank arrays.
## CVXPY's test uses np.random.rand(2,1,3,1,1,4) — a 6-D array —
## which CVXR cannot express (CLAUDE.md rule: "CVXR only supports
## 2D shapes"). In 2D: c(1,1) and c(n,1) have no removable trivial
## dims; c(1,n) -> c(n,1) is just t(). No CVXR primitive is
## missing — Squeeze is N/A here, not a port that's outstanding.
skip("Squeeze N/A in CVXR's 2D shape system; CVXPY test uses 6-D input")
})
# ═══════════════════════════════════════════════════════════════════════
# test_diag_offset (test_atoms.py line 809)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_diag_offset
test_that("DiagVec/DiagMat with k offset", {
## CVXPY test_matrix: np.array([[1,2,3],[4,5,6],[7,8,9]])
## In R (column-major), to match: row 1=[1,2,3], row 2=[4,5,6], row 3=[7,8,9]
test_matrix <- matrix(c(1, 4, 7, 2, 5, 8, 3, 6, 9), nrow = 3, ncol = 3)
test_vector <- c(1, 2, 3)
offsets <- c(0L, 1L, -1L, 2L)
## Helper to extract k-th diagonal from a matrix (NumPy convention)
np_diag <- function(m, k = 0L) {
n <- nrow(m)
p <- ncol(m)
if (k >= 0) {
idx <- seq_len(min(n, p - k))
return(m[cbind(idx, idx + k)])
} else {
idx <- seq_len(min(n + k, p))
return(m[cbind(idx - k, idx)])
}
}
for (offset in offsets) {
## Matrix -> vector (DiagMat with offset)
a_cvxr <- as.numeric(value(DiagMat(Constant(test_matrix), k = offset)))
a_np <- np_diag(test_matrix, offset)
expect_equal(a_cvxr, a_np, tolerance = 1e-10)
## Vector -> matrix (DiagVec with offset)
A_cvxr <- as.matrix(value(DiagVec(test_vector, k = offset)))
## R diag(v, nrow=n+abs(k)) doesn't do offset; build manually
n <- length(test_vector)
sz <- n + abs(offset)
A_np <- matrix(0, sz, sz)
if (offset >= 0) {
for (i in seq_along(test_vector)) A_np[i, i + offset] <- test_vector[i]
} else {
for (i in seq_along(test_vector)) A_np[i - offset, i] <- test_vector[i]
}
expect_equal(A_cvxr, A_np, tolerance = 1e-10)
}
## CVXPY: cp.diag(Variable(5), 1).size == 36
X <- DiagVec(Variable(5), k = 1L)
expect_equal(prod(X@shape), 36L)
})
# ═══════════════════════════════════════════════════════════════════════
# test_Trace (test_atoms.py line 850)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_Trace
test_that("trace(A) resolves to Trace class", {
A <- Variable(c(4, 4))
t <- matrix_trace(A)
## CVXPY: isinstance(t, cp.Trace)
expect_true(S7::S7_inherits(t, CVXR:::Trace))
})
# ═══════════════════════════════════════════════════════════════════════
# test_trace_complex2real (test_atoms.py line 875)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_trace_complex2real
test_that("trace in complex problem", {
X <- Variable(c(2, 2), complex = TRUE)
problem <- Problem(Minimize(cvxr_norm(matrix_trace(X))), list(X == 2))
result <- psolve(problem, solver = "CLARABEL")
expect_equal(result, 4.0, tolerance = 1e-5)
})
# ═══════════════════════════════════════════════════════════════════════
# test_trace_dgp2dcp (test_atoms.py line 881)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_trace_dgp2dcp
test_that("trace in DGP problem", {
X <- Variable(c(2, 2), pos = TRUE)
problem <- Problem(Minimize(matrix_trace(X)), list(X == 2))
result <- psolve(problem, gp = TRUE)
expect_equal(result, 4.0, tolerance = 1e-5)
})
# ═══════════════════════════════════════════════════════════════════════
# test_cvar (test_atoms.py line 1137)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_cvar
test_that("CVaR atom evaluation and LP usage", {
## Test CVaR computation against alternative LP formulation
set.seed(1)
m <- 100L
x_data <- rnorm(m)
betas <- c(0.1, 0.5, 0.9, 0.95, 0.99)
for (beta in betas) {
## CVaR via atom
cvar_val <- as.numeric(value(cvar(x_data, beta)))
## CVaR via alternative LP formulation: alpha + 1/((1-beta)*m) * sum(pos(x - alpha))
alpha <- Variable()
objective <- Minimize(alpha + 1.0 / ((1 - beta) * m) * sum_entries(pos(x_data - alpha)))
prob_alt <- Problem(objective)
cvar_alt_val <- as.numeric(psolve(prob_alt, solver = "CLARABEL"))
expect_equal(cvar_val, cvar_alt_val, tolerance = 1e-4)
}
})
# ═══════════════════════════════════════════════════════════════════════
# test_bmat (test_atoms.py line 1219)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_bmat
test_that("bmat block matrix construction", {
## CVXPY builds a block matrix from list of lists
v_np <- matrix(1, 3, 1)
## Expected shape: (5, 2)
expr <- bmat(list(
list(v_np, v_np),
list(matrix(0, 2, 1), matrix(c(1, 2), 2, 1))
))
expect_equal(expr@shape, c(5L, 2L))
## Check numeric value
expected <- rbind(
cbind(v_np, v_np),
cbind(matrix(0, 2, 1), matrix(c(1, 2), 2, 1))
)
expect_equal(as.matrix(value(expr)), expected, tolerance = 1e-10)
})
# ═══════════════════════════════════════════════════════════════════════
# test_cumprod (test_atoms.py line 1279)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_cumprod
test_that("CumProd in DGP for both axes", {
## CVXPY tests both axis=0 and axis=1 with DGP
## CVXPY axis=0 -> CVXR axis=2 (column-wise)
## CVXPY axis=1 -> CVXR axis=1 (row-wise)
for (cvxpy_axis in c(0L, 1L)) {
r_axis <- if (cvxpy_axis == 0L) 2L else 1L
x <- Variable(c(4, 3), pos = TRUE)
expr <- CVXR:::Cumprod(x, axis = r_axis)
## Constant needs to be elementwise positive
x_val <- matrix(1:12, nrow = 4, ncol = 3)
## Compute expected: R apply with cumprod along the axis
if (r_axis == 2L) {
target <- apply(x_val, 2, cumprod)
} else {
target <- t(apply(x_val, 1, cumprod))
}
prob <- Problem(Minimize(sum_entries(expr)), list(x == x_val))
psolve(prob, gp = TRUE)
expect_equal(as.matrix(value(expr)), target, tolerance = 1e-4)
}
})
# ═══════════════════════════════════════════════════════════════════════
# test_convolve (test_atoms.py line 1308)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_convolve
test_that("conv (convolve): sign propagation and errors", {
## NOTE: CVXR uses conv() which returns (n+m-1, 1) shape, while CVXPY
## convolve returns (n+m-1,). We test the equivalent functionality.
## Sign propagation: nonneg constant * nonneg param -> nonneg
a <- c(1, 1, 1)
b_pos <- Parameter(2, nonneg = TRUE)
expr <- conv(a, b_pos)
expect_true(is_nonneg(expr))
## Shape: len(a) + len(b) - 1 = 3 + 2 - 1 = 4 -> (4, 1)
expect_equal(expr@shape, c(4L, 1L))
## Nonpos param -> nonpos
b_neg <- Parameter(2, nonpos = TRUE)
expr2 <- conv(a, b_neg)
expect_true(is_nonpos(expr2))
## Error: matrix input rejected (CVXPY: "scalar or 1D")
x <- Variable(2)
expect_error(conv(matrix(c(0, 1, 0, 1), 2, 2), x), regexp = ".*vector.*")
})
# ═══════════════════════════════════════════════════════════════════════
# test_ptp (test_atoms.py line 1341)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_ptp
test_that("ptp: peak-to-peak with axis and keepdims", {
## CVXPY: a = np.array([[10., -10., 3.0], [6., 0., -1.5]])
a <- matrix(c(10, -10, 3, 6, 0, -1.5), nrow = 2, byrow = TRUE)
## Overall ptp = max - min = 10 - (-10) = 20
expr <- ptp(a)
expect_equal(expr@shape, c(1L, 1L))
expect_equal(as.numeric(value(expr)), 20.0, tolerance = 1e-10)
## axis=0 (CVXPY) -> axis=2 (CVXR): column-wise ptp
## col 1: max(10,6)-min(10,6) = 4, col 2: max(-10,0)-min(-10,0) = 10, col 3: max(3,-1.5)-min(3,-1.5)=4.5
expr_ax2 <- ptp(a, axis = 2L)
expect_equal(as.numeric(value(expr_ax2)), c(4, 10, 4.5), tolerance = 1e-10)
## axis=1 (CVXPY) -> axis=1 (CVXR): row-wise ptp
## row 1: 10-(-10)=20, row 2: 6-(-1.5)=7.5
expr_ax1 <- ptp(a, axis = 1L)
expect_equal(as.numeric(value(expr_ax1)), c(20, 7.5), tolerance = 1e-10)
## axis=0 keepdims=TRUE (CVXPY) -> axis=2 keepdims=TRUE (CVXR)
## Expected shape: (1, 3)
expr_ax2_kd <- ptp(a, axis = 2L, keepdims = TRUE)
expect_equal(expr_ax2_kd@shape, c(1L, 3L))
expect_equal(as.numeric(value(expr_ax2_kd)), c(4, 10, 4.5), tolerance = 1e-10)
## axis=1 keepdims=TRUE (CVXPY) -> axis=1 keepdims=TRUE (CVXR)
## Expected shape: (2, 1)
expr_ax1_kd <- ptp(a, axis = 1L, keepdims = TRUE)
expect_equal(expr_ax1_kd@shape, c(2L, 1L))
expect_equal(as.numeric(value(expr_ax1_kd)), c(20, 7.5), tolerance = 1e-10)
## Curvature: ptp on Variable is convex
x_var <- Variable(10)
expr_var <- ptp(x_var)
expect_equal(curvature(expr_var), "CONVEX")
})
# ═══════════════════════════════════════════════════════════════════════
# test_stats (test_atoms.py line 1374)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_stats
test_that("mean/var/std with axis and keepdims", {
## CVXPY: a = np.array([[10., 10., 3.0], [6., 0., 1.5]])
a <- matrix(c(10, 10, 3, 6, 0, 1.5), nrow = 2, byrow = TRUE)
## Overall mean, var, std
expr_mean <- cvxr_mean(a)
expr_var <- cvxr_var(a)
expr_std <- cvxr_std(a)
expect_true(is_nonneg(expr_mean))
expect_true(is_nonneg(expr_var))
expect_true(is_nonneg(expr_std))
## Expected values (NumPy defaults: ddof=0)
np_mean <- mean(a)
np_var <- mean((a - mean(a))^2) # population variance
np_std <- sqrt(np_var)
expect_equal(as.numeric(value(expr_mean)), np_mean, tolerance = 1e-10)
expect_equal(as.numeric(value(expr_var)), np_var, tolerance = 1e-6)
expect_equal(as.numeric(value(expr_std)), np_std, tolerance = 1e-6)
## ddof tests
for (ddof in c(0, 1)) {
expr_var_d <- cvxr_var(a, ddof = ddof)
expr_std_d <- cvxr_std(a, ddof = ddof)
n <- length(a)
expected_var <- sum((a - mean(a))^2) / (n - ddof)
expected_std <- sqrt(expected_var)
expect_equal(as.numeric(value(expr_var_d)), expected_var, tolerance = 1e-6)
expect_equal(as.numeric(value(expr_std_d)), expected_std, tolerance = 1e-6)
}
## Axis tests (CVXPY axis=0 -> CVXR axis=2, CVXPY axis=1 -> CVXR axis=1)
for (cvxpy_axis in c(0L, 1L)) {
r_axis <- if (cvxpy_axis == 0L) 2L else 1L
for (keepdims in c(TRUE, FALSE)) {
expr_mean_ax <- cvxr_mean(a, axis = r_axis, keepdims = keepdims)
## Expected from R: apply then mean
if (r_axis == 2L) {
expected_mean <- apply(a, 2, mean)
} else {
expected_mean <- apply(a, 1, mean)
}
if (keepdims) {
if (r_axis == 2L) {
expected_shape <- c(1L, ncol(a))
} else {
expected_shape <- c(nrow(a), 1L)
}
}
expect_equal(as.numeric(value(expr_mean_ax)), expected_mean, tolerance = 1e-10)
}
}
})
# ═══════════════════════════════════════════════════════════════════════
# test_mixed_norm (test_atoms.py line 1617)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_mixed_norm
test_that("mixed_norm evaluation", {
## CVXPY: y = Variable((5,5)); obj = Minimize(mixed_norm(y, "inf", 1))
## prob = Problem(obj, [y == ones((5,5))]), result = 5.
y <- Variable(c(5, 5))
obj <- Minimize(mixed_norm(y, Inf, 1))
prob <- Problem(obj, list(y == matrix(1, 5, 5)))
result <- psolve(prob, solver = "CLARABEL")
expect_equal(result, 5.0, tolerance = 1e-3)
})
# ═══════════════════════════════════════════════════════════════════════
# test_indicator (test_atoms.py line 1641)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_indicator
test_that("Indicator transform", {
x <- Variable()
constraints <- list(x >= 0, x <= 1)
expr <- indicator(constraints)
## x = 0.5 -> in feasible set -> value 0
value(x) <- 0.5
expect_equal(as.numeric(value(expr)), 0.0)
## x = 2 -> outside feasible set -> value Inf
value(x) <- 2.0
expect_equal(as.numeric(value(expr)), Inf)
})
# ═══════════════════════════════════════════════════════════════════════
# test_vdot (test_atoms.py line 1742)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_vdot
test_that("vdot / scalar_product", {
p <- rep(1, 4)
v <- Variable(c(4, 1))
## Minimize vdot(v, p) s.t. v >= 1 -> optimal v = 1, objective = 4
obj <- Minimize(vdot(v, p))
prob <- Problem(obj, list(v >= 1))
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(v)), rep(1, 4), tolerance = 1e-4)
## Same with scalar_product
obj2 <- Minimize(scalar_product(v, p))
prob2 <- Problem(obj2, list(v >= 1))
psolve(prob2, solver = "CLARABEL")
expect_equal(as.numeric(value(v)), rep(1, 4), tolerance = 1e-4)
## With a parameter
p_param <- Parameter(c(4, 1))
v2 <- Variable(c(4, 1))
value(p_param) <- matrix(1, 4, 1)
obj3 <- Minimize(vdot(v2, p_param))
prob3 <- Problem(obj3, list(v2 >= 1))
psolve(prob3, solver = "CLARABEL")
expect_equal(as.numeric(value(v2)), rep(1, 4), tolerance = 1e-4)
obj4 <- Minimize(scalar_product(v2, p_param))
prob4 <- Problem(obj4, list(v2 >= 1))
psolve(prob4, solver = "CLARABEL")
expect_equal(as.numeric(value(v2)), rep(1, 4), tolerance = 1e-4)
})
# ═══════════════════════════════════════════════════════════════════════
# test_conj (test_atoms.py line 1807)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_conj
test_that("Conj atom in constraints", {
## CVXPY: v = Variable(4); obj = Minimize(sum(v)); prob = Problem(obj, [conj(v) >= 1])
## For real variables, conj(v) == v, so this is just v >= 1.
v <- Variable(c(4, 1))
obj <- Minimize(sum_entries(v))
prob <- Problem(obj, list(CVXR:::conj_expr(v) >= 1))
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(v)), rep(1, 4), tolerance = 1e-4)
})
# ═══════════════════════════════════════════════════════════════════════
# test_partial_trace (test_atoms.py line 1833)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_partial_trace
test_that("partial_trace multi-subsystem", {
## CVXPY: rho_ABC = rho_A (x) rho_B (x) rho_C with complex random matrices
## CVXPY axis is 0-based; CVXR axis is 1-based
## NOTE: R Matrix package lacks complex sparse zgeMatrix class; skip if unavailable
has_complex_matrix <- tryCatch({methods::getClass("zgeMatrix"); TRUE},
error = function(e) FALSE)
skip_if(!has_complex_matrix, "R Matrix package lacks zgeMatrix (complex sparse)")
set.seed(1)
rho_A <- matrix(complex(real = runif(16), imaginary = runif(16)), 4, 4)
rho_A <- rho_A / sum(diag(rho_A))
rho_B <- matrix(complex(real = runif(9), imaginary = runif(9)), 3, 3)
rho_B <- rho_B / sum(diag(rho_B))
rho_C <- matrix(complex(real = runif(4), imaginary = runif(4)), 2, 2)
rho_C <- rho_C / sum(diag(rho_C))
rho_AB <- kronecker(rho_A, rho_B)
rho_AC <- kronecker(rho_A, rho_C)
temp <- kronecker(rho_AB, rho_C)
rho_ABC <- Variable(shape = dim(temp), complex = TRUE)
value(rho_ABC) <- temp
## Trace out C (CVXPY axis=2 -> CVXR axis=3)
rho_AB_test <- partial_trace(rho_ABC, c(4, 3, 2), axis = 3L)
expect_equal(value(rho_AB_test), rho_AB, tolerance = 1e-10)
## Trace out B (CVXPY axis=1 -> CVXR axis=2)
rho_AC_test <- partial_trace(rho_ABC, c(4, 3, 2), axis = 2L)
expect_equal(value(rho_AC_test), rho_AC, tolerance = 1e-10)
## Trace out B from AB (CVXPY axis=1 -> CVXR axis=2)
rho_A_test <- partial_trace(rho_AB_test, c(4, 3), axis = 2L)
expect_equal(value(rho_A_test), rho_A, tolerance = 1e-10)
## Trace out A from AB (CVXPY axis=0 -> CVXR axis=1)
rho_B_test <- partial_trace(rho_AB_test, c(4, 3), axis = 1L)
expect_equal(value(rho_B_test), rho_B, tolerance = 1e-10)
## Trace out A from AC (CVXPY axis=0 -> CVXR axis=1)
rho_C_test <- partial_trace(rho_AC_test, c(4, 2), axis = 1L)
expect_equal(value(rho_C_test), rho_C, tolerance = 1e-10)
})
# ═══════════════════════════════════════════════════════════════════════
# test_partial_trace_exceptions (test_atoms.py line 1873)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_partial_trace_exceptions
test_that("partial_trace exceptions", {
## Non-square matrix
X <- Variable(c(4, 3))
expect_error(partial_trace(X, dims = c(2, 3), axis = 1L),
regexp = ".*square.*")
## Dims don't match matrix size
X6 <- Variable(c(6, 6))
expect_error(partial_trace(X6, dims = c(2, 4), axis = 1L),
regexp = ".*dimension.*subsystems.*")
})
# ═══════════════════════════════════════════════════════════════════════
# test_partial_transpose (test_atoms.py line 1888)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_partial_transpose
test_that("partial_transpose multi-subsystem", {
## NOTE: R Matrix package lacks complex sparse zgeMatrix class; skip if unavailable
has_complex_matrix <- tryCatch({methods::getClass("zgeMatrix"); TRUE},
error = function(e) FALSE)
skip_if(!has_complex_matrix, "R Matrix package lacks zgeMatrix (complex sparse)")
set.seed(1)
rho_A <- matrix(complex(real = runif(64), imaginary = runif(64)), 8, 8)
rho_A <- rho_A / sum(diag(rho_A))
rho_B <- matrix(complex(real = runif(36), imaginary = runif(36)), 6, 6)
rho_B <- rho_B / sum(diag(rho_B))
rho_C <- matrix(complex(real = runif(16), imaginary = runif(16)), 4, 4)
rho_C <- rho_C / sum(diag(rho_C))
rho_TC <- kronecker(kronecker(rho_A, rho_B), t(rho_C))
rho_TB <- kronecker(kronecker(rho_A, t(rho_B)), rho_C)
rho_TA <- kronecker(kronecker(t(rho_A), rho_B), rho_C)
temp <- kronecker(kronecker(rho_A, rho_B), rho_C)
rho_ABC <- Variable(shape = dim(temp), complex = TRUE)
value(rho_ABC) <- temp
## Transpose C (CVXPY axis=2 -> CVXR axis=3)
rho_TC_test <- partial_transpose(rho_ABC, c(8, 6, 4), axis = 3L)
expect_equal(value(rho_TC_test), rho_TC, tolerance = 1e-10)
## Transpose B (CVXPY axis=1 -> CVXR axis=2)
rho_TB_test <- partial_transpose(rho_ABC, c(8, 6, 4), axis = 2L)
expect_equal(value(rho_TB_test), rho_TB, tolerance = 1e-10)
## Transpose A (CVXPY axis=0 -> CVXR axis=1)
rho_TA_test <- partial_transpose(rho_ABC, c(8, 6, 4), axis = 1L)
expect_equal(value(rho_TA_test), rho_TA, tolerance = 1e-10)
})
# ═══════════════════════════════════════════════════════════════════════
# test_partial_transpose_exceptions (test_atoms.py line 1926)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_partial_transpose_exceptions
test_that("partial_transpose exceptions", {
## Non-square matrix
X <- Variable(c(4, 3))
expect_error(partial_transpose(X, dims = c(2, 3), axis = 1L),
regexp = ".*square.*")
## Dims don't match
X6 <- Variable(c(6, 6))
expect_error(partial_transpose(X6, dims = c(2, 4), axis = 1L),
regexp = ".*dimension.*subsystems.*")
})
# ═══════════════════════════════════════════════════════════════════════
# test_flatten (test_atoms.py line 1956)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_flatten
test_that("vec/flatten with order", {
## Constant argument, F-order
A <- 0:9
reshaped_F <- matrix(A, nrow = 2, ncol = 5, byrow = FALSE) # F-order reshape
## vec uses F-order (column-major) by default
expr_F <- vec(Constant(reshaped_F))
expect_equal(as.numeric(value(expr_F)), A, tolerance = 1e-10)
## C-order: use reshape_expr
reshaped_C <- matrix(A, nrow = 2, ncol = 5, byrow = TRUE)
expr_C <- reshape_expr(Constant(reshaped_C), c(prod(dim(reshaped_C)), 1L), order = "C")
expect_equal(as.numeric(value(expr_C)), A, tolerance = 1e-10)
## Variable argument, F-order
x <- Variable(c(2, 5))
reshaped_F_expected <- matrix(A, nrow = 2, ncol = 5, byrow = FALSE)
expr_vf <- vec(x)
prob <- Problem(Minimize(0), list(expr_vf == matrix(A, ncol = 1)))
psolve(prob, solver = "CLARABEL")
expect_equal(as.matrix(value(x)), reshaped_F_expected, tolerance = 1e-4)
## Variable argument, C-order
x2 <- Variable(c(2, 5))
reshaped_C_expected <- matrix(A, nrow = 2, ncol = 5, byrow = TRUE)
expr_vc <- reshape_expr(x2, c(prod(dim(reshaped_C_expected)), 1L), order = "C")
prob2 <- Problem(Minimize(0), list(expr_vc == matrix(A, ncol = 1)))
psolve(prob2, solver = "CLARABEL")
expect_equal(as.matrix(value(x2)), reshaped_C_expected, tolerance = 1e-4)
})
# ═══════════════════════════════════════════════════════════════════════
# test_tr_inv (test_atoms.py line 1990)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestAtoms::test_tr_inv
test_that("tr_inv in SDP", {
## minimize tr_inv(X) s.t. X >> 0, trace(X) == 1
## Optimal value = T^2 with X = I/T
T_dim <- 5L
X <- Variable(c(T_dim, T_dim), symmetric = TRUE)
constraints <- list(X %>>% 0, matrix_trace(X) == 1)
prob <- Problem(Minimize(tr_inv(X)), constraints)
psolve(prob, solver = "CLARABEL")
## Best value is T^2
expect_equal(prob@.cache$value, T_dim^2, tolerance = 1e-2)
## Optimal X = I/T
X_expect <- diag(T_dim) / T_dim
expect_equal(as.matrix(value(X)), X_expect, tolerance = 1e-3)
})
# ═══════════════════════════════════════════════════════════════════════
# TestDotsort tests (test_atoms.py line 2035)
# ═══════════════════════════════════════════════════════════════════════
## @cvxpy test_atoms.py::TestDotsort::test_sum_k_largest_equivalence
test_that("dotsort: sum_k_largest equivalence", {
x_var <- Variable(5)
x_val <- c(1, 3, 2, -5, 0)
w <- c(1, 1, 1, 0)
expr <- dotsort(x_var, w)
expect_true(is_convex(expr))
expect_true(CVXR:::is_incr(expr, 1L))
prob <- Problem(Minimize(expr), list(x_var == x_val))
psolve(prob, solver = "CLARABEL")
## sum of 3 largest: sort(x_val) desc = [3, 2, 1, 0, -5] -> top 3 = 3+2+1 = 6
expected <- sum(sort(x_val, decreasing = TRUE)[1:3])
expect_equal(value(objective(prob)), expected, tolerance = 1e-4)
})
## @cvxpy test_atoms.py::TestDotsort::test_sum_k_smallest_equivalence
test_that("dotsort: sum_k_smallest equivalence (negated)", {
x_var <- Variable(5)
x_val <- c(1, 3, 2, -5, 0)
w <- c(-1, -1, -1, 0)
expr <- -dotsort(x_var, w)
expect_true(is_concave(expr))
expect_true(CVXR:::is_decr(-dotsort(x_var, w), 1L))
prob <- Problem(Maximize(expr), list(x_var == x_val))
psolve(prob, solver = "CLARABEL")
## sum of 3 smallest: sort(x_val) = [-5, 0, 1, 2, 3] -> bottom 3 = -5+0+1 = -4
expected <- sum(sort(x_val)[1:3])
expect_equal(value(objective(prob)), expected, tolerance = 1e-4)
})
## @cvxpy test_atoms.py::TestDotsort::test_copy
test_that("dotsort: copy", {
x_var <- Variable(5)
w <- c(1, 2)
atom <- dotsort(x_var, w)
copy <- CVXR:::expr_copy(atom)
expect_equal(class(copy)[1], class(atom)[1])
## Args have same content
expect_equal(copy@args[[1]]@id, atom@args[[1]]@id)
## Copy with new args
copy2 <- CVXR:::expr_copy(atom, args = list(x_var, Constant(w)))
expect_equal(class(copy2)[1], class(atom)[1])
expect_true(copy2@args[[1]]@id == atom@args[[1]]@id)
})
## @cvxpy test_atoms.py::TestDotsort::test_exceptions
test_that("dotsort: exceptions", {
x_var <- Variable(5)
## len(w) > len(x)
expect_error(dotsort(x_var, c(1, 2, 3, 4, 5, 8)),
regexp = ".*size.*W.*less.*equal.*size.*X.*")
## Two variable expressions for W
expect_error(dotsort(x_var, Variable(3)),
regexp = ".*W.*must be constant.*")
## Swapped arguments (constant X, variable W)
expect_error(dotsort(c(1, 2, 3), x_var),
regexp = ".*W.*must be constant.*")
## Non-DCP composition
expect_error(psolve(Problem(Minimize(dotsort(abs(x_var), c(-1, 1))))),
regexp = ".*DCP.*")
})
## @cvxpy test_atoms.py::TestDotsort::test_list
test_that("dotsort: list w input", {
x_var <- Variable(5)
r <- c(2, 1, 0, -1, -1)
w <- c(1.2, 1.1)
expr <- dotsort(x_var, w)
prob <- Problem(Maximize(sum_entries(r * x_var)),
list(x_var >= 0, expr <= 1, sum_entries(x_var) == 1))
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(expr)), 1.0, tolerance = 1e-4)
})
## @cvxpy test_atoms.py::TestDotsort::test_parameter
test_that("dotsort: parameter w", {
x_var <- Variable(5)
x_val <- c(1, 3, 2, -5, 0)
## Positive parameter -> is_incr (CVXPY uses pos=True and nonneg=True)
expect_true(CVXR:::is_incr(dotsort(x_var, Parameter(2, pos = TRUE)), 1L))
expect_true(CVXR:::is_incr(dotsort(x_var, Parameter(2, nonneg = TRUE)), 1L))
## Negative parameter -> is_decr, not is_incr
expect_false(CVXR:::is_incr(dotsort(x_var, Parameter(2, neg = TRUE)), 1L))
expect_true(CVXR:::is_decr(dotsort(x_var, Parameter(2, neg = TRUE)), 1L))
## Fixed parameter value
w_p <- Parameter(2)
value(w_p) <- matrix(c(1, 0), ncol = 1)
expr <- dotsort(x_var, w_p)
expect_false(CVXR:::is_incr(expr, 1L))
expect_false(CVXR:::is_decr(expr, 1L))
prob <- Problem(Minimize(expr), list(x_var == x_val))
psolve(prob, solver = "CLARABEL")
## w_p = [1, 0] -> padded to [1, 0, 0, 0, 0]
## dotsort = sort(x_val) . sort(w_padded) = [-5,0,1,2,3] . [0,0,0,0,1]
expected <- sort(x_val) %*% sort(c(1, 0, 0, 0, 0))
expect_equal(as.numeric(value(objective(prob))), as.numeric(expected), tolerance = 1e-4)
## Change parameter value and resolve
value(w_p) <- matrix(c(-1, -1), ncol = 1)
prob2 <- Problem(Minimize(dotsort(x_var, w_p)), list(x_var == x_val))
psolve(prob2, solver = "CLARABEL")
expected2 <- sort(x_val) %*% sort(c(-1, -1, 0, 0, 0))
expect_equal(as.numeric(value(objective(prob2))), as.numeric(expected2), tolerance = 1e-4)
})
## @cvxpy test_atoms.py::TestDotsort::test_non_fixed_x
test_that("dotsort: non-fixed x optimization", {
x_var <- Variable(5)
r <- c(2, 1, 0, -1, -1)
w <- c(1.2, 1.1)
expr <- dotsort(x_var, w)
prob <- Problem(Maximize(sum_entries(r * x_var)),
list(x_var >= 0, expr <= 1, sum_entries(x_var) == 1))
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(expr)), 1.0, tolerance = 1e-4)
## The two largest x values dotted with w should equal 1
x_sorted <- sort(as.numeric(value(x_var)), decreasing = TRUE)
expect_equal(sum(x_sorted[1:2] * sort(w, decreasing = TRUE)), 1.0, tolerance = 1e-4)
## Test with unordered w of length 3
r2 <- c(2, 1, 0, -1, -1)
w2 <- c(1.2, 1.1, 1.3)
expr2 <- dotsort(x_var, w2)
prob2 <- Problem(Maximize(sum_entries(r2 * x_var)),
list(x_var >= 0, expr2 <= 1, sum_entries(x_var) == 1))
psolve(prob2, solver = "CLARABEL")
expect_equal(as.numeric(value(expr2)), 1.0, tolerance = 1e-4)
x_sorted2 <- sort(as.numeric(value(x_var)), decreasing = TRUE)
expect_equal(sum(x_sorted2[1:3] * sort(w2, decreasing = TRUE)), 1.0, tolerance = 1e-4)
})
# ====================================================================
# DOMAIN TESTS (test_domain.py)
# ====================================================================
# ── test_entr ──────────────────────────────────────────────────────
## @cvxpy test_domain.py::TestDomain::test_entr
test_that("domain: entr(a) domain constrains a >= 0 (CVXPY parity)", {
## CVXPY: dom = cp.entr(a).domain; min a s.t. dom => a ~= 0
a <- Variable(name = "a")
dom <- CVXR:::domain(entr(a))
expect_length(dom, 1)
prob <- Problem(Minimize(a), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(a)), 0, tolerance = 1e-4)
})
# ── test_geo_mean ─────────────────────────────────────────────────
## @cvxpy test_domain.py::TestDomain::test_geo_mean
test_that("domain: geo_mean domain constrains x >= 0 (CVXPY parity)", {
## CVXPY: min sum(x) s.t. geo_mean(x).domain => sum ~= 0
x <- Variable(2, name = "x")
dom <- CVXR:::domain(geo_mean(x))
prob <- Problem(Minimize(sum_entries(x)), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(prob)), 0, tolerance = 1e-4)
## geo_mean with weights [0, 2] => x[1] free, x[2] >= 0
x <- Variable(2, name = "x")
dom <- CVXR:::domain(geo_mean(x, c(0, 2)))
dom <- c(dom, list(x >= -1))
prob <- Problem(Minimize(sum_entries(x)), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(x)), c(-1, 0), tolerance = 1e-4)
## geo_mean with weights [0, 1, 1] on z(3)
z <- Variable(3, name = "z")
dom <- CVXR:::domain(geo_mean(z, c(0, 1, 1)))
dom <- c(dom, list(z >= -1))
prob <- Problem(Minimize(sum_entries(z)), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(z)), c(-1, 0, 0), tolerance = 1e-4)
})
# ── test_log ──────────────────────────────────────────────────────
## @cvxpy test_domain.py::TestDomain::test_log
test_that("domain: log(a) domain constrains a >= 0 (CVXPY parity)", {
## CVXPY: dom = cp.log(a).domain; min a s.t. dom => a ~= 0
a <- Variable(name = "a")
dom <- CVXR:::domain(log(a))
prob <- Problem(Minimize(a), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(a)), 0, tolerance = 1e-4)
})
# ── test_log_det ──────────────────────────────────────────────────
## @cvxpy test_domain.py::TestDomain::test_log_det
test_that("domain: log_det(A + I) domain constrains A + I >> 0 (CVXPY parity)", {
## CVXPY: min sum(diag(A)) s.t. log_det(A+I).domain => -2
A <- Variable(c(2, 2), name = "A")
dom <- CVXR:::domain(log_det(A + diag(2)))
prob <- Problem(Minimize(matrix_trace(A)), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(prob)), -2, tolerance = 1e-2)
})
# ── test_matrix_frac ──────────────────────────────────────────────
## @cvxpy test_domain.py::TestDomain::test_matrix_frac
test_that("domain: matrix_frac(x, A + I) domain constrains A + I >> 0 (CVXPY parity)", {
## CVXPY: min sum(diag(A)) s.t. matrix_frac(x, A+I).domain => -2
x <- Variable(2, name = "x")
A <- Variable(c(2, 2), name = "A")
dom <- CVXR:::domain(matrix_frac(x, A + diag(2)))
prob <- Problem(Minimize(matrix_trace(A)), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(prob)), -2, tolerance = 1e-2)
})
# ── test_partial_problem ──────────────────────────────────────────
## @cvxpy test_domain.py::TestDomain::test_partial_problem
test_that("domain: partial_optimize domain (CVXPY parity)", {
skip_if_not_installed("clarabel")
a <- Variable(name = "a")
x <- Variable(2, name = "x")
for (obj in list(Minimize(a^-1), Maximize(log(a)))) {
orig_prob <- Problem(obj, list(x + a >= c(5, 8)))
expr <- partial_optimize(orig_prob, dont_opt_vars = list(x, a))
dom <- domain(expr)
constr <- c(list(a >= -100, x >= 0), dom)
prob <- Problem(Minimize(sum_entries(x + a)), constr)
psolve(prob, solver = "CLARABEL")
expect_equal(value(prob), 13, tolerance = 1e-3)
expect_true(as.numeric(value(a)) >= 0)
}
})
# ── test_pnorm ────────────────────────────────────────────────────
## @cvxpy test_domain.py::TestDomain::test_pnorm
test_that("domain: p_norm(a, -0.5) domain constrains a >= 0 (CVXPY parity)", {
## CVXPY: dom = cp.pnorm(a, -0.5).domain; min a s.t. dom => a ~= 0
a <- Variable(name = "a")
dom <- CVXR:::domain(p_norm(a, -0.5))
prob <- Problem(Minimize(a), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(prob)), 0, tolerance = 1e-4)
})
# ── test_power ────────────────────────────────────────────────────
## @cvxpy test_domain.py::TestDomain::test_power
test_that("domain: power atom domains constrain sign correctly (CVXPY parity)", {
## sqrt(a) domain => a >= 0, min a ~= 0
a <- Variable(name = "a")
dom <- CVXR:::domain(sqrt(a))
prob <- Problem(Minimize(a), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(a)), 0, tolerance = 1e-4)
## square(a) domain is empty => min a with a >= -100 => -100
a <- Variable(name = "a")
dom <- CVXR:::domain(power(a, 2))
prob <- Problem(Minimize(a), c(dom, list(a >= -100)))
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(a)), -100, tolerance = 1e-4)
## a^(-1) domain => a >= 0, min a ~= 0
a <- Variable(name = "a")
dom <- CVXR:::domain(power(a, -1))
prob <- Problem(Minimize(a), c(dom, list(a >= -100)))
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(a)), 0, tolerance = 1e-4)
## a^3 domain => a >= 0, min a ~= 0
a <- Variable(name = "a")
dom <- CVXR:::domain(power(a, 3))
prob <- Problem(Minimize(a), c(dom, list(a >= -100)))
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(a)), 0, tolerance = 1e-4)
})
# ── test_quad_over_lin ────────────────────────────────────────────
## @cvxpy test_domain.py::TestDomain::test_quad_over_lin
test_that("domain: quad_over_lin(x, a) domain constrains a >= 0 (CVXPY parity)", {
## CVXPY: dom = cp.quad_over_lin(x, a).domain; min a s.t. dom => a ~= 0
a <- Variable(name = "a")
x <- Variable(2, name = "x")
dom <- CVXR:::domain(quad_over_lin(x, a))
prob <- Problem(Minimize(a), dom)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(a)), 0, tolerance = 1e-4)
})
# ====================================================================
# POWER ATOM APPROX TESTS (test_power_atom.py)
# ====================================================================
#
# Helper to get cone counts from problem data (mirrors CVXPY _get_cone_counts)
.get_cone_counts <- function(prob, solver = "CLARABEL") {
pd <- problem_data(prob, solver = solver)
dims <- pd$data$dims
list(
soc = length(dims@soc),
p3d = length(dims@p3d),
pnd = length(dims@pnd)
)
}
# ── TestPowerAtom ──────────────────────────────────────────────────
## @cvxpy test_power_atom.py::TestPowerAtom::test_dunder_pow_returns_approx
test_that("power: ^ operator returns PowerApprox (CVXPY parity)", {
## CVXPY: x**p uses PowerApprox so it canonicalizes via SOC.
x <- Variable(3)
expr <- x^2
expect_true(S7_inherits(expr, CVXR:::PowerApprox))
expect_true(S7_inherits(expr, CVXR:::Power))
expr2 <- x^0.5
expect_true(S7_inherits(expr2, CVXR:::PowerApprox))
expr3 <- x^3
expect_true(S7_inherits(expr3, CVXR:::PowerApprox))
})
## @cvxpy test_power_atom.py::TestPowerAtom::test_approx_controls_cone_type
test_that("power: approx=TRUE uses SOC, approx=FALSE uses power cones (CVXPY parity)", {
skip_if_not_installed("clarabel")
x <- Variable(3)
obj <- Minimize(x[1] + x[2] - x[3])
## approx=TRUE -> SOC cones, no power cones
prob <- Problem(obj, list(power(x, 3.3, approx = TRUE) <= rep(1, 3)))
cc <- .get_cone_counts(prob, "CLARABEL")
expect_gt(cc$soc, 0)
expect_equal(cc$p3d, 0)
## approx=FALSE -> power cones, no SOC
prob2 <- Problem(obj, list(power(x, 3.3, approx = FALSE) <= rep(1, 3)))
cc2 <- .get_cone_counts(prob2, "CLARABEL")
expect_equal(cc2$soc, 0)
expect_gt(cc2$p3d, 0)
})
## @cvxpy test_power_atom.py::TestPowerAtom::test_approx_and_exact_agree
test_that("power: approx and exact give same answer for all p ranges (CVXPY parity)", {
skip_if_not_installed("clarabel")
## p < 0 (low), 0 < p < 1 (mid), p > 1 non-integer (high), p > 1 even integer
cases <- list(
list(p = -1.5, direction = "<="),
list(p = 0.8, direction = ">="),
list(p = 4.5, direction = "<="),
list(p = 8, direction = "<=")
)
for (case in cases) {
p <- case$p
direction <- case$direction
x <- Variable(3)
if (direction == "<=") {
constr_a <- list(power(x, p, approx = TRUE) <= rep(1, 3))
} else {
constr_a <- list(power(x, p, approx = TRUE) >= rep(1, 3))
}
obj <- Minimize(x[1] + x[2] - x[3] + (x[2] + x[3])^2)
prob <- Problem(obj, constr_a)
psolve(prob, solver = "CLARABEL")
x_approx <- as.numeric(value(x))
if (direction == "<=") {
constr_e <- list(power(x, p, approx = FALSE) <= rep(1, 3))
} else {
constr_e <- list(power(x, p, approx = FALSE) >= rep(1, 3))
}
obj2 <- Minimize(x[1] + x[2] - x[3] + (x[2] + x[3])^2)
prob2 <- Problem(obj2, constr_e)
psolve(prob2, solver = "CLARABEL")
expect_true(status(prob2) %in% c("optimal", "optimal_inaccurate"),
info = paste("p =", p))
expect_equal(as.numeric(value(x)), x_approx, tolerance = 1e-3,
info = paste("p =", p))
}
})
## @cvxpy test_power_atom.py::TestPowerAtom::test_approx_false_errors_without_power_cone_support
test_that("power: approx=FALSE errors when solver lacks power cone support (CVXPY parity)", {
skip_if_not_installed("ECOSolveR")
x <- Variable(3)
prob <- Problem(
Minimize(x[1] + x[2] - x[3]),
list(power(x, 3.3, approx = FALSE) <= rep(1, 3))
)
## ECOS does not support power cones => should error
expect_error(psolve(prob, solver = "ECOS"))
})
## @cvxpy test_power_atom.py::TestPowerAtom::test_approx_warning
test_that("power: warning fires for approx=TRUE with many SOCs (CVXPY parity)", {
skip_if_not_installed("clarabel")
x <- Variable(3)
obj <- Minimize(x[1] + x[2] - x[3])
## approx=TRUE on a power-cone-capable solver should warn
expect_warning(
psolve(Problem(obj, list(power(x, 3.3, approx = TRUE) <= rep(1, 3))),
solver = "CLARABEL"),
regexp = "Power atom"
)
## approx=FALSE should not warn
expect_no_warning(
psolve(Problem(obj, list(power(x, 3.3, approx = FALSE) <= rep(1, 3))),
solver = "CLARABEL")
)
})
# ── TestGeoMeanApprox ──────────────────────────────────────────────
## @cvxpy test_power_atom.py::TestGeoMeanApprox::test_approx_controls_cone_type
test_that("geo_mean: approx=TRUE uses SOC, approx=FALSE uses power cones (CVXPY parity)", {
skip_if_not_installed("clarabel")
x <- Variable(3, pos = TRUE)
## approx=TRUE -> SOC cones, no PowConeND
obj <- Maximize(geo_mean(x, approx = TRUE))
prob <- Problem(obj, list(sum_entries(x) <= 3))
cc <- .get_cone_counts(prob, "CLARABEL")
expect_gt(cc$soc, 0)
expect_equal(cc$pnd, 0)
## approx=FALSE -> PowConeND, no SOC
obj2 <- Maximize(geo_mean(x, approx = FALSE))
prob2 <- Problem(obj2, list(sum_entries(x) <= 3))
cc2 <- .get_cone_counts(prob2, "CLARABEL")
expect_equal(cc2$soc, 0)
expect_gt(cc2$pnd, 0)
})
## @cvxpy test_power_atom.py::TestGeoMeanApprox::test_approx_and_exact_agree
test_that("geo_mean: approx and exact give same answer (CVXPY parity)", {
skip_if_not_installed("clarabel")
cases <- list(
list(weights = NULL, n = 3L), # uniform weights, 3 vars
list(weights = c(1, 2, 1), n = 3L), # non-uniform weights
list(weights = NULL, n = 4L) # uniform weights, 4 vars
)
for (case in cases) {
w <- case$weights
n <- case$n
x <- Variable(n, pos = TRUE)
constr <- list(sum_entries(x) <= n, x[1] >= 0.5)
prob <- Problem(Maximize(geo_mean(x, w, approx = TRUE)), constr)
val_a <- psolve(prob, solver = "CLARABEL")
x_a <- as.numeric(value(x))
prob2 <- Problem(Maximize(geo_mean(x, w, approx = FALSE)), constr)
val_e <- psolve(prob2, solver = "CLARABEL")
expect_true(status(prob2) %in% c("optimal", "optimal_inaccurate"),
info = paste("n =", n))
expect_equal(val_e, val_a, tolerance = 1e-3,
info = paste("n =", n, "value"))
expect_equal(as.numeric(value(x)), x_a, tolerance = 1e-3,
info = paste("n =", n, "x"))
}
})
## @cvxpy test_power_atom.py::TestGeoMeanApprox::test_approx_warning
test_that("geo_mean: warning fires for approx=TRUE with many SOCs (CVXPY parity)", {
skip_if_not_installed("clarabel")
x <- Variable(5, pos = TRUE)
constr <- list(sum_entries(x) <= 5)
expect_warning(
psolve(Problem(Maximize(geo_mean(x, approx = TRUE)), constr),
solver = "CLARABEL"),
regexp = "geo_mean"
)
expect_no_warning(
psolve(Problem(Maximize(geo_mean(x, approx = FALSE)), constr),
solver = "CLARABEL")
)
})
# ── TestGeoMeanSingleWeight ───────────────────────────────────────
## @cvxpy test_power_atom.py::TestGeoMeanSingleWeight::test_single_weight_is_affine
test_that("geo_mean: single weight is affine (CVXPY parity)", {
## CVXPY: geo_mean(x, [0, 0, 1]) should be affine.
x <- Variable(3)
g <- geo_mean(x, c(0, 0, 1))
expect_true(is_convex(g))
expect_true(is_concave(g))
expect_true(is_affine(g))
})
## @cvxpy test_power_atom.py::TestGeoMeanSingleWeight::test_single_weight_value
test_that("geo_mean: single weight w=(0,0,1) equals x[3] (CVXPY parity)", {
skip_if_not_installed("clarabel")
## CVXPY: geo_mean(x, [0, 0, 1]) should equal x[2] (0-based) = x[3] (1-based).
x <- Variable(3)
prob <- Problem(Maximize(geo_mean(x, c(0, 0, 1))),
list(x <= c(1, 2, 3), x >= 0))
val <- psolve(prob, solver = "CLARABEL")
expect_equal(val, 3.0, tolerance = 1e-3)
})
## @cvxpy test_power_atom.py::TestGeoMeanSingleWeight::test_single_weight_nonneg_domain
test_that("geo_mean: single weight still requires x >= 0 domain (CVXPY parity)", {
## CVXPY: geo_mean(x, [0, 0, 1]).domain should have nonnegativity.
x <- Variable(3)
g <- geo_mean(x, c(0, 0, 1))
dom <- CVXR:::domain(g)
expect_gt(length(dom), 0)
})
## @cvxpy test_power_atom.py::TestGeoMeanSingleWeight::test_single_weight_nonneg_enforced
test_that("geo_mean: solver enforces x >= 0 for single-weight element (CVXPY parity)", {
skip_if_not_installed("clarabel")
## CVXPY: Minimizing x[0] with geo_mean(x, [1]) >= 0 => x = 0 (domain x >= 0).
x <- Variable(1)
prob <- Problem(Minimize(x[1]),
list(geo_mean(x, c(1)) >= 0, x <= 5))
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(x)), 0.0, tolerance = 1e-3)
})
## @cvxpy test_power_atom.py::TestGeoMeanSingleWeight::test_single_weight_no_cones
test_that("geo_mean: single non-zero weight produces no cone constraints (CVXPY parity)", {
skip_if_not_installed("clarabel")
## CVXPY: Single non-zero weight should not produce any SOC/PowCone3D/PowConeND.
x <- Variable(3, pos = TRUE)
for (approx in c(TRUE, FALSE)) {
prob <- Problem(
Maximize(geo_mean(x, c(0, 0, 1), approx = approx)),
list(sum_entries(x) <= 3)
)
cc <- .get_cone_counts(prob, "CLARABEL")
expect_equal(cc$soc, 0, info = paste("approx =", approx, "SOC"))
expect_equal(cc$p3d, 0, info = paste("approx =", approx, "PowCone3D"))
expect_equal(cc$pnd, 0, info = paste("approx =", approx, "PowConeND"))
}
})
# ── TestPnormApprox ────────────────────────────────────────────────
## @cvxpy test_power_atom.py::TestPnormApprox::test_approx_controls_cone_type
test_that("pnorm: approx=TRUE uses SOC, approx=FALSE uses power cones (CVXPY parity)", {
skip_if_not_installed("clarabel")
x <- Variable(3)
constr <- list(sum_entries(x) >= 3, x >= 0)
## approx=TRUE -> SOC, no PowCone3D
prob <- Problem(Minimize(p_norm(x, 3, approx = TRUE)), constr)
cc <- .get_cone_counts(prob, "CLARABEL")
expect_gt(cc$soc, 0)
expect_equal(cc$p3d, 0)
## approx=FALSE -> PowCone3D, (SOC may be 0 or not but p3d > 0)
prob2 <- Problem(Minimize(p_norm(x, 3, approx = FALSE)), constr)
cc2 <- .get_cone_counts(prob2, "CLARABEL")
expect_gt(cc2$p3d, 0)
})
## @cvxpy test_power_atom.py::TestPnormApprox::test_approx_and_exact_agree
test_that("pnorm: approx and exact give same answer (CVXPY parity)", {
skip_if_not_installed("clarabel")
cases <- list(
list(p = 3, sense = "Minimize", direction = ">=", rhs = 3),
list(p = 2.5, sense = "Minimize", direction = ">=", rhs = 3),
list(p = 0.5, sense = "Maximize", direction = "<=", rhs = 3)
)
for (case in cases) {
p <- case$p
x <- Variable(3, pos = (p < 1))
if (case$direction == ">=") {
constr <- list(sum_entries(x) >= case$rhs, x >= 0, x[1] <= 2)
} else {
constr <- list(sum_entries(x) <= case$rhs, x >= 0.1)
}
sense_fn <- if (case$sense == "Minimize") Minimize else Maximize
prob <- Problem(sense_fn(p_norm(x, p, approx = TRUE)), constr)
val_a <- psolve(prob, solver = "CLARABEL")
x_a <- as.numeric(value(x))
prob2 <- Problem(sense_fn(p_norm(x, p, approx = FALSE)), constr)
val_e <- psolve(prob2, solver = "CLARABEL")
expect_true(status(prob2) %in% c("optimal", "optimal_inaccurate"),
info = paste("p =", p))
expect_equal(val_e, val_a, tolerance = 1e-3,
info = paste("p =", p, "value"))
expect_equal(as.numeric(value(x)), x_a, tolerance = 1e-3,
info = paste("p =", p, "x"))
}
})
## @cvxpy test_power_atom.py::TestPnormApprox::test_approx_warning
test_that("pnorm: warning fires for approx=TRUE with many SOCs (CVXPY parity)", {
skip_if_not_installed("clarabel")
x <- Variable(3)
constr <- list(sum_entries(x) >= 3, x >= 0)
expect_warning(
psolve(Problem(Minimize(p_norm(x, 3.3, approx = TRUE)), constr),
solver = "CLARABEL"),
regexp = "pnorm"
)
expect_no_warning(
psolve(Problem(Minimize(p_norm(x, 3.3, approx = FALSE)), constr),
solver = "CLARABEL")
)
})
# ── TestInvProdApprox ─────────────────────────────────────────────
## @cvxpy test_power_atom.py::TestInvProdApprox::test_approx_controls_cone_type
test_that("inv_prod: approx=TRUE uses SOC, approx=FALSE uses power cones (CVXPY parity)", {
skip_if_not_installed("clarabel")
x <- Variable(3, pos = TRUE)
constr <- list(sum_entries(x) >= 3)
## approx=TRUE -> SOC, no power cones
prob <- Problem(Minimize(inv_prod(x, approx = TRUE)), constr)
cc <- .get_cone_counts(prob, "CLARABEL")
expect_gt(cc$soc, 0)
expect_equal(cc$p3d, 0)
expect_equal(cc$pnd, 0)
## approx=FALSE -> power cones (3D and/or ND)
prob2 <- Problem(Minimize(inv_prod(x, approx = FALSE)), constr)
cc2 <- .get_cone_counts(prob2, "CLARABEL")
expect_equal(cc2$soc, 0)
expect_gt(cc2$p3d + cc2$pnd, 0)
})
## @cvxpy test_power_atom.py::TestInvProdApprox::test_approx_produces_correct_types
test_that("inv_prod: approx flag propagates to inner geo_mean and power atoms (CVXPY parity)", {
x <- Variable(3, pos = TRUE)
## approx=TRUE -> PowerApprox wrapping GeoMeanApprox
expr_approx <- inv_prod(x, approx = TRUE)
expect_true(S7_inherits(expr_approx, CVXR:::PowerApprox))
inner_a <- expr_approx@args[[1L]]
expect_true(S7_inherits(inner_a, CVXR:::GeoMeanApprox))
## approx=FALSE -> Power (not PowerApprox) wrapping GeoMean (not GeoMeanApprox)
expr_exact <- inv_prod(x, approx = FALSE)
expect_true(S7_inherits(expr_exact, CVXR:::Power))
expect_false(S7_inherits(expr_exact, CVXR:::PowerApprox))
inner_e <- expr_exact@args[[1L]]
expect_true(S7_inherits(inner_e, CVXR:::GeoMean))
expect_false(S7_inherits(inner_e, CVXR:::GeoMeanApprox))
})
## @cvxpy test_power_atom.py::TestInvProdApprox::test_approx_and_exact_agree
test_that("inv_prod: approx and exact give same answer (CVXPY parity)", {
skip_if_not_installed("clarabel")
x <- Variable(3, pos = TRUE)
constr <- list(sum_entries(x) >= 3, x <= 5)
prob <- Problem(Minimize(inv_prod(x, approx = TRUE)), constr)
val_a <- psolve(prob, solver = "CLARABEL")
prob2 <- Problem(Minimize(inv_prod(x, approx = FALSE)), constr)
val_e <- psolve(prob2, solver = "CLARABEL")
expect_true(status(prob2) %in% c("optimal", "optimal_inaccurate"))
expect_equal(val_e, val_a, tolerance = 1e-3)
})
# ====================================================================
# PERSPECTIVE TESTS (test_perspective.py)
# ====================================================================
## @cvxpy test_perspective.py::test_p_norms
test_that("perspective: p-norms p=1 and p=2 (CVXPY parity)", {
skip_if_not_installed("clarabel")
## p = 1
x <- Variable(3)
s <- Variable(nonneg = TRUE, name = "s")
f <- p_norm(x, 1)
obj <- perspective(f, s)
constr <- list(s == 1, x >= c(1, 2, 3))
prob <- Problem(Minimize(obj), constr)
val <- psolve(prob, solver = "CLARABEL")
## Reference: sum(x) = 1+2+3 = 6, prob.value^1 == 6
expect_equal(val, 6.0, tolerance = 1e-3)
## p = 2
x <- Variable(3)
s <- Variable(nonneg = TRUE, name = "s")
f <- p_norm(x, 2)
obj <- perspective(f, s)
constr <- list(s == 1, x >= c(1, 2, 3))
prob <- Problem(Minimize(obj), constr)
val <- psolve(prob, solver = "CLARABEL")
## Reference: ||[1,2,3]||_2 = sqrt(14) ~ 3.7417
## CVXPY: prob.value**2 == ref_prob.value = 14
expect_equal(val^2, 14.0, tolerance = 1e-3)
expect_equal(as.numeric(value(x)), c(1, 2, 3), tolerance = 1e-3)
})
## @cvxpy test_perspective.py::test_lse
test_that("perspective: log_sum_exp (CVXPY parity)", {
skip_if_not_installed("clarabel")
x <- Variable(3)
s <- Variable(nonneg = TRUE)
f <- log_sum_exp(x)
obj <- perspective(f, s)
constr <- list(1 <= s, s <= 2, c(1, 2, 3) <= x)
prob <- Problem(Minimize(obj), constr)
val <- psolve(prob, solver = "CLARABEL")
## Reference from CVXPY: 3.407606
expect_equal(val, 3.407606, tolerance = 1e-3)
expect_equal(as.numeric(value(x)), c(1, 2, 3), tolerance = 1e-3)
expect_equal(as.numeric(value(s)), 1.0, tolerance = 1e-3)
})
## @cvxpy test_perspective.py::test_lse_atom
test_that("perspective: log_sum_exp atom form (CVXPY parity)", {
skip_if_not_installed("clarabel")
## This is identical to test_lse in CVXPY (same fixture, same test).
x <- Variable(3)
s <- Variable(nonneg = TRUE)
f_exp <- log_sum_exp(x)
obj <- perspective(f_exp, s)
constr <- list(1 <= s, s <= 2, c(1, 2, 3) <= x)
prob <- Problem(Minimize(obj), constr)
val <- psolve(prob, solver = "CLARABEL")
expect_equal(val, 3.407606, tolerance = 1e-3)
expect_equal(as.numeric(value(x)), c(1, 2, 3), tolerance = 1e-3)
expect_equal(as.numeric(value(s)), 1.0, tolerance = 1e-3)
})
## @cvxpy test_perspective.py::test_quad_persp_persp
test_that("perspective: perspective of perspective (CVXPY parity)", {
skip_if_not_installed("clarabel")
## Reference: quad_over_lin(x, s) + r*x - 4*s with x >= 2, s <= 0.5
## r = 2 => ref_val = 10, ref_x = 2, ref_s = 0.5
r <- 2
x <- Variable()
s <- Variable(nonneg = TRUE)
t_var <- Variable(nonneg = TRUE)
f_exp <- square(x) + r * x - 4
obj_inner <- perspective(f_exp, s)
obj <- perspective(obj_inner, t_var)
## f(x) -> s*f(x/s) -> t*(s/t)*f((x/t)/(s/t)) = s*f(x/s)
constr <- list(0.1 <= s, s <= 0.5, x >= 2, 0.1 <= t_var, t_var <= 0.5)
prob <- Problem(Minimize(obj), constr)
val <- psolve(prob, solver = "CLARABEL")
expect_equal(val, 10.0, tolerance = 1e-3)
expect_equal(as.numeric(value(x)), 2.0, tolerance = 1e-3)
expect_equal(as.numeric(value(s)), 0.5, tolerance = 1e-3)
})
## @cvxpy test_perspective.py::test_quad_quad
test_that("perspective: power(x, 4) perspective == quad_over_lin(quad_over_lin(x, s), s) (CVXPY parity)", {
skip_if_not_installed("clarabel")
## Reference: quad_over_lin(quad_over_lin(x, s), s) with x >= 5, s <= 3
ref_x <- Variable()
ref_s <- Variable(nonneg = TRUE)
obj <- quad_over_lin(quad_over_lin(ref_x, ref_s), ref_s)
constr <- list(ref_x >= 5, ref_s <= 3)
ref_prob <- Problem(Minimize(obj), constr)
ref_val <- psolve(ref_prob, solver = "CLARABEL")
## Perspective problem: f(x) = x^4, perspective = x^4 / s^3
x <- Variable()
s <- Variable(nonneg = TRUE)
f <- power(x, 4)
obj <- perspective(f, s)
constr <- list(x >= 5, s <= 3)
prob <- Problem(Minimize(obj), constr)
val <- psolve(prob, solver = "CLARABEL")
expect_equal(val, ref_val, tolerance = 1e-3)
expect_equal(as.numeric(value(x)), 5.0, tolerance = 1e-3)
expect_equal(as.numeric(value(s)), 3.0, tolerance = 1e-3)
})
## @cvxpy test_perspective.py::test_psd_tr_persp
test_that("perspective: trace(P) with PSD variable (CVXPY parity)", {
skip_if_not_installed("scs")
## Reference: minimize trace(P) s.t. P == I_2.
ref_P <- Variable(c(2, 2), PSD = TRUE)
ref_prob <- Problem(Minimize(matrix_trace(ref_P)),
list(ref_P == diag(2)))
ref_val <- psolve(ref_prob, solver = "SCS")
## Perspective: trace is homogenous of degree 1 so its perspective is itself.
P <- Variable(c(2, 2), PSD = TRUE)
s <- Variable(nonneg = TRUE)
prob <- Problem(Minimize(perspective(matrix_trace(P), s)),
list(P == diag(2), s == 1))
val <- psolve(prob, solver = "SCS")
expect_equal(val, ref_val, tolerance = 1e-3)
})
## @cvxpy test_perspective.py::test_psd_mf_persp
test_that("perspective: matrix_frac with PSD variable (CVXPY parity)", {
skip_if_not_installed("scs")
## matrix_frac is homogenous of degree 1, so its perspective is itself.
## n=3 picked as a representative non-trivial size from CVXPY's [2, 3, 11].
n <- 3L
ref_x <- Variable(n)
ref_P <- Variable(c(n, n), PSD = TRUE)
ref_prob <- Problem(Minimize(matrix_frac(ref_x, ref_P)),
list(ref_x == 5, ref_P == diag(n)))
ref_val <- psolve(ref_prob, solver = "SCS")
x <- Variable(n)
P <- Variable(c(n, n), PSD = TRUE)
s <- Variable(nonneg = TRUE)
prob <- Problem(Minimize(perspective(matrix_frac(x, P), s)),
list(x == 5, P == diag(n), s == 1))
val <- psolve(prob, solver = "SCS")
expect_equal(val, ref_val, tolerance = 1e-2)
})
## @cvxpy test_perspective.py::test_psd_tr_square
test_that("perspective: square(trace(P)) with PSD variable (CVXPY parity)", {
skip_if_not_installed("scs")
## Tr(X)^2 perspective is quad_over_lin(Tr(X), s).
n <- 3L
ref_s <- Variable(nonneg = TRUE)
ref_P <- Variable(c(n, n), PSD = TRUE)
ref_prob <- Problem(Minimize(quad_over_lin(matrix_trace(ref_P), ref_s)),
list(ref_s <= 5, ref_P %>>% diag(n)))
ref_val <- psolve(ref_prob, solver = "SCS")
P <- Variable(c(n, n), PSD = TRUE)
s <- Variable(nonneg = TRUE)
f <- perspective(square(matrix_trace(P)), s)
prob <- Problem(Minimize(perspective(f, s)),
list(s <= 5, P %>>% diag(n)))
val <- psolve(prob, solver = "SCS")
expect_equal(val, ref_val, tolerance = 1e-3)
})
## @cvxpy test_perspective.py::test_diag
test_that("perspective: trace with diag variable (CVXPY parity)", {
skip_if_not_installed("clarabel")
## Reference: minimize trace(X) s.t. diag(X) >= [1, 2], X diagonal.
X_ref <- Variable(c(2, 2), diag = TRUE)
ref_prob <- Problem(Minimize(matrix_trace(X_ref)),
list(diag(X_ref) >= c(1, 2)))
ref_val <- psolve(ref_prob, solver = "CLARABEL")
## trace is homogenous degree 1, perspective is itself.
X <- Variable(c(2, 2), diag = TRUE)
s <- Variable(nonneg = TRUE)
prob <- Problem(Minimize(perspective(matrix_trace(X), s)),
list(diag(X) >= c(1, 2), s == 1))
val <- psolve(prob, solver = "CLARABEL")
expect_equal(val, ref_val, tolerance = 1e-3)
})
## @cvxpy test_perspective.py::test_dpp
test_that("perspective: is_dpp returns FALSE for parameterized f (CVXPY parity)", {
## CVXPY: perspective(square(a+x), s).is_dpp() == False
x <- Variable()
s <- Variable(nonneg = TRUE)
a <- Parameter()
obj1 <- perspective(square(a + x), s)
expect_false(is_dpp(obj1))
## CVXPY: perspective(log(a+x), s).is_dpp() == False
obj2 <- perspective(log(a + x), s)
expect_false(is_dpp(obj2))
})
## @cvxpy test_perspective.py::test_s_eq_0
test_that("perspective: s=0 with recession function (CVXPY parity)", {
skip_if_not_installed("clarabel")
## Problem where the optimal s is s = 0.
## CVXPY: f = x + 1, f_recession = x, constr = [-x^2 + 1 >= 0]
x <- Variable(1)
s <- Variable(1, nonneg = TRUE)
f <- x + 1
f_recession <- x
obj <- perspective(f, s, f_recession = f_recession)
constr <- list(-square(x) + 1 >= 0)
prob <- Problem(Minimize(obj), constr)
psolve(prob, solver = "CLARABEL")
expect_equal(as.numeric(value(x)), -1, tolerance = 1e-3)
expect_equal(as.numeric(value(s)), 0, tolerance = 1e-3)
})
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.