
Set the epsilon-constraint multi-objective method
Source:R/set_method_epsilon_constraint.R
set_method_epsilon_constraint.RdConfigure a Problem object to be solved with the
epsilon-constraint multi-objective method.
In this method, one objective is designated as the primary objective
and is optimized directly, while the remaining objectives are transformed
into \(\varepsilon\)-constraints. Multiple subproblems are generated using
a run design supplied through runs.
This function does not solve the problem. It stores the method configuration
in x$data$method, to be used later by solve.
Usage
set_method_epsilon_constraint(
x,
primary,
aliases = NULL,
runs = NULL,
eps = NULL,
mode = NULL,
n_points = NULL,
include_extremes = NULL,
lexicographic = TRUE,
lexicographic_tol = 1e-08,
control = NULL
)Arguments
- x
A
Problemobject.- primary
Character string giving the alias of the primary objective to optimize directly.
- aliases
Optional character vector of objective aliases to include. By default, all registered objective aliases are used. The value of
primarymust be included inaliases.- runs
A run design created with
run_gridorrun_manual. For epsilon-constraint methods,run_grid()requests automatic epsilon-level generation, whilerun_manual()requires columns namedeps_<alias>for each constrained objective.- eps
Deprecated. Epsilon specification used by the previous
mode = "manual"interface. It may be a named numeric vector or a named list of numeric vectors. New code should useruns = run_manual(...)instead.- mode
Deprecated. Previous interface selector, either
"manual"or"auto". New code should useruns = run_manual(...)orruns = run_grid(...)instead.- n_points
Deprecated. Previous automatic-grid argument. New code should use
runs = run_grid(n = ...)instead.- include_extremes
Deprecated. Previous automatic-grid argument. New code should use
runs = run_grid(n = ..., include_extremes = ...)instead.- lexicographic
Logical scalar. If
TRUE, compute automatic-grid extreme points lexicographically whenruns = run_grid(...)is used.- lexicographic_tol
Numeric scalar \(\ge 0\). Tolerance used in lexicographic extreme-point computation.
- control
A control object created with
mo_control. It controls how infeasible runs, runs without a solution, and unexpected errors are handled.
Value
An updated Problem object with the epsilon-constraint method
configuration stored in x$data$method.
Details
Use this method when one objective should be optimized directly while the remaining objectives are controlled through explicit performance thresholds.
General idea
Suppose that \(m \ge 2\) objective functions have already been registered in the problem: $$ f_1(x), f_2(x), \dots, f_m(x). $$
The epsilon-constraint method selects one of them as the primary objective, say \(f_p(x)\), and treats the remaining objectives as constrained objectives.
For a fixed vector of epsilon levels, the method solves subproblems in which the primary objective is optimized directly and the remaining objectives are imposed through epsilon constraints.
A representative formulation is:
$$ \max \; f_p(x) $$
subject to
$$ f_k(x) \ge \varepsilon_k, \qquad k \in \mathcal{C}, $$
together with all original feasibility constraints of the planning problem, where \(\mathcal{C}\) is the set of constrained objectives.
Depending on the sense of each objective, the internal implementation may transform minimization and maximization objectives into equivalent solver-ready constrained forms. The method always follows the same principle:
one objective is optimized directly,
all remaining objectives are imposed through \(\varepsilon\)-constraints.
By solving the problem repeatedly for different epsilon levels, the method generates a set of trade-off solutions.
Run designs
Epsilon-constraint runs are specified through the runs argument. This
argument must be created with either run_grid or
run_manual.
run_grid(n = ...) requests automatic generation of epsilon levels
during solve. The epsilon levels are computed from
extreme-point or payoff information. In the current implementation,
run_grid() for epsilon-constraint supports exactly two objectives:
one primary objective and one constrained objective.
run_manual() allows users to provide explicit epsilon combinations.
In manual epsilon-constraint runs, each row is one optimization run and
columns must be named eps_<alias>, where <alias> is the alias of
a constrained objective. For example, if primary = "benefit" and
aliases = c("benefit", "cost", "loss"), the manual run table must
contain columns eps_cost and eps_loss.
In run_manual(), each row is used exactly as supplied. The function
does not automatically create a Cartesian product of epsilon values. If a
Cartesian product is desired, it should be created explicitly by the user,
for example with expand.grid, and then passed to
run_manual().
The older arguments eps, mode, n_points, and
include_extremes are deprecated. They are still accepted for
backwards compatibility and are internally converted to run_grid() or
run_manual() designs.
Atomic objectives requirement
The epsilon-constraint method can only be used with atomic objectives that
have already been registered under aliases. These aliases are typically
created by calling objective setters with an alias argument, for
example:
x <- x |>
add_objective_max_benefit(alias = "benefit") |>
add_objective_min_cost(alias = "cost") |>
add_objective_min_fragmentation(alias = "frag")The primary argument selects which registered objective is optimized
directly. The remaining aliases are treated as constrained objectives.
Automatic epsilon grids
When runs = run_grid(n = ...) is used, the epsilon grid is not built
immediately. Instead, it is constructed later during solve
using extreme-point or payoff information.
In the current implementation, automatic epsilon-grid generation supports exactly two objectives:
one primary objective,
one constrained objective.
Therefore, if runs = run_grid(...), then aliases must contain
exactly two objective aliases. Problems with three or more objectives must
use runs = run_manual(...).
If include_extremes = TRUE is supplied inside run_grid(), the
automatically generated grid includes the extreme values of the constrained
objective. Otherwise, only interior values are used.
If lexicographic = TRUE, the extreme points used to generate the grid
are computed lexicographically. In that case, one objective is optimized
first, and then the second objective is optimized while constraining the
first to remain within lexicographic_tol of its optimum.
Manual epsilon runs
Manual run designs support two or more objectives. They are the general way to use the epsilon-constraint method when more than two objectives are involved.
For example, with one primary objective and two constrained objectives, a manual run design may contain:
data.frame(
eps_cost = c(4, 6, 8),
eps_loss = c(0, 1, 1)
)This creates three runs, not a full Cartesian grid. To create all
combinations, use expand.grid() before calling run_manual().
Failure handling
The control argument controls how failed runs are handled. It must be
created with mo_control.
Some epsilon levels may define infeasible subproblems. By default, failed
runs can be retained in the returned SolutionSet with missing
objective values, while feasible runs are preserved. Alternatively, users can
request that the solve stops when an infeasible run, a run without a solution,
or an unexpected error is encountered.
Stored configuration
The configured method stores:
name = "epsilon_constraint",type = "epsilon_constraint",primary,aliases,constrained,runs,lexicographic configuration,
control.
With runs = run_grid(...), the actual epsilon design is generated
later during solve. With runs = run_manual(...), the
explicit user-supplied run design is stored and then used by
solve.
Examples
# Small toy problem
pu_tbl <- data.frame(
id = 1:4,
cost = c(1, 2, 3, 4)
)
feat_tbl <- data.frame(
id = 1:2,
name = c("feature_1", "feature_2")
)
dist_feat_tbl <- data.frame(
pu = c(1, 1, 2, 3, 4),
feature = c(1, 2, 2, 1, 2),
amount = c(5, 2, 3, 4, 1)
)
actions_df <- data.frame(
id = c("conservation", "restoration"),
name = c("conservation", "restoration")
)
effects_df <- data.frame(
pu = c(1, 2, 3, 4, 1, 2, 3, 4),
action = c("conservation", "conservation", "conservation", "conservation",
"restoration", "restoration", "restoration", "restoration"),
feature = c(1, 1, 1, 1, 2, 2, 2, 2),
benefit = c(2, 1, 0, 1, 3, 0, 1, 2),
loss = c(0, 0, 1, 0, 0, 1, 0, 0)
)
x <- create_problem(
pu = pu_tbl,
features = feat_tbl,
dist_features = dist_feat_tbl,
cost = "cost"
) |>
add_actions(actions_df, cost = c(conservation = 1, restoration = 2)) |>
add_effects(effects_df) |>
add_objective_min_cost(alias = "cost") |>
add_objective_max_benefit(alias = "benefit") |>
add_objective_min_loss(alias = "loss")
# Automatic epsilon grid for two objectives
x1 <- set_method_epsilon_constraint(
x,
primary = "benefit",
aliases = c("benefit", "cost"),
runs = run_grid(n = 5, include_extremes = TRUE),
lexicographic = TRUE,
lexicographic_tol = 1e-8
)
x1$data$method
#> $name
#> [1] "epsilon_constraint"
#>
#> $type
#> [1] "epsilon_constraint"
#>
#> $primary
#> [1] "benefit"
#>
#> $aliases
#> [1] "benefit" "cost"
#>
#> $constrained
#> [1] "cost"
#>
#> $runs
#> $type
#> [1] "grid"
#>
#> $n
#> [1] 5
#>
#> $include_extremes
#> [1] TRUE
#>
#> attr(,"class")
#> [1] "RunGrid" "RunDesign"
#>
#> $lexicographic
#> [1] TRUE
#>
#> $lexicographic_tol
#> [1] 1e-08
#>
#> $control
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
#> $slack_upper_bound
#> [1] 1e+06
#>
#> attr(,"class")
#> [1] "MOControl" "list"
#>
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
# Manual runs with one constrained objective
eps_runs <- data.frame(
eps_cost = c(4, 6, 8)
)
x2 <- set_method_epsilon_constraint(
x,
primary = "benefit",
aliases = c("benefit", "cost"),
runs = run_manual(eps_runs)
)
x2$data$method
#> $name
#> [1] "epsilon_constraint"
#>
#> $type
#> [1] "epsilon_constraint"
#>
#> $primary
#> [1] "benefit"
#>
#> $aliases
#> [1] "benefit" "cost"
#>
#> $constrained
#> [1] "cost"
#>
#> $runs
#> $type
#> [1] "manual"
#>
#> $values
#> eps_cost
#> 1 4
#> 2 6
#> 3 8
#>
#> attr(,"class")
#> [1] "RunManual" "RunDesign"
#>
#> $lexicographic
#> [1] TRUE
#>
#> $lexicographic_tol
#> [1] 1e-08
#>
#> $control
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
#> $slack_upper_bound
#> [1] 1e+06
#>
#> attr(,"class")
#> [1] "MOControl" "list"
#>
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
# Manual runs with more than two objectives
eps_runs_3obj <- data.frame(
eps_cost = c(4, 6, 8),
eps_loss = c(0, 1, 1)
)
x3 <- set_method_epsilon_constraint(
x,
primary = "benefit",
aliases = c("benefit", "cost", "loss"),
runs = run_manual(eps_runs_3obj)
)
x3$data$method
#> $name
#> [1] "epsilon_constraint"
#>
#> $type
#> [1] "epsilon_constraint"
#>
#> $primary
#> [1] "benefit"
#>
#> $aliases
#> [1] "benefit" "cost" "loss"
#>
#> $constrained
#> [1] "cost" "loss"
#>
#> $runs
#> $type
#> [1] "manual"
#>
#> $values
#> eps_cost eps_loss
#> 1 4 0
#> 2 6 1
#> 3 8 1
#>
#> attr(,"class")
#> [1] "RunManual" "RunDesign"
#>
#> $lexicographic
#> [1] TRUE
#>
#> $lexicographic_tol
#> [1] 1e-08
#>
#> $control
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
#> $slack_upper_bound
#> [1] 1e+06
#>
#> attr(,"class")
#> [1] "MOControl" "list"
#>
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
# Cartesian epsilon design created explicitly by the user
eps_cartesian <- expand.grid(
eps_cost = c(4, 6, 8),
eps_loss = c(0, 1),
KEEP.OUT.ATTRS = FALSE
)
x4 <- set_method_epsilon_constraint(
x,
primary = "benefit",
aliases = c("benefit", "cost", "loss"),
runs = run_manual(eps_cartesian)
)
x4$data$method
#> $name
#> [1] "epsilon_constraint"
#>
#> $type
#> [1] "epsilon_constraint"
#>
#> $primary
#> [1] "benefit"
#>
#> $aliases
#> [1] "benefit" "cost" "loss"
#>
#> $constrained
#> [1] "cost" "loss"
#>
#> $runs
#> $type
#> [1] "manual"
#>
#> $values
#> eps_cost eps_loss
#> 1 4 0
#> 2 6 0
#> 3 8 0
#> 4 4 1
#> 5 6 1
#> 6 8 1
#>
#> attr(,"class")
#> [1] "RunManual" "RunDesign"
#>
#> $lexicographic
#> [1] TRUE
#>
#> $lexicographic_tol
#> [1] 1e-08
#>
#> $control
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
#> $slack_upper_bound
#> [1] 1e+06
#>
#> attr(,"class")
#> [1] "MOControl" "list"
#>
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
# Backwards-compatible deprecated usage
x5 <- set_method_epsilon_constraint(
x,
primary = "benefit",
aliases = c("benefit", "cost"),
mode = "manual",
eps = list(cost = c(4, 6, 8))
)
#> Warning: `eps/mode/n_points/include_extremes` is deprecated. Use `runs = run_grid(...) or runs = run_manual(...)` instead.
x5$data$method
#> $name
#> [1] "epsilon_constraint"
#>
#> $type
#> [1] "epsilon_constraint"
#>
#> $primary
#> [1] "benefit"
#>
#> $aliases
#> [1] "benefit" "cost"
#>
#> $constrained
#> [1] "cost"
#>
#> $runs
#> $type
#> [1] "manual"
#>
#> $values
#> run_id eps_cost
#> 1 1 4
#> 2 2 6
#> 3 3 8
#>
#> attr(,"class")
#> [1] "RunManual" "RunDesign"
#>
#> $lexicographic
#> [1] TRUE
#>
#> $lexicographic_tol
#> [1] 1e-08
#>
#> $control
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
#> $slack_upper_bound
#> [1] 1e+06
#>
#> attr(,"class")
#> [1] "MOControl" "list"
#>
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
# Control failure handling
x6 <- set_method_epsilon_constraint(
x,
primary = "benefit",
aliases = c("benefit", "cost"),
runs = run_manual(data.frame(eps_cost = c(4, 6, 8))),
control = mo_control(
stop_on_infeasible = FALSE,
stop_on_no_solution = FALSE,
stop_on_error = TRUE
)
)
x6$data$method
#> $name
#> [1] "epsilon_constraint"
#>
#> $type
#> [1] "epsilon_constraint"
#>
#> $primary
#> [1] "benefit"
#>
#> $aliases
#> [1] "benefit" "cost"
#>
#> $constrained
#> [1] "cost"
#>
#> $runs
#> $type
#> [1] "manual"
#>
#> $values
#> eps_cost
#> 1 4
#> 2 6
#> 3 8
#>
#> attr(,"class")
#> [1] "RunManual" "RunDesign"
#>
#> $lexicographic
#> [1] TRUE
#>
#> $lexicographic_tol
#> [1] 1e-08
#>
#> $control
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>
#> $slack_upper_bound
#> [1] 1e+06
#>
#> attr(,"class")
#> [1] "MOControl" "list"
#>
#> $stop_on_infeasible
#> [1] FALSE
#>
#> $stop_on_no_solution
#> [1] FALSE
#>
#> $stop_on_error
#> [1] TRUE
#>