R/280_reductions_solvers_conic_solvers_highs_conif.R

Defines functions .highs_set_user_options .unpack_highs_options validate_column_name

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

## CVXPY SOURCE: reductions/solvers/conic_solvers/highs_conif.py
## HiGHS conic solver interface for LP/MILP problems
##
## HiGHS conic path: for LP and MILP problems (Zero + NonNeg only).
## Uses the standard ConicSolver pipeline (format_constraints -> negate A).
## MIP-capable: supports boolean and integer variables.
##
## QP problems are handled by HiGHS_QP_Solver (qp_solvers/highs_qp_solver.R).


# -- LP-format column-name validation ----------------------------------------
## CVXPY SOURCE: highs_conif.py lines 32-49
## Validates a variable/column name against HiGHS LP-file format rules; used
## when writing a model to an .lp file.  A pure string check -- no dependency
## on the highs package.  Pattern is CVXPY 1.9.3's VALID_COLUMN_NAME_PATTERN
## (Python's `{,254}` written as PCRE `{0,254}`): the tail class ends `.=()<>`.
## 1.8.2's also allowed `[` and `]` there and listed them on the symbols line
## of the message; 1.9.3 dropped both (claims audit C0774). The "not begin
## with" line still names `[]`, as upstream's does.

VALID_COLUMN_NAME_PATTERN <- paste0(
  "^(?!st$|bounds$|min$|max$|bin$|binary$|gen$|semi$|end$)",
  "[a-df-zA-DF-Z\"!#$%&/}{,;?@_\u2018\u2019'`|~]{1}",
  "[a-zA-Z0-9\"!#$%&/}{,;?@_\u2018\u2019'`|~.=()<>]{0,254}$"
)

INVALID_COLUMN_NAME_MESSAGE_TEMPLATE <- paste0(
  "Invalid column name: {name}",
  "\nA column name must:",
  "\n- not be equal to one of the keywords: st, bounds, min, max, bin, binary, gen, semi or end",
  "\n- not begin with a number, the letter e or E or any of the following characters: .=()<>[]",
  "\n- be alphanumeric (a-z, A-Z, 0-9) or one of these symbols: \"!#$%&/}{,;?@_\u2018\u2019'`|~.=()<>",
  "\n- be no longer than 255 characters."
)

validate_column_name <- function(name) {
  ## CVXPY SOURCE: highs_conif.py validate_column_name (lines 46-49)
  if (!grepl(VALID_COLUMN_NAME_PATTERN, name, perl = TRUE)) {
    msg <- gsub("{name}", name, INVALID_COLUMN_NAME_MESSAGE_TEMPLATE, fixed = TRUE)
    ## Escape literal braces so cli/glue renders them verbatim.
    msg <- gsub("}", "}}", gsub("{", "{{", msg, fixed = TRUE), fixed = TRUE)
    cli::cli_abort(msg)
  }
  invisible(NULL)
}


# -- Solver options (shared with HiGHS_QP_Solver) -----------------------------
## CVXPY SOURCE: highs_conif.py:98-103
##   def unpack_highs_options_inplace(solver_opts) -> None:
##       highs_options = solver_opts.pop("highs_options", dict())
##       solver_opts.update(dict(**solver_opts, **highs_options))
## Options may be nested under `highs_options` to dodge a name clash with a
## psolve() argument -- HiGHS's own option is called `solver`. The
## `dict(**a, **b)` merge raises TypeError on a duplicate keyword; abort
## likewise rather than let one copy silently win. Returns the merged list.

.unpack_highs_options <- function(solver_opts) {
  highs_options <- solver_opts[["highs_options"]]
  solver_opts[["highs_options"]] <- NULL
  if (length(highs_options) == 0L) return(solver_opts)
  if (!is.list(highs_options) || is.null(names(highs_options)) ||
      any(!nzchar(names(highs_options)))) {
    cli_abort("{.arg highs_options} must be a named list of HiGHS options.")
  }
  dup <- intersect(names(highs_options), names(solver_opts))
  if (length(dup) > 0L) {
    cli_abort(paste("HiGHS option{?s} {.val {dup}} given both directly and inside",
                    "{.arg highs_options}."))
  }
  c(solver_opts, highs_options)
}

