refac: Refactor piecewise API to a stateless construction layer + tangent_lines utility#638
refac: Refactor piecewise API to a stateless construction layer + tangent_lines utility#638
Conversation
…on layer Remove PiecewiseExpression, PiecewiseConstraintDescriptor, and the piecewise() function. Replace with an overloaded add_piecewise_constraints() that supports both a 2-variable positional API and an N-variable dict API for linking 3+ expressions through shared lambda weights. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change add_piecewise_constraints() to use keyword-only parameters (x=, y=, x_points=, y_points=) instead of positional args. Add detailed docstring documenting the mathematical meaning of equality vs inequality constraints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The N-variable path was not broadcasting breakpoints to cover extra dimensions from the expressions (e.g. time), resulting in shared lambda variables across timesteps. Also simplify CHP example to use breakpoints() factory and add plot. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The plotting helper now accepts a single breakpoints DataArray with a "var" dimension, supporting both 2-variable and N-variable examples. Replaces the inline CHP plot with a single function call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the N-variable core formulation with shared lambda weights, explain how the 2-variable case maps to it, and detail the inequality case (auxiliary variable + bound). Remove all references to the removed piecewise() function and descriptor classes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add linopy.piecewise_envelope() as a standalone linearization utility that returns tangent-line LinearExpressions — no auxiliary variables. Users combine it with regular add_constraints for inequality bounds. Remove sign parameter, LP method, convexity detection, and all inequality logic from add_piecewise_constraints. The piecewise API now only does equality linking (the core formulation). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
More accurate name — the function computes tangent lines per segment, not necessarily a convex/concave envelope. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single function doesn't justify a separate module. tangent_lines lives next to breakpoints() and segments() — all stateless helpers for the piecewise workflow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add prominent section explaining the fundamental difference: - add_piecewise_constraints: exact equality, needs aux variables - tangent_lines: one-sided bounds, pure LP, no aux variables - tangent_lines with == is infeasible (overconstrained) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace keyword-only (x=, y=, x_points=, y_points=) and dict-based
(exprs=, breakpoints=) forms with a single tuple-based API:
m.add_piecewise_constraints(
(power, [0, 30, 60, 100]),
(fuel, [0, 36, 84, 170]),
)
2-var and N-var are the same pattern — no separate convenience API.
Internally stacks all breakpoints along a link dimension and uses
a unified formulation path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The _pwl_var dimension now shows variable names (e.g. "power", "fuel")
instead of generic indices ("0", "1"), making generated constraints
easier to debug and inspect.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… notebook The piecewise() function was removed but api.rst still referenced it. Also replace xr.concat with breakpoints() in plot cells to avoid pandas StringDtype compatibility issue on newer xarray. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Silences xarray FutureWarning about default coords kwarg changing. No behavior change — we concatenate along new dimensions where coord handling is irrelevant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Example 8 (fleet of generators with per-entity breakpoints) to the notebook. Also drop scalar coordinates from breakpoints before stacking to handle bp.sel(var="power") without MergeError. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
for more information, see https://pre-commit.ci
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The descriptor API was never released, so for users this is all new. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reorder: Quick Start -> API -> When to Use What -> Breakpoint Construction -> Formulation Methods -> Advanced Features. Add per-entity, slopes, and N-variable examples. Deduplicate code samples. Fold generated-variables tables into compact lists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
for more information, see https://pre-commit.ci
|
@FabianHofmann This is pretty urgent to me, as the current piecewise API is already on master and should NOT be included in the next release in my opinion. The current code can be reorganized a bit, but id like to hear your thoughts about the API first. |
…code Remove _add_pwl_sos2_core and _add_pwl_incremental_core which were never called, and inline the single-caller _add_dpwl_sos2_core. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: use _to_linexpr in tangent_lines instead of manual dispatch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: rename _validate_xy_points to _validate_breakpoint_shapes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: clean up duplicate section headers in piecewise.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: convert expressions once in _broadcast_points Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: remove unused _compute_combined_mask Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: validate method early, compute trailing_nan_only once Move method validation to add_piecewise_constraints entry point and avoid calling _has_trailing_nan_only multiple times on the same data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: deduplicate stacked mask expansion in _add_continuous_nvar Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: remove redundant isinstance guards in tangent_lines _coerce_breaks already returns DataArray inputs unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: rename _extra_coords to _var_coords_from with explicit exclude set Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: clarify transitive validation in breakpoint shape check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: remove skip_nan_check parameter NaN breakpoints are always handled automatically via masking. The skip_nan_check flag added API surface for minimal value — it only asserted no NaN (misleading name) and skipped mask computation (negligible performance gain). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: remove unused PWL_AUX/LP/LP_DOMAIN constants Remnants of the old LP method that was removed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: always return link constraint from incremental path Both SOS2 and incremental branches now consistently return the link constraint, making the return value predictable for callers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: split _add_continuous into _add_sos2 and _add_incremental Extract the SOS2 and incremental formulations into separate functions. Add _stack_along_link helper to deduplicate the expand+concat pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: rename test classes to match current function names TestPiecewiseEnvelope -> TestTangentLines TestSolverEnvelope -> TestSolverTangentLines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: use _stack_along_link for expression stacking Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: use generic param names in _validate_breakpoint_shapes Rename x_points/y_points to bp_a/bp_b to reflect N-variable context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: extract _to_seg helper in tangent_lines for rename+reassign pattern Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: extract _strip_nan helper for NaN filtering in slopes mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: extract _breakpoints_from_slopes, add _to_seg docstring Move the ~50 line slopes-to-points conversion out of breakpoints() into _breakpoints_from_slopes, keeping breakpoints() as a clean validation-then-dispatch function. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve mypy errors in _strip_nan and _stack_along_link types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: remove duplicate slopes validation in breakpoints() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: move _rename_to_segments to module level, fix extra blank line Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add validation and edge-case tests for piecewise module Cover error paths and edge cases: non-1D input, slopes mode with DataArray y0, non-numeric breakpoint coords, segment dim mismatch, disjunctive >2 pairs, disjunctive interior NaN, expression name fallback, incremental NaN masking, and scalar coord handling. Coverage: 92% -> 97% Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve ruff and mypy errors - Use `X | Y` instead of `(X, Y)` in isinstance (UP038) - Remove unused `dim` variable in _add_continuous (F841) - Fix docstring formatting (D213) - Remove unnecessary type: ignore comment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
|
edit: did some refactoring anyway, grouped in #641 |
Refactor _add_disjunctive to use the same stacked N-variable pattern as _add_continuous. Removes the 2-variable restriction — disjunctive now supports any number of (expression, breakpoints) pairs with a single unified link constraint. - Remove separate x_link/y_link in favor of single _link with _pwl_var dim - Remove PWL_Y_LINK_SUFFIX import (no longer needed) - Add test for 3-variable disjunctive Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
for more information, see https://pre-commit.ci
N-variable disjunctive + follow-up notesDone: Refactored |
Additional follow-upsSingle-variable support: All formulation methods (SOS2, incremental, disjunctive) should support a single Segments with 2+ breakpoints: Currently |
Comparison with JuMP's piecewise linear formulationsJuMP's approach (via PiecewiseLinearOpt.jl)JuMP's core
The default is JuMP also supports bivariate PWL via triangulation (UnionJack, K1, Stencil, BestFit patterns) for Reference: Huchette & Vielma, "Nonconvex piecewise linear functions: Advanced formulations and simple modeling tools" (2017). Two separate concerns: piecewise structure vs. SOS2 enforcementJuMP mixes these into a single Piecewise formulation structure (what
SOS2 adjacency enforcement (what
This separation means:
JuMP bundles all 8 as piecewise methods. We separate structure from enforcement — cleaner architecture, broader applicability. What linopy has on top of JuMP
Why our architecture doesn't block any of theseThe design in this PR is method-agnostic by construction:
Our N-variable linking vs. JuMP's triangulationThese solve different problems (complementary, not competing):
Our 3-variable Follow up: TriangulationTriangulation is mathematically quite a bit more complex and needs different data. It takes a mesh instead of a 1D breakpoint sequence. (mesh). Its clearly a follow up, probably as a new |
Summary
Follows up on the discussion in #602. Replaces the descriptor pattern (
PiecewiseExpression,PiecewiseConstraintDescriptor,piecewise()) with a stateless construction layer. Establishes a clean separation between piecewise equality constraints and mathematically very different tangent-line inequality bounds.Final API
Each
(expression, breakpoints)tuple links a variable to its breakpoints. All tuples share interpolation weights, coupling them on the same curve segment.Key changes
piecewise()function and descriptor classes — no intermediate state objectsadd_piecewise_constraints— each(expression, breakpoints)pair is a positional arg. 2-var and N-var are the same pattern.tangent_lines()utility — computes tangent-lineLinearExpressionper segment. Pure LP, no auxiliary variables. Separate from piecewise equality.signparameter removed — piecewise only does equality linking. Inequality viatangent_lines+ regularadd_constraints._pwl_vardimension showspower, fuel, heatinstead of0, 1, 2Variable.__le__,__ge__,__eq__simplified (no more piecewise dispatch)Design principles
Model,Variables,Constraints,Expression— no new user-facing typestangent_linescreates zero variables — it returns aLinearExpressionwith one tangent line per segmentNotebook examples
tangent_linesfor inequalitybreakpoints(slopes=...)activeparameterOther API designs considered
A) Descriptor pattern (PR #602, current master)
Rejected: Introduces
PiecewiseExpressionandPiecewiseConstraintDescriptoras user-facing state. Breaks the "only Variables, Constraints, Expressions" principle. Structurally limited to 2-variablex → y.B) Dict of expressions + shared breakpoints DataArray
Considered: Explicit, supports N-variable. But requires string keys to match breakpoint coordinates — an indirection layer.
C) Keyword-only 2-variable + dict N-variable (two separate forms)
Considered: Clear but maintains two separate code paths. The 2-var form doesn't generalize.
D) List of expressions + breakpoints array (name-matching)
Rejected: Magic name matching — breaks if variable names don't match breakpoint coordinates.
E) Chosen: Tuple-based
Selected: Each pair collocates expression + breakpoints. No string keys, no separate dict, no name matching. 2-var and N-var are the same pattern. Minimal, explicit, composable.
Test plan
🤖 Generated with Claude Code