Skip to contents

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 — correctness fixes, matrix norms as in base R, CVXPY 1.9 solver selection
  • CVXR 1.9.2 — correctness fixes, solver-options overhaul, faster solves
  • CVXR 1.9.1 — disciplined nonlinear programming, derivatives, bounds propagation, new atoms
  • CVXR 1.8.x — the ground-up S7 rewrite

CVXR 1.9.3

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.

Correctness

These fixes change results. The new results are the answers to the problem you wrote.

  • Gurobi solved quadratic objectives with cross terms, such as 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.
  • CVXOPT reported feasible problems with only equality constraints as infeasible, and unbounded problems as optimal.
  • perspective() of a function of a symmetric, PSD or NSD matrix variable returned a wrong optimum.
  • A DGP problem with parameters could be reported DPP and solved with wrong solver data.
  • 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.
  • A complex variable declared 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.
  • Constants from Matrix classes other than dgCMatrix were misread: a unit Diagonal(n) was all zeros, so quad_form(x, Diagonal(n)) was unbounded. Present in 1.9.2.
  • A sparse 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.
  • A CLARABEL or OSQP warm re-solve after a parameter became zero, or nonzero, returned the previous optimum as optimal. 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.
  • Calling 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.
  • On an expression, 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.

Matrix norms follow base R

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.

X <- matrix(c(1, -2, 3, 4), 2, 2)
value(cvxr_norm(Constant(X), 1))   # 7, as norm(X, "1")
value(norm1(Constant(X)))          # 10, the entrywise 1-norm

Base R functions keep their meaning

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.

Solver selection

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:

  • Commercial solvers are preferred for QPs and mixed-integer QPs too.
  • SCIP solves mixed-integer QPs.
  • A named HiGHS, Gurobi, CPLEX or Xpress solves an LP through its conic interface.
  • Falling back to a non-default solver warns.
  • ECOS_BB is never chosen unless you name it.

When no installed solver fits, the error reads “No installed solver could handle this problem.”

Solvers

  • MOSEK takes its status from the solution status, as CVXPY does, so an iteration or time limit is no longer an error by itself. The simplex optimizers work.
  • HiGHS accepts options nested in a highs_options list. An option HiGHS rejects is now an error rather than being dropped.
  • Gurobi warm_start = TRUE seeds the start point from the variables’ values.
  • CPLEX returns dual values for continuous problems with second-order cones; they were all missing.
  • SCS exposes the infeasibility certificate of an infeasible solve through dual_value().
  • solver_stats(prob)@extra_stats now carries the raw solver result for OSQP, ECOS, Gurobi, HiGHS and PIQP as well.
  • GLPK_MI reports an infeasible mixed-integer problem as infeasible; it was a solver error.
  • Solving no longer advances R’s random number stream, so set.seed() followed by a solve and then random draws gives the same draws as without the solve. Present in every earlier version.

New features

  • geo_mean() accepts arguments of any shape and gains axis and keepdims. cvxr_var() gains them too.
  • Exact 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.
  • A quadratic objective over a structured variable (symmetric, PSD, diag, …) now solves on QP solvers.
x <- Variable(pos = TRUE)
y <- Variable(pos = TRUE)
curvature(x * y)                # "LOG-LOG AFFINE"
curvature(ceiling(Variable()))  # "QUASILINEAR"

Performance

  • The first solve of a parametric (DPP) problem compiles about twice as fast and uses far less memory.
  • A DPP problem with a parametric quadratic objective keeps its compiled form across solves.
  • psolve(nlp = TRUE) no longer makes dense copies of sparse constant matrices.

CVXR 1.9.2

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.

Correctness: weighted quadratic objectives

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 duals

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

Solver options

Passing solver options is now more uniform, and options can no longer vanish silently:

  • The standard tolerance names feastol, reltol, abstol, and num_iter now also map onto MOSEK and HiGHS.
  • A standard option that a solver cannot express now produces a warning (condition class cvxr_unmapped_solver_param) instead of being silently dropped.
  • MOSEK accepts CVXPY-style 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.
  • SCS accepts eps, which sets both eps_abs and eps_rel.
psolve(prob, solver = "MOSEK", eps = 1e-8)
psolve(prob, solver = "SCS", eps = 1e-6)

New warnings and catchable errors

  • A parameterized problem that is not DPP now warns that repeated solves will not be faster, matching CVXPY; the answers are unchanged.
  • 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.
  • Five error conditions — 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.

Mixed-integer declarations

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.

Performance

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

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
  • New smooth atoms usable anywhere in a DNLP: sin(), cos(), tan(), sinh(), tanh(), asinh(), atanh(), normcdf(), and prod().
  • Nonconvex problems may have several local optima. best_of = n solves from n random initial points (drawn from variable bounds or sample_bounds()) and keeps the best.
  • The NLP path is powered by automatic derivatives from the optional 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.

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

  • 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 = ...).

Solvers

  • CPLEX now solves LP / SOCP / MI-LP / MI-SOCP through the conic path.
  • Native variable bounds: HiGHS, Gurobi, CPLEX, XPRESS, PIQP, and SCIP now consume dense numeric variable bounds directly (including parametric bounds for HiGHS), avoiding extra bound constraints and speeding up DPP re-solves.

Bug fixes (also affected 1.8.x)

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

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

  • S7 class system replaces S4 for all expression, constraint, and problem classes. Significantly faster construction and method dispatch.
  • 15 solvers: CLARABEL (default), SCS, OSQP, HiGHS, MOSEK, Gurobi, GLPK, GLPK_MI, ECOS, ECOS_BB, CPLEX, CVXOPT, PIQP, SCIP, and XPRESS.
  • Mixed-integer programming via GLPK_MI, ECOS_BB, Gurobi, CPLEX, HiGHS, SCIP, or XPRESS (boolean = TRUE or integer = TRUE in Variable()).
  • Parameter support via Parameter() class and EvalParams reduction.
  • 50+ atom classes covering LP, QP, SOCP, SDP, exponential cone, and power cone problems.
  • DPP (Disciplined Parameterized Programming) for efficient parameter re-solve with compilation caching.
  • DGP (Disciplined Geometric Programming) via psolve(prob, gp = TRUE).
  • DQCP (Disciplined Quasiconvex Programming) via psolve(prob, qcp = TRUE).
  • Complex variable support via Variable(n, complex = TRUE).
  • Warm-start support for several solvers (OSQP, SCS, Gurobi, MOSEK, CLARABEL, HiGHS).
  • Matrix package interoperability via 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.

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

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

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

Backward-Compatibility Aliases

  • tv() 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 multiplication
  • Old solve() still works and returns a compatibility list
  • Old function names (problem_status, getValue, etc.) still work but emit once-per-session deprecation warnings

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 = NA → axis = 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