## CVXPY SOURCE: highs_conif.py:313-320 (highs_qpif.py:212-219)
##   for name, value in solver_opts.items():
##       if solver.setOptionValue(name, value) == hp.HighsStatus.kError:
##           raise ValueError(
##               f"HIGHS returned status kError for option (name, value): ({name}, {value})")
## R-SPECIFIC: highs::hi_solver_set_options() discards the HighsStatus, so until
## 1.9.2.9159 an unknown option was silently dropped (HiGHS only wrote to
## stderr) and a mistyped value was coerced -- as.double("abc") is NaN, which
## HiGHS ACCEPTS for a double option (measured, highs 1.14.0-2). The public
## per-option setter highs::hi_solver_set_option() returns the status
## (kError = -1) but also asserts the name is a known option (an R error) and
## coerces the value (an R warning); all three are HiGHS rejecting the option,
## so all three are kError here (claims audit C0780/C0865).

.highs_set_user_options <- function(solver, solver_opts) {
  for (opt_name in names(solver_opts)) {
    value <- solver_opts[[opt_name]]
    status <- tryCatch(highs::hi_solver_set_option(solver, opt_name, value),
                       error = function(e) -1L,
                       warning = function(w) -1L)
    if (identical(as.integer(status), -1L)) {
      shown <- if (is.null(value)) "NULL" else paste(format(value), collapse = ", ")
      cli_abort(paste0("HIGHS returned status kError for option (name, value): (",
                       opt_name, ", ", shown, ")"))
    }
  }
  invisible(NULL)
}


# -- HiGHS status map (shared with HiGHS_QP_Solver) --------------------------
## CVXPY SOURCE: highs_conif.py lines 118-131 (highs_qpif.py lines 41-54)
## R highs returns integer HighsModelStatus codes (Python highspy returns the
## enum names). Codes measured with R highs 1.14.0.2 and checked against the
## highspy enum table (notes/upstream_report_cvxpy_highs_unknown.md).

HIGHS_STATUS_MAP <- list(
  "7"  = OPTIMAL,                    # kOptimal
  "8"  = INFEASIBLE,                 # kInfeasible
  "9"  = INFEASIBLE_OR_UNBOUNDED,    # kUnboundedOrInfeasible
  "10" = UNBOUNDED,                  # kUnbounded
  "11" = USER_LIMIT,                 # kObjectiveBound
  "12" = USER_LIMIT,                 # kObjectiveTarget
  "13" = USER_LIMIT,                 # kTimeLimit
  "14" = USER_LIMIT,                 # kIterationLimit
  "16" = USER_LIMIT                  # kSolutionLimit (mip_max_improving_sols)
)
## Codes 0-6 (kNotset .. kModelEmpty), 15 (kUnknown), 17 (kInterrupt),
## 18 (kMemoryLimit), 19 (kHighsInterrupt) -> SOLVER_ERROR, the default
## fallback of .status_from_map. Upstream maps none of these except
## kNotset/kModelError/kSolveError (also SOLVER_ERROR) and falls back to
## UNKNOWN for the rest; see the divergence note in reduction_invert below.
## Until 1.9.2.9145 the map had "15" where "16" belongs, so a solution-limit
## stop aborted instead of returning the incumbent (claims-audit row C0776).

# -- HiGHS_Conic_Solver class -------------------------------------------------
## CVXPY SOURCE: highs_conif.py class HIGHS(ConicSolver)
## Only supports Zero and NonNeg constraints (LP/MILP only, no SOC).
## MIP_CAPABLE = TRUE (supports MILP).

HiGHS_Conic_Solver <- new_class("HiGHS_Conic_Solver", parent = ConicSolver,
  package = "CVXR",
  constructor = function() {
    if (FALSE) new_object(S7_object())  ## S7 static-check guard
    .fast_new(HiGHS_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),
      EXP_CONE_ORDER = NULL,
      REQUIRES_CONSTR = FALSE
    )
  }
)

method(solver_name, HiGHS_Conic_Solver) <- function(x) HIGHS_SOLVER

# -- reduction_accepts ---------------------------------------------------------
## Override: HiGHS conic only handles Zero + NonNeg.
## Rejects SOC, ExpCone, PSD, PowCone3D constraints.

method(reduction_accepts, HiGHS_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)
  }, logical(1L)))
}

