R/277_reductions_solvers_conic_solvers_cplex_conif.R

Defines functions .cplex_solution_feasibility .cplex_get_status .cplex_normalize_solstat

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

## CVXPY SOURCE: reductions/solvers/conic_solvers/cplex_conif.py
## CPLEX conic solver interface via Rcplex.
##
## CVXPY 1.9 parity notes:
##  - Mirrors cplex_conif.py for LP/SOCP/MI-LP/MI-SOCP: Zero and NonNeg are
##    linear rows; SOC blocks are represented by auxiliary variables plus one
##    convex quadratic constraint per cone.
##  - Rcplex exposes QCP solving through `Rcplex_solve_QCP()` but does not
##    return QCP dual-slack vectors, so SOC dual recovery is intentionally
##    omitted unless Rcplex grows that API.


# -- CPLEX status map ----------------------------------------------------------
## Maps Rcplex integer status codes to CVXR status strings.
## LP codes: 1-6, MIP codes: 101-108+
## CVXPY SOURCE: cplex_conif.py get_status() / cplex_qpif.py invert()

## CPX status constants, transcribed from
## CPLEX_Studio2211/cplex/include/ilcplex/cpxconst.h. Named, not inlined,
## because the flat table these replace carried COMMENTS that were wrong -- and
## a wrong comment on a magic number is invisible. Checked against the header:
## the old table labelled 10 "abort_user" (it is ABORT_IT_LIM), 11
## "abort_iteration_limit" (ABORT_TIME_LIM), 12 "abort_time_limit"
## (ABORT_OBJ_LIM), 13 "abort_dettime_limit" (ABORT_USER), 6 "feasible"
## (NUM_BEST; FEASIBLE is 23) and 115 "MIP_infeasible_or_unbounded"
## (MIP_OPTIMAL_INFEAS; MIP INForUNBD is 119). The labels were shifted by one
## through the 10-13 block, and the mappings followed the labels.
CPX_STAT_OPTIMAL              <- 1L
CPX_STAT_UNBOUNDED            <- 2L
CPX_STAT_INFEASIBLE           <- 3L
CPX_STAT_INForUNBD            <- 4L
CPX_STAT_OPTIMAL_INFEAS       <- 5L
CPX_STAT_NUM_BEST             <- 6L
CPX_STAT_ABORT_IT_LIM         <- 10L
CPX_STAT_ABORT_TIME_LIM       <- 11L
CPX_STAT_ABORT_OBJ_LIM        <- 12L
CPX_STAT_ABORT_USER           <- 13L
CPX_STAT_ABORT_PRIM_OBJ_LIM   <- 21L
CPX_STAT_ABORT_DUAL_OBJ_LIM   <- 22L
CPX_STAT_FEASIBLE             <- 23L
CPX_STAT_FIRSTORDER           <- 24L
CPX_STAT_ABORT_DETTIME_LIM    <- 25L
CPX_STAT_BENDERS_NUM_BEST     <- 41L
CPXMIP_OPTIMAL                <- 101L
CPXMIP_OPTIMAL_TOL            <- 102L
CPXMIP_INFEASIBLE             <- 103L
CPXMIP_SOL_LIM                <- 104L
CPXMIP_NODE_LIM_FEAS          <- 105L
CPXMIP_NODE_LIM_INFEAS        <- 106L
CPXMIP_TIME_LIM_FEAS          <- 107L
CPXMIP_TIME_LIM_INFEAS        <- 108L
CPXMIP_FAIL_FEAS              <- 109L
CPXMIP_FAIL_INFEAS            <- 110L
CPXMIP_MEM_LIM_FEAS           <- 111L
CPXMIP_MEM_LIM_INFEAS         <- 112L
CPXMIP_ABORT_FEAS             <- 113L
CPXMIP_ABORT_INFEAS           <- 114L
CPXMIP_OPTIMAL_INFEAS         <- 115L
CPXMIP_FAIL_FEAS_NO_TREE      <- 116L
CPXMIP_FAIL_INFEAS_NO_TREE    <- 117L
CPXMIP_UNBOUNDED              <- 118L
CPXMIP_INForUNBD              <- 119L
CPXMIP_FEASIBLE_RELAXED_SUM   <- 120L
CPXMIP_OPTIMAL_RELAXED_SUM    <- 121L
CPXMIP_FEASIBLE_RELAXED_INF   <- 122L
CPXMIP_OPTIMAL_RELAXED_INF    <- 123L
CPXMIP_FEASIBLE_RELAXED_QUAD  <- 124L
CPXMIP_OPTIMAL_RELAXED_QUAD   <- 125L
CPXMIP_ABORT_RELAXED          <- 126L
CPXMIP_FEASIBLE               <- 127L
CPXMIP_POPULATESOL_LIM        <- 128L
CPXMIP_OPTIMAL_POPULATED      <- 129L
CPXMIP_OPTIMAL_POPULATED_TOL  <- 130L
CPXMIP_DETTIME_LIM_FEAS       <- 131L
CPXMIP_DETTIME_LIM_INFEAS     <- 132L
CPX_STAT_FEASIBLE_RELAXED_SUM  <- 14L
CPX_STAT_OPTIMAL_RELAXED_SUM   <- 15L
CPX_STAT_FEASIBLE_RELAXED_INF  <- 16L
CPX_STAT_OPTIMAL_RELAXED_INF   <- 17L
CPX_STAT_FEASIBLE_RELAXED_QUAD <- 18L
CPX_STAT_OPTIMAL_RELAXED_QUAD  <- 19L


# -- .cplex_normalize_solstat ------------------------------------------------
## CVXPY SOURCE: cplex_conif.py:75-145 (`_handle_solve_status`) -- collapse the
## MIP status codes onto their LP equivalents so the ladder below only has to
## reason about one vocabulary.
.cplex_normalize_solstat <- function(solstat) {
  if (solstat == CPXMIP_OPTIMAL) return(CPX_STAT_OPTIMAL)
  if (solstat == CPXMIP_INFEASIBLE) return(CPX_STAT_INFEASIBLE)
  if (solstat %in% c(CPXMIP_TIME_LIM_FEAS, CPXMIP_TIME_LIM_INFEAS))
    return(CPX_STAT_ABORT_TIME_LIM)
  if (solstat %in% c(CPXMIP_DETTIME_LIM_FEAS, CPXMIP_DETTIME_LIM_INFEAS))
    return(CPX_STAT_ABORT_DETTIME_LIM)
  if (solstat %in% c(CPXMIP_ABORT_FEAS, CPXMIP_ABORT_INFEAS))
    return(CPX_STAT_ABORT_USER)
  if (solstat == CPXMIP_OPTIMAL_INFEAS) return(CPX_STAT_OPTIMAL_INFEAS)
  if (solstat == CPXMIP_INForUNBD) return(CPX_STAT_INForUNBD)
  if (solstat == CPXMIP_UNBOUNDED) return(CPX_STAT_UNBOUNDED)
  ## feasopt results. CVXR never calls feasopt, so reaching one means the model
  ## was built by something other than this interface. Upstream raises
  ## AssertionError (cplex_conif.py:121-123); same here.
  if (solstat %in% c(CPX_STAT_FEASIBLE_RELAXED_SUM, CPXMIP_FEASIBLE_RELAXED_SUM,
                     CPX_STAT_OPTIMAL_RELAXED_SUM, CPXMIP_OPTIMAL_RELAXED_SUM,
                     CPX_STAT_FEASIBLE_RELAXED_INF, CPXMIP_FEASIBLE_RELAXED_INF,
                     CPX_STAT_OPTIMAL_RELAXED_INF, CPXMIP_OPTIMAL_RELAXED_INF,
                     CPX_STAT_FEASIBLE_RELAXED_QUAD, CPXMIP_FEASIBLE_RELAXED_QUAD,
                     CPX_STAT_OPTIMAL_RELAXED_QUAD, CPXMIP_OPTIMAL_RELAXED_QUAD)) {
    cli_abort("feasopt status encountered: {.val {solstat}}.")
  }
  ## Conflict-refiner results, 30-39. Same reasoning (cplex_conif.py:125-136).
  if (solstat >= 30L && solstat <= 39L) {
    cli_abort("conflict refiner status encountered: {.val {solstat}}.")
  }
  if (solstat %in% c(CPX_STAT_FEASIBLE, CPXMIP_FEASIBLE)) return(CPX_STAT_FEASIBLE)
  if (solstat == CPX_STAT_BENDERS_NUM_BEST) return(CPX_STAT_NUM_BEST)
  solstat
}


# -- .cplex_get_status -------------------------------------------------------
## CVXPY SOURCE: cplex_conif.py:147-204 (`get_status`).
##
## This replaces a FLAT TABLE, and the difference is not stylistic: upstream
## reads two secondary conditions -- `model.solution.is_primal_feasible()` and
## `is_dual_feasible()` -- that decide the answer for eleven status codes. A
## table cannot express "iteration limit, but a feasible point was found", so
## CVXR answered it wrongly. Measured, `psolve(prob, solver = "CPLEX",
## itlim = 1)` on a 60-variable LP: CPLEX returns status 10 WITH a feasible
## primal and duals, and CVXR reported
##     Solver "CPLEX" failed.
## discarding a usable solution. CVXPY returns `optimal_inaccurate` with it.
##
## `pfeas` / `dfeas`: Rcplex exposes no is_primal_feasible()/is_dual_feasible(),
## so they are read off the returned solution -- a primal iterate is present iff
## `xopt` is non-NULL and non-NA (verified: itlim = 1 yields a full xopt, a
## tilim so small the solve never starts yields NA), and duals likewise from
## `extra$lambda`. Upstream notes at cplex_conif.py:149 that dfeas is always
## false for a MIP; CVXR passes `dfeas = FALSE` for the MIP path rather than
## inferring it.
.cplex_get_status <- function(solstat, pfeas, dfeas) {
  if (is.null(solstat) || length(solstat) != 1L || is.na(solstat)) {
    return(SOLVER_ERROR)
  }
  solstat <- .cplex_normalize_solstat(as.integer(solstat))

  if (solstat %in% c(CPXMIP_NODE_LIM_INFEAS, CPXMIP_FAIL_INFEAS,
                     CPXMIP_MEM_LIM_INFEAS, CPXMIP_FAIL_INFEAS_NO_TREE,
                     CPX_STAT_NUM_BEST)) {
    return(SOLVER_ERROR)
  }
  if (solstat %in% c(CPX_STAT_ABORT_USER, CPX_STAT_ABORT_IT_LIM,
                     CPX_STAT_ABORT_TIME_LIM, CPX_STAT_ABORT_DETTIME_LIM,
                     CPX_STAT_ABORT_OBJ_LIM, CPX_STAT_ABORT_PRIM_OBJ_LIM,
                     CPX_STAT_ABORT_DUAL_OBJ_LIM, CPXMIP_ABORT_RELAXED,
                     CPX_STAT_FIRSTORDER)) {
    return(if (pfeas) OPTIMAL_INACCURATE else SOLVER_ERROR)
  }
  if (solstat %in% c(CPXMIP_NODE_LIM_FEAS, CPXMIP_SOL_LIM,
                     CPXMIP_POPULATESOL_LIM, CPXMIP_FAIL_FEAS,
                     CPXMIP_MEM_LIM_FEAS, CPXMIP_FAIL_FEAS_NO_TREE,
                     CPX_STAT_FEASIBLE)) {
    return(if (dfeas) OPTIMAL else OPTIMAL_INACCURATE)
  }
  if (solstat %in% c(CPX_STAT_OPTIMAL, CPXMIP_OPTIMAL_TOL,
                     CPX_STAT_OPTIMAL_INFEAS, CPXMIP_OPTIMAL_POPULATED,
                     CPXMIP_OPTIMAL_POPULATED_TOL)) {
    return(OPTIMAL)
  }
  if (solstat %in% c(CPX_STAT_INFEASIBLE, CPXMIP_OPTIMAL_RELAXED_SUM,
                     CPXMIP_OPTIMAL_RELAXED_INF, CPXMIP_OPTIMAL_RELAXED_QUAD)) {
    return(INFEASIBLE)
  }
  if (solstat %in% c(CPXMIP_FEASIBLE_RELAXED_QUAD, CPXMIP_FEASIBLE_RELAXED_INF,
                     CPXMIP_FEASIBLE_RELAXED_SUM)) {
    return(SOLVER_ERROR)
  }
  if (solstat == CPX_STAT_UNBOUNDED) return(UNBOUNDED)
  if (solstat == CPX_STAT_INForUNBD) return(INFEASIBLE_OR_UNBOUNDED)
  SOLVER_ERROR
}


