Skip to content

Rewrite of PiecewiseLinearOpt.jl (v0.5.0) - #62

Open
trulsf wants to merge 56 commits into
masterfrom
tf/rewrite_new
Open

trulsf wants to merge 56 commits into
masterfrom
tf/rewrite_new

Conversation

@trulsf

@trulsf trulsf commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

This PR is a full rewrite of the package based on the work initiated in the PR #40. The rewrite has been done using AI tools (Claude Opus 5.5 and Claude Fable 5.1) in a guided manner. The goal has primarily been to offer the same functionality as the old version, but with a more modern Julia design aligned with other JuMP related packages (e.g. MathOptAI) and with improved documentation, updated CI and test coverage. A priority was also to make the package easy to extend with new methods.

The following is a short summary of the changes as provided by Claude:

Rewrite of PiecewiseLinearOpt.jl (v0.5.0)

This PR rewrites the whole package. It is a breaking release.

What changes

  • New API: piecewiselinear returns a PWLFormulation. It accepts scalar, vector and generic JuMP inputs. PWLFunction is callable, and its structure and method options are concrete type parameters. Univariate, bivariate and D ≥ 3 functions are supported, and you can pass triangulations or value matrices.
  • Directions: Graph(), Epigraph() and Hypograph() set the direction. New UnivariatePWLRelaxation and ConvexPieces add binaries only for the convex pieces in the relaxed direction.
  • Formulations:
    • Existing methods ported and fixed.
    • IndependentBranching formulations added (Lyu, Hicks & Huchette 2023), including optimal biclique covers.
    • GreedyTriangleSelection added. It is the upper bound and fallback for OptimalTriangleSelection, which caches covers and has time limits.
    • Multivariate MultipleChoice added, with exact coefficients.
    • Celaya formulations removed.
  • Defaults: each function gets a default method, and the bivariate default depends on the triangulation.
  • Extension interface: PWLMethod, SOS2Method, TriangleSelectionMethod, Scalar, segment accessors and cell_diagonals are now public and documented.
  • Validation: non-finite breakpoints and values are rejected, univariate segment continuity is checked, and unsupported methods give clear errors.
  • Docs: new Documenter.jl site in JuMP style, with two tutorials, a page on biclique covers, figures, and guidance on choosing a method based on benchmarks. The README is trimmed to an example and links.
  • Tests and CI:
    • Tests are split by topic, with one file per formulation, and run in parallel. A full mode runs every method combination. Aqua runs in its own workflow.
    • The large .h5 instance files and the exhaustive tests are removed.
    • CI targets Julia 1.10 LTS. JuliaFormatter v2 and dependabot are added.

Migration

Code written for v0.4 needs updating. See the docs for the new piecewiselinear / PWLFunction API and the method names.

🤖 Generated with Claude Code

joehuchette and others added 30 commits March 4, 2020 10:57
* Do not track manifest

* Bump dependencies and use HiGHS as test solver

* Correct type in dictionary

* Add field for segment structure and constructors to match old interface

* Specialize piecewiselinear for typical 1D and 2D usage

* Include segment structure in triangle selection

* Update tests to reflect changes, use HiGHS and run all bivariate

* Fix triangulation

* Avoid explicit use of MOI
…pper/Lower/BestFit, make PWLFunction callable
…s, make the hyperplane representation internal
@trulsf
trulsf requested a review from odow October 22, 2026 09:29
@odow

odow commented Oct 25, 2026

Copy link
Copy Markdown
Member

So let's step back and take the opportunity to re-think the API:

using JuMP
import HiGHS
import PiecewiseLinearOpt
model = Model(HiGHS.Optimizer)
@variable(model, x)
@variable(model, y)
f = PiecewiseLinearOpt.PiecewiseLinear(sin, 0:0.5:2π)
g = PiecewiseLinearOpt.PiecewiseLinear(
    0:0.25:1,
    0:0.25:1;
    method = SixStencil(ZigZagBinary()),
) do a, b
    return (a - 1)^2 + (b - 0.5)^2
