Skip to content

Add support for semi-continuous and semi-integer variables#75

Open
jeffreyhanson wants to merge 5 commits into
masterfrom
semi-vars
Open

Add support for semi-continuous and semi-integer variables#75
jeffreyhanson wants to merge 5 commits into
masterfrom
semi-vars

Conversation

@jeffreyhanson
Copy link
Copy Markdown
Collaborator

@jeffreyhanson jeffreyhanson commented Mar 31, 2026

This PR adds support for semi-continuous and semi-integer variables to the solver. The implementation is largely adapted from the lot size example distributed with the CBC software (https://github.com/coin-or/Cbc/blob/master/examples/lotsize.cpp). To help verify correctness, the PR includes additional tests for semi-continuous variables. I have also done some preliminary testing with models in my own work that use semi-continuos variables and this PR correctly yields optimal solutions. @dirkschumacher, when you get a chance, could you please review this PR?

@jeffreyhanson jeffreyhanson marked this pull request as ready for review March 31, 2026 22:57
@dirkschumacher
Copy link
Copy Markdown
Owner

Interesting. Will take a deeper look once copilot is done.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds semi-continuous / semi-integer variable support to cbc_solve() by passing semi-variable metadata from R into the C++ CBC model and adding CBC lotsize branching objects; also updates documentation/site artifacts and adds tests for semi-continuous behavior.

Changes:

  • Add is_semi argument to cbc_solve() and marshal semi-variable indices/bounds into the C++ solver call.
  • Add CBC lotsize branching objects (CbcLotsize) for semi variables in the C++ backend.
  • Add testthat coverage for basic semi-continuous feasibility/infeasibility cases and regenerate pkgdown docs.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
R/cbc_solve.R Adds is_semi API, prepares semi bounds, and extends the .Call interface.
src/cpp_cbc_solve.cpp Adds lotsize branching objects for semi variables in the CBC model.
src/rcbc-init.c Updates registered .Call signature/arity for the extended solver entry point.
tests/testthat/test-cbc-solver.R Adds tests for semi-continuous variables.
man/cbc_solve.Rd Documents new is_semi parameter.
DESCRIPTION Bumps package version / roxygen note.
docs/** Regenerated pkgdown site outputs (HTML, sitemap, assets).

Comment thread R/cbc_solve.R
Comment on lines +53 to +55
#' (\code{FALSE}) not a semi-continuous or semi-integer variable.
#' Note that arguments should have one value per decision variable
#' (i.e. column in \code{mat}).
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The is_semi parameter docs don’t explain how semi variables are interpreted in terms of bounds (typically: variable must be either 0 or in [col_lb, col_ub], and the provided col_lb is the minimum non-zero value). Document these requirements/semantics (and any restrictions like requiring finite bounds) so callers know how to set col_lb/col_ub when is_semi is TRUE.

Suggested change
#' (\code{FALSE}) not a semi-continuous or semi-integer variable.
#' Note that arguments should have one value per decision variable
#' (i.e. column in \code{mat}).
#' or (\code{FALSE}) not a semi-continuous or semi-integer variable.
#' Note that arguments should have one value per decision variable
#' (i.e. column in \code{mat}).
#' When \code{is_semi[i]} is \code{TRUE}, variable \code{i} must be either
#' \code{0} or lie in the interval \code{[col_lb[i], col_ub[i]]}. In this
#' case, \code{col_lb[i]} is interpreted as the minimum non-zero value for
#' the variable, not merely as an ordinary lower bound. If
#' \code{is_integer[i]} is \code{TRUE}, the variable is semi-integer;
#' otherwise it is semi-continuous. Callers should therefore provide
#' appropriate bounds for semi variables, typically with a finite positive
#' \code{col_lb[i]} and a finite \code{col_ub[i]}, rather than relying on the
#' default infinite bounds.

Copilot uses AI. Check for mistakes.
expect_equal(res$objective_value, 80)
expect_true(res$is_proven_optimal)
})

Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current semi-variable tests cover only integer-valued semi-continuous outcomes and don’t distinguish semi-continuous vs semi-integer behavior. Add tests that (1) require a fractional value above the semi lower bound (should be feasible for semi-continuous when is_integer is FALSE), and (2) mark the variable as both is_integer=TRUE and is_semi=TRUE to verify semi-integer behavior (e.g., fractional requirement should become infeasible).

Suggested change
test_that("is_semi allows fractional value above lower bound when not integer", {
res <- cbc_solve(
obj = c(0, 10),
mat = matrix(c(1, 0, 0, 1),
ncol = 2, nrow = 2
),
row_ub = c(2, 5.5),
row_lb = c(2, 5.5),
col_ub = c(2, 10),
col_lb = c(0, 5),
is_integer = c(FALSE, FALSE),
is_semi = c(FALSE, TRUE),
max = FALSE,
cbc_args = list("logLevel" = 0)
)
expect_equal(res$column_solution, c(2, 5.5))
expect_equal(res$objective_value, 55)
expect_true(res$is_proven_optimal)
})
test_that("is_semi with integer variable yields semi-integer infeasibility for fractional requirement", {
res <- cbc_solve(
obj = c(0, 10),
mat = matrix(c(1, 0, 0, 1),
ncol = 2, nrow = 2
),
row_ub = c(2, 5.5),
row_lb = c(2, 5.5),
col_ub = c(2, 10),
col_lb = c(0, 5),
is_integer = c(FALSE, TRUE),
is_semi = c(FALSE, TRUE),
max = FALSE,
cbc_args = list("logLevel" = 0)
)
expect_equal(res$is_proven_infeasible, TRUE)
})

Copilot uses AI. Check for mistakes.
Comment thread R/cbc_solve.R
Comment on lines +355 to +356
col_semi_lb <- col_lb[is_semi]
col_lb[is_semi] <- 0
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semi variables typically allow the value 0 even when col_lb[is_semi] > 0 (domain is {0} ∪ [lb, ub]). However, the current initial-solution validation uses initial_solution >= col_lb, which would reject a valid initial value of 0 for semi-integer variables. Adjust the initial_solution lower-bound check to treat semi variables specially (allow 0 or >= semi lower bound).

Copilot uses AI. Check for mistakes.
Comment thread src/cpp_cbc_solve.cpp
Comment on lines +70 to +72
CbcObject **semiRules = new CbcObject *[nSc];
double semiRanges[] = {0.0, 0.0, 0.0, 0.0};
if (nSc > 0) {
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semiRules is allocated unconditionally, but only deleted inside if (nSc > 0). When nSc == 0, this leaks the array allocation. Allocate semiRules only inside the if block (or always delete[] it after the conditional).

Suggested change
CbcObject **semiRules = new CbcObject *[nSc];
double semiRanges[] = {0.0, 0.0, 0.0, 0.0};
if (nSc > 0) {
double semiRanges[] = {0.0, 0.0, 0.0, 0.0};
if (nSc > 0) {
CbcObject **semiRules = new CbcObject *[nSc];

Copilot uses AI. Check for mistakes.
Comment thread src/cpp_cbc_solve.cpp
delete[] semiRules;
}

// ininitialize model with solver data
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: // ininitialize model with solver data should be // initialize model with solver data.

Suggested change
// ininitialize model with solver data
// initialize model with solver data

Copilot uses AI. Check for mistakes.
Comment thread R/cbc_solve.R
Comment on lines +351 to +356
# prepare arguments for semi variables
## here we store the original semi variable bounds and override
## the lower bounds for these variables to be 0 - this is needed
## to accommodate the CBC API
col_semi_lb <- col_lb[is_semi]
col_lb[is_semi] <- 0
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semi variables can currently have col_lb = -Inf/Inf (the defaults) because there are no additional checks when is_semi is TRUE. This leads to passing non-finite bounds via colLowerSemi into the CBC lotsize branching object, which is not a meaningful semi-continuous definition and can cause solver errors/undefined behavior. Add explicit validation for semi variables (e.g., require finite col_lb[is_semi] and typically col_lb[is_semi] >= 0 (or > 0), and ensure col_ub[is_semi] is finite and >= col_lb[is_semi]).

Copilot uses AI. Check for mistakes.
@jeffreyhanson
Copy link
Copy Markdown
Collaborator Author

Thanks @dirkschumacher! Let me know if you have any questions? My C skills are pretty lacking - so apologies if there's any issues/mistakes - this implementation is largely a very close/unedited translation of the CBC example (i.e., https://github.com/coin-or/Cbc/blob/master/examples/lotsize.cpp). Also, in case you're interested, my experience with this branch is that semi-continuous variables slow down the performance of CBC a lot (eg., moreso that highs or gurobi), because many of the heuristic algoirthms that CBC uses to improve solutions aren't compatible with semi-continuous variables (i.e., because the log file prints out a warning saying X many heuristic algorithms/methods are disabled when using these variable types).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants