R/275_reductions_solvers_conic_solvers_mosek_conif.R

Defines functions .mosek_extract_attr dims_to_mosek_cones

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

## CVXPY SOURCE: reductions/solvers/conic_solvers/mosek_conif.py
## MOSEK solver interface via Rmosek
##
## MOSEK is a commercial conic solver with native support for LP, QP,
## SOCP, ExpCone, PowCone, and SDP. Uses the standard ConicSolver pipeline
## (ConeMatrixStuffing -> format_constraints -> ConicSolver.reduction_apply).
##
## Key design:
## - Zero constraints -> prob$A + prob$bc (equality bounds, blc = buc = b)
## - NonNeg constraints -> prob$A + prob$bc (inequality bounds, blc = -Inf, buc = b)
## - Conic cones (SOC, PSD, ExpCone, PowCone3D) -> ACC (prob$F + prob$g + prob$cones)
## - PSD via SVEC_PSD_CONE in ACC (not bar variables)
## - SCS's lower-tri column-major with sqrt2 scaling matches MOSEK's SVEC_PSD_CONE
## - ExpCone order: MOSEK PEXP(x,y,z) where x >= y*exp(z/y)
##   vs CVXR (x,y,z) where z >= y*exp(x/y), so EXP_CONE_ORDER = c(2,1,0)
## - QP via prob$qobj lower-tri triplets
## - Rmosek SVEC_PSD_CONE bug workaround: dummy bardim + barc when PSD present


# -- MOSEK status map ----------------------------------------------
## Maps MOSEK solution status strings to CVXR status constants.

MOSEK_STATUS_MAP <- list(
  "OPTIMAL"                  = OPTIMAL,
  "INTEGER_OPTIMAL"          = OPTIMAL,           # MIP: optimal integer solution
  "PRIMAL_INFEASIBLE_CER"    = INFEASIBLE,
  "DUAL_INFEASIBLE_CER"      = UNBOUNDED,
  "NEAR_OPTIMAL"             = OPTIMAL_INACCURATE,
  "PRIMAL_ILLPOSED_CER"      = INFEASIBLE,
  "DUAL_ILLPOSED_CER"        = UNBOUNDED,
  "PRIMAL_AND_DUAL_FEASIBLE" = OPTIMAL_INACCURATE,
  "PRIMAL_FEASIBLE"          = OPTIMAL_INACCURATE, # MIP: feasible (e.g. time-limited)
  ## Re-pointed to OPTIMAL_INACCURATE, on a COPY of the map, when the user
  ## passes `accept_unknown = TRUE`. See solve_via_data / reduction_invert.
  ## CVXPY SOURCE: mosek_conif.py:456-458.
  "UNKNOWN"                  = SOLVER_ERROR
)

## CVXPY SOURCE: mosek_conif.py:457 -- the option key. Named like
## CLARABEL_ACCEPT_UNKNOWN (clarabel_conif.R:64), which uses the same carrier.
MOSEK_ACCEPT_UNKNOWN <- "accept_unknown"

# -- dims_to_mosek_cones: ConeDims -> prob$cones matrix -------------
## Returns a 3-row matrix with one column per cone block.
## Row 1: type string, Row 2: dimension, Row 3: conepar.
## Also returns the total number of ACC rows (for building F and g).

