What’s New in CVXR

Anqi Fu, Balasubramanian Narasimhan, and Stephen Boyd

2026-06-08

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.1

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.

Disciplined Nonlinear Programming (DNLP)

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 path

See the DNLP Tutorial for worked examples.

Derivatives and sensitivity analysis

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 x

Reverse 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.

Bounds propagation and richer variable bounds

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 atoms

Variable 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.

New atoms and DPP refinements

Solvers

Bug fixes (also affected 1.8.x)

Performance

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

Complete Rewrite Using S7

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.

New Features

New solve interface

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 status

The old solve() still works but returns a backward-compatible list:

result <- solve(prob)
result$value       # optimal value
result$getValue(x) # variable value (deprecated)
result$status      # problem status

Breaking Changes from CVXR 1.x

API changes

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)

Axis parameter changes

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

PSD constraints use PSD(A - B) instead of A %>>% B (though %>>% and %<<% operators are still available for backward compatibility).

Solver changes

  • Removed: CBC
  • Added: HiGHS (LP, QP, MILP), Gurobi (LP, QP, SOCP, MIP), CVXOPT (LP, SOCP), PIQP (QP), SCIP, and XPRESS
  • Default solver: CLARABEL (replaces ECOS)

Supported solvers

Solver R Package Type Problem Classes
CLARABEL clarabel Conic LP, QP, SOCP, SDP, ExpCone, PowCone
SCS scs Conic LP, QP, SOCP, SDP, ExpCone, PowCone
MOSEK Rmosek Conic LP, QP, SOCP, SDP, ExpCone, PowCone
ECOS ECOSolveR Conic LP, SOCP, ExpCone
ECOS_BB ECOSolveR Conic LP, SOCP, ExpCone + MI
GUROBI gurobi Conic/QP LP, QP, SOCP, MI
GLPK Rglpk Conic LP
GLPK_MI Rglpk Conic LP, MILP
HIGHS highs Conic/QP LP, QP, MILP
CVXOPT cccp Conic LP, SOCP
OSQP osqp QP LP, QP
CPLEX Rcplex Conic/QP LP, QP, SOCP, MI
PIQP piqp QP LP, QP
SCIP scip Conic LP, MILP, SOCP, MI-SOCP
XPRESS xpress Conic/QP LP, QP, SOCP, MI

Smooth nonlinear programs additionally use the IPOPT and UNO NLP solvers (see CVXR 1.9.1).

New Atoms and Functions

Convenience atoms

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

Math function dispatch

Standard R math functions work directly on CVXR expressions:

x <- Variable(3)
abs(x)        # elementwise absolute value
sqrt(x)       # elementwise square root
sum(x)        # sum of entries
max(x)        # maximum entry
norm(x, "2")  # Euclidean norm

Boolean logic atoms

For mixed-integer programming: Not(), And(), Or(), Xor(), implies(), iff().

Other new atoms

  • perspective(f, s) for perspective functions
  • FiniteSet(expr, values) constraint for discrete optimization
  • ceil_expr(), floor_expr() for DQCP problems
  • condition_number(), gen_lambda_max(), dist_ratio() for DQCP

Backward-Compatibility Aliases

Migration Guide

To migrate code from CVXR 1.x to 1.8.x:

  1. Replace result <- solve(problem) with opt_val <- psolve(problem)

  2. Replace result$getValue(x) with value(x)

  3. Replace result$value with the return value from psolve()

  4. Replace result$status with status(problem)

  5. Replace result$getDualValue(con) with dual_value(con)

  6. Update solver names: "ECOS""CLARABEL", "GLPK""HIGHS"

  7. Update axis arguments: axis = NAaxis = NULL (row/column axis values 1 and 2 are unchanged)

  8. Replace A %>>% B with PSD(A - B) if desired

  9. 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.

  10. 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.

CRAN Submission Tip

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.

## Content of <your_pkg>/R/zzz.R

.onLoad <- function(libname, pkgname) {
  CVXR::exclude_solvers("MOSEK")
}
.onUnload <- function(libname, pkgname) {
  CVXR::include_solvers("MOSEK")
}

Further Reading