Register an R function as callback into Xpress
|
|
|
| Type: | Programming |
| Rating: | 3 (intermediate) |
| Description: | This example shows how to collect data about solution process using callbacks. |
| File(s): | callback.R |
| Data file(s): | flp.lp |
|
|
|
| callback.R |
#####################################
# This file is part of the #
# Xpress-R interface examples #
# #
# (c) 2022-2024 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
#' details about that.
#' - 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.
#'
#'
#' # Reading the Example
#'
#' 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.
#'
## ----Read The Example---------------------------------------------------------
# this also creates a problem
p <- readprob(createprob(), "flp.lp")
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------------------------------------------------------
callback <- function(prob, header) {
cat(paste(header, "dual bound:", getdblattrib(prob, xpress:::BESTBOUND),
"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----------------------------------------------------
headeropt <- "OPTCALLBACK:"
headerinf <- "INFCALLBACK:"
addcboptnode(p, function(prob) {callback(prob, headeropt)})
addcbinfnode(p, function(prob) {callback(prob, headerinf)})
#'
#' ## 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
setintcontrol(p, xpress:::THREADS,1)
xprs_optimize(p)
# We retrieve the solution print the solution values for the x variables
sol <- getsolution(p)$x
# 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---------------------------------------------------------------
cat(readLines("optnodecallbacklog.txt"), sep = "\n")
|
© 2001-2024 Fair Isaac Corporation. All rights reserved. This documentation is the property of Fair Isaac Corporation (“FICO”). Receipt or possession of this documentation does not convey rights to disclose, reproduce, make derivative works, use, or allow others to use it except solely for internal evaluation purposes to determine whether to purchase a license to the software described in this documentation, or as otherwise set forth in a written software license agreement between you and FICO (or a FICO affiliate). Use of this documentation and the software described in it must conform strictly to the foregoing permitted uses, and no other use is permitted.