dims_to_mosek_cones <- function(cone_dims) {
  cols <- list()

  ## NOTE: NonNeg is handled via prob$A/bc bounds, NOT ACC cones.

  ## SOC -> QUAD
  if (length(cone_dims@soc) > 0L) {
    for (q in cone_dims@soc) {
      cols <- c(cols, list(list("QUAD", q, NULL)))
    }
  }

  ## PSD -> SVEC_PSD_CONE
  if (length(cone_dims@psd) > 0L) {
    for (d in cone_dims@psd) {
      svec_dim <- (d * (d + 1L)) %/% 2L
      cols <- c(cols, list(list("SVEC_PSD_CONE", svec_dim, NULL)))
    }
  }

  ## ExpCone -> PEXP (3 elements each)
  if (cone_dims@exp > 0L) {
    for (i in seq_len(cone_dims@exp)) {
      cols <- c(cols, list(list("PEXP", 3L, NULL)))
    }
  }

  ## PowCone3D -> PPOW (3 elements each, with alpha parameters)
  ## MOSEK PPOW expects conepar = c(alpha, 1-alpha) for x1^alpha * x2^(1-alpha) >= |x3|
  ## CVXR stores single alpha in cone_dims@p3d
  if (length(cone_dims@p3d) > 0L) {
    for (alpha in cone_dims@p3d) {
      cols <- c(cols, list(list("PPOW", 3L, c(alpha, 1 - alpha))))
    }
  }

  if (length(cols) == 0L) return(NULL)

  ## Build 3-row matrix
  cones_mat <- matrix(list(), nrow = 3, ncol = length(cols))
  rownames(cones_mat) <- c("type", "dim", "conepar")
  for (j in seq_along(cols)) {
    cones_mat[1, j] <- cols[[j]][[1L]]
    cones_mat[2, j] <- cols[[j]][[2L]]
    cones_mat[3, j] <- list(cols[[j]][[3L]])
  }
  cones_mat
}

# -- Mosek_Solver class --------------------------------------------

#' @keywords internal
Mosek_Solver <- new_class("Mosek_Solver", parent = ConicSolver, package = "CVXR",
  constructor = function() {
    if (FALSE) new_object(S7_object())  ## S7 static-check guard
    .fast_new(Mosek_Solver, S7_object(),
      .cache = new.env(parent = emptyenv()),
      MIP_CAPABLE = TRUE,
      BOUNDED_VARIABLES = FALSE,
      ## CVXPY SOURCE: mosek_conif.py lines 110-111 -- BUT the scaling flag is a
      ## DELIBERATE divergence.  CVXPY sets PSD_SQRT2_SCALING = False because its
      ## MOSEK interface feeds the unscaled lower triangle to a bare PSD cone;
      ## CVXR's MOSEK interface uses MOSEK's SVEC_PSD_CONE, deliberately reusing
      ## SCS's sqrt(2)-scaled lower-tri format (ADR DM.2), so it must pass TRUE.
      ## Copying CVXPY's value mechanically here would break every MOSEK SDP:
      ## the principle is minimize deviation, not transcribe blindly (D_19.6).
      PSD_TRIANGLE_KIND = TriangleKind$LOWER,
      PSD_SQRT2_SCALING = TRUE,
      ## CVXPY SOURCE: mosek_conif.py:109 -- SvecPSD: MOSEK's SVEC_PSD_CONE
      ## takes the packed triangle, which PSDToSvecPSD produces.
      SUPPORTED_CONSTRAINTS = list(Zero, NonNeg, SOC, ExpCone,
                                   PowCone3D, SvecPSD),
      EXP_CONE_ORDER = c(2L, 1L, 0L),
      REQUIRES_CONSTR = FALSE
    )
  }
)

method(solver_name, Mosek_Solver) <- function(x) MOSEK_SOLVER