# -- solve_via_data ------------------------------------------------------------
## CVXPY SOURCE: highs_conif.py solve_via_data()
##
## Receives conic data from ConicSolver.apply():
## A (negated), b, c, dims -- format: -A*x + s = b, s in K
##
## For HiGHS, undo the conic negation since HiGHS expects lhs <= Ax <= rhs:
##   Zero rows: A_z*x = b_z -> lhs = rhs = b_z (A is already un-negated for Zero)
##   NonNeg rows: solver has -A_raw*x + s = b, s >= 0 -> A_raw*x <= b
##     -> A_gurobi = solver_data$A (which is -formatted_A = A_raw for Zero,
##       = -A_raw for NonNeg). For HiGHS: negate NonNeg rows back.
##
## Actually simpler: use the raw ConeMatrixStuffing data from the conic base.
## ConicSolver.apply() gives us solver_data$A = -formatted_A and solver_data$B = formatted_b.
## For Zero: formatted_A = -A_raw, so solver_data$A = A_raw, b = -b_raw
## For NonNeg: formatted_A = A_raw, so solver_data$A = -A_raw, b = b_raw
## HiGHS needs lhs <= Ax <= rhs where A = original:
##   Zero: A_raw*x = -b_raw -> but -b_raw != what we want. Let's think differently.
##
## From ConeMatrixStuffing: A_raw*x + b_raw = 0 (Zero), A_raw*x + b_raw >= 0 (NonNeg)
## format_constraints: Zero -> -I block -> formatted_A[Zero] = -A_raw, formatted_b[Zero] = -b_raw
##                     NonNeg -> I block -> formatted_A[NonNeg] = A_raw, formatted_b[NonNeg] = b_raw
## ConicSolver.apply: solver_data$A = -formatted_A, solver_data$B = formatted_b
##   Zero: solver_A = A_raw, solver_b = -b_raw -> A_raw*x + s = -b_raw, s=0 -> A_raw*x = -b_raw
##   NonNeg: solver_A = -A_raw, solver_b = b_raw -> -A_raw*x + s = b_raw, s>=0 -> A_raw*x <= b_raw
## HiGHS: lhs <= Ax <= rhs
##   Zero: A=A_raw, lhs=rhs=-b_raw -> already from solver_A, solver_b -> A=solver_A, lhs=rhs=solver_b
##   NonNeg: A=A_raw=-solver_A, lhs=-Inf, rhs=b_raw=solver_b -> A=-solver_A, rhs=solver_b
##
## So: for the combined matrix, negate NonNeg rows of solver_A to get original A,
##     and use solver_b as the rhs.

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

  ## CVXPY SOURCE: highs_conif.py:216-218 (warm_start, solver_cache plumbed via ...).
  dots <- list(...)
  solver_cache <- dots[["solver_cache"]]

  c_vec <- data[[SD_C]]
  nvars <- length(c_vec)
  dims <- data[[SD_DIMS]]

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

  A_solver <- data[[SD_A]]  # = -formatted_A
  b_solver <- data[[SD_B]]  # = formatted_b

  ## Build HiGHS constraint matrix and bounds
  ## From conic convention: A_solver * x + s = b_solver, s in K
  ## Zero rows (first zero_dim): A_solver * x + 0 = b_solver -> A*x = b (equality)
  ##   -> A_highs = A_solver, lhs = rhs = b_solver
  ## NonNeg rows (next nonneg_dim): A_solver * x + s = b_solver, s >= 0 -> A*x <= b
  ##   -> A_highs = A_solver, lhs = -Inf, rhs = b_solver

  if (total_rows > 0L) {
    A_highs <- A_solver
    lhs <- b_solver
    rhs <- b_solver

    if (nonneg_dim > 0L) {
      ineq_idx <- (zero_dim + 1L):(zero_dim + nonneg_dim)
      ## A_solver rows are already correct (no negation needed)
      lhs[ineq_idx] <- -Inf
    }
  } else {
    A_highs <- Matrix::sparseMatrix(i = integer(0), j = integer(0),
                                     x = numeric(0), dims = c(0L, nvars))
    lhs <- numeric(0)
    rhs <- numeric(0)
  }

  ## Ensure A is dgCMatrix
  if (!inherits(A_highs, "dgCMatrix")) {
    A_highs <- methods::as(A_highs, "dgCMatrix")
  }

  ## Q matrix (quadratic objective) -- should be NULL for LP/MILP
  ## MIQP rejection -- HiGHS does not support mixed-integer QP
  if (!is.null(data[[SD_P]])) {
    is_mip <- length(data[["bool_idx"]] %||% integer(0)) > 0L ||
              length(data[["int_idx"]] %||% integer(0)) > 0L
    if (is_mip) {
      cli_abort(c(
        "HiGHS does not support mixed-integer QP (MIQP).",
        "i" = "Use LP constraints with integer variables, or remove integer/boolean attributes for QP problems."
      ))
    }
  }
  Q <- if (!is.null(data[[SD_P]])) {
    methods::as(methods::as(data[[SD_P]], "generalMatrix"), "CsparseMatrix")
  } else {
    NULL
  }

  ## Variable bounds and types
  lower <- data[[LOWER_BOUNDS]] %||% rep(-Inf, nvars)
  upper <- data[[UPPER_BOUNDS]] %||% rep(Inf, nvars)
  types <- rep(1L, nvars)  # 1=continuous

  ## MIP variable types
  bool_idx <- data[["bool_idx"]]
  if (length(bool_idx) > 0L) {
    for (idx in bool_idx) {
      types[idx] <- 2L
      lower[idx] <- max(lower[idx], 0)
      upper[idx] <- min(upper[idx], 1)
    }
  }

  int_idx <- data[["int_idx"]]
  if (length(int_idx) > 0L) {
    for (idx in int_idx) {
      types[idx] <- 2L
    }
  }

  ## Build HiGHS control
  ctrl <- highs::highs_control()
  ctrl$log_to_console <- verbose
  ## CVXPY SOURCE: highs_conif.py:312 sets only log_to_console. R-SPECIFIC: the
  ## R `highs` 1.14 persistent-solver API installs a log callback whose console
  ## output is gated by `output_flag` (master switch, default TRUE), so
  ## log_to_console alone no longer silences HiGHS. Mirror CVXPY's intent
  ## (quiet unless verbose) by also setting output_flag.
  ctrl$output_flag <- verbose

  ## CVXPY SOURCE: highs_conif.py:310 -- nested `highs_options` are unpacked
  ## before the option loop. (Upstream also pops `write_model_file` here and
  ## writes the model itself with validated column names; that feature is not
  ## ported -- the R highs package has no column-name setter, see
  ## notes/highs_r_package_mrs.md -- so the name reaches HiGHS as its own
  ## option, as before.)
  solver_opts <- .unpack_highs_options(solver_opts)

  ## CVXPY SOURCE: highs_conif.py:265-351.
  ## Persistent-solver flow (highs_model -> hi_new_solver -> optional
  ## hi_solver_set_solution -> hi_solver_run -> getter calls) replaces
  ## the one-shot highs::highs_solve() call.  Necessary so that warm-start
  ## can feed the prior solution into the new solver instance via
  ## hi_solver_set_solution(), mirroring CVXPY's `solver.setSolution()` at
  ## highs_conif.py:336.  Requires highs >= 1.14 (persistent-solver API).
  model <- highs::highs_model(
    Q       = Q,
    L       = c_vec,
    lower   = lower,
    upper   = upper,
    A       = A_highs,
    lhs     = lhs,
    rhs     = rhs,
    types   = types,
    maximum = FALSE,
    offset  = 0
  )
  solver <- highs::hi_new_solver(model)
  highs::hi_solver_set_options(solver, ctrl)
  ## CVXPY SOURCE: highs_conif.py:313-320 -- user options set one by one AFTER
  ## the verbosity defaults (so `output_flag = TRUE` wins), kError aborts.
  .highs_set_user_options(solver, solver_opts)

  ## CVXPY SOURCE: highs_conif.py:332-336 (warm-start primal/dual feed-in).
  ## If we have a cached solution from a prior solve and its status was
  ## SOLUTION_PRESENT and the dimensions match, hand it to HiGHS as the
  ## starting point.  Any failure falls through to a cold solve.
  cache_key <- HIGHS_SOLVER
  if (warm_start && !is.null(solver_cache) &&
      exists(cache_key, envir = solver_cache)) {
    cached <- get(cache_key, envir = solver_cache)
    old_status <- HIGHS_STATUS_MAP[[as.character(cached$result$status)]]
    if (!is.null(old_status) && old_status %in% SOLUTION_PRESENT &&
        length(cached$result$solver_msg$col_value) == nvars &&
        length(cached$result$solver_msg$row_value) == nrow(A_highs)) {
      tryCatch({
        prior <- cached$result$solver_msg
        highs::hi_solver_set_solution(
          solver,
          col_value   = prior$col_value,
          row_value   = prior$row_value,
          col_dual    = prior$col_dual,
          row_dual    = prior$row_dual,
          value_valid = isTRUE(prior$value_valid),
          dual_valid  = isTRUE(prior$dual_valid)
        )
      }, error = function(e) NULL)
    }
  }

  ## CVXPY SOURCE: highs_conif.py:338-351 (run + collect result fields).
  ## Result shape matches the prior highs::highs_solve() return so that
  ## reduction_invert() below is unchanged.
  highs::hi_solver_run(solver)
  solution <- highs::hi_solver_get_solution(solver)
  info <- highs::hi_solver_info(solver)
  result <- list(
    primal_solution = solution[["col_value"]],
    objective_value = info[["objective_function_value"]],
    status          = highs::hi_solver_status(solver),
    status_message  = highs::hi_solver_status_message(solver),
    solver_msg      = solution,
    info            = info,
    run_time        = highs::hi_solver_get_run_time(solver)  # highs_conif.py:346
  )

  ## CVXPY SOURCE: highs_conif.py:348-349
  ##   if results["model_status"] == "kInfeasible":
  ##       results["dual_ray"] = solver.getDualRay()
  ## The dual (Farkas) ray certifies primal infeasibility; reduction_invert()
  ## maps it onto the constraints' dual_value.  Requires highs >= 1.14 for
  ## hi_solver_get_dual_ray(), which DESCRIPTION already mandates.
  if (identical(HIGHS_STATUS_MAP[[as.character(result$status)]], INFEASIBLE)) {
    result$dual_ray <- tryCatch(highs::hi_solver_get_dual_ray(solver),
                                error = function(e) NULL)
  }

  ## CVXPY SOURCE: highs_conif.py:353-354 (cache for next warm-start).
  if (!is.null(solver_cache)) {
    assign(cache_key,
           list(solver = solver, result = result),
           envir = solver_cache)
  }

  result
}

# -- reduction_invert ----------------------------------------------------------
## CVXPY SOURCE: highs_conif.py lines 162-214
## Dual sign: negate ALL row_duals (matching CVXPY highs_conif.py line 188)

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

  status_code <- solution$status
  ## DELIBERATE DIVERGENCE, kept on purpose: upstream falls back to `s.UNKNOWN`
  ## (highs_conif.py:170, highs_qpif.py:82), which is NOT a solver status -- it
  ## is the curvature/sign sentinel at settings.py:202 and is in none of
  ## SOLUTION_PRESENT / INF_OR_UNB / INACCURATE / ERROR. A solve that returns it
  ## therefore misses the `s.ERROR` branch of unpack_results (problem.py:1452)
  ## and dies in unpack() with "Cannot unpack invalid solution" instead of a
  ## SolverError naming the solver. Eight reachable HighsModelStatus values hit
  ## that path, including kMemoryLimit and both interrupt statuses.
  ##
  ## Upstream's own warm-start paths (highs_conif.py:334, highs_qpif.py:233)
  ## use SOLVER_ERROR on the same map, as does every other solver interface, so
  ## this reads as a slip rather than a design choice.
  ##
  ## Reported to CVXPY -- draft and full verification log in
  ## notes/upstream_report_cvxpy_highs_unknown.md. Revisit if upstream changes it.
  status <- .status_from_map(HIGHS_STATUS_MAP, status_code)

  ## CVXPY SOURCE: highs_conif.py:164-167 -- SOLVE_TIME = run_time, EXTRA_STATS = info.
  ## Both were missing until 1.9.2.9137 (claims-audit row C0863).
  info <- solution$info
  if (!is.null(solution$run_time)) attr_list[[RK_SOLVE_TIME]] <- solution$run_time
  if (!is.null(info)) attr_list[[RK_EXTRA_STATS]] <- info

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

    ## CVXPY SOURCE: highs_conif.py:194-200 -- NUM_ITERS is the sum of the five
    ## iteration counts, set only when a solution is present. The R highs
    ## package (1.14.0-2) does not report pdlp_iteration_count, so that term is
    ## 0 until it does.
    if (!is.null(info)) {
      attr_list[[RK_NUM_ITERS]] <- (info$ipm_iteration_count %||% 0L) +
                                   (info$crossover_iteration_count %||% 0L) +
                                   (info$pdlp_iteration_count %||% 0L) +
                                   (info$qp_iteration_count %||% 0L) +
                                   (info$simplex_iteration_count %||% 0L)
    }

    primal_vars <- list()
    primal_vars[[as.character(inverse_data[[SOLVER_VAR_ID]])]] <- solution$primal_solution

    ## Check if MIP (duals not valid for MIP)
    is_mip <- isTRUE(inverse_data[["is_mip"]])

    if (!is_mip && !is.null(solution$solver_msg) &&
        isTRUE(solution$solver_msg$dual_valid)) {
      ## Negate ALL row_duals -- matching CVXPY highs_conif.py line 188
      raw_dual <- solution$solver_msg$row_dual
      y <- -raw_dual

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

      ineq_dual <- if (inverse_data[[SD_DIMS]]@nonneg > 0L) {
        zero_dim <- inverse_data[[SD_DIMS]]@zero
        nonneg_dim <- inverse_data[[SD_DIMS]]@nonneg
        get_dual_values(
          y[(zero_dim + 1L):(zero_dim + nonneg_dim)],
          extract_dual_value,
          inverse_data[[SOLVER_NEQ_CONSTR]]
        )
      } else {
        list()
      }

      dual_vars <- c(eq_dual, ineq_dual)
    } else {
      dual_vars <- list()
    }

    Solution(status, opt_val, primal_vars, dual_vars, attr_list)
  } else {
    ## CVXPY SOURCE: highs_conif.py:203-213
    ##   if status == s.INFEASIBLE:
    ##       dual_ray = -np.array(results["dual_ray"][2])
    ##       dual_vars = utilities.get_dual_values(
    ##           dual_ray, utilities.extract_dual_value,
    ##           inverse_data[HIGHS.EQ_CONSTR] + inverse_data[HIGHS.NEQ_CONSTR])
    ##   sol = failure_solution(status, attr, dual_vars)
    ##
    ## R-SPECIFIC guard (the two accessors disagree on the no-ray case):
    ## hi_solver_get_dual_ray() sets has_dual_ray = FALSE and dual_ray = NULL
    ## when HiGHS reports infeasible without producing a ray -- measured for
    ## solver = "ipm" and "pdlp", and for column-bound infeasibility, on BOTH
    ## highs 1.14.0-2 and 1.15.1.  highspy returns a ZERO VECTOR instead, which
    ## is why CVXPY's unguarded [2] does not error there: it silently reports an
    ## all-zero "certificate" satisfying y >= 0 and A'y = 0 but with b'y = 0,
    ## which certifies nothing.  Unguarded in R this would instead be a
    ## zero-length y and a length mismatch in get_dual_values().  Degrade to no
    ## duals -- the pre-existing behavior -- rather than error or fabricate.
    dual_vars <- list()
    ray <- solution$dual_ray
    if (status == INFEASIBLE && !is.null(ray) && isTRUE(ray$has_dual_ray)) {
      y <- -as.numeric(ray$dual_ray)
      zero_dim <- inverse_data[[SD_DIMS]]@zero
      nonneg_dim <- inverse_data[[SD_DIMS]]@nonneg
      if (length(y) == zero_dim + nonneg_dim) {
        eq_dual <- if (zero_dim > 0L) {
          get_dual_values(y[seq_len(zero_dim)], extract_dual_value,
                          inverse_data[[SOLVER_EQ_CONSTR]])
        } else {
          list()
        }
        ineq_dual <- if (nonneg_dim > 0L) {
          get_dual_values(y[(zero_dim + 1L):(zero_dim + nonneg_dim)],
                          extract_dual_value,
                          inverse_data[[SOLVER_NEQ_CONSTR]])
        } else {
          list()
        }
        dual_vars <- c(eq_dual, ineq_dual)
      }
    }
    failure_solution(status, attr_list, dual_vars)
  }
}

# -- print ---------------------------------------------------------------------

method(print, HiGHS_Conic_Solver) <- function(x, ...) {
  cat("HiGHS_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 Oct. 19, 2026, 5:07 p.m.