Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
^\.github$
^_pkgdown\.yml$
^README\.Rmd$
^README\.html$
^docs$
^pkgdown$
^inst/copy_r_source\.R$
Expand Down
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Imports:
clarabel (>= 0.11),
cli (>= 3.6),
gmp (>= 0.7),
highs (>= 1.12),
highs (>= 1.14),
osqp (>= 1.0),
scs (>= 3.2),
slam (>= 0.1)
Expand Down
13 changes: 13 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ and interval-bounds propagation with native solver-bound support.
* Variables now report `Parameter`s embedded in expression bounds and include
those bounds in DPP/DGP compliance checks.

## HiGHS warm-start and column-name validation

* HiGHS warm-start is now enabled. `psolve(prob, solver = "HIGHS",
warm_start = TRUE)` reuses the previous solution via the persistent-solver
API (`hi_solver_set_solution`) across LP, MILP, and QP paths. This requires
`highs (>= 1.14)`, now available on CRAN; on older `highs` the warm-start
tests skip and solves fall back to cold starts.
* `validate_column_name()` checks a name against the HiGHS LP-file column-name
rules, mirroring CVXPY's `highs_conif.validate_column_name`.
* Known limitation: writing a model to a file with the original variable names
is not yet supported — the R `highs` package exposes no column-name setter,
so a written model would carry generic names (`c0`, `c1`, ...).

## Geometric and parameterized programming

* Positive (DGP) variables now accept numeric *and* parametric bounds under
Expand Down
108 changes: 104 additions & 4 deletions R/276_reductions_solvers_conic_solvers_highs_conif.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,40 @@
## 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 31-48
## 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 byte-identical to CVXPY's
## VALID_COLUMN_NAME_PATTERN (Python's `{,254}` written as PCRE `{0,254}`).

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 45-48)
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)
}


