Skip to content

feat: Unify piecewise API behind add_piecewise_formulation (sign + LP dispatch)#638

Open
FBumann wants to merge 48 commits intomasterfrom
feat/piecewise-api-refactor
Open

feat: Unify piecewise API behind add_piecewise_formulation (sign + LP dispatch)#638
FBumann wants to merge 48 commits intomasterfrom
feat/piecewise-api-refactor

Conversation

@FBumann
Copy link
Copy Markdown
Collaborator

@FBumann FBumann commented Apr 1, 2026

Summary

Follows up on #602. Replaces the descriptor pattern (PiecewiseExpression, PiecewiseConstraintDescriptor, piecewise()) with a single, stateless construction layer: add_piecewise_formulation.

add_piecewise_formulation becomes 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)

# 2-variable: fuel = f(power)
m.add_piecewise_formulation(
    (power, [0, 30, 60, 100]),
    (fuel,  [0, 36, 84, 170]),
)

# N-variable (CHP plant — same pattern, more tuples)
m.add_piecewise_formulation(
    (power, [0, 30, 60, 100]),
    (fuel,  [0, 40, 85, 160]),
    (heat,  [0, 25, 55, 95]),
)

# Disjunctive (disconnected operating regions)
m.add_piecewise_formulation(
    (power, linopy.segments([(0, 0), (50, 80)])),
    (cost,  linopy.segments([(0, 0), (125, 200)])),
)

# Per-entity breakpoints (different curves per generator)
m.add_piecewise_formulation(
    (power, linopy.breakpoints({"gas": [0, 30, 60, 100], "coal": [0, 50, 100, 150]}, dim="gen")),
    (fuel,  linopy.breakpoints({"gas": [0, 40, 90, 180], "coal": [0, 55, 130, 225]}, dim="gen")),
)

# Breakpoints from slopes
m.add_piecewise_formulation(
    (power, [0, 30, 60, 100]),
    (fuel,  linopy.breakpoints(slopes=[1.2, 1.4, 1.7], x_points=[0, 30, 60, 100], y0=0)),
)

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.

# fuel ≤ f(power).  "auto" picks the cheapest correct formulation — a pure
# LP chord formulation when the curvature matches the sign (concave + "<=",
# or convex + ">="), SOS2/incremental otherwise.
m.add_piecewise_formulation(
    (fuel,  [0, 20, 30, 35], "<="),  # bounded
    (power, [0, 10, 20, 30]),         # pinned
)

On a matching-curvature curve this dispatches to method="lp": one chord inequality per piece plus a domain bound x_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):

  • At most one tuple may carry a non-equality sign.
  • With 3+ tuples, all signs must be "==".

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.

valid or rejectable

Unit-commitment gating — active

# active=1: power in [30, 100], fuel = f(power)
# active=0: power = 0, fuel = 0
m.add_piecewise_formulation(
    (power, [30, 60, 100]),
    (fuel,  [40, 90, 170]),
    active=commit,  # binary variable
)

Works with SOS2, incremental, and disjunctive. With a bounded tuple, deactivation only pushes the signed bound to 0 — set lower=0 on naturally non-negative outputs (fuel, cost, heat) and the combination forces y = 0 automatically.

Low-level: tangent_lines

# Returns a LinearExpression with one chord per piece — no variables.
# Most users should prefer add_piecewise_formulation with a bounded tuple,
# which builds on this helper and adds domain bounds + curvature checks.
t = linopy.tangent_lines(power, x_pts, y_pts)
m.add_constraints(fuel <= t)

Key changes

New public API

  • add_piecewise_formulation — one entry point for all piecewise constructions. Returns a PiecewiseFormulation carrying .method (resolved formulation), .convexity (when well-defined), .variables, .constraints.
  • Per-tuple sign — optional 3rd tuple element, default "==". At most one non-equality; 3+ tuples must all be equality.
  • method parameter — "auto" (default), "sos2", "incremental", "lp".
  • active parameter — 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")

  1. 2-variable inequality on a matching-curvature curvelp (chord + domain bounds, no aux vars)
  2. Strictly monotonic breakpointsincremental (deltas + binaries)
  3. Otherwisesos2 (lambdas + SOS2 constraint)
  4. Disjunctive (segments)sos2 with binary segment selection

