Skip to content

Commit ac51f5e

Browse files
committed
Irace callback feature
(cherry picked from commit bec759c)
1 parent e491c25 commit ac51f5e

10 files changed

Lines changed: 249 additions & 34 deletions

File tree

‎NEWS.md‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
**If you are viewing this file on CRAN, please check [latest news on the irace website](https://mlopez-ibanez.github.io/irace/news/index.html) where the formatting is also better.**
22

3-
# irace 4.4.9000
43

4+
# irace 4.5
5+
6+
## New features and improvements
7+
8+
* #98 irace callback: Add an optional `iterationCallback` scenario function that is invoked after each completed irace iteration.
9+
This allows irace users to monitor tuning progress or collect intermediate results without modifying irace internals. Check the user guide for more details and an example implementation.
510

611
# irace 4.4.4
712

8-
* The package contains an example of using `irace` to tune algorithms
9-
implemented in Julia language.
13+
* The package contains an example of using `irace` to tune algorithms
14+
implemented in Julia language.
1015

11-
* Fix #94: `sample_model.ParamOrd()` now samples uniformly when the model
12-
mean is `NA`, avoiding `NA` values for active ordinal parameters.
13-
Reported by @Saethox.
16+
* Fix #94: `sample_model.ParamOrd()` now samples uniformly when the model
17+
mean is `NA`, avoiding `NA` values for active ordinal parameters.
18+
Reported by @Saethox.
1419

15-
* Fix #97: `psRace()` errors when the post-selection race has nothing left to
16-
run. Fixed by @ivchicano.
20+
* Fix #97: `psRace()` errors when the post-selection race has nothing left to
21+
run. Fixed by @ivchicano.
1722

1823
# irace 4.4.3
1924

‎R/irace-options.R‎

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,20 @@
1616
"minExperiments", "maxTime", "budgetEstimation", "minMeasurableTime",
1717
"parallel", "loadBalancing", "mpi", "batchmode", "quiet", "debugLevel",
1818
"seed", "softRestart", "softRestartThreshold", "elitist", "elitistNewInstances",
19-
"elitistLimit", "repairConfiguration", "capping", "cappingAfterFirstTest",
20-
"cappingType", "boundType", "boundMax", "boundDigits", "boundPar",
21-
"boundAsTimeout", "postselection", "aclib", "nbIterations", "nbExperimentsPerIteration",
22-
"minNbSurvival", "nbConfigurations", "mu", "confidence"), type = c("x",
23-
"x", "x", "x", "p", "p", "p", "p", "x", "x", "p", "p", "p", "s",
24-
"p", "p", "b", "p", "p", "x", "i", "b", "s", "i", "i", "i", "p",
25-
"p", "s", "i", "i", "x", "x", "p", "b", "i", "i", "i", "r", "r",
26-
"i", "b", "b", "s", "b", "i", "i", "b", "r", "b", "i", "i", "x",
27-
"b", "b", "s", "s", "i", "i", "i", "b", "b", "b", "i", "i", "i",
28-
"i", "i", "r"), short = c("-h", "-v", "-c", "-i", "", "-s", "",
29-
"-p", "", "", "", "-l", "", "", "", "", "", "", "", "", "", "",
19+
"elitistLimit", "repairConfiguration", "iterationCallback", "capping",
20+
"cappingAfterFirstTest", "cappingType", "boundType", "boundMax",
21+
"boundDigits", "boundPar", "boundAsTimeout", "postselection",
22+
"aclib", "nbIterations", "nbExperimentsPerIteration", "minNbSurvival",
23+
"nbConfigurations", "mu", "confidence"), type = c("x", "x", "x",
24+
"x", "p", "p", "p", "p", "x", "x", "p", "p", "p", "s", "p", "p",
25+
"b", "p", "p", "x", "i", "b", "s", "i", "i", "i", "p", "p", "s",
26+
"i", "i", "x", "x", "p", "b", "i", "i", "i", "r", "r", "i", "b",
27+
"b", "s", "b", "i", "i", "b", "r", "b", "i", "i", "x", "x", "b",
28+
"b", "s", "s", "i", "i", "i", "b", "b", "b", "i", "i", "i", "i",
29+
"i", "r"), short = c("-h", "-v", "-c", "-i", "", "-s", "", "-p",
30+
"", "", "", "-l", "", "", "", "", "", "", "", "", "", "", "",
3031
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
31-
"", "", "", "", "", "", "-q", "", "", "", "", "-e", "", "", "",
32+
"", "", "", "", "", "-q", "", "", "", "", "-e", "", "", "", "",
3233
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""
3334
), long = c("--help", "--version", "--check", "--init", "--only-test",
3435
"--scenario", "--exec-dir", "--parameter-file", "", "", "--configurations-file",
@@ -43,7 +44,7 @@
4344
"--parallel", "--load-balancing", "--mpi", "--batchmode", "--quiet",
4445
"--debug-level", "--seed", "--soft-restart", "--soft-restart-threshold",
4546
"--elitist", "--elitist-new-instances", "--elitist-limit", "",
46-
"--capping", "--capping-after-first-test", "--capping-type",
47+
"", "--capping", "--capping-after-first-test", "--capping-type",
4748
"--bound-type", "--bound-max", "--bound-digits", "--bound-par",
4849
"--bound-as-timeout", "--postselection", "--aclib", "--iterations",
4950
"--experiments-per-iteration", "--min-survival", "--num-configurations",
@@ -53,13 +54,13 @@
5354
"", "{configurationID} {instanceID} {seed} {instance} {bound} {targetRunnerArgs}",
5455
"0", "0", "", "", "", "0", "0", NA, "0", "0.05", "0.01", "0",
5556
"1", "0", "0", "0", "0", NA, "1", "1e-04", "1", "1", "2", "",
56-
NA, "0", "median", "candidate", "0", "0", "1", "1", "1", "0",
57-
"0", "0", "0", "0", "5", "0.95"), domain = c(NA, NA, NA, NA,
57+
"", NA, "0", "median", "candidate", "0", "0", "1", "1", "1",
58+
"0", "0", "0", "0", "0", "5", "0.95"), domain = c(NA, NA, NA,
5859
NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,
59-
NA, NA, "F-test,t-test,t-test-holm,t-test-bonferroni", NA, NA,
60+
NA, NA, NA, "F-test,t-test,t-test-holm,t-test-bonferroni", NA,
6061
NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,
61-
NA, NA, "sge,pbs,torque,slurm,htcondor", NA, NA, NA, NA, NA,
62-
NA, NA, NA, NA, NA, NA, "median,mean,worst,best", "instance,candidate",
62+
NA, NA, NA, "sge,pbs,torque,slurm,htcondor", NA, NA, NA, NA,
63+
NA, NA, NA, NA, NA, NA, NA, NA, "median,mean,worst,best", "instance,candidate",
6364
NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA), description = c("Show this help.",
6465
"Show irace package version.", "Check scenario.", "Initialize the working directory with template config files.",
6566
"Only test the configurations given in the file passed as argument.",
@@ -103,6 +104,7 @@ NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA), description = c("Show this help
103104
"Number of instances added to the execution list before previous instances in elitist irace.",
104105
"In elitist irace, maximum number per race of elimination tests that do not eliminate a configuration. Use 0 for no limit.",
105106
"User-defined R function that takes a configuration generated by irace and repairs it.",
107+
"Optional R function called after each irace iteration with the iteration number, ordered elite configurations, and progress information.",
106108
"Enable the use of adaptive capping, a technique designed for minimizing the computation time of configurations. Capping is enabled by default if `elitist` is active, `maxTime > 0` and `boundMax > 0`.",
107109
"If set to 1, elimination due to capping only happens after `firstTest` instances are seen.",
108110
"Measure used to obtain the execution bound from the performance of the elite configurations: median, mean, worst, best.",
@@ -130,10 +132,11 @@ NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA), description = c("Show this help
130132
"maxTime", "budgetEstimation", "minMeasurableTime", "parallel",
131133
"loadBalancing", "mpi", "batchmode", "quiet", "debugLevel", "seed",
132134
"softRestart", "softRestartThreshold", "elitist", "elitistNewInstances",
133-
"elitistLimit", "repairConfiguration", "capping", "cappingAfterFirstTest",
134-
"cappingType", "boundType", "boundMax", "boundDigits", "boundPar",
135-
"boundAsTimeout", "postselection", "aclib", "nbIterations", "nbExperimentsPerIteration",
136-
"minNbSurvival", "nbConfigurations", "mu", "confidence"), class = "data.frame")
135+
"elitistLimit", "repairConfiguration", "iterationCallback", "capping",
136+
"cappingAfterFirstTest", "cappingType", "boundType", "boundMax",
137+
"boundDigits", "boundPar", "boundAsTimeout", "postselection",
138+
"aclib", "nbIterations", "nbExperimentsPerIteration", "minNbSurvival",
139+
"nbConfigurations", "mu", "confidence"), class = "data.frame")
137140
.irace.params.names <- c("scenarioFile", "execDir", "parameterFile", "parameters", "initConfigurations",
138141
"configurationsFile", "logFile", "recoveryFile", "instances",
139142
"trainInstancesDir", "trainInstancesFile", "sampleInstances",
@@ -145,10 +148,11 @@ NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA), description = c("Show this help
145148
"minExperiments", "maxTime", "budgetEstimation", "minMeasurableTime",
146149
"parallel", "loadBalancing", "mpi", "batchmode", "quiet", "debugLevel",
147150
"seed", "softRestart", "softRestartThreshold", "elitist", "elitistNewInstances",
148-
"elitistLimit", "repairConfiguration", "capping", "cappingAfterFirstTest",
149-
"cappingType", "boundType", "boundMax", "boundDigits", "boundPar",
150-
"boundAsTimeout", "postselection", "aclib", "nbIterations", "nbExperimentsPerIteration",
151-
"minNbSurvival", "nbConfigurations", "mu", "confidence")
151+
"elitistLimit", "repairConfiguration", "iterationCallback", "capping",
152+
"cappingAfterFirstTest", "cappingType", "boundType", "boundMax",
153+
"boundDigits", "boundPar", "boundAsTimeout", "postselection",
154+
"aclib", "nbIterations", "nbExperimentsPerIteration", "minNbSurvival",
155+
"nbConfigurations", "mu", "confidence")
152156
## FIXME: If these values are special perhaps they should be saved in $state ?
153157
.irace.params.recover <- c("instances", "seed", "testInstances",
154158
# We need this because this data may mutate

‎R/irace.R‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,27 @@ irace_run <- function(scenario)
12131213
race_state$print_mem_used()
12141214
}
12151215
}
1216+
if (!is.null(scenario$iterationCallback)) {
1217+
progress <- list(
1218+
nbIterations = nbIterations,
1219+
maxExperiments = scenario$maxExperiments,
1220+
experimentsUsed = experimentsUsed,
1221+
remainingBudget = remainingBudget,
1222+
remainingBudgetEstimated = scenario$maxTime > 0L,
1223+
currentBudget = currentBudget,
1224+
currentBudgetUsed = raceResults$experimentsUsed,
1225+
maxTime = scenario$maxTime,
1226+
timeUsed = timeUsed,
1227+
remainingTime = if (scenario$maxTime > 0L)
1228+
scenario$maxTime - timeUsed
1229+
else NA_real_,
1230+
boundEstimate = boundEstimate)
1231+
withr::with_preserve_seed(
1232+
scenario$iterationCallback(
1233+
iteration = indexIteration,
1234+
elites = data.table::copy(elite_configurations),
1235+
progress = progress))
1236+
}
12161237
indexIteration <- indexIteration + 1L
12171238
} # end of repeat
12181239
}