# -- .cplex_solution_feasibility ---------------------------------------------
## The pfeas / dfeas pair for an Rcplex result. `is_mip` forces dfeas FALSE,
## mirroring the note at cplex_conif.py:149.
.cplex_solution_feasibility <- function(solution, is_mip = FALSE) {
  xopt <- solution$xopt
  lambda <- solution$extra$lambda
  list(
    pfeas = !is.null(xopt) && length(xopt) > 0L && !anyNA(xopt),
    dfeas = !isTRUE(is_mip) && !is.null(lambda) && length(lambda) > 0L &&
            !anyNA(lambda)
  )
}

# -- CPLEX_Conic_Solver class ----------------------------------------
## CVXPY SOURCE: cplex_conif.py lines 207-214

#' @keywords internal
CPLEX_Conic_Solver <- new_class("CPLEX_Conic_Solver", parent = ConicSolver,
  package = "CVXR",
  constructor = function() {
    if (FALSE) new_object(S7_object())  ## S7 static-check guard
    .fast_new(CPLEX_Conic_Solver, S7_object(),
      .cache = new.env(parent = emptyenv()),
      MIP_CAPABLE = TRUE,
      BOUNDED_VARIABLES = TRUE,
      PSD_TRIANGLE_KIND = NA_character_,
      PSD_SQRT2_SCALING = NA,
      SUPPORTED_CONSTRAINTS = list(Zero, NonNeg, SOC),
      EXP_CONE_ORDER = NULL,
      REQUIRES_CONSTR = FALSE
    )
  }
)

method(solver_name, CPLEX_Conic_Solver) <- function(x) CPLEX_SOLVER

# -- reduction_accepts ------------------------------------------------

method(reduction_accepts, CPLEX_Conic_Solver) <- function(x, problem, ...) {
  if (!is.list(problem) || is.null(problem[["constraints"]])) return(FALSE)
  constrs <- problem[["constraints"]]
  all(vapply(constrs, function(c) {
    .s7_is(c, Zero) || .s7_is(c, NonNeg) || .s7_is(c, SOC)
  }, logical(1L)))
}

# -- reduction_apply --------------------------------------------------
## CVXPY SOURCE: cplex_conif.py apply() inherits ConicSolver structure.

method(reduction_apply, CPLEX_Conic_Solver) <- function(x, problem, ...) {
  data <- problem
  inv_data <- list()

  inv_data[[SOLVER_VAR_ID]] <- data[["x_id"]]

  constraints <- data[["constraints"]]
  cone_dims <- data[[SD_DIMS]]
  inv_data[[SD_DIMS]] <- cone_dims

  constr_map <- group_constraints(constraints)
  inv_data[[SOLVER_EQ_CONSTR]] <- constr_map[["Zero"]]
  inv_data[[SOLVER_NEQ_CONSTR]] <- c(
    constr_map[["NonNeg"]], constr_map[["SOC"]]
  )

  formatted <- format_constraints(
    constraints, data[[SD_A]], data[[SD_B]],
    exp_cone_order = x@EXP_CONE_ORDER
  )

  solver_data <- list()
  solver_data[[SD_C]] <- data[[SD_C]]
  solver_data[[SD_A]] <- -formatted$A
  solver_data[[SD_B]] <- formatted$b
  solver_data[[SD_DIMS]] <- cone_dims
  solver_data[[LOWER_BOUNDS]] <- data[[LOWER_BOUNDS]]
  solver_data[[UPPER_BOUNDS]] <- data[[UPPER_BOUNDS]]
  if (!is.null(data[[SD_P]])) solver_data[[SD_P]] <- data[[SD_P]]

  if (!is.null(data[["bool_idx"]])) solver_data[["bool_idx"]] <- data[["bool_idx"]]
  if (!is.null(data[["int_idx"]]))  solver_data[["int_idx"]]  <- data[["int_idx"]]

  inv_data[[SD_OFFSET]] <- data[[SD_OFFSET]]
  inv_data[["is_mip"]] <- (length(data[["bool_idx"]] %||% integer(0)) > 0L ||
                            length(data[["int_idx"]]  %||% integer(0)) > 0L)

  list(solver_data, inv_data)
}

# -- solve_via_data ---------------------------------------------------
## CVXPY SOURCE: cplex_conif.py lines 290-379 and add_model_soc_constr()
## lines 442-523. Rcplex takes one sparse `Amat` plus a QCP `QC` list:
##   min 0.5 x'Qx + c'x
##   s.t. Amat x {sense} bvec
##        q_i'x + x'Q_i x {L/G} r_i

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

  A <- data[[SD_A]]
  b <- data[[SD_B]]
  c_vec <- data[[SD_C]]
  dims <- data[[SD_DIMS]]
  n_orig <- length(c_vec)

  zero_dim <- dims@zero
  nonneg_dim <- dims@nonneg
  linear_dim <- zero_dim + nonneg_dim
  soc_total <- sum(dims@soc)
  n_soc_aux <- soc_total
  n_total <- n_orig + n_soc_aux

  ## Linear constraints are ordered Zero, NonNeg, then SOC rows.
  if (linear_dim > 0L) {
    A_linear <- A[seq_len(linear_dim), , drop = FALSE]
    b_linear <- b[seq_len(linear_dim)]
    sense <- character(linear_dim)
    if (zero_dim > 0L) sense[seq_len(zero_dim)] <- "E"
    if (nonneg_dim > 0L) sense[(zero_dim + 1L):linear_dim] <- "L"
  } else {
    A_linear <- Matrix::sparseMatrix(i = integer(0), j = integer(0),
                                      x = numeric(0), dims = c(0L, n_orig))
    b_linear <- numeric(0)
    sense <- character(0)
  }

  soc_eq_rows <- list()
  soc_eq_rhs <- numeric(0)
  qc_Q <- list()
  qc_L <- list()
  qc_dir <- character(0)
  qc_b <- numeric(0)
  aux_offset <- 0L

  if (soc_total > 0L) {
    soc_start <- linear_dim
    for (k in dims@soc) {
      soc_rows <- (soc_start + 1L):(soc_start + k)
      A_soc <- A[soc_rows, , drop = FALSE]
      b_soc <- b[soc_rows]

      ## CVXPY relation: aux_i = b_i - A_i x.
      ## Rcplex linear row form: -A_i x - aux_i = -b_i.
      for (i in seq_len(k)) {
        aux_col <- n_orig + aux_offset + i
        full_row <- Matrix::sparseMatrix(i = integer(0), j = integer(0),
                                          x = numeric(0), dims = c(1L, n_total))
        full_row[1L, seq_len(n_orig)] <- -A_soc[i, , drop = TRUE]
        full_row[1L, aux_col] <- -1.0
        soc_eq_rows[[length(soc_eq_rows) + 1L]] <- full_row
        soc_eq_rhs <- c(soc_eq_rhs, -b_soc[i])
      }

      ## Quadratic SOC row: sum(aux_tail^2) - aux_head^2 <= 0.
      aux_base <- n_orig + aux_offset
      q_idx <- aux_base + seq_len(k)
      q_val <- c(-1.0, rep(1.0, k - 1L))
      Qc <- Matrix::sparseMatrix(i = q_idx, j = q_idx, x = q_val,
                                  dims = c(n_total, n_total))
      qc_Q[[length(qc_Q) + 1L]] <- Qc
      qc_L[[length(qc_L) + 1L]] <- rep(0, n_total)
      qc_dir <- c(qc_dir, "L")
      qc_b <- c(qc_b, 0)

      aux_offset <- aux_offset + k
      soc_start <- soc_start + k
    }
  }

  ## Expand original linear rows to include SOC auxiliaries.
  if (linear_dim > 0L && n_soc_aux > 0L) {
    A_linear <- cbind(
      A_linear,
      Matrix::sparseMatrix(i = integer(0), j = integer(0), x = numeric(0),
                            dims = c(linear_dim, n_soc_aux))
    )
  }

  if (length(soc_eq_rows) > 0L) {
    A_soc_eq <- do.call(rbind, soc_eq_rows)
    Amat <- if (linear_dim > 0L) rbind(A_linear, A_soc_eq) else A_soc_eq
    bvec <- c(b_linear, soc_eq_rhs)
    sense <- c(sense, rep("E", length(soc_eq_rhs)))
  } else {
    Amat <- A_linear
    bvec <- b_linear
  }

  n_user_linear <- linear_dim
  if (nrow(Amat) == 0L) {
    ## Rcplex LP/QP paths expect at least one row; keep it out of dual mapping.
    Amat <- Matrix::sparseMatrix(i = integer(0), j = integer(0),
                                  x = numeric(0), dims = c(1L, n_total))
    bvec <- 0
    sense <- "L"
  }

  ## Objective: Rcplex uses 0.5 x'Qx + c'x, matching CVXR's P convention.
  c_full <- c(c_vec, rep(0, n_soc_aux))
  Qmat <- data[[SD_P]]
  if (!is.null(Qmat)) {
    if (n_soc_aux > 0L) {
      Qmat <- Matrix::bdiag(
        Qmat,
        Matrix::sparseMatrix(i = integer(0), j = integer(0), x = numeric(0),
                              dims = c(n_soc_aux, n_soc_aux))
      )
    }
    if (!inherits(Qmat, "dgCMatrix")) {
      Qmat <- methods::as(methods::as(Qmat, "generalMatrix"), "CsparseMatrix")
    }
  }

  lb_orig <- data[[LOWER_BOUNDS]] %||% rep(-Inf, n_orig)
  ub_orig <- data[[UPPER_BOUNDS]] %||% rep(Inf, n_orig)
  lb <- c(lb_orig, rep(-Inf, n_soc_aux))
  ub <- c(ub_orig, rep(Inf, n_soc_aux))

  if (soc_total > 0L) {
    soc_aux_offset <- 0L
    for (k in dims@soc) {
      head_idx <- n_orig + soc_aux_offset + 1L
      lb[head_idx] <- 0
      soc_aux_offset <- soc_aux_offset + k
    }
  }

  bool_idx <- data[["bool_idx"]]
  int_idx <- data[["int_idx"]]
  is_mip <- length(bool_idx %||% integer(0)) > 0L ||
    length(int_idx %||% integer(0)) > 0L

  vtype <- NULL
  if (is_mip) {
    vtype <- rep("C", n_total)
    if (length(bool_idx) > 0L) {
      for (idx in bool_idx) {
        vtype[idx] <- "B"
        lb[idx] <- max(lb[idx], 0)
        ub[idx] <- min(ub[idx], 1)
      }
    }
    if (length(int_idx) > 0L) {
      for (idx in int_idx) vtype[idx] <- "I"
    }
  }

  control <- list(trace = if (verbose) 1L else 0L)
  for (opt_name in names(solver_opts)) {
    control[[opt_name]] <- solver_opts[[opt_name]]
  }

  QC <- if (length(qc_Q) > 0L) {
    list(QC = list(Q = qc_Q, L = qc_L), dir = qc_dir, b = qc_b)
  } else {
    NULL
  }

  .cplex_call <- function() {
    Rcplex::Rcplex_solve_QCP(
      cvec = as.double(c_full),
      Amat = Amat,
      bvec = as.double(bvec),
      Qmat = Qmat,
      QC = QC,
      lb = as.double(lb),
      ub = as.double(ub),
      control = control,
      objsense = "min",
      sense = sense,
      vtype = vtype
    )
  }

  result <- tryCatch({
    if (!verbose) {
      res <- NULL
      capture.output(res <- .cplex_call(), type = "message")
      res
    } else {
      .cplex_call()
    }
  },
    error = function(e) {
      list(xopt = NA, obj = NA, status = -1L,
           extra = list(lambda = NA, slack = NA),
           .error = conditionMessage(e))
    }
  )

  result$.n_orig <- n_orig
  result$.n_user_linear <- n_user_linear
  result$.has_soc <- soc_total > 0L
  result$.is_mip <- is_mip
  result
}

# -- reduction_invert -------------------------------------------------

method(reduction_invert, CPLEX_Conic_Solver) <- function(x, solution, inverse_data, ...) {
  attr_list <- list()
  attr_list[[RK_EXTRA_STATS]] <- solution

  ## CVXPY SOURCE: cplex_conif.py:147-204 -- the decision procedure, not a
  ## table: eleven status codes are resolved by whether a primal (or dual)
  ## iterate is present. See `.cplex_get_status`.
  feas <- .cplex_solution_feasibility(solution,
                                      is_mip = isTRUE(inverse_data[["is_mip"]]))
  status <- .cplex_get_status(solution$status, feas$pfeas, feas$dfeas)

  if (status %in% SOLUTION_PRESENT) {
    opt_val <- solution$obj + inverse_data[[SD_OFFSET]]

    primal_vars <- list()
    primal_vars[[as.character(inverse_data[[SOLVER_VAR_ID]])]] <-
      solution$xopt[seq_len(solution$.n_orig)]

    dual_vars <- list()
    is_mip <- isTRUE(solution$.is_mip)
    has_soc <- isTRUE(solution$.has_soc)

    ## Rcplex does not expose QCP dual-slack data. For SOC problems, skip duals
    ## rather than returning a partial map that omits the SOC cone entries.
    if (!is_mip && !has_soc && !is.null(solution$extra$lambda) &&
        !any(is.na(solution$extra$lambda))) {
      y <- -solution$extra$lambda
      if (solution$.n_user_linear < length(y)) {
        y <- y[seq_len(solution$.n_user_linear)]
      }
      dims <- inverse_data[[SD_DIMS]]
      zero_dim <- dims@zero

      eq_dual <- if (zero_dim > 0L) {
        get_dual_values(
          y[seq_len(zero_dim)],
          extract_dual_value,
          inverse_data[[SOLVER_EQ_CONSTR]]
        )
      } else {
        list()
      }

      nonneg_dim <- dims@nonneg
      ineq_vec <- if (nonneg_dim > 0L) {
        y[(zero_dim + 1L):(zero_dim + nonneg_dim)]
      } else {
        numeric(0)
      }

      ineq_dual <- if (length(ineq_vec) > 0L) {
        get_dual_values(
          ineq_vec,
          extract_dual_value,
          inverse_data[[SOLVER_NEQ_CONSTR]]
        )
      } else {
        list()
      }

      dual_vars <- c(eq_dual, ineq_dual)
    }

    Solution(status, opt_val, primal_vars, dual_vars, attr_list)
  } else {
    failure_solution(status, attr_list)
  }
}

method(print, CPLEX_Conic_Solver) <- function(x, ...) {
  cat("CPLEX_Conic_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.