-
Notifications
You must be signed in to change notification settings - Fork 5
Description
Feature Request: Piecewise Linear Constraints (with SOS2 support)
Motivation
Piecewise linear (PWL) constraints are essential for modeling nonlinear relationships in linear/mixed-integer programs — e.g., cost curves, efficiency curves, fuel consumption, or any function that can be approximated by line segments.
The core use case is linking one or more variables through a piecewise linear relationship: given breakpoints that define a nonlinear curve, constrain variables to lie on that curve. For example, linking a generator's power output to its fuel cost, or linking flow to pressure drop.
Currently, pyoframe has no support for piecewise linear constraints. The underlying solver interface (PyOptInterface) already provides model.add_sos_constraint() for SOS2 constraints, so backend support exists — it just needs to be exposed through pyoframe's API.
Background
Piecewise Linear Functions
A piecewise linear function approximates a nonlinear function
The standard convex combination (SOS2) formulation introduces auxiliary
-
$x = \sum_i \lambda_i \cdot x_i$ (input linking) -
$y = \sum_i \lambda_i \cdot f(x_i)$ (output linking) -
$\sum_i \lambda_i = 1$ (convexity) -
$\lambda$ form an SOS2 set (adjacency — at most two adjacent$\lambda_i$ non-zero)
SOS Constraints
- SOS1 (Type 1): At most one variable in the set can be non-zero.
- SOS2 (Type 2): At most two variables can be non-zero, and they must be adjacent in the ordered set. This is the building block for piecewise linear interpolation.
Proposed API
The primary interface should focus on linking variables through piecewise linear relationships:
import pyoframe as pf
import polars as pl
m = pf.Model()
# --- Scalar example ---
m.x = pf.Variable(lb=0, ub=10)
breakpoints_x = [0, 2, 5, 8, 10]
breakpoints_y = [0, 3, 4, 6, 10]
# Link x and y through a piecewise linear curve
m.pwl = pf.PiecewiseLinear(m.x, m.y, breakpoints_x, breakpoints_y)
# --- Indexed example (e.g., different cost curves per generator) ---
m.power = pf.Variable({"gen": ["g1", "g2"]}, lb=0)
m.cost = pf.Variable({"gen": ["g1", "g2"]}, lb=0)
# Breakpoints can be a DataFrame with different curves per index
breakpoints = pl.DataFrame({
"gen": ["g1", "g1", "g1", "g2", "g2", "g2"],
"bp": [ 0, 50, 100, 0, 80, 150],
"power_bp": [0, 50, 100, 0, 80, 150],
"cost_bp": [0, 30, 80, 0, 50, 120],
})
m.cost_curve = pf.PiecewiseLinear(
{"power": m.power, "cost": m.cost},
breakpoints,
)
m.minimize = m.cost.sum()Multi-variable linking
A key capability (as in linopy's design) is linking multiple variables simultaneously through shared interpolation weights. This is essential when a single underlying parameter (e.g., operating point) determines multiple outputs (e.g., both cost and emissions).
# Shared lambda weights link power, cost, AND emissions together
m.curves = pf.PiecewiseLinear(
{"power": m.power, "cost": m.cost, "emissions": m.emissions},
breakpoints, # contains columns for each linked variable
)Formulation Methods
Different formulation methods suit different problem structures (cf. linopy PR #576):
| Method | Description | Requirements | Variables |
|---|---|---|---|
| SOS2 (convex combination) | General PWL using SOS2 | Any breakpoint order |
|
| Incremental (delta) | Pure LP formulation | Strictly monotonic breakpoints |
|
| Disjunctive | Disconnected segments | Forbidden zones / gaps | Binary + |
The incremental method is particularly attractive because it requires no SOS2 or binary variables — it's a pure LP formulation that works when breakpoints are monotonic. An method="auto" option could auto-select based on breakpoint structure.
Low-level SOS Support
While the main goal is the piecewise linear API, exposing raw SOS1/SOS2 constraints is also useful for advanced users:
# SOS2: at most two adjacent x[i] are non-zero
m.sos2 = pf.SOS2(m.x, weights=[1, 2, 3, 4, 5])
# SOS1: at most one x[i] is non-zero
m.sos1 = pf.SOS1(m.x)Design considerations:
- SOS constraints are fundamentally different from linear/quadratic constraints (no LHS/RHS/sense) — they may need a distinct class.
- Solver support: Gurobi and COPT support SOS natively; HiGHS has limited support; Ipopt does not.
Implementation Notes
- PyOptInterface already supports
model.add_sos_constraint(variables, SOSType, weights). - A
supports_soscapability flag should be added to the_Solverdataclass in_constants.py. - The incremental formulation could be implemented without any new solver features (pure LP), making it available on all solvers including HiGHS.
- Dimensioned piecewise linear constraints need careful design — breakpoints may vary per index (e.g., different cost curves per generator).
Reference
- linopy PR #576: Piecewise Linear Constraints — comprehensive implementation with SOS2, incremental, and disjunctive methods
- PyOptInterface SOS constraint docs
- Wikipedia: Special Ordered Set