Nothing
#####
## DO NOT EDIT THIS FILE!! EDIT THE SOURCE INSTEAD: rsrc_tree/reductions/solvers/solving_chain.R
#####
## CVXPY SOURCE: reductions/solvers/solving_chain.py
## SolvingChain -- a Chain with a terminal solver reduction
##
## Dual-interface architecture matching CVXPY:
## - SOLVER_MAP_CONIC: conic path solvers (ConicSolver subclasses)
## - SOLVER_MAP_QP: QP path solvers (QpSolver subclasses)
## - resolve_and_build_chain(): resolve the solver from the problem's
## structure (ProblemForm, problems/problem_form.R), then build the chain
## for it; the solver's class decides QP or conic, as in CVXPY 1.9
## (ADR D_19.8)
# -- Solver registries --------------------------------------------
## Two registries matching CVXPY defines.py:
## SOLVER_MAP_CONIC for conic path, SOLVER_MAP_QP for QP path.
## QP solver preference order (matches CVXPY defines.py QP_SOLVERS)
## Lazy-init via delayedAssign: solver objects constructed on first access.
SOLVER_MAP_QP <- new.env(hash = TRUE, parent = emptyenv())
delayedAssign(OSQP_SOLVER, OSQP_QP_Solver(), assign.env = SOLVER_MAP_QP)
delayedAssign(GUROBI_SOLVER, Gurobi_QP_Solver(), assign.env = SOLVER_MAP_QP)
delayedAssign(CPLEX_SOLVER, CPLEX_QP_Solver(), assign.env = SOLVER_MAP_QP)
delayedAssign(HIGHS_SOLVER, HiGHS_QP_Solver(), assign.env = SOLVER_MAP_QP)
delayedAssign(PIQP_SOLVER, PIQP_QP_Solver(), assign.env = SOLVER_MAP_QP)
delayedAssign(XPRESS_SOLVER, XPRESS_QP_Solver(), assign.env = SOLVER_MAP_QP)
## CVXPY SOURCE: defines.py:80-84,92 -- QP_SOLVERS is the SOLVER_MAP_QP order.
QP_SOLVER_PREFERENCE <- c(OSQP_SOLVER, GUROBI_SOLVER, CPLEX_SOLVER, XPRESS_SOLVER, HIGHS_SOLVER, PIQP_SOLVER)
## Conic solver preference order (matches CVXPY defines.py CONIC_SOLVERS)
## Lazy-init via delayedAssign: solver objects constructed on first access.
SOLVER_MAP_CONIC <- new.env(hash = TRUE, parent = emptyenv())
delayedAssign(CLARABEL_SOLVER, Clarabel_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(SCS_SOLVER, SCS_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(MOSEK_SOLVER, Mosek_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(GUROBI_SOLVER, Gurobi_Conic_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(CPLEX_SOLVER, CPLEX_Conic_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(HIGHS_SOLVER, HiGHS_Conic_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(GLPK_SOLVER, GLPK_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(GLPK_MI_SOLVER, GLPK_MI_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(ECOS_SOLVER, ECOS_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(ECOS_BB_SOLVER, ECOS_BB_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(CVXOPT_SOLVER, CVXOPT_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(SCIP_SOLVER, SCIP_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(XPRESS_SOLVER, XPRESS_Conic_Solver(), assign.env = SOLVER_MAP_CONIC)
delayedAssign(DIFFCP_SOLVER, DIFFCP_Solver(), assign.env = SOLVER_MAP_CONIC)
## CVXPY SOURCE: defines.py:71-78,91 -- CONIC_SOLVERS is the SOLVER_MAP_CONIC
## order, restricted to the solvers CVXR has. The order matters in one place:
## `.fallback_solver` walks INSTALLED_SOLVERS, which lists the conic map's
## names first, then the QP-only ones (defines.py:120-129). DIFFCP is listed
## as upstream lists it, but `.solver_package_available()` is FALSE for it
## (it has no `.SOLVER_PACKAGES` entry), so it is never auto-selected; it is
## reached only through `requires_grad = TRUE`.
CONIC_SOLVER_PREFERENCE <- c(MOSEK_SOLVER, CLARABEL_SOLVER, SCS_SOLVER,
ECOS_SOLVER, CPLEX_SOLVER, GUROBI_SOLVER,
GLPK_SOLVER, GLPK_MI_SOLVER, CVXOPT_SOLVER,
XPRESS_SOLVER, DIFFCP_SOLVER, SCIP_SOLVER,
HIGHS_SOLVER, ECOS_BB_SOLVER)
## Package names for solver availability checks
.SOLVER_PACKAGES <- list(
CLARABEL = "clarabel",
SCS = "scs",
OSQP = "osqp",
HIGHS = "highs",
MOSEK = "Rmosek",
GUROBI = "gurobi",
GLPK = "Rglpk",
GLPK_MI = "Rglpk",
ECOS = "ECOSolveR",
ECOS_BB = "ECOSolveR",
CPLEX = "Rcplex",
CVXOPT = "cccp",
PIQP = "piqp",
SCIP = "scip",
XPRESS = "xpress"
)
## Check if a solver package is installed and not excluded
## Is this solver's R package physically usable?
## (Real install with version >= required minimum, not a stub.)
## PERFORMANCE (2026-08-13). Memoised for the session. Which packages are
## installed cannot change under a running R process in any way this predicate
## should react to, and the MOSEK branch is expensive: `packageVersion()` reads
## and parses Rmosek's DESCRIPTION off disk on every chain construction
## (measured 85us) for users who have Rmosek installed. CVXPY resolves the same
## question exactly once per process -- `INSTALLED_SOLVERS = installed_solvers()`
## is evaluated at import time (defines.py:129) -- so caching here is closer to
## CVXPY's behavior than probing was, not further from it. The exclusion set is
## deliberately NOT part of the key: it is applied by the caller
## `.solver_package_available()`, which stays live.
.solver_package_usable <- function(solver_name) {
cached <- .cvxr_env$solver_usable[[solver_name]]
if (!is.null(cached)) return(cached)
ans <- .solver_package_usable_uncached(solver_name)
if (is.null(.cvxr_env$solver_usable)) .cvxr_env$solver_usable <- list()
.cvxr_env$solver_usable[[solver_name]] <- ans
ans
}
.solver_package_usable_uncached <- function(solver_name) {
## Rmosek on CRAN is an ancient stub; only versions >= 10 are usable.
if (solver_name == "MOSEK") {
if (!requireNamespace("Rmosek", quietly = TRUE)) return(FALSE)
return(utils::packageVersion("Rmosek") >= "10")
}
pkg <- .SOLVER_PACKAGES[[solver_name]]
if (is.null(pkg)) return(FALSE)
requireNamespace(pkg, quietly = TRUE)
}
## Is this solver available for use? (Usable AND not excluded.)
.solver_package_available <- function(solver_name) {
if (solver_name %in% .cvxr_env$excluded_solvers) return(FALSE)
.solver_package_usable(solver_name)
}
## Runtime guard: abort if the named solver's package isn't usable.
.require_solver_package <- function(solver_name) {
if (.solver_package_usable(solver_name)) return(invisible(TRUE))
pkg <- .SOLVER_PACKAGES[[solver_name]]
cli_abort("Solver {.code {solver_name}} unavailable: package {.pkg {pkg}} is missing or unusable.",
class = "SolverError") ## CVXPY: raise SolverError("... is not installed."), solving_chain.py:301
}
# -- SolvingChain class -------------------------------------------
## CVXPY SOURCE: solving_chain.py lines 456-567
SolvingChain <- new_class("SolvingChain", parent = Chain, package = "CVXR",
properties = list(
solver = class_any # the terminal solver instance
),
constructor = function(reductions = list()) {
if (FALSE) new_object(S7_object()) ## S7 static-check guard
solver_inst <- if (length(reductions) > 0L) {
reductions[[length(reductions)]]
} else {
NULL
}
.fast_new(SolvingChain, S7_object(),
.cache = new.env(parent = emptyenv()),
reductions = reductions,
solver = solver_inst
)
}
)
method(print, SolvingChain) <- function(x, ...) {
names <- vapply(x@reductions, function(r) short_class_name(r),
character(1L))
cat(sprintf("SolvingChain(%s)\n", paste(names, collapse = " -> ")))
invisible(x)
}
# -- .preserve_variable_bounds() ------------------------------------
## CVXPY SOURCE: solving_chain.py:227-228 wires
## `CvxAttr2Constr(reduce_bounds = not solver_instance.BOUNDED_VARIABLES)`.
##
## The solver's BOUNDED_VARIABLES flag alone decides, as upstream. A sparse
## bound can only sit on a sparsity variable (leaf.py:746-772, ported in
## leaf.R `.validate_sparse_bound`), and CvxAttr2Constr turns it into the
## reduced variable's dense bound (cvx_attr2constr.py:229-247,
## `.cvxattr_sparse_bound_attr`), so matrix stuffing never sees a Matrix bound
## (matrix_stuffing.py:48-52,84-88 treats one as an internal error).
##
## Until 1.9.3 this scanned the problem and returned FALSE when any variable
## carried a Matrix bound, lowering EVERY bound to constraints -- a leftover
## from when CVXR accepted sparse bounds on dense variables. Measured on
## HiGHS with Variable(2, sparsity = 1, bounds = list(<sparse -1>, 5)): CVXPY
## 1.9.3 passes lower_bounds [-1], upper_bounds [5], no nonneg rows; the scan
## gave NULL tensors and three nonneg rows.
.preserve_variable_bounds <- function(problem, solver_inst) {
isTRUE(solver_inst@BOUNDED_VARIABLES)
}
# -- .lookup_by_name() --------------------------------------------
## CVXPY SOURCE: solving_chain.py:52-66
## The installed QP and conic instances for a solver name; gp suppresses QP.
## "Installed" here ignores exclude_solvers(): a solver the user NAMES is used
## even when excluded from auto-selection (the exclusion API is CVXR's; CVXPY
## has none). DIFFCP has no `.SOLVER_PACKAGES` entry, so its package is
## checked directly (CVXPY DIFFCP.import_solver imports diffcp).
.solver_installed_by_name <- function(name) {
if (identical(name, DIFFCP_SOLVER)) return(requireNamespace("diffcp", quietly = TRUE))
.solver_package_usable(name)
}
.lookup_by_name <- function(solver_name, gp) {
upper <- toupper(solver_name)
qp <- SOLVER_MAP_QP[[upper]]
if (!is.null(qp) && !.solver_installed_by_name(upper)) qp <- NULL
conic <- SOLVER_MAP_CONIC[[upper]]
if (!is.null(conic) && !.solver_installed_by_name(upper)) conic <- NULL
if (gp) qp <- NULL
list(qp = qp, conic = conic)
}
# -- .installed_solver_names() ------------------------------------
## CVXPY SOURCE: defines.py:120-129 -- INSTALLED_SOLVERS: the names of the
## conic map, then the QP map, keeping those installed. Here "installed" is
## `.solver_package_available()`, which honors exclude_solvers().
.installed_solver_names <- function() {
nms <- unique(c(CONIC_SOLVER_PREFERENCE, QP_SOLVER_PREFERENCE))
nms[vapply(nms, .solver_package_available, logical(1L), USE.NAMES = FALSE)]
}
# -- .fallback_solver() -------------------------------------------
## CVXPY SOURCE: solving_chain.py:69-103
## Last resort when no solver was named and `pick_default_solver` found none:
## the first installed non-commercial solver that can solve the problem, with
## a warning, or an error.
## RECORDED DIVERGENCE (user, 2026-10-14): ECOS_BB is skipped, so it is never
## auto-selected. CVXPY's fallback can reach it, and it returns 1 at x = 0
## with status "optimal" for min exp(x), x >= 1.5, x integer, in CVXPY 1.9.3
## and CVXR alike (docsreg_9204/ecosbb_probe.{R,py}).
## RECORDED DIVERGENCE (ADR D_PORT.14): upstream's texts name pip packages and
## a cvxpy.org URL; CVXR names the R packages and drops the URL. The warnings
## are shown once per session (per solver, for the first), as Python's
## default filter shows each text once per call site.
.fallback_solver <- function(problem_form, use_quad_obj = TRUE) {
for (name in .installed_solver_names()) {
if (name %in% .COMMERCIAL_CONIC_SOLVERS) next
if (identical(name, ECOS_BB_SOLVER)) next
for (inst in list(SOLVER_MAP_CONIC[[name]], SOLVER_MAP_QP[[name]])) {
if (!is.null(inst) && can_solve(inst, problem_form, use_quad_obj)) {
cli_warn(c(
"The default solvers are not available. Using {.val {name}}.",
"i" = "Consider specifying a solver explicitly via the {.arg solver} argument."),
.frequency = "once",
.frequency_id = paste0("cvxr_default_solvers_unavailable_", name))
return(inst)
}
}
}
.no_solver_could_handle(problem_form)
}
## CVXPY SOURCE: solving_chain.py:84-103 -- the raise tail of _fallback_solver.
.no_solver_could_handle <- function(problem_form) {
if (.pf_is_mixed_integer(problem_form)) {
if (!.pf_is_lp(problem_form)) {
cli_warn(c(
"Your problem is mixed-integer but not an LP.",
"i" = "If your problem is nonlinear, consider installing SCIP (R package {.pkg scip}) to solve it."),
.frequency = "once", .frequency_id = "cvxr_mixed_integer_not_lp")
}
cli_abort(c(
"You need a mixed-integer solver for this model.",
"i" = "Refer to the documentation {.url https://cvxr.rbind.io/examples/solvers/using-other-solvers.html#solver-capabilities} for discussion on this topic.",
"i" = "Install the SCIP solver (R package {.pkg scip}) for mixed-integer nonlinear problems, or HiGHS (R package {.pkg highs}) for mixed-integer LPs."
), class = "SolverError") ## CVXPY: raise SolverError, solving_chain.py:91
}
cli_abort("No installed solver could handle this problem.",
class = "SolverError") ## CVXPY: raise SolverError, solving_chain.py:101
}
# -- .resolve_solver() --------------------------------------------
## CVXPY SOURCE: solving_chain.py:248-353
## The solver instance to build the chain for. NULL picks a default from the
## problem's structure; a name is looked up in both maps. CVXR's `solver`
## argument is a name, so upstream's custom-Solver-instance branch
## (solving_chain.py:279-295) has no counterpart.
##
## gp: `.lookup_by_name` drops the QP instance ("GP mode suppresses QP";
## problem_form.py:289 "There is no QP path for GP"), and a named solver with
## no conic interface is refused (solving_chain.py:305-310). CVXPY's own
## message there is unreachable (the "not installed" branch at 300-303 fires
## first); CVXR checks the name against the maps before installation, so it
## emits the message CVXPY intends and its test names
## (test-dgp-cvxpy-parity.R).
##
## `use_quad_obj` is R-only and reaches every capability question
## (RECORDED DIVERGENCE at `can_solve`, solver.R).
.resolve_solver <- function(solver, problem_form, gp, use_quad_obj = TRUE) {
constant <- length(variables(problem_form@problem)) == 0L
if (is.character(solver)) {
name <- toupper(solver)
if (gp && is.null(SOLVER_MAP_CONIC[[name]]) && !is.null(SOLVER_MAP_QP[[name]])) {
cli_abort(c(
"When {.code gp = TRUE}, {.arg solver} must be a conic solver (received {.val {name}}).",
"i" = "Try calling {.fn psolve} with {.code solver = \"CLARABEL\"}."
), class = "SolverError") ## CVXPY: raise SolverError, solving_chain.py:306
}
found <- .lookup_by_name(name, gp)
qp_inst <- found$qp
conic_inst <- found$conic
if (is.null(qp_inst) && is.null(conic_inst)) {
## A known name whose R package is missing gets the package named
## (ADR D_PORT.14); an unknown name gets upstream's text.
if (!is.null(.SOLVER_PACKAGES[[name]])) .require_solver_package(name)
cli_abort("The solver {.val {name}} is not installed.",
class = "SolverError") ## CVXPY: raise SolverError, solving_chain.py:301
}
## Constant problems are handled by ConstantSolver; any installed
## solver is acceptable.
if (constant) return(if (!is.null(conic_inst)) conic_inst else qp_inst)
## Prefer QP when the objective is quadratic; otherwise conic first and
## QP last.
quadratic <- .pf_has_quadratic_objective(problem_form)
candidates <- list()
if (!is.null(qp_inst) && quadratic) candidates <- c(candidates, list(qp_inst))
if (!is.null(conic_inst)) candidates <- c(candidates, list(conic_inst))
if (!is.null(qp_inst) && !quadratic) candidates <- c(candidates, list(qp_inst))
for (inst in candidates) {
if (can_solve(inst, problem_form, use_quad_obj)) return(inst)
}
.abort_cannot_solve(name, candidates, problem_form, use_quad_obj)
}
if (!is.null(solver)) {
cli_abort("The {.arg solver} argument must be a solver name or {.code NULL}.",
class = "SolverError") ## CVXPY: raise SolverError, solving_chain.py:335
}
## No solver named.
if (constant) {
## Constant problems are handled by ConstantSolver; return any installed
## solver to satisfy the chain construction.
for (name in .installed_solver_names()) {
for (inst in list(SOLVER_MAP_CONIC[[name]], SOLVER_MAP_QP[[name]])) {
if (!is.null(inst)) return(inst)
}
}
}
default <- pick_default_solver(problem_form, use_quad_obj)
if (!is.null(default)) return(default)
.fallback_solver(problem_form, use_quad_obj)
}
## CVXPY SOURCE: solving_chain.py:330-332 -- "The solver %s cannot solve this
## problem." CVXR says why when it can: no mixed-integer support (with the
## installed MIP-capable solvers to use instead), or the cones the solver
## lacks; tests pin both texts (test-partial-optimize.R,
## test-problem-form-parity-v19.R). Upstream's text is the last resort.
.abort_cannot_solve <- function(name, candidates, problem_form, use_quad_obj = TRUE) {
mixed_integer <- .pf_is_mixed_integer(problem_form)
if (mixed_integer &&
!any(vapply(candidates, function(i) isTRUE(i@MIP_CAPABLE), logical(1L)))) {
all_s <- unique(c(QP_SOLVER_PREFERENCE, CONIC_SOLVER_PREFERENCE))
mip_solvers <- character(length(all_s))
mi <- 0L
for (s in all_s) {
if (!.solver_package_available(s)) next
qp_i <- SOLVER_MAP_QP[[s]]
conic_i <- SOLVER_MAP_CONIC[[s]]
if ((!is.null(qp_i) && qp_i@MIP_CAPABLE) ||
(!is.null(conic_i) && conic_i@MIP_CAPABLE)) {
mi <- mi + 1L
mip_solvers[[mi]] <- s
}
}
mip_solvers <- mip_solvers[seq_len(mi)]
suggest <- if (length(mip_solvers) > 0L) {
paste0("Use ", paste0("{.val ", mip_solvers, "}", collapse = " or "), " instead.")
} else {
"Install a MIP-capable solver such as {.val HIGHS} or {.val GUROBI}."
}
cli_abort(c(
"Solver {.val {name}} does not support mixed-integer problems.",
"i" = suggest
), class = "SolverError") ## CVXPY: raise SolverError, solving_chain.py:330
}
## A mixed-integer QP whose QP interface is not MIP-capable (HiGHS): say
## so, and name the installed solvers that can take the problem.
if (mixed_integer && .pf_has_quadratic_objective(problem_form) &&
any(vapply(candidates, function(i) .s7_is(i, QpSolver) && !isTRUE(i@MIP_CAPABLE),
logical(1L)))) {
able <- Filter(function(s) {
any(vapply(list(SOLVER_MAP_CONIC[[s]], SOLVER_MAP_QP[[s]]), function(i) {
!is.null(i) && can_solve(i, problem_form, use_quad_obj)
}, logical(1L)))
}, .installed_solver_names())
suggest <- if (length(able) > 0L) {
paste0("Try solver: ", paste(able, collapse = ", "), ".")
} else {
"No installed solver can solve this mixed-integer QP."
}
cli_abort(c(
"Mixed-integer QP (MIQP) is not supported by solver {.val {name}}.",
"i" = suggest
), class = "SolverError") ## CVXPY: raise SolverError, solving_chain.py:330
}
## The cones the LAST candidate tried still lacks (the conic interface when
## there is one, since the QP interface supports fewer cones).
inst <- candidates[[length(candidates)]]
conic_cand <- Filter(function(i) .s7_is(i, ConicSolver), candidates)
if (length(conic_cand) > 0L) inst <- conic_cand[[1L]]
supported <- if (mixed_integer) mi_supported_constraints(inst) else
inst@SUPPORTED_CONSTRAINTS
quad_obj <- isTRUE(use_quad_obj) && supports_quad_obj(inst) &&
.pf_has_quadratic_objective(problem_form)
cones <- .nonpos_as_nonneg(.pf_cones(problem_form, quad_obj = quad_obj))
check <- .solver_supports_cones_in(supported, cones)
if (!check$ok) {
cone_str <- paste(vapply(check$unsupported, .cone_name, character(1L)),
collapse = ", ")
cli_abort(c(
"Solver {.val {name}} does not support the required cone types: {cone_str}.",
"i" = "This problem requires {cone_str} cones. Choose a solver that supports them."
), class = "SolverError") ## CVXPY: raise SolverError, solving_chain.py:330
}
cli_abort("The solver {.val {name}} cannot solve this problem.",
class = "SolverError") ## CVXPY: raise SolverError, solving_chain.py:330
}
# -- .required_cone_types() -------------------------------------
## R-only: the non-LP cones (all but Zero, NonNeg, NonPos) of the problem's
## full ProblemForm cone set (problem_form.R), as a list of S7 class objects.
## Selection no longer calls this; it stays as a test-facing summary
## (test-problem-form-parity-v19.R, test-partial-optimize.R).
.required_cone_types <- function(problem) {
Filter(function(cone) !(.cone_name(cone) %in% c("Zero", "NonNeg", "NonPos")),
.pf_cones(ProblemForm(problem)))
}
# -- .solver_supports_cones() ----------------------------------
## Check if a conic solver supports the required cone types.
.solver_supports_cones <- function(solver_inst, required_cones) {
.solver_supports_cones_in(solver_inst@SUPPORTED_CONSTRAINTS, required_cones)
}
.solver_supports_cones_in <- function(supported, required_cones) {
## CVXPY SOURCE: solving_chain.py:222-223 -- a cone the solver does not take
## directly still counts as supported when an EXACT conversion reaches one it
## does (PSD -> SvecPSD for every packed-triangle solver). `expand_cones`
## rewrites the requirement set; what survives it must be natively supported.
expanded <- expand_cones(required_cones, supported)
## By class name, as `.cone_has` (solver.R) -- see the note there.
missing <- !vapply(expanded$cones, function(cone) .cone_has(supported, cone),
logical(1L), USE.NAMES = FALSE)
list(ok = !any(missing), unsupported = expanded$cones[missing],
exact_targets = expanded$exact_targets)
}
# -- COMMERCIAL_SOLVERS --------------------------------------------
## CVXPY SOURCE: defines.py:115-117, in upstream's order, restricted to the
## solvers CVXR has. Read by `pick_default_solver` (problem_form.R; tried
## first unless the opt-out is set) and `.fallback_solver` (always skipped).
.COMMERCIAL_CONIC_SOLVERS <- c(MOSEK_SOLVER, GUROBI_SOLVER, CPLEX_SOLVER,
XPRESS_SOLVER)
# -- resolve_and_build_chain --------------------------------------
## The enforce_dpp refusal (CVXPY: raise DPPError(DPP_ERROR_MSG),
## solving_chain.py:204). One function, two callers: the chain builder below
## on a fresh build, and `.compile()` (problems/problem.R) on a cache hit whose
## chain was built for a DPP-violating problem.
.abort_enforce_dpp <- function() {
cli_abort(c(
"Problem does not follow DPP rules but {.code enforce_dpp = TRUE}.",
"i" = "Set {.code enforce_dpp = FALSE} to canonicalize it as a non-DPP problem."
), class = "DPPError")
}
## CVXPY SOURCE: solving_chain.py:356-413
## Resolve the solver from the problem's structure, then build the chain for
## it. The DCP/DGP check runs first (make_problem_form), then the solver is
## chosen, then the DPP check, which needs to know the solver (#3142).
resolve_and_build_chain <- function(problem, solver = NULL, gp = FALSE,
opts = solver_opts(),
enforce_dpp = FALSE, ignore_dpp = FALSE) {
problem_form <- make_problem_form(problem, gp, ignore_dpp)
solver_instance <- .resolve_solver(solver, problem_form, gp,
use_quad_obj = !isFALSE(opts$use_quad_obj))
.build_solving_chain(problem, solver_instance, problem_form = problem_form,
gp = gp, enforce_dpp = enforce_dpp,
ignore_dpp = ignore_dpp, opts = opts)
}
## CVXPY 1.8.2's name for the same entry point (solving_chain.py:179 at
## v1.8.2), kept because CVXR's callers and tests use it.
construct_solving_chain <- function(problem, solver = NULL, gp = FALSE,
opts = solver_opts(),
enforce_dpp = FALSE, ignore_dpp = FALSE) {
resolve_and_build_chain(problem, solver = solver, gp = gp, opts = opts,
enforce_dpp = enforce_dpp, ignore_dpp = ignore_dpp)
}
# -- .build_solving_chain -----------------------------------------
## CVXPY SOURCE: solving_chain.py:106-245
## The reduction chain for one chosen solver: solver context, then the
## pre-canonicalization reductions (problem + gp only), then the
## canonicalization reductions (problem_form + solver context).
.build_solving_chain <- function(problem, solver_instance, problem_form = NULL,
gp = FALSE, enforce_dpp = FALSE,
ignore_dpp = FALSE, opts = solver_opts()) {
## CVXPY SOURCE: solving_chain.py:154-155
## Zero-variable problems: use ConstantSolver (evaluates constraints directly)
if (length(variables(problem)) == 0L) {
return(SolvingChain(reductions = list(ConstantSolver())))
}
if (is.null(problem_form)) problem_form <- ProblemForm(problem)
## CVXPY SOURCE: solving_chain.py:160-178 -- SolverInfo up front, with the
## MIP-aware constraint set; SOC-approx canonicalizers read it to warn when
## the solver actually supports power cones.
supported <- if (.pf_is_mixed_integer(problem_form)) {
mi_supported_constraints(solver_instance)
} else {
solver_instance@SUPPORTED_CONSTRAINTS
}
solver_context <- SolverInfo(
solver_name = solver_name(solver_instance),
solver_supported_constraints = supported,
solver_supports_bounds = isTRUE(solver_instance@BOUNDED_VARIABLES),
psd_triangle_kind = solver_instance@PSD_TRIANGLE_KIND,
psd_sqrt2_scaling = solver_instance@PSD_SQRT2_SCALING
)
reductions <- list()
## 0. DPP-aware parameter handling
## CVXPY SOURCE: solving_chain.py lines 192-212
has_params <- length(parameters(problem)) > 0L
obj_expr <- problem@objective@args[[1L]]
## CVXPY SOURCE: solving_chain.py:200 (#3142) --
## `quad_form_dpp = 'qp' if solver_instance.supports_quad_obj() else None`.
quad_obj_capable <- supports_quad_obj(solver_instance)
## "qp" relaxation applies only to a quadratic objective that a quad-obj
## solver will take directly (so parametric P in the OBJECTIVE is DPP;
## constraint quad_forms stay non-DPP -- see .problem_is_dpp).
## PERF (v1.9): gate on has_params. The relaxation only makes a PARAMETRIC P
## objective DPP, so it is meaningless without parameters. Without this gate a
## quadratic-objective, parameter-FREE problem (e.g. the Kalman cells) ran a
## redundant SECOND .problem_is_dpp full-tree curvature pass (the since-removed
## dpp_ok_std + dpp_ok), a per-node S7-dispatch cost that scales with
## expression size (+10-19% on node-dense problems vs 1.8.2-1). See
## notes/v19_perf_regression_dpp_rootcause.md. #3142 is preserved: parametric
## quad_form(x,P) always has has_params = TRUE.
quad_form_dpp <- if (!gp && opts$use_quad_obj && quad_obj_capable &&
has_quadratic_term(obj_expr) && has_params) {
"qp"
} else {
NULL
}
## DPP context depends on gp flag (CVXPY: dpp_context = 'dgp' if gp else 'dcp')
## CVXPY SOURCE: solving_chain.py:200-201 -- ONE DPP pass, under the quad_form
## relaxation when a quad-obj solver takes the objective directly (#3142);
## `quad_form_dpp` is NULL otherwise. (Until 1.9.2.9161 a second, standard-
## rules pass computed `relaxation_enabled` to exempt the parametric quad_form
## from the EvalParams bake below; the bake is gone, so is the pass.)
## The gp branch is problem.is_dpp('dgp') too, so a parametric variable bound
## that is not log-log affine makes the problem non-DPP (problem.py:393-400);
## until 1.9.2.9169 it called .is_dgp_dpp, i.e. is_dgp(dpp=True), which skips
## the bounds (claims audit decision 29 side finding 1).
if (gp) {
dpp_ok <- .problem_is_dpp(problem, "dgp")
} else {
dpp_ok <- .problem_is_dpp(problem, "dcp", quad_form_dpp)
}
## CVXPY SOURCE: solving_chain.py:202-207 -- ignore_dpp forces the non-DPP
## path; enforce_dpp raises; otherwise a non-DPP parameterized problem WARNS
## instead of silently falling back:
##
## if ignore_dpp or not is_dpp:
## if not ignore_dpp and enforce_dpp: raise DPPError(DPP_ERROR_MSG)
## if not ignore_dpp: warn(DPP_ERROR_MSG)
## reductions = [EvalParams()] + reductions
##
## Both branches read the TRUE DPP status, and the ignore_dpp line below
## OVERWRITES `dpp_ok`, so capture it first.
dpp_violated <- has_params && !dpp_ok
if (isTRUE(ignore_dpp)) dpp_ok <- FALSE
## The abort tests `dpp_violated`, NOT `dpp_ok`: the line above OVERWRITES
## `dpp_ok` to FALSE for `ignore_dpp`, so testing it here asks the wrong
## question. Upstream's guard is `if not ignore_dpp and enforce_dpp`
## (solving_chain.py:203) -- ignore_dpp short-circuits the enforce check --
## and `dpp_violated` is `has_params && !dpp_ok` captured before the
## overwrite, i.e. upstream's `not is_dpp`.
##
## No psolve() path reaches here with both flags set: `.compile()`
## (problems/problem.R:470-475) rejects the contradictory pair first, as upstream
## does at problem.py:817-818, and for the same stated reason (before the
## cache key). This matters only for a direct call to the chain constructor,
## where CVXR now behaves as CVXPY's does.
if (isTRUE(enforce_dpp) && !isTRUE(ignore_dpp) && dpp_violated) {
.abort_enforce_dpp() ## CVXPY: raise DPPError, solving_chain.py:204
}
## CVXPY v1.9.2 parity (audit finding #5): warn on the ordinary path. Without
## this a user whose re-solves are NOT being accelerated is never told -- the
## problem solves correctly, just without the DPP fast path, which is exactly
## the silent-underperformance case upstream's message exists to prevent.
## `ignore_dpp = TRUE` means the user already knows, so it stays silent.
## The abort above returns first, so at most one of the two ever fires.
if (dpp_violated && !isTRUE(ignore_dpp)) {
cli_warn(c(
"Solving a parameterized problem that is not DPP.",
"i" = "Subsequent solves will not be faster than the first one.",
"i" = "Check with {.fn is_dpp}; see {.url https://www.cvxpy.org/tutorial/dpp/index.html}.",
"i" = "Set {.code ignore_dpp = TRUE} to silence this."
), class = "DPPWarning") ## CVXPY: warn(DPP_ERROR_MSG), solving_chain.py:206
}
## CVXPY SOURCE: solving_chain.py:202-207 -- EvalParams is prepended only when
## `ignore_dpp or not is_dpp`. Through 1.9.2.9161 CVXR also baked every
## parametric quadratic objective that was DPP by the standard rules (the
## Phase 3d `qp_with_params` rule, "QP tensor path not yet implemented"),
## which dropped the parameter program and recanonicalized on every re-solve:
## measured 3.9 ms against 1.6 ms for a 50-variable sum_squares(y - q) under
## CLARABEL, 4.0 ms against 1.5 ms under OSQP. The tensor path handles these
## problems: the quad canonicalizers route a non-Variable argument through an
## auxiliary variable (quad_over_lin_canon.R), so a parameter only ever enters
## affine constraint data, and twelve problem/solver pairs agreed with the
## baked path and with CVXPY 1.9.3 to twelve digits. Removed at 1.9.2.9162
## (claims audit C0919, decision 26), together with the `relaxation_enabled`
## second DPP pass that existed only to exempt #3142's parametric quad_form.
##
## Deliberate feature gap: complex parameters use EvalParams before
## Complex2Real. Ordinary complex solves are supported, but the DPP fast path
## would need complex sparse parameter tensors; R's Matrix package does not
## provide the required complex sparse representation here. The
## complex-dpp-parity tests document the deferred CVXPY fast-path behavior
## (notes/revisit_complex_params_evalparams.md).
has_complex_params <- has_params &&
any(vapply(parameters(problem), function(p) is_complex(p) || is_imag(p),
logical(1L)))
if (has_params && (!dpp_ok || has_complex_params)) {
reductions <- c(reductions, list(EvalParams()))
}
## 1. Complex2Real: if any leaf is complex, reduce to real
if (complex2real_accepts(problem)) {
reductions <- c(reductions, list(Complex2Real()))
}
## 2. DGP path: insert Dgp2Dcp reduction (G7). DCP/DGP compliance was
## checked by make_problem_form (problem_form.R) before the solver was
## resolved, as upstream checks it (problem_form.py:414-432).
## Chain order: [EvalParams] -> [Complex2Real] -> [Dgp2Dcp] -> [FlipObjective]
## -> [Dcp2Cone] -> [CvxAttr2Constr] -> [ConeMatrixStuffing] -> [Solver]
if (gp) {
reductions <- c(reductions, list(Dgp2Dcp()))
}
## 3. Flip objective if Maximize -> Minimize
if (.s7_is(problem@objective, Maximize)) {
reductions <- c(reductions, list(FlipObjective()))
}
## 3a. FiniteSet -> MIP reduction (used by both QP and conic pathways)
## CVXPY SOURCE: solving_chain.py lines 188-190
if (any(vapply(problem@constraints, function(c) .s7_is(c, FiniteSet), logical(1)))) {
reductions <- c(reductions, list(Valinvec2mixedint()))
}
## 4. Canonicalization reductions (problem_form + solver_context)
## CVXPY SOURCE: solving_chain.py:214-244. A QP solver always needs
## quad_obj = TRUE in matrix stuffing, because its apply expects the P
## matrix; a conic solver gets it only for a quadratic objective it can take.
use_quad <- !isFALSE(opts$use_quad_obj)
is_qp_solver <- .s7_is(solver_instance, QpSolver)
quad_obj <- use_quad && supports_quad_obj(solver_instance) &&
(is_qp_solver || .pf_has_quadratic_objective(problem_form))
## The exact cone conversions the chosen solver needs (solver.py:26-67).
cones <- .nonpos_as_nonneg(.pf_cones(problem_form, quad_obj = quad_obj))
exact_targets <- expand_cones(cones, supported)$exact_targets
reductions <- c(reductions, list(
Dcp2Cone(quad_obj = quad_obj, solver_context = solver_context),
CvxAttr2Constr(reduce_bounds = !.preserve_variable_bounds(problem, solver_instance))
))
if (length(exact_targets) > 0L) {
reductions <- c(reductions, list(
ExactCone2Cone(target_cones = exact_targets,
solver_context = solver_context)))
}
## CVXPY's ApproxCone2Cone, EliminateZeroSized, SOCDim3 and
## ExtractDirectCones (solving_chain.py:233-243) have no CVXR counterpart
## in this chain; the chain is otherwise the one CVXR built before the port.
reductions <- c(reductions, list(
ConeMatrixStuffing(quad_obj = quad_obj),
solver_instance
))
chain <- SolvingChain(reductions = reductions)
## R-only carrier: upstream's `not is_dpp` for a parameterized problem, as
## computed above (has_params && !dpp_ok, before the ignore_dpp overwrite).
## `.compile()` (problems/problem.R) reads it on a cache hit under
## `enforce_dpp = TRUE` so the DPPError above is raised again without a
## rebuild. The cache key omits enforce_dpp, as CVXPY's does (problem.py:118-119);
## claims audit C0540, decision 25, ADR D_PORT.13.
chain@.cache$dpp_violated <- dpp_violated
chain
}
# -- solve_via_data (SolvingChain) ---------------------------------
## Delegates to the terminal solver, managing the solver_cache.
## When `problem` is supplied, uses the problem's solver_cache
## (for warm-start state persistence across repeated solves).
## S7 method (registered via S7::methods_register() in .onLoad; not exported and
## not separately documented -- it carries an extra `problem` arg beyond the
## generic, which roxygen 8 would emit as a bare `solve_via_data(..., problem=)`
## usage that R's codoc cannot distinguish from the generic. The generic's
## `@param ...` documents `problem`. See the generic in generics.R.
method(solve_via_data, SolvingChain) <- function(x, data, warm_start = FALSE,
verbose = FALSE,
solver_opts = list(), ...,
problem = NULL) {
## CVXPY SOURCE: solving_chain.py:564 -- upstream calls
## `_validate_problem_data(data)` HERE as well as in `SolvingChain.solve`
## (:516), with the comment naming why: "These are the two possible entry
## points for executing the solving chain." CVXR had only the psolve() site
## (problem.R), so the documented decomposed API --
## problem_data() -> solve_via_data() -> problem_unpack_results() -- accepted
## poisoned data: a NaN planted in `c` reached Clarabel, which returned status
## 10 and garbage that propagated to the caller. CVXPY raises ValueError.
.validate_problem_data(data)
solver_cache <- if (!is.null(problem)) {
.get_solver_cache(problem)
} else {
new.env(parent = emptyenv())
}
solve_via_data(x@solver, data, warm_start, verbose, solver_opts,
solver_cache = solver_cache)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.