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. |
|
@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? |
Adds a comparison table to doc/piecewise-linear-constraints.rst summarising sos2/incremental/lp/disjunctive on segment layout, supported signs, tuple count, curvature, auxiliaries, active=, and solver requirements. Also exposes PiecewiseFormulation and slopes_to_points in doc/api.rst. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(piecewise): per-tuple sign + categorized internal flow
Public API
- Drop the formulation-level `sign=` keyword on `add_piecewise_formulation`.
Pass the sign per-tuple as an optional 3rd element instead:
`(y, y_pts, "<=")` instead of `sign="<="`.
- Tuples without a sign default to "=="; the bounded tuple need not be first.
- Validate: at most one tuple may carry a non-equality sign; with 3 or more
tuples all signs must be "==" (the multi-input bounded case is reserved
for a future bivariate / triangulated piecewise API).
- Old `sign=` callers get a clear `TypeError` pointing to the new shape.
Internal flow
- Introduce `_PwlInputs` to carry the categorized inputs (`bounded_*` vs
`pinned_*`) through the dispatch chain. `_build_links`, `_try_lp`,
`_lp_eligibility`, `_add_continuous`, `_add_disjunctive` all consume it
directly — no more positional "first tuple is special" convention.
- User's tuple order is preserved end-to-end.
Tests
- Migrate ~30 callers to per-tuple sign.
- Drop tests of the now-rejected N>=3 + non-equality case
(`TestNVariableInequality`, the two CHP `TestHandComputedAnchors` cases,
`test_nvar_inequality_bounds_first_tuple`).
- Add tests for: removed-`sign=`-keyword migration error, multiple bounded
tuples rejected, N>=3 + non-equality rejected, bounded tuple in the
second position still routes to LP.
Docs
- Rewrite the "sign parameter" section of doc/piecewise-linear-constraints.rst
for per-tuple sign. Update the comparison table, examples, and the
release notes entry.
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(piecewise): leverage per-tuple notation, rephrase restrictions as invitations
Drops vestigial framing from the old API throughout the user docs and example
notebooks. The "first-tuple convention" and "N−1 jointly pinned" scaffolding
existed only to explain why position 0 was special — with per-tuple sign that
explanation isn't needed. Each tuple's role is now visible at the call site.
Restrictions (one bounded tuple max; 3+ must be all-equality) are reframed as
invitations: "open an issue at https://github.com/PyPSA/linopy/issues if you
have a use case." We don't actually know what shape future support takes —
better to invite scoping than to commit to a specific "future bivariate /
triangulated piecewise API" we haven't designed.
- doc/piecewise-linear-constraints.rst: rewrite the restrictions block, the
N-variable linking section, and the SOS2 generated-names list to use the
new framing. Update See Also link target.
- examples/piecewise-inequality-bounds.ipynb: rewrite intro, math, code, and
summary cells. Drop the four cells (10–13) that were dedicated to the
now-rejected 3-variable inequality case (the 3D ribbon plot and its
"first-tuple convention" justification). Notebook executes end-to-end on
Gurobi.
- examples/piecewise-linear-constraints.ipynb: drop the 3-variable CHP
inequality demo (cells 12–13); the joint-equality CHP case is already in
section 6. Update the inequality intro for per-tuple sign.
- linopy/piecewise.py: rephrase docstring restrictions and the entry-point
ValueError to invite an issue rather than promise a specific future API.
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
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
I thought a bit more about it, and its much clearer having the sign next to the variable/factor. THis also extends naturally to triangulation etc, and feels less "magic". I refactored it in #664. This also made the docs cleaner. I also decided to disallow inequalities for 3+ variables, as im not sure about the mathematical formulation and would rather ship the PR as is without further discussion on the not yet needed 3+ != case. |
The function now accepts **kwargs to give a clear TypeError on the removed `sign=` keyword, so mypy doesn't flag the call site and the ignore is unused. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…time introspection - Reorder formulation sections LP → SOS2 → Incremental → Disjunctive (simple to complex) in both the comparison table and method subsections. - Disambiguate the breakpoints() vs segments() factories: connected curve vs disjoint operating regions consumed by the disjunctive formulation. - Replace the brittle "Generated variables and constraints" listing with a short "Inspecting generated objects" pointer to the returned PiecewiseFormulation's .variables / .constraints live views, since exact name suffixes are an implementation detail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
for more information, see https://pre-commit.ci
…plication - Add a "Formulation" math block to the per-tuple sign section in the rst for the bounded-tuple link split (pinned equality + signed output_link). This was previously only spelled out as a math block in the notebook while the rst described it in prose. - Drop the "Mathematical formulation" cell from the inequality notebook: the all-equality / LP-chord / incremental blocks were verbatim copies of what's already in the rst's method subsections. - Update the notebook's intro to point at the reference page for the math and frame the notebook as geometry / dispatch / feasible-region focused. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…concept Both terms are used in the PWL literature — Wikipedia and the Northwestern optimization wiki use them interchangeably; JuMP's PiecewiseLinearOpt.jl prefers "pieces", Pyomo's API leans "segments". So the rename isn't about correctness. The reason is local to this codebase: segments() is a public factory that returns disjoint operating regions for the disjunctive formulation. Using the same word for "linear part of a connected curve" creates avoidable ambiguity — most visibly in the method comparison table, where the row "Segment layout: Connected / Connected / Connected / Disconnected" silently switches meaning between the LP/SOS2/Incremental columns (linear pieces of one curve) and the Disjunctive column (disjoint operating regions). After the rename: - piece — a linear part between adjacent breakpoints on a connected piecewise-linear curve. Used in: LP chord math, SOS2/incremental prose, the tangent_lines dim name (_breakpoint_piece), and LP_PIECE_DIM. - segment — a disjoint operating region in the disjunctive formulation. Used in: the segments() factory, SEGMENT_DIM, and the disjunctive section's prose. segments() keeps its name because it is geometrically accurate (each entry is a segment of the real line, with gaps between them) and renaming the public factory would be churn. The exposed _breakpoint_seg dim was already flagged as evolving via EvolvingAPIWarning, so renaming it now is in scope. Also adds a short Terminology block at the top of the docs so the breakpoint / piece / segment distinction is visible before any prose uses the terms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor: Disambiguate usage of "segment" by renaming to "piece" in most contextsBoth terms are used in the PWL literature — Wikipedia and the Northwestern optimization wiki use them interchangeably; JuMP's PiecewiseLinearOpt.jl prefers "pieces", Pyomo's API leans "segments". So the rename isn't about correctness - its about readability. I added a clear definition into the docs |
…before SOS2) The auto-dispatch (piecewise.py:1285) picks Incremental over SOS2 whenever breakpoints are strictly monotonic — Incremental is the default for connected curves, with SOS2 reserved as the fallback for non-monotonic layouts. The doc had the opposite ordering (LP → SOS2 → Incremental), which made SOS2 look like the canonical MIP encoding. Reorder the comparison table columns and method subsections to: LP → Incremental → SOS2 → Disjunctive, matching dispatch preference. Also link the SOS2 section to :ref:`sos-reformulation` so users can see the actual Big-M MIP form their solver receives when reformulate_sos applies — that's the math most users effectively get, not the abstract SOS2 adjacency constraint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group the two piecewise tutorial notebooks under the reference page via a sub-toctree, instead of listing them as flat sibling entries in the top-level User Guide toctree. The reference page becomes the natural landing for piecewise content: sidebar shows reference → [equality tutorial, inequality tutorial], and the User Guide toctree is freed up to scale when triangulation / 2-D piecewise lands. No file moves and existing :doc: cross-references keep resolving — the notebook document names are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two release-note entries used "segment" for the meaning that the rest of the codebase now calls "piece" (linear part between adjacent breakpoints): - tangent_lines: "per-segment chord" → "per-piece chord" - slopes_to_points: "segment slopes" → "per-piece slopes" linopy.segments() is unchanged — it remains the public factory for disjoint operating regions in the disjunctive formulation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d leftovers - The headline ``add_piecewise_formulation`` entry named "the per-tuple sign convention" as something that "may be refined" — but the per-tuple sign IS the shipped convention; what may shift are the restrictions within it (at most one bounded tuple, N≥3 all equality). Reword to name those concrete restrictions as the change candidates. - Drop the "active + non-equality sign semantics" mention — that was a corner case resolved during the refactor, not something users need to see in the headline note. - Fold the three breakpoint-construction helpers (breakpoints, segments, slopes_to_points) into a single line — eight piecewise bullets was more granular than the feature warrants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@coroa @FabianHofmann I think everything is sorted out now and the api is both intuitive, well documented, tested and future proof. Please take a look |
## Summary - New `ConversionCurve` element ties two or more flows of a `Converter` along a piecewise-linear curve (dict form for the common all-equality case, list-of-tuples for per-flow `'=='`/`'<='`/`'>='` bounds). - Wraps linopy's new `add_piecewise_formulation` (PR PyPSA/linopy#638) with auto-dispatch between `lp` (matching-curvature 2-flow inequality), `incremental` (monotonic), and `sos2`. Status-gated via the existing component-level on/off binary; per-timestep availability scales the upper breakpoint. - Pins linopy to the PR-638 head until that PR is released. - Math doc (`docs/math/converters.md`) and a new notebook (`07-piecewise.ipynb`) cover the formulation, method table, status gating, and a hands-on boiler-with-part-load-curve walkthrough. Closes #25. ## Test plan - [x] `uv run pytest tests/math_port/test_piecewise.py` — 30 tests pass (validation, two-/three-flow interpolation, segment selection, time-varying breakpoints, status gating, running-cost integration). - [x] Full suite green; mypy + ruff clean. - [x] Notebook executes end-to-end; both optimizations solve to optimum. - [x] IO roundtrip exercised by parametrized fixtures (auxiliary λ / order-binary variables survive NetCDF). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added piecewise non-linear conversion modeling enabling complex efficiency curves and multi-flow conversions with automatic method selection and configurable operating bounds. * **Documentation** * Added comprehensive guide and interactive tutorial notebook demonstrating piecewise conversion workflows, including efficiency curves, combined heat and power systems, and on/off control scenarios. * **Chores** * Updated dependency versions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>


Summary
Follows up on #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[, sign])tuple links a variable to its breakpoints. All tuples share interpolation weights, coupling them on the same curve piece. The optional 3rd element marks one tuple as bounded by the curve instead of pinned to it.Equality (default)
Inequality bounds — per-tuple sign
Append
"<="or">="as a third tuple element to mark one expression as bounded by the curve. The other tuples stay pinned. The bounded tuple need not be first — its role is visible at the call site.On a matching-curvature curve this dispatches to
method="lp": one chord inequality per piece plus a domain boundx_min ≤ x ≤ x_max, and no auxiliary variables. Mismatched curvature (convex +"<="/ 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.Restrictions (current):
"==".The N-input bounded case (e.g.
z >= f(x, y)) is rejected with an error that invites a GitHub issue if anyone has a concrete use case — we don't promise a specific future API shape.Unit-commitment gating —
activeWorks with SOS2, incremental, and disjunctive. With a bounded tuple, deactivation only pushes the signed bound to
0— 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 aPiecewiseFormulationcarrying.method(resolved formulation),.convexity(when well-defined),.variables,.constraints."==". At most one non-equality; 3+ tuples must all be equality.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 pointer tomethod="auto".PiecewiseFormulation.methodand.convexitypersist across netCDF round-trip.Architecture (internal)
_PwlInputsdataclass categorizes the user's tuples intopinned_*andbounded_*slots — explicit role assignment, no positional convention. Carries through the whole dispatch chain._PwlLinksis the stacked link representation consumed by SOS2/incremental/disjunctive builders;_build_links()produces it from_PwlInputs._try_lp()/_resolve_sos2_vs_incremental()flatten the dispatch —_add_continuousis ~10 lines of real logic._PwlInputs, not by assuming position 0.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.(y, y_pts, "<=")⇒y ≤ f(...))._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) Global
sign=keyword (initial implementation, refactored in #664)The original implementation used a single formulation-level
sign=kwarg with a positional convention ("the first tuple is the bounded one whensign != '=='"). #664 refactored this to per-tuple sign because the role belongs to a specific tuple — encoding it globally + positionally was a wart that required a paragraph of docs to teach. The internal dispatch was rewritten from positional access to explicit categorization (_PwlInputswithbounded_*andpinned_*slots).D) Chosen: per-tuple sign on the tuple it applies to
Future work this enables
z ≥ f(x, y)). The reserved N≥3-inequality slot reads naturally as the epigraph of a bivariate function:(z, z_pts, ">="), (x, x_pts), (y, y_pts), triangulation=tris. Vertex coordinates stay as plain 1-DDataArrays; the simplex adjacency rides along as one shared sibling kwarg._PwlInputswould grow a single optional field; the formulation builders for the triangulated path are new but consume the same input carrier.Defered Features
Out of scope / follow-ups
Directions the design enables or considered but deliberately not shipping in this PR:
z ≥ f(x, y)). The reserved N≥3-inequality slot reads naturally as the epigraph of a bivariate function:(z, z_pts, ">="), (x, x_pts), (y, y_pts), triangulation=tris. Vertex coordinates stay as plain 1-DDataArrays; the simplex adjacency rides along as one shared sibling kwarg._PwlInputswould grow a single optional field; the formulation builders for the triangulated path are new but consume the same input carrier.breakpoints(slopes=..., x_points=..., y0=...)is shipped today. What's deferred is letting slopes inherit the x grid from a sibling tuple (e.g.(fuel, Slopes([1.2, 1.4, 1.7], y0=0))paired with a reference tuple that carries the x grid). Two viable shapes were discussed — extendingbreakpoints(slopes=...)to allow omittedx_points, or introducing a small frozen-dataclassSlopesvalue type — each with tradeoffs around type-system visibility and dispatch clarity.EvolvingAPIWarningcovers either direction as a non-breaking follow-up.Notebook examples
examples/piecewise-linear-constraints.ipynb(main tutorial):pwfautovssos2vsincrementalgive the same optimumsegments()for gaps in operation"<="(auto-dispatches LP)activeparameterexamples/piecewise-inequality-bounds.ipynb(deep-dive on per-tuple sign / LP):method="lp"raise)Formulation math lives in the reference page, not in the notebook.
Test plan
pytest test/test_piecewise_constraints.py test/test_piecewise_feasibility.py— 518 passedPiecewiseFormulation.methodand.convexitypersist acrossto_netcdf/read_netcdf🤖 Generated with Claude Code