The resolved method is logged at INFO; when LP is skipped, the log explains why (curvature, NaN layout, tuple count, or active).

Correctness properties

  • _detect_convexity is direction-invariant — ascending and descending x give the same label for the same graph.
  • NaN-padded per-entity breakpoints are masked everywhere, including the LP chord constraints (padded pieces don't create spurious y ≤ 0 constraints).
  • method="lp" validates curvature + sign + tuple count + active absence upfront; mismatch raises with a clear pointer to method="auto".
  • PiecewiseFormulation.method and .convexity persist across netCDF round-trip.

Architecture (internal)

  • _PwlInputs dataclass categorizes the user's tuples into pinned_* and bounded_* slots — explicit role assignment, no positional convention. Carries through the whole dispatch chain.
  • _PwlLinks is 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_continuous is ~10 lines of real logic.
  • User tuple order is preserved end-to-end; the bounded one is found via _PwlInputs, not by assuming position 0.

Removed / deprecated

  • PiecewiseExpression, PiecewiseConstraintDescriptor, linopy.piecewise() — descriptor pattern gone.
  • Old add_piecewise_constraints shape that dispatched on Variable.__le__/__ge__/__eq__ return types.
  • Variable operator overloads (__le__, __ge__, __eq__) simplified — no more piecewise dispatch.

Design principles

  • API surface stays minimal: Model, Variables, Constraints, Expression, plus the single PiecewiseFormulation return type. No user-facing intermediate state.
  • Piecewise is a construction layer that produces regular linopy objects.
  • One entry point for equality and inequality — per-tuple sign reads as the relation it encodes ((y, y_pts, "<=")y ≤ f(...)).
  • Variable names as link coords: the internal _pwl_var dimension shows power, fuel, heat instead of 0, 1, 2.

Alternative designs considered

A) Descriptor pattern (PR #602, previous master)

pw = linopy.piecewise(x, x_pts, y_pts)
m.add_piecewise_constraints(pw == y)

Rejected: Introduces PiecewiseExpression and PiecewiseConstraintDescriptor as user-facing state. Breaks the "only Variables, Constraints, Expressions" principle. Structurally limited to 2-variable x → y.

B) Dict of expressions + shared breakpoints DataArray

m.add_piecewise_formulation(exprs={"power": power, "fuel": fuel}, breakpoints=bp)

Considered: Supports N-variable. But requires string keys to match breakpoint coordinates — an indirection layer.

C) Global sign= keyword (initial implementation, refactored in #664)

m.add_piecewise_formulation((fuel, y_pts), (power, x_pts), sign="<=")

The original implementation used a single formulation-level sign= kwarg with a positional convention ("the first tuple is the bounded one when sign != '=='"). #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 (_PwlInputs with bounded_* and pinned_* slots).

D) Chosen: per-tuple sign on the tuple it applies to

m.add_piecewise_formulation(
    (fuel,  y_pts, "<="),  # bounded (role visible at the call site)
    (power, x_pts),         # pinned
)

