This vignette highlights notable user-facing changes in each release
of the S7 rewrite of CVXR, newest first. For the complete,
fine-grained list see the package NEWS file
(news(package = "CVXR")). For the introductory tutorial,
see vignette("cvxr_intro"); for worked examples, visit the
CVXR website.
CVXR 1.9.3 tracks CVXPY 1.9.3. Like 1.9.2, it is chiefly
a correctness release: several defects that returned
wrong answers with no error or warning are fixed. Matrix norms now mean
what they mean in base R, and the solver is chosen the way CVXPY 1.9
chooses it.
These fixes change results. The new results are the answers to the problem you wrote.
quad_form(w, Sigma) in a portfolio problem,
with every cross term halved. This was present in 1.9.2. Gurobi’s
second-order-cone duals also had the wrong sign.perspective() of a function of a
symmetric, PSD or NSD matrix
variable returned a wrong optimum.gmatmul() with parameters on both sides gave a wrong
answer under gp = TRUE.psolve(solver = "CLARABEL", warm_start = TRUE) returned
the previous optimum when the cones had changed.PSD or NSD
lost its cone.sum_entries(x, axis = 2) of a one-row expression gave
wrong answers or crashed R. Present in 1.9.2.%*% with a scalar operand that does not conform, as in
2 %*% x for a 2 x 3 x, or with a 1 x 1 operand
that does not conform, built a product that gave a scalar product or
crashed R. Both are now errors, as in base R; use * to
multiply by a scalar. A scalar operand that conforms, as in
x %*% 2 for a 3 x 1 x, is a product, as in
base R.sum(), max(), min() and
prod() of an expression took a named axis as
one more operand, so sum(x, axis = 2) was
sum(x + 2). They now honor axis and
keepdims.sum(), prod(), max() and
min() with more than one operand now mean what they mean in
base R: sum(x, 2) is sum(x) + 2 and
max(x, y) is one scalar, the largest entry of either. They
were sum(x + 2), prod(x * y) and the
elementwise maximum and minimum; use max_elemwise() and
min_elemwise() for those.max_elemwise() and min_elemwise() take
na.rm, and hstack() and vstack()
take deparse.level, as pmax() and
rbind() do, with no effect.
max_elemwise(u, v, na.rm = TRUE) used to take
TRUE as a third operand. Named operands, as from
do.call(vstack, split(...)), are accepted as before.x / A with a matrix or vector A divided
every entry of x by A[1, 1]. It is now
entrywise, as in CVXPY. Present in 1.9.2.x / d with shapes that do not broadcast, such as a 2 x
3 x and a length-3 d, recycled the divisor
silently. It is now an error, as in CVXPY; a row or column divisor still
broadcasts.value() of a sum, a product or an index of a
sparse-matrix constant failed with “non-conformable arrays” or
“subscript out of bounds”, and a sum of two sparse constants came back
flattened to one column. These now evaluate; a mixed sparse and dense
result is a base matrix, a sparse one stays sparse.dgCMatrix were
misread: a unit Diagonal(n) was all zeros, so
quad_form(x, Diagonal(n)) was unbounded. Present in
1.9.2.bounds matrix bound only its stored entries.
As in CVXPY, it is now accepted only on a sparsity variable
with the same pattern. Present in 1.9.2.conv(), cumsum() and kron()
were treated as log-log affine, so a geometric program containing them
was accepted and could solve to a wrong value. Such a problem is now
rejected, as in CVXPY.problem_data() on a geometric program between
two solves made the next solve after a parameter change return the
previous optimum. problem_data() now goes through the same
compile cache as psolve().max_elemwise() and min_elemwise() of a
column and a row recycled instead of broadcasting, so
psolve() returned a wrong objective.log(x, base) ignored
base, outer(x, y, FUN) ignored
FUN and diag(x, nrow) ignored
nrow. base is now honored; a FUN
other than "*", or a nrow, is an error.x[NULL, ] of an expression returned all of
x, and x[c(TRUE, NA, FALSE), ] dropped the
NA. Both are now errors.cumsum(X) of a matrix expression is now column-wise, as
in CVXPY and as cumprod() and cummax() already
were.With no axis, cvxr_norm(X, p) and
norm(X, type) on a matrix now agree with base R’s
norm() and with CVXPY:
p = 1 is the maximum absolute column sum.p = 2 is the largest singular value.p = Inf is the maximum absolute row sum.They used to be entrywise. A row vector is a 1 x n matrix, as in base
R. Any other p on a matrix is now an error. For the
entrywise norms use cvxr_norm(X, "fro"),
norm1(X) or p_norm(X, p).
cvxr_norm(X, "nuc") now gives the nuclear norm. With no
type, norm(X) of an expression is the spectral
norm, as in CVXPY; on an ordinary R matrix it is base R’s one-norm.
CVXR masks diag(), norm(),
outer(), sd(), var() and
convolve() so that they also work on expressions. On
anything that is not an expression, each is now exactly the base R or
stats function, called as written. On an expression each follows CVXPY,
and an argument meant only for ordinary data, such as
na.rm, is an error.
power() is different: it is CVXPY’s
power(x, p) for any argument, as before, and the new
cvxr_power() is the same function. It masks the unrelated
stats::power(), a glm() link, so with
CVXR attached write stats::power() in a
glm() family.
The solver is now chosen as in CVXPY 1.9: first from the problem’s structure, and then the solver’s QP or conic interface follows. As a result, a problem may be solved by a different installed solver than in 1.9.2:
When no installed solver fits, the error reads “No installed solver could handle this problem.”
highs_options list. An option HiGHS rejects is now an error
rather than being dropped.warm_start = TRUE seeds the
start point from the variables’ values.dual_value().solver_stats(prob)@extra_stats now carries the raw
solver result for OSQP, ECOS, Gurobi, HiGHS and PIQP as well.infeasible; it was a solver error.set.seed() followed by a solve and then random draws gives
the same draws as without the solve. Present in every earlier
version.geo_mean() accepts arguments of any shape and gains
axis and keepdims. cvxr_var()
gains them too.geo_mean(x, approx = FALSE) and
PowConeND constraints now solve on SCS and MOSEK as well as
Clarabel.curvature() reports log-log and quasi curvature instead
of "UNKNOWN". The new curvatures() returns
every curvature that applies, and log_log_curvature() and
is_diag() are new as well.symmetric, PSD, diag, …) now
solves on QP solvers.psolve(nlp = TRUE) no longer makes dense copies of
sparse constant matrices.CVXR 1.9.2 tracks CVXPY 1.9.2. It is primarily a
correctness release: several defects that returned
plausible-looking wrong answers with no error or warning are fixed,
solver options are easier to pass and harder to lose silently, and
solving is faster across the board.
A constant weight multiplying sum(square(x)) was
discarded when the quadratic objective was assembled, so penalized
least-squares problems were solved with the wrong penalty — ridge
regression being the common case, where every value of the penalty
parameter returned the same fit. The defect had been present since
1.8.2; sum_squares(), quad_form(), and
p_norm(x, 2)^2 were never affected. If you used a weighted
sum(square(x)) objective, results can legitimately change
with this release — they are now the answers to the problem you
wrote.
dual_value() returns constraint-shaped dualsdual_value() now returns a matrix shaped like its
constraint rather than a flat vector, making the documented return type
accurate. Linear indexing (d[3]) is unchanged, since R
matrices index linearly in column-major order, but code calling
length() or dim() on a dual may need
adjusting. Scalar constraints, SOC, and
ExpCone duals are deliberately unchanged, matching
CVXPY.
Passing solver options is now more uniform, and options can no longer vanish silently:
feastol,
reltol, abstol, and num_iter now
also map onto MOSEK and HiGHS.cvxr_unmapped_solver_param) instead of being silently
dropped.mosek_params
and the eps keyword, which fans out to MOSEK’s tolerance
parameters exactly as in CVXPY; all options are routed to the correct
Rmosek parameter groups.eps, which sets both
eps_abs and eps_rel.matrix_frac() with an ill-conditioned second argument
now returns a value (as CVXPY does) and warns, where it previously
raised R’s “computationally singular” error.DPPError,
DGPError, DQCPError, DNLPError,
and ParameterError — are now catchable by class with
tryCatch(), as in CVXPY; previously they could only be
matched by message text.A partial boolean/integer index
list (for example Variable(2, boolean = c(1))) now
correctly makes the problem mixed-integer. Such problems previously
appeared to solve with a continuous solver; they now require a
mixed-integer-capable solver and say so if none is installed.
1.9.2 is roughly a third faster than 1.9.1 overall, with substantially lower memory use on larger problems, and DQCP problems solve dramatically faster: the bisection now compiles its subproblem once and re-solves it per iterate instead of rebuilding it from scratch.
CVXR 1.9.1 is the first CRAN release since 1.8.2 and is
a large one: it folds in the internal 1.8.2-1 and 1.9.0 development
cycles. Its headline additions are disciplined nonlinear programming, a
derivative / sensitivity-analysis API, and interval-bounds propagation
with native solver-bound support.
CVXR 1.9.1 extends modeling beyond convex optimization
to smooth nonlinear programs, which need not be convex.
You build the problem from differentiable atoms, check it with
is_dnlp(), and solve it with
psolve(prob, nlp = TRUE). Every DCP problem is also a DNLP,
and the disciplined nonlinear grammar additionally allows smooth atoms
in forms DCP forbids (for example, a product of two variable-dependent
expressions).
x <- Variable(2)
prob <- Problem(Minimize(sum_squares(x - c(1, 2))))
is_dnlp(prob) # TRUE
psolve(prob, nlp = TRUE) # solved through the NLP pathsin(), cos(), tan(),
sinh(), tanh(), asinh(),
atanh(), normcdf(), and
prod().best_of = n solves from n random initial
points (drawn from variable bounds or sample_bounds()) and
keeps the best.sparsediff package and a nonlinear solver. Two are
supported, both in Enhances (so guard their use with
requireNamespace()): UNO (the
Uno package — headed for CRAN, self-contained, and the
practical default for R users) and IPOPT (the
ipopt package — not on CRAN owing to licensing, so rarely
installed). With nlp = TRUE, IPOPT is preferred when
present (matching CVXPY), otherwise UNO is used. The UNO
path also recovers constraint duals via dual_value(); the
IPOPT path returns none, matching CVXPY.See the DNLP Tutorial for worked examples.
CVXR 1.9.1 adds the ability to differentiate the
solution map of a disciplined problem — to see how the optimal
solution responds to small changes in the parameters (sensitivity
analysis) and to compute gradients of scalar functions of the solution.
Request derivatives at solve time with
requires_grad = TRUE.
Forward mode (perturb parameters, see the change in the solution):
psolve(problem, requires_grad = TRUE)
delta(a) <- da # perturbation of parameter a
derivative(problem) # propagate forward
delta(x) # resulting change in variable xReverse mode (gradient of the solution with respect to parameters):
psolve(problem, requires_grad = TRUE)
backward(problem) # propagate backward
gradient(a) # d(solution) / d(a)The chain rule is wired through the Dgp2Dcp (log/exp)
and Complex2Real reductions, so geometric and complex
problems differentiate too. The derivative API is backed by the optional
diffcp R package. See the Derivatives
examples and Sensitivity
Analysis.
get_bounds() now works on any
expression, not just variables, propagating interval bounds
through affine, elementwise, and piecewise-linear atoms:
x <- Variable(3, bounds = list(-1, 2))
get_bounds(A %*% x + b) # bounds propagated through the affine map
get_bounds(abs(x)) # and through atomsVariable bounds may also be sparse Matrix
objects or symbolic bounds involving
Parameters; symbolic bounds are enforced at solve time and
update on DPP re-solves. Positive (DGP) variables accept numeric
and parametric bounds under gp = TRUE.
convolve() — the (numpy-style) name
for the 1-D discrete-convolution conv() atom; falls through
to stats::convolve() on numeric input.is_dpp() gains a context
argument ("dcp" or "dgp"), matching
CVXPY’s is_dpp(context = ...).problem_data() / get_problem_data() now
take an explicit gp argument; previously
gp = TRUE passed through ... was silently
ignored, compiling a geometric program as a DCP problem.psolve() now takes explicit enforce_dpp
and ignore_dpp arguments, matching CVXPY’s
solve(); previously they were silently swallowed by
....Canonicalization and solving are now faster than the 1.8.2 CRAN release (roughly 5–13% lower wall-clock on solve-dominated problems such as many small constraints, SOCPs, and Kalman smoothing), with deterministic memory allocation unchanged.
CVXR 1.8.x is a ground-up rewrite using R’s S7 object system, designed to be isomorphic with CVXPY 1.8.2 for long-term maintainability. It is approximately 4–5x faster than the previous S4-based release. This section summarizes the key changes from CVXR 1.x that may affect users.
boolean = TRUE or
integer = TRUE in Variable()).Parameter()
class and EvalParams reduction.psolve(prob, gp = TRUE).psolve(prob, qcp = TRUE).Variable(n, complex = TRUE).as_cvxr_expr(). Matrix package objects
(dgCMatrix, dgeMatrix, dsCMatrix,
ddiMatrix, sparseVector) use S4 dispatch which
preempts S7/S3, so they cannot be used directly with CVXR operators.
Wrapping with as_cvxr_expr() converts them to CVXR
Constant objects while preserving sparsity (unlike
as.matrix() which densifies). Base R matrix
and numeric objects work natively without wrapping.The primary solve function is now psolve(), which
returns the optimal value directly:
library(CVXR)
x <- Variable(2)
prob <- Problem(Minimize(sum_squares(x)), list(x >= 1))
opt_val <- psolve(prob) # returns optimal value directly
x_val <- value(x) # extract variable value
prob_status <- status(prob) # check statusThe old solve() still works but returns a
backward-compatible list:
| Old API | New API |
|---|---|
solve(problem) |
psolve(problem) |
result$getValue(x) |
value(x) |
result$value |
return value of psolve() |
result$status |
status(problem) |
result$getDualValue(con) |
dual_value(con) |
problem_status(prob) |
status(prob) |
problem_solution(prob) |
solution(prob) |
get_problem_data(prob, solver) |
problem_data(prob, solver) |
The axis parameter now uses R’s apply()
convention (1-based indexing):
| Old CVXR | New CVXR | Meaning |
|---|---|---|
axis = 1 |
axis = 1 |
Row-wise reduction (unchanged) |
axis = 2 |
axis = 2 |
Column-wise reduction (unchanged) |
axis = NA |
axis = NULL |
All entries |
Passing axis = 0 now produces an informative error with
migration guidance.
PSD constraints use PSD(A - B) instead of
A %>>% B (though %>>% and
%<<% operators are still available for backward
compatibility).
The current table of solvers and the problem classes each one handles is kept on the website: Solver Capabilities.
Smooth nonlinear programs additionally use the IPOPT and
UNO NLP solvers (see CVXR
1.9.1).
| Function | Description |
|---|---|
ptp(x) |
Peak-to-peak (range): max(x) - min(x) |
cvxr_mean(x) |
Arithmetic mean along an axis |
cvxr_std(x) |
Standard deviation |
cvxr_var(x) |
Variance |
vdot(x, y) |
Vector dot product (inner product) |
cvxr_outer(x, y) |
Outer product of two vectors |
inv_prod(x) |
Reciprocal of product of entries |
loggamma(x) |
Elementwise log of gamma function |
log_normcdf(x) |
Elementwise log of standard normal CDF |
cummax_expr(x) |
Cumulative maximum along an axis |
dotsort(X, W) |
Weighted sorted dot product |
Standard R math functions work directly on CVXR expressions:
For mixed-integer programming: Not(),
And(), Or(), Xor(),
implies(), iff().
perspective(f, s) for perspective functionsFiniteSet(expr, values) constraint for discrete
optimizationceil_expr(), floor_expr() for DQCP
problemscondition_number(), gen_lambda_max(),
dist_ratio() for DQCPtv() is deprecated; use
total_variation() (still works but warns once)norm2(x) is deprecated; use
p_norm(x, 2) (still works but warns once)multiply(x, y) is deprecated; use
x * y for elementwise multiplicationsolve() still works and returns a compatibility
listproblem_status,
getValue, etc.) still work but emit once-per-session
deprecation warningsTo migrate code from CVXR 1.x to 1.8.x:
Replace result <- solve(problem) with
opt_val <- psolve(problem)
Replace result$getValue(x) with
value(x)
Replace result$value with the return value from
psolve()
Replace result$status with
status(problem)
Replace result$getDualValue(con) with
dual_value(con)
Update solver names: "ECOS" →
"CLARABEL", "GLPK" →
"HIGHS"
Update axis arguments: axis = NA →
axis = NULL (row/column axis values 1 and 2 are
unchanged)
Replace A %>>% B with PSD(A - B)
if desired
Wrap Matrix package objects with as_cvxr_expr()
before using them in CVXR expressions (e.g.,
as_cvxr_expr(A) %*% x instead of A %*% x when
A is a dgCMatrix or other Matrix class). This
preserves sparsity. Base R matrices need no wrapping.
Dimension-preserving operations. CVXR 1.8
preserves 2D shapes throughout, matching CVXPY. In particular, axis
reductions like sum_entries(X, axis = 2) now return a
proper row vector of shape (1, n) rather than collapsing to
a 1D vector. When comparing such a result with an R numeric vector
(which CVXR treats as a column), you may need to use t() or
matrix(..., nrow = 1) to match shapes:
## Old (worked in CVXR 1.x because axis reductions were 1D):
sum_entries(X, axis = 2) == target_vec
## New (wrap target as row vector to match the (1, n) shape):
sum_entries(X, axis = 2) == t(target_vec)Similarly, if you extract a scalar from a CVXR result and need a
plain numeric value, use as.numeric() to drop the matrix
dimensions.
If you encounter issues involving the Rmosek package
while submitting your package to CRAN, include the following code in
<your_pkg>/R/zzz.R to resolve the issue.