# -- HiGHS status map (shared with HiGHS_QP_Solver) --------------------------
## CVXPY SOURCE: highs_conif.py lines 57-69
## R highs returns integer status codes (unlike Python highspy enum names).
Expand Down Expand Up @@ -99,6 +133,10 @@ method(solve_via_data, HiGHS_Conic_Solver) <- function(x, data, warm_start = FAL
solver_opts = list(), ...) {
.require_solver_package(HIGHS_SOLVER)

## CVXPY SOURCE: highs_conif.py:201 (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]]
Expand Down Expand Up @@ -182,13 +220,25 @@ method(solve_via_data, HiGHS_Conic_Solver) <- function(x, data, warm_start = FAL
## Build HiGHS control
ctrl <- highs::highs_control()
ctrl$log_to_console <- verbose
## CVXPY SOURCE: highs_conif.py:296 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

for (opt_name in names(solver_opts)) {
ctrl[[opt_name]] <- solver_opts[[opt_name]]
}

## Call HiGHS
result <- highs::highs_solve(
## CVXPY SOURCE: highs_conif.py:250-338.
## 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:320. Requires highs >= 1.14 (persistent-solver API).
model <- highs::highs_model(
Q = Q,
L = c_vec,
lower = lower,
Expand All @@ -198,10 +248,60 @@ method(solve_via_data, HiGHS_Conic_Solver) <- function(x, data, warm_start = FAL
rhs = rhs,
types = types,
maximum = FALSE,
offset = 0,
control = ctrl
offset = 0
)
solver <- highs::hi_new_solver(model)
highs::hi_solver_set_options(solver, ctrl)

## CVXPY SOURCE: highs_conif.py:316-320 (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:323-331 (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
)

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

result
}

Expand Down
71 changes: 66 additions & 5 deletions R/285_reductions_solvers_qp_solvers_highs_qpif.R
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ method(solve_via_data, HiGHS_QP_Solver) <- function(x, data, warm_start = FALSE,
solver_opts = list(), ...) {
.require_solver_package(HIGHS_SOLVER)

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

L_vec <- data[["q"]]
nvars <- length(L_vec)

Expand Down Expand Up @@ -126,14 +130,25 @@ method(solve_via_data, HiGHS_QP_Solver) <- function(x, data, warm_start = FALSE,
## Build HiGHS control
ctrl <- highs::highs_control()
ctrl$log_to_console <- verbose
## CVXPY SOURCE: highs_qpif.py:208 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

## Apply user-specified solver options
for (opt_name in names(solver_opts)) {
ctrl[[opt_name]] <- solver_opts[[opt_name]]
}

## Call HiGHS
result <- highs::highs_solve(
## CVXPY SOURCE: highs_qpif.py:175-248.
## 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 so that warm-start can feed
## the prior solution via hi_solver_set_solution(), mirroring CVXPY's
## `solver.setSolution()` at highs_qpif.py:232. Requires highs >= 1.14.
model <- highs::highs_model(
Q = Q,
L = L_vec,
lower = lower,
Expand All @@ -143,11 +158,57 @@ method(solve_via_data, HiGHS_QP_Solver) <- function(x, data, warm_start = FALSE,
rhs = rhs,
types = types,
maximum = FALSE,
offset = 0,
control = ctrl
offset = 0
)
solver <- highs::hi_new_solver(model)
highs::hi_solver_set_options(solver, ctrl)

## CVXPY SOURCE: highs_qpif.py:228-232 (warm-start primal/dual feed-in).
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)]]
nrow_A <- if (is.null(A)) 0L else nrow(A)
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) {
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_qpif.py:235-243 (run + collect result fields).
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
)

## Store len_eq for dual splitting
## CVXPY SOURCE: highs_qpif.py:247-248 (cache for next warm-start).
if (!is.null(solver_cache)) {
assign(cache_key,
list(solver = solver, result = result),
envir = solver_cache)
}

## Store len_eq for dual splitting (used by reduction_invert).
result$.len_eq <- len_eq
result
}
Expand Down
10 changes: 5 additions & 5 deletions docs/articles/cvxr_intro.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions docs/articles/cvxr_intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,13 @@ result <- psolve(problem, solver = "CLARABEL", verbose = TRUE) ## verbose = TRUE
#> ────────────────────────────────── CVXR v1.9.1 ─────────────────────────────────
#> ℹ Problem: 1 variable, 2 constraints (QP)
#> ℹ Compilation: "CLARABEL" via CVXR::Dcp2Cone -> CVXR::CvxAttr2Constr -> CVXR::ConeMatrixStuffing -> CVXR::Clarabel_Solver
#> ℹ Compile time: 0.013s
#> ℹ Compile time: 0.022s
#> ─────────────────────────────── Numerical solver ───────────────────────────────
#> ──────────────────────────────────── Summary ───────────────────────────────────
#> ✔ Status: optimal
#> ✔ Optimal value: 1287.63
#> ℹ Compile time: 0.013s
#> ℹ Solver time: 0.011s
#> ℹ Compile time: 0.022s
#> ℹ Solver time: 0.017s
round(value(betaHat), 3)
#> [,1]
#> [1,] 0.000
Expand Down Expand Up @@ -238,7 +238,7 @@ sessionInfo()
#>
#> loaded via a namespace (and not attached):
#> [1] Matrix_1.7-5 piqp_0.6.2 jsonlite_2.0.0 compiler_4.6.0
#> [5] highs_1.12.0-3 Rcpp_1.1.1-1.1 slam_0.1-55 cccp_0.3-3
#> [5] highs_1.14.0-2 Rcpp_1.1.1-1.1 slam_0.1-55 cccp_0.3-3
#> [9] jquerylib_0.1.4 systemfonts_1.3.2 textshaping_1.0.5 yaml_2.3.12
#> [13] fastmap_1.2.0 clarabel_0.11.2 lattice_0.22-9 R6_2.6.1
#> [17] scip_1.10.0-3 knitr_1.51 htmlwidgets_1.6.4 backports_1.5.1
Expand Down
2 changes: 1 addition & 1 deletion docs/articles/whats_new.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions docs/news/index.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading