Development Plans for CVXR

Catching Up with CVXPY — A Progress Report

Balasubramanian Narasimhan

Stanford University

Anqi Fu

2026-02-19

Actually…


As you probably guessed, that was a joke.

Really, this is hard work.

Let me give you a proper progress report.

The Mission

  • CVXR: The R port of CVXPY
  • Developer community: 2 (yes, two)
  • Released version: 1.0-15 (S4 classes)
  • Has fallen behind CVXPY in features

But:

  • New AI development tools
  • New facilities in R (S7 classes)
  • Offer hope of catching up

What is R?

For the Python developers in the room:

  • Created 1993 (descendant of S, 1976)
  • #1 language in statistics and biostatistics
  • 20,000+ packages on CRAN
  • Bioconductor, tidyverse, Shiny
  • Vectorized, functional, multiple dispatch

“Everything that exists is an object. Everything that happens is a function call.”

— John Chambers

R’s Object Systems

R has four major OOP systems. Yes, four.

S3 — Duck typing

# Just slap on a class attribute
person <- list(name = "Alice", age = 30)
class(person) <- "person"

# Methods: generic.class
print.person <- function(x, ...)
  cat("Person:", x$name, "\n")

S4 — Formal & strict

setClass("Person",
  slots = list(name = "character",
               age  = "numeric"))
setGeneric("greet",
  function(x) standardGeneric("greet"))
setMethod("greet", "Person",
  function(x) cat("Hi,", x@name, "\n"))

R6 — Reference semantics

Person <- R6::R6Class("Person",
  public = list(
    name = NULL,
    initialize = function(name)
      self$name <- name,
    greet = function()
      cat("Hi,", self$name, "\n")
  ))

S7 — The new hope (2024)

Person <- new_class("Person",
  properties = list(
    name = class_character,
    age  = class_numeric
  ))
method(greet, Person) <- function(x)
  cat("Hi,", x@name, "\n")

S4: The Verbose Way

The Abs atom in old CVXR — one of 100+ atoms to implement:

.Abs <- setClass("Abs", representation(x = "Expression"),
                 contains = "Elementwise")

Abs <- function(x) { .Abs(x = x) }

setMethod("initialize", "Abs", function(.Object, ..., x) {
  .Object@x <- x
  callNextMethod(.Object, ..., atom_args = list(.Object@x))
})
setMethod("to_numeric",      "Abs", function(object, values) abs(values[[1]]))
setMethod("sign_from_args",  "Abs", function(object) c(TRUE, FALSE))
setMethod("is_atom_convex",  "Abs", function(object) TRUE)
setMethod("is_atom_concave", "Abs", function(object) FALSE)
setMethod("is_incr", "Abs", function(object, idx) is_nonneg(object@args[[idx]]))
setMethod("is_decr", "Abs", function(object, idx) is_nonpos(object@args[[idx]]))
setMethod("is_pwl",  "Abs", function(object) is_pwl(object@args[[1]]))
# ... and more for .grad, .domain, graph_implementation ...

Now imagine doing this 100+ times, keeping in sync with CVXPY.

R Package Structure

CVXR/
├── DESCRIPTION      # Metadata, dependencies, version
├── R/               # All R source files ← FLAT directory!
│   ├── expression.R
│   ├── variable.R
│   ├── abs.R
│   └── ...246 files...
├── src/             # C/C++ code (canonicalization)
├── man/             # Documentation (one .Rd per export)
├── tests/           # Test files
└── vignettes/       # Long-form documentation

The problem: CVXPY has a deep directory tree with naturally occurring filename clashes (e.g. atoms/affine/sum.py vs atoms/axis_atom/sum.py).

R packages require a flat R/ directory.

How do you flatten without collisions — and still find things?

Mirroring CVXPY: The rsrc_tree Solution

Develop in an isomorphic tree:

rsrc_tree/
├── atoms/
│   ├── elementwise/
│   │   ├── abs.R
│   │   └── exp.R
│   └── affine/
│       ├── reshape.R
│       └── index.R
├── reductions/
│   └── dcp2cone/
│       └── canonicalizers/
└── ...

Every file starts with:

## CVXPY SOURCE: atoms/elementwise/abs.py

Build script flattens to R/:

R/
├── 045_atoms_elementwise_abs.R
├── 046_atoms_elementwise_exp.R
├── 047_atoms_affine_reshape.R
├── 048_atoms_affine_index.R
├── 112_reductions_dcp2cone_...R
└── ...

Numeric prefixes control load order.

One script. Automatic. Reversible.

Enter S7

The next generation R OOP system (R Consortium, 2024):

Abs <- new_class("Abs", parent = Elementwise,
  constructor = function(x) {
    x <- as_expr(x)
    new_object(S7_object(), args = list(x), shape = x@shape)
  }
)

method(sign_from_args,  Abs) <- function(x) list(is_nonneg = TRUE, is_nonpos = FALSE)
method(is_atom_convex,  Abs) <- function(x) TRUE
method(is_atom_concave, Abs) <- function(x) FALSE
method(is_incr,         Abs) <- function(x, idx, ...) is_nonneg(x@args[[idx + 1L]])
method(is_decr,         Abs) <- function(x, idx, ...) is_nonpos(x@args[[idx + 1L]])
method(numeric_value,   Abs) <- function(x, values, ...) abs(values[[1L]])

Clean. Compact. One file per atom.

S7 ≈ Python

CVXPY (Python)

class Abs(Elementwise):

    def __init__(self, x):
        super().__init__(x)

    def sign_from_args(self):
        return (True, False)

    def is_atom_convex(self):
        return True

    def is_atom_concave(self):
        return False

    def numeric(self, values):
        return np.abs(values[0])

CVXR (S7/R)

Abs <- new_class("Abs",
  parent = Elementwise, ...)

method(sign_from_args, Abs) <-
  function(x)
    list(is_nonneg = TRUE,
         is_nonpos = FALSE)

method(is_atom_convex, Abs) <-
  function(x) TRUE

method(is_atom_concave, Abs) <-
  function(x) FALSE

method(numeric_value, Abs) <-
  function(x, values, ...)
    abs(values[[1L]])

The structure maps almost 1:1. Perhaps an AI can translate it…

So We Tried It


A systematic, AI-assisted rewrite of CVXR.

With guardrails:

  • Isomorphic file structure (mandatory)
  • 15 architectural constraints
  • 150+ documented design decisions
  • Rigorous test mapping to CVXPY

The Journey

Design Choices That Mattered

Structural

  • Isomorphic file tree — every CVXPY .py maps to an R .R at the same path
  • S3 Ops handler for arithmetic (not S7 method()) — handles unary -x correctly
  • C++ bridge unchanged — reuse CVXcanon from old CVXR

Practical

  • Cache pattern mirroring CVXPY’s @lazyprop with R environments
  • Dual sign convention documented per solver (13 solvers, 5 problem types)
  • Every decision recorded (150+ in decisions.md)

Testing Kept Us Honest

  • 3,247 test blocks, each annotated:

    ## @cvxpy test_problem.py::TestLP::test_basic
    test_that("basic LP solves", {
      x <- Variable(2)
      prob <- Problem(Minimize(sum(x)),
                      list(x >= 1))
      result <- psolve(prob)
      expect_equal(result, 2, tolerance = 1e-4)
    })
  • Cross-referenced with 1,641 CVXPY tests

  • Validation scripts to find gaps

Coverage:

Category Count
CVXPY parity 514
R-specific 2,197
Documented N/A 699
Total 3,247

Solver Progression

Beyond DCP

Features that old CVXR never had:

  • DGP — Disciplined Geometric Programming
    • Log-space transformations, 6 DGP atoms
  • DPP — Disciplined Parameterized Programming
    • Tensor-aware C++ bridge, fast re-solve
  • DQCP — Disciplined Quasiconvex Programming
    • Bisection solver, ceil/floor atoms
  • Complex numbers — Full Complex2Real reduction
    • 50+ canonicalizers, Hermitian variables
  • Warm-start — 7 of 13 solvers
  • Logic atoms — Not, And, Or, Xor, implies, iff
  • Perspective, FiniteSet constraints

Does it actually work?



Well, let’s see…

The Documentation Website


67 working examples

From basic LP to advanced DGP/DPP/DQCP


All tested. All running. All documented.

What’s in the Docs

Getting Started

  • Quick Introduction
  • Gentle Introduction
  • Basic: LP, QP, SOCP, SDP, MIQP

Regression (9)

  • Huber, Logistic, Quantile
  • Censored, Isotonic
  • Elastic Net, Pliable Lasso

ML & Finance

  • Lasso, Ridge, SVM
  • Portfolio Optimization
  • Kelly Strategy

Applications (14)

  • Robust Kalman Filter
  • TV Inpainting
  • Antenna Array Design
  • Channel Capacity
  • …and 10 more

Advanced

  • DGP: 6 examples
  • DPP: 2 examples
  • DQCP: 4 examples

Solvers

  • Parameters & tuning
  • Warm starts
  • Speed optimization
  • 13 solver configs

By the Numbers

Metric Value
Expectations passing 7,199
Failures 0
test_that() blocks 3,247
CVXPY parity tests 514 / 1,641
Source files 246
Solvers 13
Metric Value
Atoms 100+
Canonicalizers 47
Documented examples 67
Design decisions 150+


3,247 test blocks × ~2 expectations each = 7,199. 514 blocks map 1:1 to CVXPY tests; the rest are R-specific or documented N/A.

Performance: Expression Building

The S4 overhead that plagued old CVXR is gone:


S4 (old)

1.16 min

1,000 variables
2.54 GB memory

S7 (new)

228 ms

1,000 variables
828 KB memory

305x faster. 3,100x less memory.

Performance: End-to-End

Lasso regression, CLARABEL solver, median of 5 runs:


Build time (problem construction):

Size Old (S4) New (S7)
200x50 10 ms 2 ms
500x100 8 ms 2 ms
1000x200 8 ms 2 ms
2000x500 9 ms 2 ms

5x faster builds, consistent across sizes. Solver time dominates for large problems.

Up to 3.3x faster for small/medium problems. For large problems, solver dominates — which is exactly where you want the bottleneck.

Room to Grow

“Premature optimization is the root of all evil.”

— Donald Knuth


  • DPP fast path: parameterized re-solve skips re-canonicalization
  • Direct solver calls bypass compilation for repeated solves
  • Profiling reveals hotspots (profvis package)
  • We got correct first. Speed comes next.

So… where are we?



v1.8.0-9044

Aligned with CVXPY 1.8.1

Keeping Up with CVXPY

The isomorphic structure isn’t just for the rewrite — it’s the maintenance strategy:

  1. CVXPY tags a new release (e.g. 1.9.0)
  2. We pin to that commit in our repo
  3. rsrc_tree/ + ## CVXPY SOURCE: annotations make diffs obvious
  4. AI-assisted translation of changed files
  5. Test mapping validates parity
  6. Repeat

No-regression guarantee:

Every test_that() block is annotated:

## @cvxpy test_qp.py::TestQP::test_warm

If something breaks:

  • The annotation tells you which CVXPY test it mirrors
  • You run that CVXPY test to see the expected behavior
  • The fix is scoped to one file (isomorphic path)

The breadcrumbs lead you straight back to CVXPY’s ground truth.

The same process that built 1.8.1 in 10 days keeps us in sync going forward.

What’s Next

Coming soon:

  • CRAN submission (R CMD check clean)
  • Performance optimization pass
  • Remaining CVXPY test parity (424 gaps)
  • Community feedback integration

Deferred:

  • Derivative/sensitivity API (needs R equivalent of diffcp)
  • Quantum computing atoms
  • Exotic cone fallbacks


We welcome feedback from the CVXPY and CVXR community.

Is this a good plan? Have we overlooked something? Tell us.

What Made This Work

  • Standing on giants’ shoulders
    • CVXPY’s clean architecture and DCP theory
    • 10 years of CVXPY community effort
  • Anqi Fu’s astonishing S4 work
    • CVXR from scratch to v1.0-15 using S4
    • That she got it working at all in S4 was a tour de force
    • The design thinking carried forward
  • S7 at the right moment
    • Finally, an R OOP that maps to Python
  • Disciplined architecture
    • Isomorphic file structure
    • 15 mandatory constraints
    • 150+ documented decisions
  • Rigorous testing from day one
    • CVXPY parity as the north star
    • Never move forward with failing tests
  • AI-assisted development
    • Claude Code for translation + testing
    • Human judgment for architecture
    • 179 commits in 10 days

Thank You


Links:

Contact:

  • Balasubramanian Narasimhan
  • Anqi Fu
  • Stanford University

We welcome your feedback!

Yes, this presentation was also made by Claude — in about an hour, from a one-page brief. It read the git history of 3 repos, ran benchmarks comparing old and new CVXR, synthesized 150+ design decisions, and wrote the typing demo. Even the SVG terminal jokes.