Skip to content

Commit fc5fa6f

Browse files
FBumannclaudeFabianHofmann
authored
feat: add piecewise linear constraint API (SOS2, incremental, disjunctive) (#576)
* feat: add piecewise linear constraint API Add `add_piecewise_constraint` method to Model class that creates piecewise linear constraints using SOS2 formulation. Features: - Single Variable or LinearExpression support - Dict of Variables/Expressions for linking multiple quantities - Auto-detection of link_dim from breakpoints coordinates - NaN-based masking with skip_nan_check option for performance - Counter-based name generation for efficiency The SOS2 formulation creates: 1. Lambda variables with bounds [0, 1] for each breakpoint 2. SOS2 constraint ensuring at most two adjacent lambdas are non-zero 3. Convexity constraint: sum(lambda) = 1 4. Linking constraints: expr = sum(lambda * breakpoints) * Fix lambda coords * rename to add_piecewise_constraints * rename to add_piecewise_constraints * fix types (mypy) * linopy/constants.py — Added PWL_DELTA_SUFFIX = "_delta" and PWL_FILL_SUFFIX = "_fill". linopy/model.py — - Added method: str = "sos2" parameter to add_piecewise_constraints() - Updated docstring with the new parameter and incremental formulation notes - Refactored: extracted _add_pwl_sos2() (existing SOS2 logic) and added _add_pwl_incremental() (new delta formulation) - Added _check_strict_monotonicity() static method - method="auto" checks monotonicity and picks accordingly - Numeric coordinate validation only enforced for SOS2 test/test_piecewise_constraints.py — Added TestIncrementalFormulation (10 tests) covering: single variable, two breakpoints, dict case, non-monotonic error, decreasing monotonic, auto-select incremental/sos2, invalid method, extra coordinates. Added TestIncrementalSolverIntegration (Gurobi-gated). * 1. Step sizes: replaced manual loop + xr.concat with breakpoints.diff(dim).rename() 2. Filling-order constraints: replaced per-segment individual add_constraints calls with a single vectorized constraint via xr.concat + LinearExpression 3. Mask computation: replaced loop over segments with vectorized slice + rename 4. Coordinate lists: unified extra_coords/lambda_coords — lambda_coords = extra_coords + [bp_dim_index], eliminating duplicate list comprehensions * rewrite filling order constraint * Fix monotonicity check * Summary Files Modified 1. linopy/constants.py — Added 3 constants: - PWL_BINARY_SUFFIX = "_binary" - PWL_SELECT_SUFFIX = "_select" - DEFAULT_SEGMENT_DIM = "segment" 2. linopy/model.py — Three changes: - Updated imports to include the new constants - Updated _resolve_pwl_link_dim with an optional exclude_dims parameter (backward-compatible) so auto-detection skips both dim and segment_dim - Added _add_dpwl_sos2 private method implementing the disaggregated convex combination formulation (binary indicators, per-segment SOS2 lambdas, convexity, and linking constraints) - Added add_disjunctive_piecewise_constraints public method with full validation, mask computation, and dispatch 3. test/test_piecewise_constraints.py — Added 7 test classes with 17 tests: - TestDisjunctiveBasicSingleVariable (3 tests) — equal segments, NaN padding, single-breakpoint segments - TestDisjunctiveDictOfVariables (2 tests) — dict with segments, auto-detect link_dim - TestDisjunctiveExtraDimensions (1 test) — extra generator dimension - TestDisjunctiveValidationErrors (5 tests) — missing dim, missing segment_dim, same dim/segment_dim, non-numeric coords, invalid expr - TestDisjunctiveNameGeneration (2 tests) — shared counter, custom name - TestDisjunctiveLPFileOutput (1 test) — LP file contains SOS2 + binary sections - TestDisjunctiveSolverIntegration (3 tests) — min/max picks correct segment, dict case with solver * docs: add piecewise linear constraints documentation Create dedicated documentation page covering all three PWL formulations: SOS2 (convex combination), incremental (delta), and disjunctive (disaggregated convex combination). Includes math formulations, usage examples, comparison table, generated variables reference, and solver compatibility. Update index.rst, api.rst, and sos-constraints.rst. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: improve disjunctive piecewise linear test coverage Add 17 new tests covering masking details, expression inputs, multi-dimensional cases, multi-breakpoint segments, and parametrized multi-solver testing. Disjunctive tests go from 17 to 34 unique methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: Add notebook to showcase piecewise linear constraint * Add cross reference to notebook * Improve notebook * docs: add release notes and cross-reference for PWL constraints Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix mypy issue in test * Improve docs about incremental * refactor and add tests * fix: reject non-trailing NaN in incremental piecewise formulation Validate that NaN breakpoints are trailing-only along dim. For method='incremental', raise ValueError on gaps. For method='auto', fall back to SOS2 instead. Add _has_trailing_nan_only helper. * further refactor * extract piecewise linear logic into linopy/piecewise.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: allow broadcasted mask * fix merge conflict in release notes * refactor: remove link_dim from piecewise constraint API The linking dimension is now always auto-detected from breakpoint coordinates matching the expression dict keys, simplifying the public API of add_piecewise_constraints and add_disjunctive_piecewise_constraints. * refactor: use LinExprLike type alias and consolidate piecewise validation Extract _validate_piecewise_expr helper to replace duplicated isinstance checks in _auto_broadcast_breakpoints and _resolve_expr. Add LinExprLike type alias to types.py. Update docs, tests, and breakpoints factory. * fix: resolve mypy errors in piecewise module * update release notes [skip ci] --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Fabian Hofmann <fab.hof@gmx.de>
1 parent b7aba5f commit fc5fa6f

13 files changed

+3993
-0
lines changed

doc/api.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Creating a model
1818
model.Model.add_variables
1919
model.Model.add_constraints
2020
model.Model.add_objective
21+
model.Model.add_piecewise_constraints
22+
model.Model.add_disjunctive_piecewise_constraints
23+
piecewise.breakpoints
2124
model.Model.linexpr
2225
model.Model.remove_constraints
2326

doc/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ This package is published under MIT license.
112112
creating-expressions
113113
creating-constraints
114114
sos-constraints
115+
piecewise-linear-constraints
116+
piecewise-linear-constraints-tutorial
115117
manipulating-models
116118
testing-framework
117119
transport-tutorial
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"path": "../examples/piecewise-linear-constraints.ipynb"
3+
}

0 commit comments

Comments
 (0)