Future work this enables

  • 2-D triangulated piecewise (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-D DataArrays; the simplex adjacency rides along as one shared sibling kwarg. _PwlInputs would grow a single optional field; the formulation builders for the triangulated path are new but consume the same input carrier.
  • Multi-bounded relations (if a use case appears) — the "at most one bounded tuple" check is a single line; the dispatch chain already handles per-tuple roles.

Defered Features

  • More polished slopes api, potentially with a new class

Out of scope / follow-ups

Directions the design enables or considered but deliberately not shipping in this PR:

  • 2-D triangulated piecewise (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-D DataArrays; the simplex adjacency rides along as one shared sibling kwarg. _PwlInputs would grow a single optional field; the formulation builders for the triangulated path are new but consume the same input carrier.
  • Multi-bounded relations (if a use case appears) — the "at most one bounded tuple" check is a single line; the dispatch chain already handles per-tuple roles.
  • More polished slopes API, potentially with a new class. 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 — extending breakpoints(slopes=...) to allow omitted x_points, or introducing a small frozen-dataclass Slopes value type — each with tradeoffs around type-system visibility and dispatch clarity. EvolvingAPIWarning covers either direction as a non-breaking follow-up.

Notebook examples

examples/piecewise-linear-constraints.ipynb (main tutorial):

# Section Feature
1 Getting started Basic 2-variable equality + method inspection via pwf
2 Picking a method auto vs sos2 vs incremental give the same optimum
3 Disjunctive segments segments() for gaps in operation
4 Inequality bounds Per-tuple "<=" (auto-dispatches LP)
5 Unit commitment active parameter
6 N-variable linking CHP plant (3 variables, joint equality)
7 Per-entity breakpoints Fleet with different curves

examples/piecewise-inequality-bounds.ipynb (deep-dive on per-tuple sign / LP):

  • Setup with a concave reference curve
  • Three methods (LP, SOS2, incremental) verified to produce the same feasible region
  • 2-D hypograph feasibility plot
  • Curvature × sign rule table — the "wrong region" cases and why
  • Auto-dispatch fallback demonstration (non-convex curve, mismatched sign, explicit 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 passed
  • Full suite (excl. solver/remote) — 1578 passed, 7 skipped
  • Both notebooks execute end-to-end against Gurobi
  • Ruff lint + format clean, mypy clean on the full repo
  • Round-trip tested: PiecewiseFormulation.method and .convexity persist across to_netcdf / read_netcdf
  • Review

🤖 Generated with Claude Code

FBumann and others added 22 commits April 1, 2026 08:36
…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>
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>
@FBumann FBumann marked this pull request as ready for review April 1, 2026 11:53
@FBumann FBumann changed the title Refactor piecewise API: stateless construction layer + tangent_lines utility refac: Refactor piecewise API to be a stateless construction layer + tangent_lines utility Apr 1, 2026
@FBumann FBumann changed the title refac: Refactor piecewise API to be a stateless construction layer + tangent_lines utility refac: Refactor piecewise API to a stateless construction layer + tangent_lines utility Apr 1, 2026
@FBumann FBumann requested a review from FabianHofmann April 1, 2026 11:54
FBumann and others added 3 commits April 1, 2026 14:03
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>
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 1, 2026

@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.
But i understand that the CSR PR is more important atm.

The current code can be reorganized a bit, but id like to hear your thoughts about the API first.

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 22, 2026

@coroa I thought a lot about the inequality in non lp piecewise relationships.
I added a notebook and plot about it in #662 (5992788). Essentially, the sign needs to be "translated" to behave like tangent lines. And we need to make a decision about the convention, because givin all options to the user makes a pretty bad UX whie "allowing" cases which probably noone wants.
Here is a visualization for feasible regions for a 2-variable case with SOS2/lambda/delta formulations based on which sign is used for which variable:

fig

Compared to tangent line one:

tangent

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>
@FBumann FBumann changed the title refac: Refactor piecewise API to a stateless construction layer + tangent_lines utility feat: Unify piecewise API behind add_piecewise_formulation (sign + LP dispatch) Apr 23, 2026
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>
@FBumann FBumann force-pushed the feat/piecewise-api-refactor branch from 45d3ea3 to 1f6b563 Compare April 23, 2026 11:15
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 23, 2026

@coroa What do you think about the new api?
I think its quite good and clear now. The testing is also already quite good imo.

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!
The one who implemented it usually has a different angle on the docs than the reviewer/user.
https://linopy--638.org.readthedocs.build/en/638/piecewise-linear-constraints.html

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 23, 2026

@coroa One API detail im not sure about yet, is where to but the sign.
It could be per tuple as a third optional parameter:

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 EvolvingAPIWarning however, im fine with keeping it and adjusting it if needed.

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

FBumann and others added 2 commits April 23, 2026 17:46
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>
@FBumann FBumann requested a review from coroa April 24, 2026 17:09
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 24, 2026

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?

FBumann and others added 2 commits April 26, 2026 17:13
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>
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 26, 2026

@coroa One API detail im not sure about yet, is where to but the sign. It could be per tuple as a third optional parameter:

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.

FBumann and others added 5 commits April 26, 2026 19:58
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>
…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>
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 27, 2026

Refactor: Disambiguate usage of "segment" by renaming to "piece" in most contexts

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 - its about readability.
I argue strongly in favor of Pieces, while keeping Segments as a collection of curves, where each curve/segment consist of several pieces (disjunctive). This is clear and mathematically sound.

I added a clear definition into the docs

FBumann and others added 4 commits April 27, 2026 07:53
…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>
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 27, 2026

@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

FBumann added a commit to FBumann/fluxopt that referenced this pull request Apr 27, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants