Piecewise linear constraints: follow-up improvements#602
Piecewise linear constraints: follow-up improvements#602FabianHofmann merged 12 commits intomasterfrom
Conversation
…ts API, LP formulation for convex/concave cases, and simplify tests
…code, improve errors
…l formulation, add domain bounds to LP formulation - Incremental method now uses binary indicator variables with link/order constraints to enforce proper segment filling order (Markowitz & Manne) - LP method now adds x ∈ [min(xᵢ), max(xᵢ)] domain bound constraints to prevent extrapolation beyond breakpoints
…only where needed
Validate trailing-NaN-only for SOS2 and disjunctive methods to prevent corrupted adjacency. Fail fast when skip_nan_check=True but breakpoints actually contain NaN.
Support reversed syntax (y == piecewise(...)) via __le__/__ge__/__eq__ dispatch in BaseExpression and ScalarLinearExpression. Fix LP example to use power == demand for more illustrative results.
- Add @overload to comparison operators (__le__, __ge__, __eq__) in BaseExpression and Variable to distinguish PiecewiseExpression from SideLike return types - Update ConstraintLike type alias to include PiecewiseConstraintDescriptor - Fix PiecewiseConstraintDescriptor.lhs type from object to LinExprLike - Fix dict/sequence type mismatches in _dict_to_array, _dict_segments_to_array, _segments_list_to_array - Remove unused type: ignore comments - Narrow ScalarLinearExpression/ScalarVariable return types to not include PiecewiseConstraintDescriptor (impossible at runtime)
* feat: add `active` parameter to piecewise linear constraints Add an `active` parameter to the `piecewise()` function that accepts a binary variable to gate piecewise linear functions on/off. This enables unit commitment formulations where a commitment binary controls the operating range. The parameter modifies each formulation method as follows: - Incremental: δ_i ≤ active (tightened bounds) + base terms × active - SOS2: Σλ_i = active (instead of 1) - Disjunctive: Σz_k = active (instead of 1) When active=0, all auxiliary variables are forced to zero, collapsing x and y to zero. When active=1, the normal PWL domain is active. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: tighten active parameter docstrings Clarify that zero-forcing is the only linear formulation possible — relaxing the constraint would require big-M or indicator constraints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add active parameter to release notes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve mypy type errors for x_base/y_base assignment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add unit commitment example to piecewise notebook Example 6 demonstrates the active parameter with a gas unit that stays off at t=1 (low demand) and commits at t=2,3 (high demand), showing power=0 and fuel=0 when the commitment binary is off. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update notebook * test: comprehensive active parameter test coverage Add tests for gaps identified in review: - Inequality + active (incremental and SOS2, on and off) - auto method selection + active (equality and auto-LP rejection) - active with LinearExpression (not just Variable) - active with NaN-masked breakpoints - LP file output comparison (active vs plain) - Multi-dimensional solver test (per-entity on/off) - SOS2 non-zero base + active off - SOS2 inequality + active off - Disjunctive active on (solver) - Fix: reject active when auto resolves to LP 159 tests pass (was 122). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract PWL_ACTIVE_BOUND_SUFFIX constant Move the active bound constraint name suffix to constants.py, consistent with all other PWL suffix constants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: remove redundant active parameter tests Keep only tests that exercise unique code paths or verify distinct mathematical properties. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
I'd have liked to review this: I still have questions about the use of breakpoints for x and y or why they are used at all. I would prefer we remove add_piecewise_constraints and merge its code into add_constraints. I don't understand the docs in regards to inequalities. They are slightly misleading, it's probably easiest to discuss once more |
|
Hey @coroa, please do the review anyway. This will likely need another follow up. I mostly wanted to avoid the misleading state in the master branch and resolve that asap. and as it is a bit more sorted now, there is no need to pressure you on the review (I know that you are on a workshop this week). |
* Refactor piecewise constraints: add piecewise/segments/slopes_to_points API, LP formulation for convex/concave cases, and simplify tests * piecewise: replace bp_dim/seg_dim params with constants, remove dead code, improve errors * Fix piecewise linear constraints: add binary indicators to incremental formulation, add domain bounds to LP formulation - Incremental method now uses binary indicator variables with link/order constraints to enforce proper segment filling order (Markowitz & Manne) - LP method now adds x ∈ [min(xᵢ), max(xᵢ)] domain bound constraints to prevent extrapolation beyond breakpoints * update signatures of breakpoints and segments, apply convexity check only where needed * update doc * Reject interior NaN and skip_nan_check+NaN in piecewise formulations Validate trailing-NaN-only for SOS2 and disjunctive methods to prevent corrupted adjacency. Fail fast when skip_nan_check=True but breakpoints actually contain NaN. * Allow piecewise() on either side of comparison operators Support reversed syntax (y == piecewise(...)) via __le__/__ge__/__eq__ dispatch in BaseExpression and ScalarLinearExpression. Fix LP example to use power == demand for more illustrative results. * Fix mypy type errors for piecewise constraint types - Add @overload to comparison operators (__le__, __ge__, __eq__) in BaseExpression and Variable to distinguish PiecewiseExpression from SideLike return types - Update ConstraintLike type alias to include PiecewiseConstraintDescriptor - Fix PiecewiseConstraintDescriptor.lhs type from object to LinExprLike - Fix dict/sequence type mismatches in _dict_to_array, _dict_segments_to_array, _segments_list_to_array - Remove unused type: ignore comments - Narrow ScalarLinearExpression/ScalarVariable return types to not include PiecewiseConstraintDescriptor (impossible at runtime) * rename header of jupyter notebook * doc: rename notebook again * feat: add active parameter to piecewise linear constraints (PyPSA#604) * feat: add `active` parameter to piecewise linear constraints Add an `active` parameter to the `piecewise()` function that accepts a binary variable to gate piecewise linear functions on/off. This enables unit commitment formulations where a commitment binary controls the operating range. The parameter modifies each formulation method as follows: - Incremental: δ_i ≤ active (tightened bounds) + base terms × active - SOS2: Σλ_i = active (instead of 1) - Disjunctive: Σz_k = active (instead of 1) When active=0, all auxiliary variables are forced to zero, collapsing x and y to zero. When active=1, the normal PWL domain is active. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: tighten active parameter docstrings Clarify that zero-forcing is the only linear formulation possible — relaxing the constraint would require big-M or indicator constraints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add active parameter to release notes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve mypy type errors for x_base/y_base assignment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add unit commitment example to piecewise notebook Example 6 demonstrates the active parameter with a gas unit that stays off at t=1 (low demand) and commits at t=2,3 (high demand), showing power=0 and fuel=0 when the commitment binary is off. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update notebook * test: comprehensive active parameter test coverage Add tests for gaps identified in review: - Inequality + active (incremental and SOS2, on and off) - auto method selection + active (equality and auto-LP rejection) - active with LinearExpression (not just Variable) - active with NaN-masked breakpoints - LP file output comparison (active vs plain) - Multi-dimensional solver test (per-entity on/off) - SOS2 non-zero base + active off - SOS2 inequality + active off - Disjunctive active on (solver) - Fix: reject active when auto resolves to LP 159 tests pass (was 122). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract PWL_ACTIVE_BOUND_SUFFIX constant Move the active bound constraint name suffix to constants.py, consistent with all other PWL suffix constants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: remove redundant active parameter tests Keep only tests that exercise unique code paths or verify distinct mathematical properties. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: FBumann <117816358+FBumann@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
@FabianHofmann This PR improved the code quality pf the feature a lot i think.
A bit more Insight with examples:What changedPR #602 replaced the old What was lostThe old API could link 3+ variables through shared SOS2 lambda weights: # OLD API
m.add_piecewise_constraints(
expr={"x": x_var, "y": y_var, "z": z_var},
breakpoints=bp, # link_dim matched dict keys
)This is no longer possible. The new API is strictly # NEW API
m.add_piecewise_constraints(piecewise(x, x_pts, y_pts) == y)Workarounds1. Chain piecewise constraintsWorks when the relationship decomposes (e.g. 2. Manual lambda formulation (essentially constructing the piecewise function yourselves)For true multi-variable linking, reproduce the SOS2 lambda formulation manually. All required operations ( import numpy as np
import pandas as pd
bp_idx = pd.Index(np.arange(n_breakpoints), name="breakpoint")
lam = m.add_variables(lower=0, upper=1, coords=[bp_idx], name="lambda")
m.add_constraints(lam.sum("breakpoint") == 1) # convexity
m.add_constraints(x == (lam * x_pts).sum("breakpoint")) # link x
m.add_constraints(y == (lam * y_pts).sum("breakpoint")) # link y
m.add_constraints(z == (lam * z_pts).sum("breakpoint")) # link z
m.add_sos_constraints(lam, sos_type=2, sos_dim="breakpoint") # adjacencySummaryThe current state is not usable for me. I would really like to bring back the more generic #558 approach. What do you think about this? |
|
@FBumann this makes totally sense and this use case was not on my radar at all, sorry for this. there were so many new aspects to think about. so let's bring it back. can we find a compromise that allows the piecewise function to be applied on 2+ variables? or do you think we should directly go for the dict approach? |
|
@FabianHofmann I'm not entirely sure we need to choose between the two approaches. If we go for a construction layer (no dedicated Piecewise class/state), there's no issue with supporting different use cases through method overloading or shared internal helpers. The key architectural decision is: should piecewise constraints introduce new state ( I'd argue strongly for the construction layer, for a few reasons:
A single overloaded method could cover both the simple and the complex case: # Simple 2-variable case
m.add_piecewise_constraints(power, fuel, x_pts, y_pts, sign="==")
# N-variable case (shared lambdas)
m.add_piecewise_constraints(
exprs={"power": power, "fuel": fuel, "heat": heat},
breakpoints=bp,
sign="==",
)Proposed first step: Agree that piecewise constraints should not introduce stored state ( |
Changes proposed in this Pull Request
Strongly needed follow-up improvements to the piecewise linear constraints feature:
piecewisefunction to create aPiecewiseExpressionwhich can be used in operations likey == piecewise(x, x_pts, y_pts)breakpointsandsegments(new) clearer.doc/piecewise-linear-constraints.rstand updated notebookChecklist
doc.doc/release_notes.rstof the upcoming release is included.