# -- MOSEK solve_via_data ------------------------------------------
## Converts CVXR solver data (A, b, c, dims, P) to MOSEK prob list
## and calls Rmosek::mosek().

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

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

  A_full <- data[[SD_A]]
  b_full <- data[[SD_B]]
  c_vec  <- data[[SD_C]]
  dims   <- data[[SD_DIMS]]
  n      <- length(c_vec)  # number of variables

  ## -- MIP plumbing (CVXPY SOURCE: mosek_conif.py _build_slack_task) ----
  ## CVXR's conic reduction stashes bool_idx/int_idx (1-based) on data.
  ## MOSEK does NOT support mixed-integer SDP (CVXPY documents this at
  ## mosek_conif.py:393-394); reject the combination upfront.
  bool_idx <- data[["bool_idx"]] %||% integer(0)
  int_idx  <- data[["int_idx"]]  %||% integer(0)
  is_mip   <- length(bool_idx) > 0L || length(int_idx) > 0L

  if (is_mip && length(dims@psd) > 0L) {
    cli::cli_abort(c(
      "MOSEK does not support mixed-integer semidefinite programs.",
      i = "Drop the integer/boolean variables, or use a different solver for the MISDP."
    ))
  }

  ## -- Build MOSEK prob list --------------------------------------
  prob <- list()
  prob$sense <- "min"
  prob$c     <- c_vec
  prob$bx    <- rbind(blx = rep(-Inf, n), bux = rep(Inf, n))

  ## Boolean variables: bound to [0, 1].  Combined with intsub (below)
  ## this is exactly MOSEK's "binary" treatment.
  if (length(bool_idx) > 0L) {
    prob$bx[1L, bool_idx] <- 0
    prob$bx[2L, bool_idx] <- 1
  }

  ## Integer subindex (1-based).  Union of boolean and integer variables:
  ## both become MSK_VAR_TYPE_INT inside MOSEK; booleans are additionally
  ## constrained to [0,1] via bx above.
  if (is_mip) {
    prob$intsub <- as.integer(sort(unique(c(bool_idx, int_idx))))
  }

  ## -- Row splitting ----------------------------------------------
  ## Solver convention: data$A * x + s = data$b, s in K
  ##
  ## MOSEK row assignment:
  ##   Zero rows     -> prob$A/bc with blc = buc = b  (equality)
  ##   NonNeg rows   -> prob$A/bc with blc = b, buc = Inf (inequality)
  ##   Conic rows    -> ACC: F*x + g in K  where F = -A, g = b
  ##
  ## NonNeg goes to prob$A/bc (not ACC) because MOSEK does not allow

  ## mixing quadratic objectives (qobj) with conic ACC constraints.

  zero_dim   <- dims@zero
  nonneg_dim <- dims@nonneg
  linear_dim <- zero_dim + nonneg_dim

  ## Linear portion (Zero + NonNeg) -> prob$A / prob$bc
  if (linear_dim > 0L) {
    A_linear <- A_full[seq_len(linear_dim), , drop = FALSE]
    b_linear <- b_full[seq_len(linear_dim)]
    prob$A <- A_linear

    ## Bounds: Zero rows get equality, NonNeg rows get one-sided
    ## Zero: A*x + s = b, s = 0  ->  A*x = b  ->  blc = buc = b
    ## NonNeg: A*x + s = b, s >= 0  ->  A*x <= b  ->  blc = -Inf, buc = b
    blc <- rep(-Inf, linear_dim)
    buc <- b_linear
    if (zero_dim > 0L) {
      blc[seq_len(zero_dim)] <- b_linear[seq_len(zero_dim)]
    }
    prob$bc <- rbind(blc = blc, buc = buc)
  } else {
    prob$A  <- Matrix::sparseMatrix(i = integer(0), j = integer(0),
                                    x = numeric(0), dims = c(0L, n))
    prob$bc <- rbind(blc = numeric(0), buc = numeric(0))
  }

  ## -- ACC: conic rows (SOC, PSD, ExpCone, PowCone3D) ------------
  ## ACC: F*x + g in K  where s = b - A*x in K  ->  F = -A, g = b
  total_rows <- nrow(A_full)
  conic_rows <- if (linear_dim < total_rows) (linear_dim + 1L):total_rows else integer(0)

  if (length(conic_rows) > 0L) {
    prob$F <- -A_full[conic_rows, , drop = FALSE]
    prob$g <- b_full[conic_rows]

    ## Build cone specifications (excluding NonNeg which is in prob$A/bc)
    cones_mat <- dims_to_mosek_cones(dims)
    if (!is.null(cones_mat)) {
      prob$cones <- cones_mat
    }
  }

  ## -- SVEC_PSD_CONE bug workaround ------------------------------
  ## Rmosek 11.1.1 BETA crashes if SVEC_PSD_CONE is used but prob$barc
  ## is not defined. Add dummy 1x1 bar variable with zero objective.
  if (length(dims@psd) > 0L) {
    prob$bardim <- c(1L)
    prob$barc <- list(j = 1L, k = 1L, l = 1L, v = 0)
  }

  ## -- QP: quadratic objective via prob$qobj ----------------------
  if (!is.null(data[[SD_P]])) {
    P <- data[[SD_P]]
    ## Extract lower-triangle triplets (MOSEK wants i >= j, 1-based)
    P_tril <- Matrix::tril(P)
    P_tril <- methods::as(P_tril, "TsparseMatrix")
    if (length(P_tril@x) > 0L) {
      prob$qobj <- list(
        i = P_tril@i + 1L,   # 1-based
        j = P_tril@j + 1L,   # 1-based
        v = P_tril@x
      )
    }
  }

  ## -- Solver options ---------------------------------------------
  ## `getinfo = TRUE` makes Rmosek return MOSEK info-items as
  ## `result$dinfo` / `result$iinfo` / `result$liinfo`.  Consumed by
  ## reduction_invert(Mosek_Solver) to populate `solver_stats`'s
  ## SOLVE_TIME, NUM_ITERS, and EXTRA_STATS fields.  Mirrors CVXPY
  ## mosek_conif.py:531-540 (which queries the same enum items via
  ## task.getdouinf / task.getintinf / task.getlintinf).
  opts <- list(verbose = if (verbose) 1L else 0L, soldetail = 1L,
               getinfo = TRUE)
  ## CVXPY SOURCE: mosek_conif.py:456-458 --
  ##     if solver_opts['accept_unknown']:
  ##         STATUS_MAP[mosek.solsta.unknown] = s.OPTIMAL_INACCURATE
  ## `accept_unknown` is a CVXPY option, not a MOSEK parameter, so it is taken
  ## out here rather than forwarded: Rmosek would carry an unrecognized name
  ## into `opts` and MOSEK would reject or ignore it.
  ##
  ## It travels to reduction_invert() on the RESULT, for the reason spelled out
  ## in clarabel_conif.R:145-152 -- upstream reads it off
  ## `inverse_data.solver_options` because its Solver.solve() runs apply/solve/
  ## invert in one call, whereas CVXR's solve API is decomposed and public, so
  ## invert only ever sees compile-time inverse_data. Same information, same
  ## precondition, different carrier.
  accept_unknown <- isTRUE(solver_opts[[MOSEK_ACCEPT_UNKNOWN]])
  solver_opts[[MOSEK_ACCEPT_UNKNOWN]] <- NULL
  ## Route options to their Rmosek carrier. Rmosek splits them: SOLVER
  ## parameters live in problem$dparam/$iparam/$sparam (mosek.Rd; both
  ## "MSK_DPAR_X" and short "X" spellings accepted -- verified
  ## empirically 2026-08-22), while `opts` holds only the INTERFACE
  ## options listed below. The previous code merged EVERYTHING into
  ## `opts`, so no solver parameter could ever reach MOSEK; worse,
  ## Rmosek then returned a result without $response$code and the
  ## unguarded comparison in reduction_invert crashed with "missing
  ## value where TRUE/FALSE needed".
  ## CVXPY SOURCE: mosek_conif.py:628-676 -- `mosek_params` entries are
  ## applied by MSK_DPAR_/MSK_IPAR_/MSK_SPAR_ prefix and leftover
  ## unknown options raise ValueError (:674-676). CVXR mirrors that
  ## contract on Rmosek's carriers, and additionally accepts Rmosek's
  ## native dparam/iparam/sparam lists.
  ## CVXPY SOURCE: mosek_conif.py:690-710 parse_eps_keyword() -- the `eps`
  ## keyword fans out to every MOSEK tolerance parameter
  ## (tolerance_params(), mosek_conif.py:713-739, reproduced verbatim
  ## below); tolerances that were set explicitly -- through ANY of CVXR's
  ## carriers, including the standard-name translation -- take precedence.
  if (!is.null(solver_opts[["eps"]])) {
    eps <- solver_opts[["eps"]]
    solver_opts[["eps"]] <- NULL
    tol_params <- c(
      "MSK_DPAR_INTPNT_CO_TOL_DFEAS", "MSK_DPAR_INTPNT_CO_TOL_INFEAS",
      "MSK_DPAR_INTPNT_CO_TOL_MU_RED", "MSK_DPAR_INTPNT_CO_TOL_PFEAS",
      "MSK_DPAR_INTPNT_CO_TOL_REL_GAP",
      "MSK_DPAR_INTPNT_TOL_DFEAS", "MSK_DPAR_INTPNT_TOL_INFEAS",
      "MSK_DPAR_INTPNT_TOL_MU_RED", "MSK_DPAR_INTPNT_TOL_PFEAS",
      "MSK_DPAR_INTPNT_TOL_REL_GAP",
      "MSK_DPAR_BASIS_REL_TOL_S", "MSK_DPAR_BASIS_TOL_S",
      "MSK_DPAR_BASIS_TOL_X",
      "MSK_DPAR_MIO_TOL_ABS_GAP", "MSK_DPAR_MIO_TOL_ABS_RELAX_INT",
      "MSK_DPAR_MIO_TOL_FEAS", "MSK_DPAR_MIO_TOL_REL_GAP")
    already_set <- c(names(solver_opts),
                     names(solver_opts[["mosek_params"]]),
                     names(solver_opts[["dparam"]]),
                     paste0("MSK_DPAR_", names(solver_opts[["dparam"]])))
    for (tp in tol_params) {
      if (!(tp %in% already_set)) solver_opts[[tp]] <- eps
    }
  }
  rmosek_iface_opts <- c("verbose", "usesol", "useparam", "soldetail",
                         "getinfo", "writebefore", "writeafter")
  route_msk <- function(name, value) {
    if (grepl("^MSK_DPAR_", name)) prob$dparam[[name]] <<- value
    else if (grepl("^MSK_IPAR_", name)) prob$iparam[[name]] <<- value
    else if (grepl("^MSK_SPAR_", name)) prob$sparam[[name]] <<- value
    else cli_abort(c(
      "Invalid MOSEK parameter {.val {name}}.",
      "i" = "Parameter names must start with {.code MSK_DPAR_}, {.code MSK_IPAR_}, or {.code MSK_SPAR_}; short names go in the {.arg dparam}/{.arg iparam}/{.arg sparam} lists."
    ))
  }
  for (opt_name in names(solver_opts)) {
    val <- solver_opts[[opt_name]]
    if (opt_name %in% c("dparam", "iparam", "sparam")) {
      ## Rmosek-native parameter lists, merged into the problem.
      prob[[opt_name]] <- utils::modifyList(
        if (is.null(prob[[opt_name]])) list() else as.list(prob[[opt_name]]),
        as.list(val))
    } else if (identical(opt_name, "mosek_params")) {
      ## CVXPY's carrier, accepted so migrated code keeps working.
      for (p in names(val)) route_msk(p, val[[p]])
    } else if (grepl("^MSK_[DIS]PAR_", opt_name)) {
      route_msk(opt_name, val)
    } else if (opt_name %in% rmosek_iface_opts) {
      opts[[opt_name]] <- val
    } else {
      ## CVXPY SOURCE: mosek_conif.py:674-676 -- unknown options raise.
      cli_abort(c(
        "Invalid option {.arg {opt_name}} for solver {.val MOSEK}.",
        "i" = "Use {.arg dparam}/{.arg iparam}/{.arg sparam} lists, MSK_*-prefixed parameter names, or the Rmosek interface options ({.val {rmosek_iface_opts}})."
      ))
    }
  }

  ## -- Warm-start: pass previous solution as initial point -------
  ## Rmosek supports warm-start via prob$sol: pass the previous
  ## solution structure and MOSEK uses it as initial point.
  ## opts$usesol = TRUE is the Rmosek default.
  ## NOTE: CVXPY does NOT implement MOSEK warm-start -- this is an
  ## R-specific enhancement using native Rmosek capabilities.
  cache_key <- MOSEK_SOLVER
  if (warm_start && !is_mip && !is.null(solver_cache) && exists(cache_key, envir = solver_cache)) {
    cached <- get(cache_key, envir = solver_cache)
    cached_sol <- cached$sol$itr
    if (!is.null(cached_sol) &&
        !is.null(MOSEK_STATUS_MAP[[cached_sol$solsta]]) &&
        MOSEK_STATUS_MAP[[cached_sol$solsta]] %in% SOLUTION_PRESENT &&
        !is.null(cached_sol$xx) && length(cached_sol$xx) == n) {
      prob$sol <- cached$sol
    }
  }

  ## -- Call MOSEK -------------------------------------------------
  result <- Rmosek::mosek(prob, opts)

  ## -- Cache for future warm-starts ------------------------------
  ## Only cache continuous solves; MIP warm-start via integer feasible
  ## solution is not implemented in this path.
  if (!is_mip && !is.null(solver_cache) && isTRUE(result$response$code == 0L) &&
      !is.null(result$sol$itr)) {
    assign(cache_key, result, envir = solver_cache)
  }

  ## Attached AFTER caching so the cached entry stays a pure solver result.
  result[[MOSEK_ACCEPT_UNKNOWN]] <- accept_unknown

  result
}

