diff --git a/.Rbuildignore b/.Rbuildignore index f2ff4ac1..408565e4 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -13,6 +13,7 @@ ^\.github$ ^_pkgdown\.yml$ ^README\.Rmd$ +^README\.html$ ^docs$ ^pkgdown$ ^inst/copy_r_source\.R$ diff --git a/DESCRIPTION b/DESCRIPTION index f0bbdd80..0d1f9866 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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) diff --git a/NEWS.md b/NEWS.md index 971ddc00..46583f7a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/R/276_reductions_solvers_conic_solvers_highs_conif.R b/R/276_reductions_solvers_conic_solvers_highs_conif.R index a2add0f7..34f8cd46 100644 --- a/R/276_reductions_solvers_conic_solvers_highs_conif.R +++ b/R/276_reductions_solvers_conic_solvers_highs_conif.R @@ -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). @@ -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]] @@ -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, @@ -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 } diff --git a/R/285_reductions_solvers_qp_solvers_highs_qpif.R b/R/285_reductions_solvers_qp_solvers_highs_qpif.R index d5f19cc2..3deefc2a 100644 --- a/R/285_reductions_solvers_qp_solvers_highs_qpif.R +++ b/R/285_reductions_solvers_qp_solvers_highs_qpif.R @@ -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) @@ -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, @@ -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 } diff --git a/docs/articles/cvxr_intro.html b/docs/articles/cvxr_intro.html index 3fda6e4c..84ca1729 100644 --- a/docs/articles/cvxr_intro.html +++ b/docs/articles/cvxr_intro.html @@ -65,7 +65,7 @@

Anqi Fu and Balasubramanian Narasimhan

-

2026-06-05

+

2026-06-08

Source: vignettes/cvxr_intro.Rmd
cvxr_intro.Rmd
@@ -203,13 +203,13 @@

Custom Constraints#> ────────────────────────────────── 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 @@ -302,7 +302,7 @@

Session Info#> #> 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 diff --git a/docs/articles/cvxr_intro.md b/docs/articles/cvxr_intro.md index 00aecf91..1e0b6bb9 100644 --- a/docs/articles/cvxr_intro.md +++ b/docs/articles/cvxr_intro.md @@ -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 @@ -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 diff --git a/docs/articles/whats_new.html b/docs/articles/whats_new.html index a8eae583..54c888bf 100644 --- a/docs/articles/whats_new.html +++ b/docs/articles/whats_new.html @@ -65,7 +65,7 @@

Anqi Fu, Balasubramanian Narasimhan, and Stephen Boyd

-

2026-06-05

+

2026-06-08

Source:
vignettes/whats_new.Rmd
whats_new.Rmd
diff --git a/docs/news/index.html b/docs/news/index.html index 848ddc16..688d1ae8 100644 --- a/docs/news/index.html +++ b/docs/news/index.html @@ -72,6 +72,13 @@

Bounds propagationVariables now report Parameters embedded in expression bounds and include those bounds in DPP/DGP compliance checks.
+

HiGHS warm-start and column-name validation

+
+

Geometric and parameterized programming