end
@objective(model, Min, f(model, x) + g(model, x, y))
optimize!(model)

or

using JuMP
import HiGHS
import PiecewiseLinearOpt
model = Model(HiGHS.Optimizer)
@variable(model, x[1:2])
f = PiecewiseLinearOpt.PiecewiseLinear(sin, model, 0:0.5:2π)
g = PiecewiseLinearOpt.PiecewiseLinear(
    (a, b) -> (a - 1)^2 + (b - 0.5)^2,
    model,
    0:0.25:1,
    0:0.25:1;
    method = SixStencil(ZigZagBinary()),
)
@objective(model, Min, f(x[1]) + g(x[1], x[2]))

@trulsf

trulsf commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

I kind of like both approaches, maybe the first best, but you would have to think a bit carefully about using a pwl function (with the same variable) twice in the same model, e.g. both in the objective and the constraints. Then each use will have its own set of variables (at least in a naive implementation) which seems kind of inefficient. Binding the function (as now) both to the model and the variable at least avoids these issues.

Since variables are attached to a model, a third option is to avoid model and get it from the variable:

using JuMP
import HiGHS
import PiecewiseLinearOpt
model = Model(HiGHS.Optimizer)
@variable(model, x)
@variable(model, y)
f = PiecewiseLinearOpt.PiecewiseLinear(sin, 0:0.5:2π)
g = PiecewiseLinearOpt.PiecewiseLinear(
    0:0.25:1,
    0:0.25:1;
    method = SixStencil(ZigZagBinary()),
) do a, b
    return (a - 1)^2 + (b - 0.5)^2
end
@objective(model, Min, f(x) + g(x, y))
optimize!(model)

@odow

odow commented Oct 25, 2026

Copy link
Copy Markdown
Member

You could do

@expression(model, fx, f(x))

and then use fx in different places.

@trulsf

trulsf commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Good point. Would you consider such an API to be a complete replacement for the old one?

@odow

odow commented Oct 25, 2026

Copy link
Copy Markdown
Member

Yeah I'd say just rip out the old one completely. No need for two ways to do the same thing

@odow odow changed the title Tf/rewrite new Rewrite of PiecewiseLinearOpt.jl (v0.5.0) Oct 26, 2026
@odow

odow commented Oct 26, 2026

Copy link
Copy Markdown
Member

What do you think of the API now?

@odow

odow commented Oct 26, 2026

Copy link
Copy Markdown
Member

One argument in favor of the original add_piecewise is #60

What about doing something like:

f = PiecewiseLinear(sin, 0:0.5:2π)
y, formulation = add_formulation(model, f, x)

(f::PiecewiseLinear)(x) = first(add_formulation(owner_model(x), f, x))

See MathOptAI for inspiration for the formulation object: https://github.com/lanl-ansi/MathOptAI.jl/blob/77ddd62cc86fd5c565d39b376ce49b4af0341457/src/MathOptAI.jl#L38-L58

@trulsf

trulsf commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

There is already a formulation available (directly inspired by MathOptAI) as PWLFormulation and an add_formulation function, see:

function add_formulation(
model::JuMP.GenericModel,
f::PiecewiseLinear{D,F},
x::Scalar...;
name::Union{Nothing,AbstractString} = nothing,
output_vars::Union{Nothing,Tuple,AbstractVector} = nothing,
) where {D,F}
I was thinking a bit about whether it is strictly needed. It provides the ability to delete the pwl from the model and get statistics/details on the formulation. It is also the only way (currently) to specify multiple output variables. I have tried to look into options for including it in the PiecewiseLinear API, but it looks a bit cumbersome.

@odow

odow commented Oct 29, 2026

Copy link
Copy Markdown
Member

I'll setup docs on master so that we can review the draft.

@odow

odow commented Oct 29, 2026

Copy link
Copy Markdown
Member

@odow

odow commented Oct 29, 2026

Copy link
Copy Markdown
Member

Nice nice nice. We have a developer call on Friday. I'll discuss this PR there.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants