feat: Unify piecewise API behind add_piecewise_formulation (sign + LP dispatch)#638
feat: Unify piecewise API behind add_piecewise_formulation (sign + LP dispatch)#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 |
|
@coroa Can we discuss this together with @FabianHofmann ? |
@FabianHofmann is on vacation this week. Can we set up something next week? Or is pressing more urgent than that? |
Ah ok i missed that. No, next week is fine :) Could you setup a meeting? |
…cewise_formulation (#642) * feat: PiecewiseFormulation return type, model groups, rename to add_piecewise_formulation - Add PiecewiseFormulation dataclass grouping all auxiliary variables and constraints created by a piecewise formulation - Add _groups registry on Model to track grouped artifacts - Model repr hides grouped items from Variables/Constraints sections and shows them in a new "Groups" section - Rename add_piecewise_constraints -> add_piecewise_formulation - Export PiecewiseFormulation from linopy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update notebook to show PiecewiseFormulation repr Reorder cells so add_piecewise_formulation is the last statement, letting Jupyter display the PiecewiseFormulation repr automatically. Add print(m) cell to show the grouped model repr. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: show tangent_lines repr in notebook Split tangent_lines cell so its LinearExpression repr is displayed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show dims in PiecewiseFormulation repr and user dims in Model groups PiecewiseFormulation now shows full dims (including internal) for each variable and constraint. Model groups section shows "over (dim1, dim2)" for user-facing dims only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: remove counts from PiecewiseFormulation repr Match style of Variables/Constraints containers which don't show counts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: rename _groups to _piecewise_formulations, use direct section name Replace generic "Groups" with "Piecewise Formulations" in Model repr. Rename internal registry and helper to match. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: move method after counts in repr to avoid looking like a dim Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: show dims before name like regular variables/constraints Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: compact piecewise formulation line in model repr Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: use backtick name style in PiecewiseFormulation repr Match Constraint repr pattern: `name` instead of 'name'. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show user dims with sizes in PiecewiseFormulation header Match Constraint repr style: `name` [dim: size, ...] — method Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clear notebook outputs to fix nbformat validation Remove jetTransient metadata and normalize cell format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: store names in PiecewiseFormulation, add IO persistence PiecewiseFormulation now stores variable/constraint names as strings with a model reference. Properties return live Views on access. This makes serialization trivial — persist as JSON in netcdf attrs, reconstruct on load. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename remaining add_piecewise_constraints reference after rebase Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: rename PWL suffix constants for clarity - PWL_X_LINK_SUFFIX/_Y_LINK_SUFFIX → PWL_LINK_SUFFIX (N-var, single link) - PWL_BINARY_SUFFIX → PWL_SEGMENT_BINARY_SUFFIX (disjunctive segment selection) - PWL_INC_BINARY_SUFFIX → PWL_ORDER_BINARY_SUFFIX (incremental ordering) - PWL_INC_LINK_SUFFIX → PWL_DELTA_BOUND_SUFFIX (δ ≤ binary) - PWL_INC_ORDER_SUFFIX → PWL_BINARY_ORDER_SUFFIX (binary_{i+1} ≤ δ_i) - PWL_FILL_SUFFIX → PWL_FILL_ORDER_SUFFIX (δ_{i+1} ≤ δ_i) 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>
From call:
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@coroa I thought a lot about the inequality in non lp piecewise relationships.
Compared to tangent line one:
We can see that we need to fix one of the variables to the slope! TO be precise, all but one variable needs to be fixed! |
…rmulation (#663) * feat: add sign parameter and LP method to add_piecewise_formulation Introduces a sign parameter ("==", "<=", ">=") with a first-tuple convention: the first tuple's expression is the signed output; all remaining tuples are treated as inputs forced to equality. A new method="lp" uses pure tangent lines (no aux variables) for 2-variable inequality cases on convex/concave curves. method="auto" automatically dispatches to LP when applicable, otherwise falls back to SOS2/incremental with the sign applied to the output link. Internally: - sign="==" keeps a single stacked link (unchanged behaviour) - sign!="==" splits: one stacked equality link for inputs plus one output link carrying the sign - LP adds per-segment chord constraints plus domain bounds on x Uses the existing SIGNS / EQUAL / LESS_EQUAL / GREATER_EQUAL constants from linopy.constants for validation and dispatch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add piecewise inequality notebook and update release notes New examples/piecewise-inequality-bounds.ipynb walks through the sign parameter, the first-tuple convention, and the LP/SOS2/incremental equivalence within the x-domain. Includes a feasibility region plot and demonstrates auto-dispatch + non-convex fallback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add 3D feasibility ribbon and mathematical formulation Adds the full mathematical formulation (equality, inequality, LP, incremental) as a dedicated markdown section, and a 3D Poly3DCollection plot showing the feasible ribbon for 3-variable sign='<=' — a 1-D curve in 3-D space extruded downward in the output axis. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: show 3D feasibility ribbon from multiple viewpoints Keeps matplotlib (consistent with other notebooks, no new deps) but renders the 3D ribbon in three side-by-side projections: perspective, (power, fuel) side view, (power, heat) top view. Easier to read than a single 3D plot in a static doc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs,fix: clarify that mismatched curvature+sign is wrong, not just loose For concave+">=" or convex+"<=", tangent lines give a feasible region that is a strict subset of the true hypograph/epigraph — rejecting points that satisfy the true constraint. This is wrong, not merely a loose relaxation. - Update error message in method="lp" to make this explicit - Correct the convexity×sign table in the notebook to mark the ✗ cases as "wrong region", not "loose" - Add tests covering concave+">=" and convex+"<=" auto-fallback + explicit lp raise Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: make LP error messages terse Error messages should state the problem and point to a fix, not teach the theory. The detailed convexity × sign semantics live in the notebook/docs, not in runtime errors. Also removes the "strict subset" claim, which was true in common cases but not watertight at domain boundaries. 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 * feat: log resolved method when method='auto' Users who care which formulation they got (e.g. LP vs MIP for performance) can see the dispatch decision in the normal log output without checking PiecewiseFormulation.method manually. Example: INFO linopy.piecewise: piecewise formulation 'pwl0': auto selected method='lp' (sign='<=', 2 pairs) Logged at info level, only when method='auto' (explicit choices are not logged — the user already knows). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: log when LP is skipped and why When method='auto' and the inequality case can't use LP (wrong number of tuples, non-monotonic x, mismatched curvature, active=...), log an info-level message explaining why before falling back to SOS2/incremental. Example: INFO linopy.piecewise: piecewise formulation 'pwl0': LP not applicable (sign='<=' needs concave/linear curvature, got 'convex'); will use SOS2/incremental instead Factored the LP-eligibility check into a new _lp_eligibility helper that returns (ok, reason) — used by auto dispatch to decide + log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: expose convexity on PiecewiseFormulation Adds a ``convexity`` attribute ({"convex", "concave", "linear", "mixed"} or None) set automatically when the shape is well-defined (exactly two tuples, non-disjunctive, strictly monotonic x). Widens two helper signatures to ``LinearExpression | None`` / ``DataArray | None`` to match their actual usage. Adds PWL_METHODS and PWL_CONVEXITIES sets to back the runtime validation; the user-facing ``Literal[...]`` hints remain the static source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: make convexity detection invariant to x-direction _detect_convexity previously treated a concave curve with decreasing x as convex (and vice-versa), because the slope sequence appears reversed when x descends. As a result, method="auto" could dispatch LP on a curvature+sign combination the implementation explicitly documents as "wrong region", and explicit method="lp" would accept the same case. Sort each entity's breakpoints by x ascending before classifying. Adds two regression tests covering auto-dispatch and explicit LP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: mask trailing-NaN segments in LP path _add_lp built one chord constraint per breakpoint segment without honouring the breakpoint mask. For per-entity inputs where some entities have fewer breakpoints (NaN tail), the NaN slope/intercept became 0 in the constraint, producing a spurious ``y ≤ 0`` for the padded segments and forcing the output to zero. Compute a per-segment validity mask (both endpoints non-NaN) and pass it through to the chord constraint via ``_add_signed_link``. Also delegates the tangent-line construction to the existing public ``tangent_lines`` helper to remove the duplicated slope/intercept math. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: correct sign param — applies to first tuple, not last The Parameters block contradicted the prose and the implementation, which use the first-tuple convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac,test: simplify _detect_convexity and add direct unit tests Collapse the per-slice numpy loop into an xarray-native classifier: NaN propagation through .diff() handles masked breakpoints, and multiplying the second-slope-difference by ``sign(dx.sum(...))`` keeps the ascending/descending-x invariance from the previous fix. Scope is deliberately single-curve; multi-entity inputs aggregate across entities. For N>2 variables (not supported by LP today) the right shape is a single-pair classifier plus a combinator at the call site — left for when the LP path generalizes. Adds TestDetectConvexity covering: basic convex/concave/linear/mixed, floating-point tolerance, too-few-points, ascending-vs-descending invariance, trailing-NaN padding, multi-entity same-shape, multi-entity mixed direction, multi-entity mixed curvature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs,test: document active + non-equality sign asymmetry active=0 pins auxiliary variables to zero, which under sign="==" forces the output to 0 exactly. Under sign="<=" or ">=" it only pushes the signed bound to 0 — the complementary side still falls back to the output variable's own upper/lower bound, which is often not what a reader expects from a "deactivated" unit. Call out the asymmetry in the ``active`` docstring and add a regression test that pins the current behaviour (minimising y under active=0 + sign="<=" goes to the variable's lb, not 0). A future change to auto-couple the complementary bound should flip that test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: extend active + sign='<=' coverage to incremental and disjunctive Parametrise the SOS2 regression over incremental as well, and add a matching test for the disjunctive (segments) path. All three methods show the same asymmetry: input pinned to 0 via the equality input link, output only signed-bounded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs,test: show the y.lower=0 recipe for active + non-equality sign Make the docstring note actionable: the usual fuel/cost/heat outputs are naturally non-negative, so setting lower=0 on the output turns the documented sign="<=" + active=0 asymmetry into a non-issue (the variable bound combined with y ≤ 0 forces y = 0 automatically). Genuinely signed outputs still need the big-M coupling called out. Pins the recipe down with a test that maximises y under active=0 and asserts y = 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: add 7 regression tests for review-flagged coverage gaps - method='lp' + active raises (silent would produce wrong model) - LP accepts a linear curve (convexity='linear', either sign) - method='auto' emits an INFO log when it skips LP - LP domain bound is enforced (x > x_max → infeasible) - LP matches SOS2 on multi-dim (entity) variables - LP vs SOS2 consistency on both sides of y ≤ f(x) - Disjunctive + sign='<=' is respected by the solver Placed in TestSignParameter (LP/sign behaviour) and TestDisjunctive (disjunctive solver) rather than a separate review-named bucket. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac: package PWL links in a dataclass and flatten auto-dispatch Addresses review issues 8, 9, 10: - Introduces ``_PwlLinks``, a single dataclass carrying the stacked-for-lambda breakpoints plus the equality- and signed-side link expressions the three builders need. The EQUAL / non-EQUAL split lives in one place (``_build_links``) instead of being duplicated in ``_add_continuous`` and ``_add_disjunctive``. - ``_add_sos2``/``_add_incremental``/``_add_disjunctive`` drop from 9–11 parameters with correlated ``Optional`` pairs down to a short list taking the links struct. ``_add_incremental`` also loses its unused ``rhs`` parameter (incremental gates via ``delta <= active``, not via a convex-sum = rhs constraint). - ``_add_continuous`` becomes ~10 lines: it either dispatches LP via ``_try_lp`` (returns bool) or builds links and hands off to a single ``_resolve_sos2_vs_incremental`` helper before calling the chosen builder. No more 5-way ``method`` branching in one body. Behaviour is unchanged — same 147 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac: rename PWL_LP_SUFFIX → PWL_CHORD_SUFFIX ``_lp`` echoed the method name without saying what the constraint does. The LP formulation adds one chord-line constraint per segment (``y <= m·x + c`` per breakpoint pair), so ``_chord`` describes the actual object being added and is independent of which method built it. Reviewer-suggested alternative; also matches the chord-of-a- piecewise-curve framing used in the notebook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: persist PiecewiseFormulation.convexity across netCDF round-trip to_netcdf was dropping the convexity field; reload defaulted it to None (e.g. concave → None). Include it in the JSON payload and pass it back to the constructor on read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: regression for PiecewiseFormulation netCDF round-trip Compare all __slots__ (except the model back-reference) so the test auto-catches any future field the IO layer forgets to persist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: rewrite piecewise reference + tutorial for the new sign/LP API Thorough pass over the user-facing piecewise docs to match the current API (sign, method="lp", first-tuple convention) rather than the pre-PR equality-only surface. doc/piecewise-linear-constraints.rst: - Quick Start now shows both equality and inequality forms. - API block updated: sign in the signature, method now lists "lp". - New top-level section "The sign parameter — equality vs inequality" covering the first-tuple convention, math for 2-var <=, hypograph/ epigraph framing, and when to reach for inequality (primarily to unlock the LP chord formulation). Spells out the equality-is-often- the-right-call recommendation when curvature doesn't match sign. - Formulation Methods gains a full "LP (chord-line) formulation" subsection with the per-segment chord math, domain bound and the curvature+sign matching rule. The auto-dispatch intro lists LP as the first branch. - Every other formulation (SOS2/incremental/disjunctive) gets a short note on how it handles sign != "==". - "Generated variables and constraints" rewritten with the current suffix names (_link, _output_link, _chord, _domain_lo/_hi, _order_binary, _delta_bound, _binary_order, _active_bound) grouped per method. - Active parameter gains a note on the non-equality sign asymmetry with a pointer to the lower=0 recipe. - tangent_lines demoted: no longer a top-level API section; one pointer lives under the LP formulation for manual-control use. - See Also now links the new inequality-bounds notebook. examples/piecewise-linear-constraints.ipynb: - Section 4 rewritten from "Tangent lines — Concave efficiency bound" to "Inequality bounds — sign='<=' on a concave curve". Shows the one-liner add_piecewise_formulation((fuel, y), (power, x), sign="<=") and prints the resolved method/convexity to make the auto-LP dispatch visible. Outro points to the dedicated inequality notebook rather than showing the low-level tangent_lines path. doc/index.rst + doc/piecewise-inequality-bounds-tutorial.nblink: - Register the existing examples/piecewise-inequality-bounds.ipynb as a Sphinx page under the User Guide toctree so it's discoverable from the docs nav. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * docs: compact the main piecewise tutorial notebook Collapse the equality sections (SOS2 / incremental / disjunctive as separate walk-throughs of the same dispatch pattern) into a single getting-started + a method-comparison table + one disjunctive example. Factor the shared dispatch pattern out of each example — model construction, demand and objective follow the same shape in every section, so the "new" cell in each only shows the one feature being introduced. 47 cells → 20; no loss of coverage (all 8 features still demonstrated: basic equality, method selection, disjunctive, sign/LP, slopes, active, N-variable, per-entity). Plot helper slimmed down to a one-curve overlay used once in the intro; later sections rely on the solution DataFrame. Links to the inequality-bounds notebook placed in the relevant sections. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * docs: split piecewise release notes + tidy tangent_lines docstring Round-2 review items that weren't already handled by earlier commits: - Split the single mega-bullet in release notes into five findable bullets: core add_piecewise_formulation API, sign / LP dispatch, active (unit commitment), .method/.convexity metadata, and tangent_lines as the low-level helper. Each of sign/LP/active/ convexity is now greppable. - tangent_lines docstring: relax "strictly increasing" to "strictly monotonic" (_detect_convexity is already direction-invariant and tangent_lines doesn't care either way), and open with a pointer to add_piecewise_formulation(sign="<=") as the preferred high-level path — tangent_lines is the low-level escape hatch. - One-line comment on _build_links explaining the intentional eq_bp/stacked_bp aliasing in the sign="==" branch. The other round-2 items (stale RST, netCDF convexity persistence) are already handled by earlier commits fbc90d4 and 3dc1c6c/5889d04 — the reviewer was working against an older snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- 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>
Squashes 17 commits of follow-up work on top of the #663 merge into this branch. Tests (test/test_piecewise_feasibility.py — new, 400+ lines): - Strategic feasibility-region equivalence. The strong test is TestRotatedObjective: for every rotation (α, β) on the unit circle, the support function min α·x + β·y under the PWL must match a vertex-enumeration oracle. Equal support functions over a dense direction set imply equal convex feasible regions. - Additional classes: TestDomainBoundary (x outside the breakpoint range is infeasible under all methods), TestPointwiseInfeasibility (y nudged past f(x) is infeasible), TestHandComputedAnchors (arithmetically trivial expected values that sanity-check the oracle itself), and TestNVariableInequality hardened with a 3-D rotated oracle, heat-off-curve infeasibility, and interior-point feasibility. - Curve dataclass + CURVES list covering concave/convex/linear/ two-segment/offset variants. Method/Sign/MethodND literal aliases for mypy-tight fixture and loop typing. - ~406 pytest items, ~30s runtime, TOL = 1e-5 globally. Tests (test/test_piecewise_constraints.py): - Hardened TestDisjunctive with sign_le_hits_correct_segment (six x-values across two segments with different slopes) and sign_le_in_forbidden_zone_infeasible. Confirms the binary-select + signed-output-link combination routes each x to the right segment's interpolation. - Local Method/Sign literal aliases so the existing loop-over-methods tests survive the tightened add_piecewise_formulation signature. EvolvingAPIWarning: - New linopy.EvolvingAPIWarning(FutureWarning) — visible by default, subclass so users can filter it precisely without affecting other FutureWarnings. Added to __all__ and re-exported at top level. - Emitted from add_piecewise_formulation and tangent_lines with a "piecewise:" message prefix. Every message points users at https://github.com/PyPSA/linopy/issues so feedback shapes what stabilises. - tangent_lines split into a public wrapper (warns) and a private _tangent_lines_impl (no warn) so _add_lp doesn't double-fire. - Message-based filter in pyproject.toml (``"ignore:piecewise:FutureWarning"``) avoids forcing pytest to import linopy at config-parse time (which broke --doctest-modules collection on Windows via a site-packages vs source-tree module clash). Docs: - doc/piecewise-linear-constraints.rst: soften "sign unlocks LP" to reflect that disjunctive + sign is always exact regardless of curvature. New paragraph in the Disjunctive Methods subsection positioning it as a first-class tool for "bounded output on disconnected operating regions". - doc/release_notes.rst: update the piecewise bullet to mention the EvolvingAPIWarning, how to silence it, and the feedback URL. - dev-scripts/piecewise-feasibility-tests-walkthrough.ipynb (new, gitignored→force-added for PR review): visual explanation of each test class — 16-direction probes + extreme points, domain-boundary probes, pointwise nudge, 3-D CHP ribbon. Dropped before master merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
45d3ea3 to
1f6b563
Compare
|
@coroa What do you think about the new api? One thing we should discuss is the notebooks and docs. If you have the time to read it, feedback and potentially changes would be great! |
|
@coroa One API detail im not sure about yet, is where to but the sign. m.add_piecewise_formulation(
(power, xp, "<="),
(fuel, yp)
)with defaulting to '==' where not specified. I decided against this api for simplicity, as using more than i single equality produces mathematically weird bounds as far as i understand it (only relevant for the 3 var case). ALso, for the 2 variable LP case, it doestn make any sense to have more than a single sign. So there is a lot of API surface that would lead to errors we need to catch. We could however also just disallow the 3 variable case with inequalities for now, as i don`t understand the use case that well. This would allow the arguably more obvious notation of having the sing right next to the expression... With the Looking forward, this API seems better suited for triangular bounding, where we could actually allow multiple inequalities. But the implementation of this is not part of this PR and quite complex i think, so we can just change it as soon as this potential feature lands |
The previous framing ("first bounded, rest forced to equality") was
correct but left two things unclear:
1. What "rest forced to equality" means when there are multiple
equality-side tuples — they are jointly constrained to a single
segment position on the curve. Pinning power AND heat to
independent values is infeasible; their values are coupled by the
shared segment parameter.
2. Which variable should occupy the first (bounded) position. A
consumption-side variable such as fuel intake yields a valid but
*loose* formulation — the characteristic curve fixes fuel draw at
a given load, so sign="<=" on fuel admits operating points the
plant cannot physically realise. Safe only when no objective
rewards driving it below the curve; otherwise the optimum can be
non-physical. The canonical choice is a dissipation path: heat
rejection (also called thermal curtailment), electrical
curtailment, or emissions after post-treatment.
The reference page also now notes that inequality can be faster than
equality — 2-variable cases with matching curvature dispatch to pure
LP, and the relaxed feasible region typically tightens the LP
relaxation for N≥3 too. Choice of sign is a speed-vs-tightness
trade-off in addition to a physics one.
Updates:
- doc/piecewise-linear-constraints.rst: reframe the sign section as
"N−1 jointly-pinned, 1 bounded", with an explicit 3-variable
example showing independent pinning of equality-side tuples is
infeasible. New "Choice of bounded tuple" paragraph opens with
heat rejection and closes with the speed-vs-tightness trade-off.
- examples/piecewise-linear-constraints.ipynb Section 4: the 3-var
CHP example now bounds ``heat`` (heat rejection) with ``power``
and ``fuel`` pinned. Prose introduces "heat rejection" /
"thermal curtailment" and notes the speed benefit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
for more information, see https://pre-commit.ci
|
One thing I didn't cover yet, is a dedicated slopes object. Should we add it? Or can we do it in a follow up? |


Summary
Follows up on the discussion in #602. Replaces the descriptor pattern (
PiecewiseExpression,PiecewiseConstraintDescriptor,piecewise()) with a single, stateless construction layer:add_piecewise_formulation.add_piecewise_formulationbecomes the one entry point for piecewise work — equality, one-sided inequality, N-variable linking, per-entity breakpoints, unit-commitment gating, and automatic method dispatch all live behind one call.tangent_lines()survives as a low-level helper for users who want to build chord expressions manually.The API
Each
(expression, breakpoints)tuple links a variable to its breakpoints. All tuples share interpolation weights, coupling them on the same curve segment.Equality (default)
Inequality bounds —
sign="<="/">="The
signparameter applies the bound to the first tuple (first-tuple convention); remaining tuples are inputs pinned to the curve.On a matching-curvature curve this dispatches to
method="lp": one chord inequality per segment plus a domain boundx_min ≤ x ≤ x_max, and no auxiliary variables. Mismatched curvature (convex +"<=", or concave +">=") describes the wrong region (not just a looser one) —method="auto"detects this and falls back to SOS2/incremental with an explanatory info log;method="lp"raises with a clear error.Inequalities with 3+ variables cant go lp. They are always sos/...
Unit-commitment gating —
activeWorks with SOS2, incremental, and disjunctive. With
sign != "==", deactivation only pushes the signed bound to0— setlower=0on naturally non-negative outputs (fuel, cost, heat) and the combination forcesy = 0automatically.Low-level:
tangent_linesKey changes
New public API
add_piecewise_formulation— one entry point for all piecewise constructions. Returns aPiecewiseFormulationobject carrying.method(resolved formulation),.convexity(when well-defined),.variables,.constraints.signparameter —"=="(default) /"<="/">=", with the first-tuple convention.methodparameter —"auto"(default),"sos2","incremental","lp".activeparameter — binary gating for unit commitment.linopy.breakpoints()/linopy.segments()factories.linopy.slopes_to_points()utility.linopy.tangent_lines()low-level helper.Auto-dispatch (
method="auto")lp(chord + domain bounds, no aux vars)incremental(deltas + binaries)sos2(lambdas + SOS2 constraint)sos2with binary segment selectionThe resolved method is logged at INFO; when LP is skipped, the log explains why (curvature, NaN layout, tuple count, or
active).Correctness properties
_detect_convexityis direction-invariant — ascending and descending x give the same label for the same graph.y ≤ 0constraints).method="lp"validates curvature + sign + tuple count +activeabsence upfront; mismatch raises with a clear fix pointer tomethod="auto".PiecewiseFormulation.methodand.convexitypersist across netCDF round-trip.Architecture (internal)
_PwlLinksdataclass carries the equality-side and signed-side link expressions;_build_links()is the single place where the sign split happens._try_lp()/_resolve_sos2_vs_incremental()flatten the dispatch —_add_continuousis ~10 lines of real logic._add_sos2/_add_incremental/_add_disjunctivetake 4–5 arguments (down from 9–11)._add_lpdelegates chord construction to the publictangent_lineshelper.Removed / deprecated
PiecewiseExpression,PiecewiseConstraintDescriptor,linopy.piecewise()— descriptor pattern gone.add_piecewise_constraintsshape that dispatched onVariable.__le__/__ge__/__eq__return types.Variableoperator overloads (__le__,__ge__,__eq__) simplified — no more piecewise dispatch.Design principles
Model,Variables,Constraints,Expression, plus the singlePiecewiseFormulationreturn type. No user-facing intermediate state.signselects the semantics,methodselects the cost._pwl_vardimension showspower, fuel, heatinstead of0, 1, 2.Alternative designs considered
A) Descriptor pattern (PR #602, previous 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: Supports N-variable. But requires string keys to match breakpoint coordinates — an indirection layer.
C) Per-tuple signs
Rejected: Creates ambiguous cases (what if tuples disagree on sign? what if only some have it?). Global
sign=+ the first-tuple convention keeps the math clean.D) Last-tuple convention (output listed last)
Rejected: The
y = f(x)convention reads left-to-right; listing the bounded output first ("first expression ≤ f(the rest)") is more natural. First-tuple convention chosen after discussion.E) Chosen: tuple-based, global
sign, first-tuple conventionNotebook examples
examples/piecewise-linear-constraints.ipynb(main tutorial, 7 sections):pwfautovssos2vsincrementalgive the same optimumsegments()for gaps in operationsign="<="(auto-dispatches LP)activeparameterexamples/piecewise-inequality-bounds.ipynb(dedicated deep-dive onsign/ LP):Test plan
_detect_convexity)PiecewiseFormulation.methodand.convexitypersist acrossto_netcdf/read_netcdf🤖 Generated with Claude Code