FICO Xpress Optimization Examples Repository
 FICO Optimization Community FICO Xpress Optimization Home

Register an R function as callback into Xpress

Description

This example shows how to collect data about solution process using callbacks.

Further explanation of this example: Xpress R Reference Manual

Source Files
By clicking on a file name, a preview is opened at the bottom of this page.

Data Files

callback.R

#####################################
# This file is part of the          #
# Xpress-R interface examples       #
#                                   #
#   (c) 2021 Fair Isaac Corporation #
#####################################
#' ---
#' title: "Using Callbacks"
#' author: Gregor Hendel
#' date: Dec. 2020
#' ---
#'

#'
## ----Drop Existing Log File, include=FALSE------------------------------------
# clear the log file output
if (file.exists("optnodecallbacklog.txt"))
file.remove("optnodecallbacklog.txt")

#'
## ----Libraries----------------------------------------------------------------
suppressMessages(library(xpress))

# load these libraries for plotting the primal and dual bound
library(ggplot2)
library(magrittr)

#'
#'  This example shows how to customize the solution process using callbacks.
#'
#'  In general, callbacks are functions that allow the user to monitor progress
#'  or control certain aspects of the search. Callbacks are invoked at certain
#'  stages of the solution produces and are given a chance to perform certain
#'  actions and/or query the current state of the search.
#'
#' # Differences between C and R callbacks
#'
#'  In C callbacks can have
#'
#'  - a return value,
#'  - input arguments,
#'  - output arguments.
#'
#'  Callbacks in R work pretty similar. The only exception is that they don't
#'  take any output arguments. Instead any output is supposed to be given by
#'  the return value. To this end, a callback function in R is supposed to
#'  return one of the following:
#'
#'  - An integer. If the C API allows the callback to return an integer then
#'    this integer is returned to the optimizer (a non-zero value usually
#'    asks for interruption of the solve). See the C documentation for more
#'  - A list. The values in the list are used as output from the callback
#'    (for callbacks that return data). The list is also checked for an
#'    element called ret. If present, this must be an integer and will be
#'    returned to the solver (in case the C callback has a return value).
#'
#'  Another difference between the C implementation and R is that in R there is
#'  no user data argument (called vContext in C). If the callback requires any
#'  data that is not directly available in the callback, then it is assumed the
#'  user makes this available in the callback's environment, for example by
#'  means of lexical scoping.
#'
#' # Limitations
#'
#'  Note that the Xpress R library does not increment the reference count
#'  of the callback function that is passed to the various addcb* functions.
#'  So it is up to the user to make sure that the reference count of these
#'  functions does not drop to zero while the optimizer might still call them.
#'
#'  Finally, due to the fact that R is inherently single-threaded, all callbacks
#'  must be executed from the thread that runs R. To this end, the createprob()
#'  function automatically sets the xpress:::CALLBACKFROMMASTERTHREAD control
#'  to 1. Don't change this if you are using any kind of callback.
#'
#'
#'
#' We read the facility location problem from the introductory example.
#' By printing the number of MIP entities, we verify
#' that this is a problem with (some) integer variables, so MIP algorithms and
#' callbacks will be applied to solve it.
#'
# this also creates a problem
print(p)

print(getintattrib(p, xpress:::MIPENTS))

#'
#'
#' # Callback Definition
#'
#' ## User Data
#' We define a data frame object progress.df in the main scope of R.
#' This is our "data" that we want to use inside the callback.
#' Thanks to R scoping rules, we can access progress.df from inside the callback.
## ----Setup User Data----------------------------------------------------------
# initialize an empty progress data frame
# this is input into the callback
progress.df <- data.frame(
"Node"=integer(),
"MIPBestObjVal"=numeric(),
"BestBound"=numeric()
)

#'
#' ## Callback Definition
#'
#' We solve this problem using the default algorithm but with two callbacks that
#' are invoked at each node for which the LP relaxation has been solved to
#' optimality or nodes that were infeasible.
#'
#' We report progress by printing the best integer solution found so far
#' and the best solution possible.
#' We append the data; Note the use of the  <<- operator,
#' which modifies the global progress data frame
#' instead of creating a local copy
#'
## ----Callback Definition------------------------------------------------------
"primal bound:", getdblattrib(prob, xpress:::MIPBESTOBJVAL),
"\n"),
file = "optnodecallbacklog.txt", append = T)

### Caution: This approach is for illustrative purposes, but super ineffective for larger search trees,
###          because the entire data frame is copied at each call.
progress.df <<- rbind(
progress.df,
data.frame(
"Node"=getintattrib(prob, xpress:::NODES),
"MIPBestObjVal"=getdblattrib(prob, xpress:::MIPBESTOBJVAL),
"BestBound"=getdblattrib(prob, xpress:::BESTBOUND)
)
)

return(0)
}

#'
#' ## Callback Registration
#'
#' We register our function as two callbacks into the loaded problem. The output of
#' the infnode callback will enrich our data set. We register the same function for
#' both 'opt'imal and 'inf'easible nodes, but with a different header.
#'
## ----Callback Registration----------------------------------------------------

#'
#' ## Invoking the Solution Process
#'
#' We redirect the output into a log file, invoke the optimization process, and print the solution.
#' For easier interpretation of the results, we solve the problem single-threaded.
#'
## ----Invoke The Solution Process----------------------------------------------
setlogfile(p, "optnodecallbacklog.txt")

# prints to stdout. This does not affect the logfile
setoutput(p)

# use only one thread for easier interpretation of the data frame
xprs_optimize(p)

# We retrieve the solution print the solution values for the x variables
sol <- xprs_getsolution(p)
# note that this print does not show in the log file, only in this code chunk.
print(sol[1:5])

#'
## ----Show The Data Frame------------------------------------------------------
print(progress.df)

#'
## ----Plot The Progress using ggplot2------------------------------------------

progress.df %>%
ggplot(aes(1:nrow(progress.df))) +
geom_line(mapping = aes(y=MIPBestObjVal, color="MIPBestObjVal")) +
geom_line(mapping = aes(y=BestBound, color="BestBound")) +
labs(
y="Objective",
x="Call",
color=NULL
) + theme_dark()

#'
#' ## The Output
#'
#' The log file output shows that our callback has been successfully applied.
#'
## ----echo=FALSE---------------------------------------------------------------