Commit fc5fa6f
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
File tree
13 files changed
+3993
-0
lines changed- doc
- examples
- linopy
- test
13 files changed
+3993
-0
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
21 | 24 | | |
22 | 25 | | |
23 | 26 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| 115 | + | |
| 116 | + | |
115 | 117 | | |
116 | 118 | | |
117 | 119 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
0 commit comments