R/272_reductions_solvers_conic_solvers_scs_conif.R

Defines functions dims_to_solver_dict_scs

#####
## DO NOT EDIT THIS FILE!! EDIT THE SOURCE INSTEAD: rsrc_tree/reductions/solvers/conic_solvers/scs_conif.R
#####

## CVXPY SOURCE: reductions/solvers/conic_solvers/scs_conif.py
## SCS solver interface
##
## SCS uses lower-triangular svec format for PSD constraints.
## Convention: A*x + s = b, s in K


# -- SCS status map ------------------------------------------------
## CVXPY SOURCE: scs_conif.py lines 127-135

SCS_STATUS_MAP <- list(
  "1"  = OPTIMAL,
  "2"  = OPTIMAL_INACCURATE,
  "-1" = UNBOUNDED,
  "-6" = UNBOUNDED_INACCURATE,
  "-2" = INFEASIBLE,
  "-7" = INFEASIBLE_INACCURATE,
  "-4" = SOLVER_ERROR,
  "-3" = SOLVER_ERROR,
  "-5" = SOLVER_ERROR
)

# -- dims_to_solver_dict_scs --------------------------------------
## CVXPY SOURCE: scs_conif.py lines 36-42
## SCS 3.0+ uses 'z' instead of 'f' for zero cone.
##
## DELIBERATE DEVIATION (2026-08-13): CVXPY guards this with a runtime
## `Version(scs.__version__) >= Version('3.0.0')` probe (scs_conif.py:40). CVXR
## does not, because DESCRIPTION requires `scs (>= 3.2)` -- the pre-3.0 arm is
## unreachable, so the probe was dead code that read and parsed scs's
## DESCRIPTION off disk on every solve. (CVXPY's own pin is `scs >= 3.2.4.post1`
## in pyproject.toml:86, so its check is equally vestigial; behavior is
## identical, only the dead branch is gone.) If the DESCRIPTION floor is ever
## lowered below 3.0, restore the probe at both sites.

dims_to_solver_dict_scs <- function(cone_dims) {
  cones <- dims_to_solver_dict(cone_dims)
  ## SCS 3.x renamed 'f' to 'z'
  cones[["z"]] <- cones[["f"]]
  cones[["f"]] <- NULL
  cones
}

# -- SCS_Solver class ---------------------------------------------
## CVXPY SOURCE: scs_conif.py lines 116-155

SCS_Solver <- new_class("SCS_Solver", parent = ConicSolver, package = "CVXR",
  constructor = function() {
    if (FALSE) new_object(S7_object())  ## S7 static-check guard
    .fast_new(SCS_Solver, S7_object(),
      .cache = new.env(parent = emptyenv()),
      MIP_CAPABLE = FALSE,
      BOUNDED_VARIABLES = FALSE,
      ## CVXPY SOURCE: scs_conif.py lines 91-92
      PSD_TRIANGLE_KIND = TriangleKind$LOWER,
      PSD_SQRT2_SCALING = TRUE,
      ## CVXPY SOURCE: scs_conif.py:88-89
      SUPPORTED_CONSTRAINTS = list(Zero, NonNeg, SOC, ExpCone,
                                   SvecPSD, PowCone3D),
      EXP_CONE_ORDER = c(0L, 1L, 2L),
      REQUIRES_CONSTR = TRUE
    )
  }
)

method(solver_name, SCS_Solver) <- function(x) SCS_SOLVER

## CVXPY v1.8.2: SCS >= 3.0 supports quadratic objective
method(supports_quad_obj, SCS_Solver) <- function(x) TRUE

# -- SCS invert ----------------------------------------------------
## CVXPY SOURCE: scs_conif.py lines 235-278
## Parses SCS-specific result format.

method(reduction_invert, SCS_Solver) <- function(x, solution, inverse_data, ...) {
  attr_list <- list()

  ## SCS result format: solution is the raw list from scs::scs()
  ## SCS 3.x: status_val in info, SCS 2.x: statusVal in info
  info <- solution[["info"]]
  status_val <- info[["status_val"]]
  if (is.null(status_val)) status_val <- info[["statusVal"]]
  status <- SCS_STATUS_MAP[[as.character(status_val)]]
  if (is.null(status)) status <- SOLVER_ERROR

  ## Timing attributes
  solve_time <- info[["solve_time"]]
  if (is.null(solve_time)) solve_time <- info[["solveTime"]]
  setup_time <- info[["setup_time"]]
  if (is.null(setup_time)) setup_time <- info[["setupTime"]]
  if (!is.null(solve_time)) attr_list[[RK_SOLVE_TIME]] <- solve_time / 1000
  if (!is.null(setup_time)) attr_list[[RK_SETUP_TIME]] <- setup_time / 1000
  attr_list[[RK_NUM_ITERS]] <- info[["iter"]]

  if (status %in% SOLUTION_PRESENT) {
    primal_val <- info[["pobj"]]
    opt_val <- primal_val + inverse_data[[SD_OFFSET]]
    primal_vars <- list()
    primal_vars[[as.character(inverse_data[[SOLVER_VAR_ID]])]] <- solution[["x"]]

    ## Dual variables: split at zero cone boundary
    y <- solution[["y"]]
    zero_dim <- inverse_data[[SD_DIMS]]@zero
    if (zero_dim > 0L) {
      eq_dual <- get_dual_values(
        y[seq_len(zero_dim)],
        extract_dual_value,
        inverse_data[[SOLVER_EQ_CONSTR]]
      )
    } else {
      eq_dual <- list()
    }
    if (zero_dim < length(y)) {
      ineq_dual <- get_dual_values(
        y[(zero_dim + 1L):length(y)],
        extract_dual_value,
        inverse_data[[SOLVER_NEQ_CONSTR]]
      )
    } else {
      ineq_dual <- list()
    }
    dual_vars <- c(eq_dual, ineq_dual)
    Solution(status, opt_val, primal_vars, dual_vars, attr_list)
  } else {
    failure_solution(status, attr_list)
  }
}

