genconstr.R#

# Copyright 2024, Gurobi Optimization, LLC
#
# In this example we show the use of general constraints for modeling
# some common expressions. We use as an example a SAT-problem where we
# want to see if it is possible to satisfy at least four (or all) clauses
# of the logical for
#
# L = (x0 or ~x1 or x2)  and (x1 or ~x2 or x3)  and
#     (x2 or ~x3 or x0)  and (x3 or ~x0 or x1)  and
#     (~x0 or ~x1 or x2) and (~x1 or ~x2 or x3) and
#     (~x2 or ~x3 or x0) and (~x3 or ~x0 or x1)
#
# We do this by introducing two variables for each literal (itself and its
# negated value), a variable for each clause, and then two
# variables for indicating if we can satisfy four, and another to identify
# the minimum of the clauses (so if it one, we can satisfy all clauses)
# and put these two variables in the objective.
# i.e. the Objective function will be
#
# maximize Obj0 + Obj1
#
#  Obj0 = MIN(Clause1, ... , Clause8)
#  Obj1 = 1 -> Clause1 + ... + Clause8 >= 4
#
# thus, the objective value will be two if and only if we can satisfy all
# clauses; one if and only if at least four clauses can be satisfied, and
# zero otherwise.
#

library(Matrix)
library(gurobi)

# Set up parameters
params <- list()
params$logfile <- 'genconstr.log'

# define primitive data
nLiterals <- 4
nClauses  <- 8
nObj      <- 2
nVars     <- 2 * nLiterals + nClauses + nObj
Literal <- function(n) {n}
NotLit  <- function(n) {n + nLiterals}
Clause  <- function(n) {2 * nLiterals + n}
Obj     <- function(n) {2 * nLiterals + nClauses + n}

Clauses <- list(c(Literal(1), NotLit(2), Literal(3)),
                c(Literal(2), NotLit(3), Literal(4)),
                c(Literal(3), NotLit(4), Literal(1)),
                c(Literal(4), NotLit(1), Literal(2)),
                c(NotLit(1), NotLit(2), Literal(3)),
                c(NotLit(2), NotLit(3), Literal(4)),
                c(NotLit(3), NotLit(4), Literal(1)),
                c(NotLit(4), NotLit(1), Literal(2)))


# Create model
model <- list()
model$modelname  <- 'genconstr'
model$modelsense <- 'max'

# Create objective function, variable names, and variable types
model$vtype    <- rep('B',nVars)
model$ub       <- rep(1,  nVars)
model$varnames <- c(sprintf('X%d',      seq(1,nLiterals)),
                    sprintf('notX%d',   seq(1,nLiterals)),
                    sprintf('Clause%d', seq(1,nClauses)),
                    sprintf('Obj%d',    seq(1,nObj)))
model$obj      <- c(rep(0, 2*nLiterals + nClauses), rep(1, nObj))

# Create linear constraints linking literals and not literals
model$A           <- spMatrix(nLiterals, nVars,
                       i = c(seq(1,nLiterals),
                             seq(1,nLiterals)),
                       j = c(seq(1,nLiterals),
                             seq(1+nLiterals,2*nLiterals)),
                       x = rep(1,2*nLiterals))
model$constrnames <- c(sprintf('CNSTR_X%d',seq(1,nLiterals)))
model$rhs         <- rep(1,   nLiterals)
model$sense       <- rep('=', nLiterals)

# Create OR general constraints linking clauses and literals
model$genconor <- lapply(1:nClauses,
                         function(i) list(resvar=Clause(i),
                                          vars  = Clauses[[i]],
                                          name  = sprintf('CNSTR_Clause%d',i)))

# Set up Obj1 = Min(Clause[1:nClauses])
model$genconmin <- list(list(resvar = Obj(1),
                             vars   = c(seq(Clause(1),Clause(nClauses))),
                             name   = 'CNSTR_Obj1'))


# the indicator constraint excepts the following vector types for the
# linear sum:
#
# - a dense vector, for example
#   c(rep(0, 2*nLiterals), rep(1, nClauses), rep(0,nObj))
# - a sparse vector, for example
#   sparseVector( x = rep(1,nClauses), i = (2*nLiterals+1):(2*nLiterals+nClauses), length=nVars)
# - In case all coefficients are the same, you can pass a vector of length
#   one with the corresponding value which gets expanded to a dense vector
#   with all values being the given one
#
# We use a sparse vector in this example
a <- sparseVector( x = rep(1,nClauses), i = (2*nLiterals+1):(2*nLiterals+nClauses), length=nVars)

# Set up Obj2 to signal if any clause is satisfied, i.e.
# we use an indicator constraint of the form:
#     (Obj2 = 1) -> sum(Clause[1:nClauses]) >= 4
model$genconind <- list(list(binvar = Obj(2),
                             binval = 1,
                             a      = a,
                             sense  = '>',
                             rhs    = 4,
                             name   = 'CNSTR_Obj2'))

# Save model
gurobi_write(model, 'genconstr.lp', params)

# Optimize
result <- gurobi(model, params = params)

# Check optimization status
if (result$status == 'OPTIMAL') {
  if (result$objval > 1.9) {
    cat('Logical expression is satisfiable\n')
  } else if(result$objval > 0.9) {
    cat('At least four clauses are satisfiable\n')
  } else {
    cat('At most three clauses may be satisfiable\n')
  }
} else {
  cat('Optimization failed\n')
}

# Clear space
rm(model,result,params,Clauses)