# -- MOSEK info-item -> attr_list helper ---------------------------
## CVXPY SOURCE: mosek_conif.py v1.8.2 lines 531-540.
##
## Pull MOSEK's per-solve info-items off the Rmosek result list (which
## carries them under `dinfo` / `iinfo` / `liinfo` when the call was
## made with `getinfo = TRUE`) and pack them into the CVXR attr_list
## using the same keys Problem$solver_stats reads (RK_SOLVE_TIME,
## RK_NUM_ITERS, RK_EXTRA_STATS).
##
## NUM_ITERS mirrors CVXPY exactly: sum of intpnt_iter + sim_primal_iter
## + sim_dual_iter + mio_num_relax (so simplex / interior-point /
## branch-and-bound nodes all roll into one number, regardless of
## which optimizer MOSEK chose).  MIO-specific fields go in
## EXTRA_STATS, matching CVXPY's dict layout.
##
## Defensive: any missing info-item resolves to 0 / NULL via the
## `%||%` fallback rather than crashing the solver pipeline.

.mosek_extract_attr <- function(result) {
  `%||%` <- function(a, b) if (is.null(a)) b else a
  dinfo  <- result$dinfo  %||% list()
  iinfo  <- result$iinfo  %||% list()
  liinfo <- result$liinfo %||% list()

  attr_list <- list()
  attr_list[[RK_SOLVE_TIME]] <- dinfo[["OPTIMIZER_TIME"]] %||% NA_real_

  n_intpnt <- iinfo[["INTPNT_ITER"]]      %||% 0L
  n_sim_p  <- iinfo[["SIM_PRIMAL_ITER"]]  %||% 0L
  n_sim_d  <- iinfo[["SIM_DUAL_ITER"]]    %||% 0L
  n_mio_r  <- iinfo[["MIO_NUM_RELAX"]]    %||% 0L
  attr_list[[RK_NUM_ITERS]] <-
    as.integer(n_intpnt + n_sim_p + n_sim_d + n_mio_r)

  extra <- list()
  extra[["mio_intpnt_iter"]]  <- liinfo[["MIO_INTPNT_ITER"]]  %||% 0L
  extra[["mio_simplex_iter"]] <- liinfo[["MIO_SIMPLEX_ITER"]] %||% 0L
  attr_list[[RK_EXTRA_STATS]] <- extra

  attr_list
}

# -- MOSEK reduction_invert ----------------------------------------
## Parses MOSEK-specific result format into a CVXR Solution.

method(reduction_invert, Mosek_Solver) <- function(x, solution, inverse_data, ...) {
  ## CVXPY SOURCE: mosek_conif.py:531-540 -- attr is populated once
  ## per solve, regardless of status.  Done here so failure paths
  ## (SOLVER_ERROR / INFEASIBLE / UNBOUNDED) still surface timing
  ## and iteration counts via solver_stats().
  attr_list <- .mosek_extract_attr(solution)

  ## Check for MOSEK errors. A missing or NA response code (Rmosek can
  ## return one when it rejects its inputs before optimizing) is a solver
  ## error too -- the old bare `!= 0L` comparison turned that case into
  ## "missing value where TRUE/FALSE needed", masking the real failure.
  if (!isTRUE(solution$response$code == 0L)) {
    return(failure_solution(SOLVER_ERROR, attr_list))
  }

  ## MIP solves return the integer-feasible point in sol$int; continuous
  ## solves return the interior-point solution in sol$itr.  Rmosek's
  ## getspecs_soltype maps MSK_SOL_ITG -> "int" (rmsk_utils_mosek.cc:144).
  ## CVXR's conic reduction sets is_mip on the inverse_data when bool/int
  ## variables are present (conic_solver.R:280).
  is_mip <- isTRUE(inverse_data[["is_mip"]])
  sol <- if (is_mip) solution$sol$int else solution$sol$itr
  if (is.null(sol)) {
    return(failure_solution(SOLVER_ERROR, attr_list))
  }

  ## Map MOSEK status to CVXR status.
  ## Mirrors CVXPY mosek_conif.py v1.8.2 lines 541-560: check prosta first
  ## for the two infeasibility certificates, then fall back to the solsta map.
  ##   - For MIP, prosta == "PRIMAL_INFEASIBLE" => INFEASIBLE
  ##     (solsta is "UNKNOWN" because no integer-feasible solution was found).
  ##   - prosta == "DUAL_INFEASIBLE" => UNBOUNDED for both continuous and MIP.
  ## (CVXPY also recovers an IIS via dualization in the dual_infeas branch;
  ## we do not have that infrastructure in CVXR yet, so we only set status.)
  prosta <- sol$prosta
  if (is_mip && identical(prosta, "PRIMAL_INFEASIBLE")) {
    status <- INFEASIBLE
  } else if (identical(prosta, "DUAL_INFEASIBLE")) {
    status <- UNBOUNDED
  } else {
    solsta <- sol$solsta
    ## CVXPY SOURCE: mosek_conif.py:456-458 -- take a COPY of the map and
    ## re-point the `unknown` entry when `accept_unknown` was requested. The
    ## flag rides on the solver result; see solve_via_data() above.
    ## Guarded on a primal iterate being present, so the status is never
    ## promoted on an empty solution -- the same precondition Clarabel's
    ## version applies (clarabel_conif.R:154-157).
    status_map <- MOSEK_STATUS_MAP
    if (isTRUE(solution[[MOSEK_ACCEPT_UNKNOWN]]) && !is.null(sol$xx)) {
      status_map[["UNKNOWN"]] <- OPTIMAL_INACCURATE
    }
    status <- status_map[[as.character(solsta)]]
    if (is.null(status)) status <- SOLVER_ERROR
  }

  if (status %in% SOLUTION_PRESENT) {
    dims <- inverse_data[[SD_DIMS]]
    opt_val <- sol$pobjval + inverse_data[[SD_OFFSET]]

    ## Primal variables from sol$xx
    primal_vars <- list()
    primal_vars[[as.character(inverse_data[[SOLVER_VAR_ID]])]] <- sol$xx

    ## MIP path: MOSEK does not return dual variables for integer
    ## solutions; return the primal-only Solution and skip the dual
    ## reconstruction below (mirrors CVXPY's MIP-path behaviour).
    if (is_mip) {
      return(Solution(status, opt_val, primal_vars, list(), attr_list))
    }

    ## -- Dual variables -------------------------------------------
    ## Rmosek 11.x does NOT return sol$y directly. Instead, linear
    ## constraint duals come from sol$slc (lower bound slack) and
    ## sol$suc (upper bound slack).
    ##
    ## Sign convention: MOSEK's y = slc - suc satisfies A^T y = c,
    ## but Clarabel/SCS use z satisfying A^T z = -c. So z = -(slc - suc)
    ## = suc - slc.  This maps both Zero and NonNeg duals correctly.
    ##
    ## ACC conic duals come from sol$doty (already in CVXR convention).
    ##
    ## CVXR inverse_data splits:
    ##   SOLVER_EQ_CONSTR  -> Zero constraints
    ##   SOLVER_NEQ_CONSTR -> [NonNeg, SOC, PSD, ExpCone, PowCone3D]
    ##
    ## We concatenate nonneg portion of linear_y with sol$doty to form
    ## the full ineq_dual vector.

    zero_dim   <- dims@zero
    nonneg_dim <- dims@nonneg
    linear_dim <- zero_dim + nonneg_dim

    ## Reconstruct dual vector from slc/suc (suc - slc = Clarabel convention)
    linear_y <- if (linear_dim > 0L && !is.null(sol$suc) &&
                    length(sol$suc) >= linear_dim) {
      sol$suc[seq_len(linear_dim)] - sol$slc[seq_len(linear_dim)]
    } else {
      numeric(0)
    }

    ## Equality (zero cone) duals
    if (zero_dim > 0L && length(linear_y) >= zero_dim) {
      eq_dual <- get_dual_values(
        linear_y[seq_len(zero_dim)],
        extract_dual_value,
        inverse_data[[SOLVER_EQ_CONSTR]]
      )
    } else {
      eq_dual <- list()
    }

    ## NonNeg duals from linear_y
    nonneg_duals <- if (nonneg_dim > 0L && length(linear_y) >= linear_dim) {
      linear_y[(zero_dim + 1L):linear_dim]
    } else {
      numeric(0)
    }

    conic_duals <- if (!is.null(sol$doty) && length(sol$doty) > 0L) {
      doty <- sol$doty

      ## Un-permute ExpCone dual blocks from MOSEK order [2,1,0] back to CVXR [0,1,2]
      if (dims@exp > 0L) {
        ## Calculate offset to ExpCone blocks within doty
        ## ACC order (no NonNeg): SOC, PSD, ExpCone, PowCone3D
        exp_offset <- sum(dims@soc)
        if (length(dims@psd) > 0L) {
          exp_offset <- exp_offset + sum(vapply(dims@psd, function(d) (d * (d + 1L)) %/% 2L, integer(1L)))
        }
        for (k in seq_len(dims@exp)) {
          base_idx <- exp_offset + (k - 1L) * 3L
          ## Swap indices 1 and 3 (MOSEK [z,y,x] -> CVXR [x,y,z])
          tmp <- doty[base_idx + 1L]
          doty[base_idx + 1L] <- doty[base_idx + 3L]
          doty[base_idx + 3L] <- tmp
        }
      }
      doty
    } else {
      numeric(0)
    }

    ## Concatenate for get_dual_values (order matches SOLVER_NEQ_CONSTR)
    full_ineq_vec <- c(nonneg_duals, conic_duals)
    if (length(full_ineq_vec) > 0L && length(inverse_data[[SOLVER_NEQ_CONSTR]]) > 0L) {
      ineq_dual <- get_dual_values(
        full_ineq_vec,
        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)
  }
}

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