# -- SCS solve_via_data --------------------------------------------
## CVXPY SOURCE: scs_conif.py lines 304-354

method(solve_via_data, SCS_Solver) <- function(x, data, warm_start = FALSE, verbose = FALSE,
                                               solver_opts = list(), ...) {
  .require_solver_package(SCS_SOLVER)

  dots <- list(...)
  solver_cache <- dots[["solver_cache"]]

  args <- list(A = data[[SD_A]], b = data[[SD_B]], c = data[[SD_C]])
  cone <- dims_to_solver_dict_scs(data[[SD_DIMS]])

  ## Parse solver options. SCS 3.x tolerance names; the pre-3.0 `eps` arm is
  ## unreachable under DESCRIPTION's `scs (>= 3.2)` -- see the note on
  ## dims_to_solver_dict_scs above.
  opts <- solver_opts
  ## CVXPY SOURCE: scs_conif.py:206-209 -- under SCS >= 3.0 the legacy `eps`
  ## keyword is replaced by eps_abs and eps_rel, both set to it (and `eps`
  ## overrides explicit eps_abs/eps_rel, exactly as upstream). Previously a
  ## user's eps= was silently ignored and the solve ran at the defaults.
  if (!is.null(opts[["eps"]])) {
    opts[["eps_abs"]] <- opts[["eps"]]
    opts[["eps_rel"]] <- opts[["eps"]]
    opts[["eps"]] <- NULL
  }
  if (is.null(opts[["eps_abs"]])) opts[["eps_abs"]] <- 1e-5
  if (is.null(opts[["eps_rel"]])) opts[["eps_rel"]] <- 1e-5
  ## Anderson acceleration. CVXPY passes nothing for these (scs_conif.py:211-212
  ## sets only the two eps), so upstream gets SCS's own C defaults. The R `scs`
  ## package's scs_control() ships DIFFERENT defaults for both, which meant CVXR
  ## ran a materially different algorithm from CVXPY on every SCS solve:
  ##
  ##                          R scs 3.2.7   SCS C default (python scs 3.2.11)
  ##   acceleration_lookback  0             10
  ##   acceleration_interval  1             10
  ##
  ## (Every other setting agrees: alpha 1.5, scale 0.1, adaptive_scale, rho_x
  ## 1e-6, eps_infeas 1e-7, normalize, max_iters 100000.)
  ##
  ## Measured on the DQCP hypersonic subproblem, the SAME A/b/c handed to python
  ## scs 3.2.11 three ways:
  ##      lookback 10 (C default) -> "infeasible",                  2,500 iters
  ##      lookback  0 (R default) -> "infeasible (inaccurate -
  ##                                  reached max_iters)",        100,000 iters
  ## The second is exactly what R scs 3.2.7 returned on that data, so the R
  ## default alone accounts for it. End to end it cost accuracy: CVXR's SCS
  ## answer was 0.1345 against a true 0.14590 (7.8% off).
  ##
  ## These are DEFAULTS, applied like eps above -- an explicit user value still
  ## wins, and every SCS solve gets them (direct, DQCP bisection subproblems,
  ## warm-started), which is what makes CVXR's SCS behave as CVXPY's does.
  if (is.null(opts[["acceleration_lookback"]]))
    opts[["acceleration_lookback"]] <- 10L
  if (is.null(opts[["acceleration_interval"]]))
    opts[["acceleration_interval"]] <- 10L

  ## Pass P for QP path -- SCS expects symmetric sparse (dsCMatrix)
  if (!is.null(data[[SD_P]])) {
    args[["P"]] <- Matrix::forceSymmetric(Matrix::triu(data[[SD_P]]), uplo = "U")
  }

  ## Warm-start: pass previous x/y/s as initial point
  ## CVXPY SOURCE: scs_conif.py lines 327-331
  cache_key <- SCS_SOLVER
  initial <- NULL
  if (warm_start && !is.null(solver_cache) && exists(cache_key, envir = solver_cache)) {
    cached <- get(cache_key, envir = solver_cache)
    initial <- list(x = cached$x, y = cached$y, s = cached$s)
  }

  ## Call SCS
  result <- scs::scs(
    A = args$A, b = args$b, obj = args$c, P = args[["P"]], cone = cone,
    initial = initial,
    control = c(list(verbose = verbose), opts)
  )

  ## Cache result for future warm-starts (only on optimal)
  ## CVXPY SOURCE: scs_conif.py lines 352-353
  if (!is.null(solver_cache)) {
    status <- SCS_STATUS_MAP[[as.character(result$info$status_val)]]
    if (!is.null(status) && status == OPTIMAL) {
      assign(cache_key, result, envir = solver_cache)
    }
  }

  result
}

method(print, SCS_Solver) <- function(x, ...) {
  cat("SCS_Solver()\n")
  invisible(x)
}

Try the CVXR package in your browser

Any scripts or data that you put into this service are public.

CVXR documentation built on Aug. 24, 2026, 9:10 a.m.