Formulation methods

The method keyword of PiecewiseLinear chooses how a function is formulated with auxiliary variables and constraints. All methods give a correct model; they differ in size and in how fast a solver finds the optimum. Which method is fastest depends on the problem and the solver, so it is worth trying a few methods on representative instances. See Huchette and Vielma (2023) for a computational comparison.

Choosing a method

The following advice is based on computational tests with HiGHS and Xpress on transportation problems with concave and discontinuous costs, resource allocation with S-shaped utilities, and bivariate functions on grids.

Below, d is the number of segments of a univariate function.

Univariate functions

The default is Logarithmic for continuous functions, MultipleChoice for discontinuous functions and DisaggregatedLogarithmic for relaxations.

MethodInteger variablesDiscontinuousRelaxation
LogarithmicEmbedding, also Logarithmic()ceil(log2(d)) binarynono
LogarithmicIndependentBranchingceil(log2(d)) binarynono
IndependentBranchingceil(log2(d)) binaryyesyes
ZigZagBinaryceil(log2(d)) binarynono
ZigZagIntegerceil(log2(d)) general integernono
DisaggregatedLogarithmicceil(log2(d)) binaryyesyes
Incrementald - 1 binarynono
ConvexCombinationd binarynono
MultipleChoiced binaryyesno
NativeSOS2none, branching by the solvernono
ConvexPiecesone binary per convex piece, none for a convex functionyesno

A relaxation, see UnivariatePWLRelaxation, is a band between a lower and an upper piecewise linear bound. The two methods that support it model both bounds with one set of binary variables.

NativeSOS2 leaves the branching to solvers that support SOS2 constraints, e.g. Gurobi or Xpress; for other solvers, such as HiGHS, MathOptInterface reformulates the constraint with binary variables, which is much slower than the other methods.

The number of binary variables is only one measure of the size of a formulation. The logarithmic methods have few binary variables but denser constraints: their number of nonzero coefficients grows like d * log2(d), against d for the linear-size methods, although for a few hundred segments the difference is within a factor of two. The figure counts both for a function with d segments:

using JuMP, PiecewiseLinearOpt, Plotsmethods = [Logarithmic(), IndependentBranching(), DisaggregatedLogarithmic(), Incremental(), MultipleChoice()]ds = [4, 8, 16, 32, 64, 128, 256]function model_size(method, d)    model = Model()    @variable(model, x)    _, formulation = add_formulation(model, PiecewiseLinear(UnivariatePWLFunction(0:d, t -> t^2); method), x)    nonzeros = sum(        length(constraint_object(c).func.terms) for        c in formulation.constraints if constraint_object(c).func isa AffExpr    )    return count(is_binary, formulation.variables), nonzerosendsizes = [model_size(method, d) for method in methods, d in ds]binaries = plot(    ds,    [first.(sizes[k, :]) for k in eachindex(methods)];    label = permutedims(string.(methods)),    legend = :topleft,    ylabel = "binary variables",)nonzeros = plot(    ds,    [last.(sizes[k, :]) for k in eachindex(methods)];    label = "",    ylabel = "nonzero coefficients",)plot(    binaries,    nonzeros;    xscale = :log2,    yscale = :log2,    xlabel = "segments",    marker = :circle,    layout = (1, 2),    size = (700, 320),    left_margin = 5Plots.mm,    bottom_margin = 5Plots.mm,)
Example block output

Bivariate functions

Bivariate functions are defined on a grid, where each cell is split into two triangles. The default method depends on the triangulation: K1 for K1Triangulation, UnionJack for UnionJackTriangulation and GreedyTriangleSelection for the others.

The grid methods select a cell with an SOS2 constraint along each axis, formulated with any univariate method in the table above (the axis_method, by default Logarithmic), and then select a triangle in the cell:

MethodTriangle selectionTriangulation
SixStencilup to 6 binaryany
NineStencilup to 9 binaryany
GreedyTriangleSelectionfew, typically 2 to 5, found by a heuristicany
OptimalTriangleSelectionsmallest number, typically 2 to 4, computed with a solverany
K12 binaryK1Triangulation
UnionJack1 binaryUnionJackTriangulation

With n₁ × n₂ grid points and a logarithmic axis method, the cell selection uses ceil(log2(n₁ - 1)) + ceil(log2(n₂ - 1)) binary variables. For solvers with support for SOS2 constraints, NativeSOS2 is also a good axis method.

GreedyTriangleSelection finds its triangle selection in milliseconds, and typically with the optimal number of binary variables. Proving that number optimal with OptimalTriangleSelection takes about a second for regular triangulations of grids up to 8×8, but can take much longer for irregular triangulations, e.g. BestFitTriangulation or RandomTriangulation. If it takes longer than its time limit, the greedy selection is used. When both the sub-solver and the solver of the model are HiGHS, and the model is solved with a single thread, give the sub-solver a single thread too, i.e. OptimalTriangleSelection(optimizer_with_attributes(HiGHS.Optimizer, "threads" => 1)), as the solve otherwise fails.

OptimalIndependentBranching instead selects a triangle directly with the smallest possible number of binary variables, which is only practical to compute for small grids, up to about 4×4 points.

Functions of more variables

ConvexCombination, DisaggregatedLogarithmic and MultipleChoice formulate functions of any number of variables. The segments must be simplices for MultipleChoice. DisaggregatedLogarithmic, the default, uses the fewest integer variables, ceil(log2(d)) for d segments. Both support discontinuous functions.

Example

Different methods give the same optimum, but different model sizes:

using JuMP, PiecewiseLinearOpt, HiGHSf(a, b) = sin(3a) * cos(2b)pwl = BivariatePWLFunction(0:0.1:1, 0:0.1:1, f)for method in (    SixStencil(),    NineStencil(),    K1(),    GreedyTriangleSelection(),    OptimalTriangleSelection(HiGHS.Optimizer),    SixStencil(IndependentBranching()),    ConvexCombination(),)    model = Model(HiGHS.Optimizer)    set_silent(model)    @variable(model, x[1:2])    (z,), formulation = add_formulation(model, PiecewiseLinear(pwl; method = method), x...)    @objective(model, Min, z)    optimize!(model)    binaries = count(is_binary, formulation.variables)    println(rpad(method, 50), "binaries: ", lpad(binaries, 3), "  minimum: ", round(objective_value(model); digits = 4))end
SixStencil(LogarithmicEmbedding())                binaries:  11  minimum: -0.4151
NineStencil(LogarithmicEmbedding())               binaries:  17  minimum: -0.4151
K1(LogarithmicEmbedding())                        binaries:  10  minimum: -0.4151
GreedyTriangleSelection(LogarithmicEmbedding(), 200)binaries:  10  minimum: -0.4151
OptimalTriangleSelection(HiGHS.Optimizer, LogarithmicEmbedding(), 60.0)binaries:  10  minimum: -0.4151
SixStencil(IndependentBranching(:biclique))       binaries:  11  minimum: -0.4151
ConvexCombination()                               binaries: 200  minimum: -0.4151