‎R/scenario.R‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,13 @@ checkScenario <- function(scenario = defaultScenario())
316316
else
317317
irace_error("'repairConfiguration' must be a function")
318318

319+
if (is.null.or.empty(scenario$iterationCallback))
320+
scenario$iterationCallback <- NULL
321+
else if (is.function.name(scenario$iterationCallback))
322+
scenario$iterationCallback <- get.function(scenario$iterationCallback)
323+
else
324+
irace_error("'iterationCallback' must be a function")
325+
319326
if (is.na(scenario$capping))
320327
scenario$capping <- (scenario$elitist && scenario$maxTime > 0 &&
321328
!is.na(scenario$boundMax) && scenario$boundMax > 0)
@@ -565,6 +572,7 @@ printScenario <- function(scenario)
565572
#' \item{`debugLevel`}{Debug level of the output of `irace`. Set this to 0 to silence all debug messages. Higher values (1, 2 or 3) provide more verbose debug messages. (Default: `0`)}
566573
#' \item{`seed`}{Seed of the random number generator (by default, generate a random seed). (Default: `NA`)}
567574
#' \item{`repairConfiguration`}{User-defined R function that takes a configuration generated by irace and repairs it. (Default: `""`)}
575+
#' \item{`iterationCallback`}{Optional R function called after each irace iteration with the iteration number, ordered elite configurations, and progress information. (Default: `""`)}
568576
#' \item{`postselection`}{Perform a postselection race after the execution of irace to consume all remaining budget (requires elitist racing). Value 0 disables the postselection race. (Default: `1`)}
569577
#' \item{`aclib`}{Enable/disable AClib mode. This option enables compatibility with GenericWrapper4AC as targetRunner script. (Default: `0`)}
570578
#' }

‎inst/templates/scenario.txt.tmpl‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@
188188
## repairs it.
189189
# repairConfiguration = ""
190190

191+
## Optional R function called after each irace iteration with the iteration
192+
## number, ordered elite configurations, and progress information.
193+
# iterationCallback = ""
194+
191195
## Enable the use of adaptive capping, a technique designed for minimizing the
192196
## computation time of configurations. Capping is enabled by default if
193197
## `elitist` is active, `maxTime > 0` and `boundMax > 0`.

‎man/defaultScenario.Rd‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎scripts/irace_options.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,16 @@
521521
"vignettes": "User-defined \\aR function that takes a configuration generated by \\irace and repairs it. See \\autoref{sec:repairconf} for details.",
522522
"section": "General options"
523523
},
524+
{
525+
"name": "iterationCallback",
526+
"type": "x",
527+
"short": "",
528+
"long": "",
529+
"default": "",
530+
"description": "Optional R function called after each irace iteration with the iteration number, ordered elite configurations, and progress information.",
531+
"vignettes": "Optional \\aR function called after each \\irace iteration with the iteration number, ordered elite configurations, and progress information. See \\autoref{sec:iteration callback} for details.",
532+
"section": "General options"
533+
},
524534
{
525535
"name": "capping",
526536
"type": "b",
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
callback_scenario <- function(log_file)
2+
{
3+
parameters <- readParameters(text = 'x "" i (1, 10)')
4+
list(
5+
targetRunner = function(experiment, scenario) {
6+
list(cost = abs(experiment$configuration[["x"]] - experiment$instance))
7+
},
8+
instances = seq_len(10L),
9+
maxExperiments = 100L,
10+
seed = 1234567L,
11+
quiet = TRUE,
12+
postselection = FALSE,
13+
logFile = log_file,
14+
parameters = parameters
15+
)
16+
}
17+
18+
test_that("iterationCallback receives named arguments after every iteration", {
19+
log_file <- withr::local_tempfile(fileext = ".Rdata")
20+
snapshots <- list()
21+
# The arguments are deliberately reordered to verify that irace calls the
22+
# callback by name. The progress argument is collected through ... to ensure
23+
# callbacks remain compatible when irace adds named arguments.
24+
callback <- function(elites, iteration, ...) {
25+
dots <- list(...)
26+
expect_named(dots, "progress")
27+
snapshots[[iteration]] <<- list(
28+
elites = data.table::copy(elites),
29+
progress = dots$progress)
30+
runif(1L)
31+
data.table::set(elites, i = 1L, j = ".ID.", value = -1L)
32+
}
33+
34+
scenario <- callback_scenario(log_file)
35+
scenario$iterationCallback <- callback
36+
with_callback <- irace(scenario)
37+
results <- read_logfile(log_file)
38+
39+
expect_length(snapshots, length(results$allElites))
40+
for (iteration in seq_along(snapshots)) {
41+
progress <- snapshots[[iteration]]$progress
42+
expect_identical(
43+
snapshots[[iteration]]$elites[[".ID."]],
44+
results$allElites[[iteration]])
45+
expect_named(progress, c(
46+
"nbIterations", "maxExperiments", "experimentsUsed",
47+
"remainingBudget", "remainingBudgetEstimated", "currentBudget",
48+
"currentBudgetUsed", "maxTime", "timeUsed", "remainingTime",
49+
"boundEstimate"))
50+
expect_gte(progress$nbIterations, iteration)
51+
expect_identical(progress$maxExperiments, 100L)
52+
expect_identical(progress$experimentsUsed + progress$remainingBudget,
53+
progress$maxExperiments)
54+
expect_false(progress$remainingBudgetEstimated)
55+
expect_gte(progress$currentBudget, progress$currentBudgetUsed)
56+
expect_gt(progress$currentBudgetUsed, 0L)
57+
expect_identical(progress$maxTime, 0L)
58+
expect_identical(progress$timeUsed, 0)
59+
expect_true(is.na(progress$remainingTime))
60+
expect_true(is.na(progress$boundEstimate))
61+
}
62+
expect_true(all(with_callback[[".ID."]] > 0L))
63+
64+
without_callback <- irace(
65+
callback_scenario(withr::local_tempfile(fileext = ".Rdata")))
66+
expect_identical(with_callback, without_callback)
67+
})
68+
69+
test_that("iterationCallback reports time-budget progress", {
70+
snapshots <- list()
71+
callback <- function(iteration, elites, progress, ...) {
72+
snapshots[[iteration]] <<- progress
73+
}
74+
scenario <- callback_scenario("")
75+
scenario$maxExperiments <- 0L
76+
scenario$maxTime <- 100
77+
scenario$targetRunner <- function(experiment, scenario) {
78+
list(
79+
cost = abs(experiment$configuration[["x"]] - experiment$instance),
80+
time = 1)
81+
}
82+
scenario$iterationCallback <- callback
83+
84+
irace(scenario)
85+
86+
expect_gt(length(snapshots), 0L)
87+
for (progress in snapshots) {
88+
expect_identical(progress$maxExperiments, 0L)
89+
expect_identical(progress$maxTime, 100L)
90+
expect_true(progress$remainingBudgetEstimated)
91+
expect_gt(progress$experimentsUsed, 0L)
92+
expect_gte(progress$currentBudget, progress$currentBudgetUsed)
93+
expect_equal(progress$remainingTime,
94+
progress$maxTime - progress$timeUsed)
95+
expect_equal(progress$boundEstimate,
96+
progress$timeUsed / progress$experimentsUsed)
97+
}
98+
})
99+
100+
test_that("iterationCallback must be NULL or a function", {
101+
scenario <- callback_scenario("")
102+
scenario$iterationCallback <- "invalid"
103+
expect_error(
104+
irace(scenario),
105+
"'iterationCallback' must be a function",
106+
fixed = TRUE
107+
)
108+
})

‎vignettes/irace-package.Rnw‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2283,6 +2283,57 @@ the ACVIZ software package~\cite{DesRitLopPer2021acviz}, which is available at \
22832283
22842284
\section{Advanced topics}
22852285
2286+
\subsection{Monitoring iterations with a callback}
2287+
\label{sec:iteration callback}
2288+
2289+
The scenario option \parameter{iterationCallback} may be set to an \aR
2290+
function that is called after every completed \irace iteration. For example,
2291+
the following callback reports the consumed budget and the identifier of the
2292+
best configuration found in each iteration:
2293+
2294+
<<iterationCallback,prompt=FALSE,eval=FALSE>>=
2295+
report_progress <- function(iteration, elites, progress, ...)
2296+
{
2297+
if (progress$maxTime > 0) {
2298+
used <- progress$timeUsed
2299+
total <- progress$maxTime
2300+
budget_type <- "time"
2301+
} else {
2302+
used <- progress$experimentsUsed
2303+
total <- progress$maxExperiments
2304+
budget_type <- "experiments"
2305+
}
2306+
message("Iteration ", iteration, "/", progress$nbIterations,
2307+
": ", used, "/", total, " ", budget_type,
2308+
" used; best configuration: ", elites[[".ID."]][1L])
2309+
}
2310+
scenario$iterationCallback <- report_progress
2311+
irace(scenario)
2312+
@
2313+
2314+
The callback is called with the named arguments \code{iteration},
2315+
\code{elites}, and \code{progress}. User-defined callbacks should use these
2316+
exact argument names and take \code{...} as their last argument so they remain
2317+
compatible if future versions of \irace provide more information. The
2318+
\code{elites} data frame is ordered from best to worst and is copied before it
2319+
is passed to the callback.
2320+
2321+
The \code{progress} list contains \code{nbIterations},
2322+
\code{maxExperiments}, \code{experimentsUsed}, \code{remainingBudget},
2323+
\code{remainingBudgetEstimated}, \code{currentBudget},
2324+
\code{currentBudgetUsed}, \code{maxTime}, \code{timeUsed},
2325+
\code{remainingTime}, and \code{boundEstimate}. The budgets
2326+
\code{remainingBudget}, \code{currentBudget}, and \code{currentBudgetUsed}
2327+
are numbers of target-algorithm executions. When \parameter{maxTime} is used,
2328+
\code{remainingBudget} is estimated from \code{boundEstimate}, and
2329+
\code{remainingBudgetEstimated} is \code{TRUE}. In that mode,
2330+
\code{remainingTime} uses the same unit as \parameter{maxTime}; otherwise,
2331+
\code{remainingTime} and \code{boundEstimate} are \code{NA}.
2332+
2333+
The callback return value is ignored. Errors raised by the callback stop
2334+
\irace. Random-number generation within the callback does not affect the
2335+
random-number sequence of the tuning run.
2336+
22862337
\subsection{Tuning budget}\label{sec:budget}
22872338
22882339
Before setting the budget for a run of \irace, please consider the number of

0 commit comments

Comments
 (0)