Commit 94a38cd
feat: Unify piecewise API behind add_piecewise_formulation (sign + LP dispatch) (#638)
* refac: replace piecewise descriptor pattern with stateless construction 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>
* refac: use keyword-only args for 2-variable piecewise API
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>
* docs: use breakpoints() in CHP example and add plot
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: broadcast N-variable breakpoints over expression dims
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>
* docs: generalize plot_pwl_results for N-variable case
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>
* docs: rewrite piecewise documentation for new API
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>
* refac: extract piecewise_envelope, remove sign from piecewise API
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>
* rename piecewise_envelope to piecewise_tangents
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>
* rename to tangent_lines — not piecewise, just linear expressions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: move tangent_lines into piecewise.py, remove linearization.py
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>
* docs: clarify equality vs inequality — when to use what
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>
* refac: tuple-based API for add_piecewise_constraints
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>
* feat: use variable names as link dimension coordinates
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>
* fix: remove piecewise.piecewise from api.rst, fix xr.concat compat in 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>
* fix: add coords='minimal' to xr.concat calls for forward compat
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>
* feat: add per-entity breakpoints example, fix scalar coord handling
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>
* docs: use fuel as x-axis in CHP plot for physical clarity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix per-entity plot to use fuel on x-axis with correct data
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
* docs: update release notes for piecewise API refactor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: frame piecewise as new feature in release notes, not refactor
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>
* fix: resolve mypy type error in incremental bp0_term assignment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: restructure piecewise documentation for readability
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>
* fix: add type: ignore comments to resolve mypy errors
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
* refac: remove dead code
* refac: inline _add_dpwl_sos2_core into _add_disjunctive, remove dead code
Remove _add_pwl_sos2_core and _add_pwl_incremental_core which were
never called, and inline the single-caller _add_dpwl_sos2_core.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: clean up piecewise module (#641)
* refac: use _to_linexpr in tangent_lines instead of manual dispatch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: rename _validate_xy_points to _validate_breakpoint_shapes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: clean up duplicate section headers in piecewise.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: convert expressions once in _broadcast_points
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: remove unused _compute_combined_mask
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: validate method early, compute trailing_nan_only once
Move method validation to add_piecewise_constraints entry point and
avoid calling _has_trailing_nan_only multiple times on the same data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: deduplicate stacked mask expansion in _add_continuous_nvar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: remove redundant isinstance guards in tangent_lines
_coerce_breaks already returns DataArray inputs unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: rename _extra_coords to _var_coords_from with explicit exclude set
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: clarify transitive validation in breakpoint shape check
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: remove skip_nan_check parameter
NaN breakpoints are always handled automatically via masking.
The skip_nan_check flag added API surface for minimal value —
it only asserted no NaN (misleading name) and skipped mask
computation (negligible performance gain).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: remove unused PWL_AUX/LP/LP_DOMAIN constants
Remnants of the old LP method that was removed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: always return link constraint from incremental path
Both SOS2 and incremental branches now consistently return the
link constraint, making the return value predictable for callers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: split _add_continuous into _add_sos2 and _add_incremental
Extract the SOS2 and incremental formulations into separate functions.
Add _stack_along_link helper to deduplicate the expand+concat pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: rename test classes to match current function names
TestPiecewiseEnvelope -> TestTangentLines
TestSolverEnvelope -> TestSolverTangentLines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: use _stack_along_link for expression stacking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: use generic param names in _validate_breakpoint_shapes
Rename x_points/y_points to bp_a/bp_b to reflect N-variable context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: extract _to_seg helper in tangent_lines for rename+reassign pattern
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: extract _strip_nan helper for NaN filtering in slopes mode
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: extract _breakpoints_from_slopes, add _to_seg docstring
Move the ~50 line slopes-to-points conversion out of breakpoints()
into _breakpoints_from_slopes, keeping breakpoints() as a clean
validation-then-dispatch function.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve mypy errors in _strip_nan and _stack_along_link types
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: remove duplicate slopes validation in breakpoints()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: move _rename_to_segments to module level, fix extra blank line
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add validation and edge-case tests for piecewise module
Cover error paths and edge cases: non-1D input, slopes mode with
DataArray y0, non-numeric breakpoint coords, segment dim mismatch,
disjunctive >2 pairs, disjunctive interior NaN, expression name
fallback, incremental NaN masking, and scalar coord handling.
Coverage: 92% -> 97%
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve ruff and mypy errors
- Use `X | Y` instead of `(X, Y)` in isinstance (UP038)
- Remove unused `dim` variable in _add_continuous (F841)
- Fix docstring formatting (D213)
- Remove unnecessary type: ignore comment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* feat: generalize disjunctive formulation to N variables
Refactor _add_disjunctive to use the same stacked N-variable pattern
as _add_continuous. Removes the 2-variable restriction — disjunctive
now supports any number of (expression, breakpoints) pairs with a
single unified link constraint.
- Remove separate x_link/y_link in favor of single _link with _pwl_var dim
- Remove PWL_Y_LINK_SUFFIX import (no longer needed)
- Add test for 3-variable disjunctive
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* feat: PiecewiseFormulation return type, model repr, rename to add_piecewise_formulation (#642)
* feat: PiecewiseFormulation return type, model groups, rename to add_piecewise_formulation
- Add PiecewiseFormulation dataclass grouping all auxiliary variables
and constraints created by a piecewise formulation
- Add _groups registry on Model to track grouped artifacts
- Model repr hides grouped items from Variables/Constraints sections
and shows them in a new "Groups" section
- Rename add_piecewise_constraints -> add_piecewise_formulation
- Export PiecewiseFormulation from linopy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: update notebook to show PiecewiseFormulation repr
Reorder cells so add_piecewise_formulation is the last statement,
letting Jupyter display the PiecewiseFormulation repr automatically.
Add print(m) cell to show the grouped model repr.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: show tangent_lines repr in notebook
Split tangent_lines cell so its LinearExpression repr is displayed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: show dims in PiecewiseFormulation repr and user dims in Model groups
PiecewiseFormulation now shows full dims (including internal) for each
variable and constraint. Model groups section shows "over (dim1, dim2)"
for user-facing dims only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: remove counts from PiecewiseFormulation repr
Match style of Variables/Constraints containers which don't show counts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: rename _groups to _piecewise_formulations, use direct section name
Replace generic "Groups" with "Piecewise Formulations" in Model repr.
Rename internal registry and helper to match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: move method after counts in repr to avoid looking like a dim
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: show dims before name like regular variables/constraints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: compact piecewise formulation line in model repr
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: use backtick name style in PiecewiseFormulation repr
Match Constraint repr pattern: `name` instead of 'name'.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: show user dims with sizes in PiecewiseFormulation header
Match Constraint repr style: `name` [dim: size, ...] — method
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clear notebook outputs to fix nbformat validation
Remove jetTransient metadata and normalize cell format.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: store names in PiecewiseFormulation, add IO persistence
PiecewiseFormulation now stores variable/constraint names as strings
with a model reference. Properties return live Views on access.
This makes serialization trivial — persist as JSON in netcdf attrs,
reconstruct on load.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: rename remaining add_piecewise_constraints reference after rebase
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refac: rename PWL suffix constants for clarity
- PWL_X_LINK_SUFFIX/_Y_LINK_SUFFIX → PWL_LINK_SUFFIX (N-var, single link)
- PWL_BINARY_SUFFIX → PWL_SEGMENT_BINARY_SUFFIX (disjunctive segment selection)
- PWL_INC_BINARY_SUFFIX → PWL_ORDER_BINARY_SUFFIX (incremental ordering)
- PWL_INC_LINK_SUFFIX → PWL_DELTA_BOUND_SUFFIX (δ ≤ binary)
- PWL_INC_ORDER_SUFFIX → PWL_BINARY_ORDER_SUFFIX (binary_{i+1} ≤ δ_i)
- PWL_FILL_SUFFIX → PWL_FILL_ORDER_SUFFIX (δ_{i+1} ≤ δ_i)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* fix: remove unused type: ignore on merge() cls assignment
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(piecewise): add sign parameter and LP method to add_piecewise_formulation (#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>
* feat(piecewise): post-#663 strategic tests, docs, and EvolvingAPIWarning
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>
* docs: N≥3 sign semantics — "N−1 jointly pinned, 1 bounded" framing
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>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* docs(piecewise): add at-a-glance method comparison table
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 (#664)
* 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>
* fix(piecewise): drop unused type: ignore on removed-sign kwarg test
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>
* docs(piecewise): reorder methods, disambiguate segments, point to runtime 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>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* docs(piecewise): consolidate inequality math in rst, drop notebook duplication
- 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>
* refactor(piecewise): rename "segment" → "piece" for the linear-piece 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>
* docs(piecewise): reorder methods to match auto-dispatch (Incremental 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>
* docs(piecewise): nest tutorial notebooks under reference page
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>
* docs(release-notes): align with piece/segment terminology
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>
* docs(release-notes): tighten piecewise entries, drop refactor-flavored 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>
* refactor(expressions): narrow __eq__ type-ignore to [override]
The descriptor pattern is gone, so the bare ``# type: ignore`` over-suppressed
mypy. ``__eq__`` still needs to declare ``Constraint`` instead of ``bool`` to
preserve linopy's expression-equals-builds-constraint semantics, so the
override-mismatch is intrinsic — narrow the directive to ``[override]``.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(io): rename piecewise netcdf attrs to variable_names/constraint_names
Use the same key names that ``PiecewiseFormulation`` uses internally
(``variable_names``/``constraint_names``) so the netcdf attribute layout
matches the dataclass field names. Read path updated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(constants): expose PWL_METHOD/PWL_CONVEXITY Literals
Promote the method and convexity string sets to Literal type aliases and
derive the runtime sets from ``get_args``. ``piecewise.py`` now uses the
aliases for type annotations on ``add_piecewise_formulation``,
``_detect_convexity``, and ``PiecewiseFormulation.convexity`` instead of
inlining the string list at every site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(repr): centralise piecewise repr in piecewise.py
Move the piecewise summary block out of ``Model.__repr__`` into
``piecewise._repr_summary``; the model now adds the section with a
single call. Also drops the ``_repr_filtered`` wrappers on
``Constraints``/``Variables`` — the model calls ``_format_items(exclude=...)``
directly, since that's all the wrappers were doing.
The variable and constraint name sets are returned separately from
``_grouped_names`` (variables and constraints live in independent
namespaces in the model, so each filter applies to its own collection).
Restores the missing docstrings on ``Constraints.__repr__`` and
``Variables.__repr__`` and corrects the wording — they describe the
respective container, not the model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(piecewise): emit EvolvingAPIWarning once per session
A single model build often calls ``add_piecewise_formulation`` /
``tangent_lines`` hundreds of times, and each emit produces a
multi-line warning that drowns out other output. Dedup per call site
via a module-level set keyed by a typed ``_EvolvingApiKey`` literal so
each entry point warns at most once per process.
Warning text now mentions the once-per-session behaviour so users know
they aren't seeing every call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* doc: add more plots to pwl notebook.
feat: add jupyter and ipykernel to dev extension in installation
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* fix(deps): drop notebook deps from [dev] extra to fix CI
8a57437 added ipykernel/jupyter/matplotlib to [dev], which transitively
pulled requests in via the jupyter metapackage. test/remote/test_oetc.py
guards itself with `pytest.importorskip("requests")` — on master that
skipped the whole file, but with requests now resolved the file got
collected and every test failed at OetcHandler() instantiation because
google-cloud-storage (the other half of the [oetc] extra) is still
absent. Result: 23 failures + 14 errors across the entire test matrix.
Notebook execution lives in test-notebooks.yml, which already installs
[docs] (ipykernel + matplotlib are there). No CI job needs notebook
deps in [dev].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(piecewise): API hygiene — alphabetise __all__, type method, drop sign= migration helper
* ``__all__`` in ``linopy/__init__.py`` is now sorted (Constraint…tangent_lines).
* ``PiecewiseFormulation.method`` typed as ``PWL_METHOD`` instead of ``str``,
matching ``convexity``.
* Drop ``**kwargs: object`` from ``add_piecewise_formulation``. The
pre-release ``sign=`` migration helper and the catch-all unknown-kwarg
error were dev backwards-compat for an unreleased API; Python's default
TypeError on unexpected kwargs covers the rest.
* Extract ``_user_dims_with_sizes`` to share the dim-collection loop
between ``_user_dims`` and ``__repr__``.
* Loop the two identical ``BREAKPOINT_DIM`` checks in
``_validate_breakpoint_shapes``.
Removes ``test_old_sign_kwarg_raises_with_migration_help`` (covered the
removed migration helper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(piecewise): single source of truth for LP eligibility
Both ``_lp_eligibility`` (auto-dispatch) and ``_try_lp`` (explicit
``method='lp'``) re-implemented the same five checks (n_tuples,
is_equality, active, monotonicity, sign+curvature) — the latter just
raised instead of returning a reason. ``_try_lp`` now always calls
``_lp_eligibility`` and translates the ``(False, reason)`` result into
either an INFO log (auto) or a ``ValueError`` (explicit lp), so
adding a new eligibility rule means editing one function instead of
two.
The raised-error wording is slightly more uniform — the eligibility
``reason`` is now embedded verbatim — but every existing assertion
matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(io): require ``convexity`` field on netCDF reload
The ``to_netcdf`` writer always emits ``convexity`` (possibly ``None``)
for every persisted ``PiecewiseFormulation``, so the reader's
``d.get("convexity")`` was masking what is actually a required field.
Switching to ``d["convexity"]`` makes the schema mismatch explicit and
matches the fail-fast posture of every other key in the same loop
(``method``, ``variable_names``, ``constraint_names``).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(piecewise): cover EvolvingAPIWarning, formulation API, LP eligibility, more netCDF cases
Closes review gaps left by #638:
* ``TestEvolvingAPIWarning`` — first-call fires, dedup holds across
repeats, ``tangent_lines`` and ``add_piecewise_formulation`` dedup
independently, ``stacklevel=3`` reports the user's call site.
Module-global dedup set is cleared by an autouse fixture so test
order doesn't matter.
* ``TestPiecewiseFormulationAPI`` — the ``.variables`` / ``.constraints``
properties and ``__repr__`` were essentially unexercised; smoke-tests
for the equality and LP shapes (the latter has empty ``variable_names``).
* ``TestLPEligibilityReasons`` — direct unit test of ``_lp_eligibility``
with handcrafted ``_PwlInputs``, parametrised over each branch (too
many tuples / all equality / active / non-monotonic / wrong-curvature
``<=`` and ``>=``) plus the success path. Uses ``_PwlInputs`` directly
so branches that the public-API short-circuits hide are still covered.
* ``TestPiecewiseNetCDFRoundtrip`` — parametrised over three shapes:
the existing 2-var equality, plus a bounded ``<=``/LP case (empty
``variable_names``) and a 3-var equality (``convexity is None``), so
the roundtrip exercises every reachable combination of persisted fields.
Asserts the reloaded ``.variables`` / ``.constraints`` properties work,
catching any future regression where the model back-reference isn't
rebound.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(piecewise): convert PiecewiseFormulation to @dataclass(slots=True)
Replaces the hand-rolled ``__slots__`` + ``__init__`` boilerplate with
``@dataclass(slots=True, repr=False)`` (the custom ``__repr__`` is
preserved). Net 17 lines removed.
Also renames the back-reference field from ``_model`` to ``model``. The
underscore was protective of an attribute that's actually a fine read:
``pwf.model`` is a sensible public access. This also lets the dataclass
init signature stay ``model=...`` without aliasing tricks — the two
existing callers (``add_piecewise_formulation`` and the netCDF reload in
``io.py``) already used ``model=`` keyword and don't need any change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(piecewise): low-friction cleanups from review
* Promote ``_pwl_var`` to ``PWL_LINK_DIM`` constant and reuse
``LP_PIECE_DIM`` in ``_add_lp`` instead of recomputing
``f"{dim}_piece"`` — one source of truth for each magic dim name.
* Tighten ``_resolve_sos2_vs_incremental`` and ``_add_continuous`` return
types to ``Literal`` / ``PWL_METHOD`` so the chain producing
``PiecewiseFormulation.method`` is type-checked end-to-end.
* Drop the dead ``raise ValueError(f"unknown method ...")`` branch in
``_resolve_sos2_vs_incremental`` — ``add_piecewise_formulation``
validates against ``PWL_METHODS`` upstream, and ``_try_lp`` already
consumed ``"lp"`` before this is called. Replaced with an ``assert``.
* ``PWL_METHODS`` / ``PWL_CONVEXITIES`` switched to ``frozenset`` to
signal immutability of the value sets.
* ``_strip_nan`` switched to ``arr[~np.isnan(arr)].tolist()`` —
vectorised, type-consistent regardless of input (sequence or
``ndarray``).
* ``_repr_summary`` uses ``len(pwl.variable_names)`` /
``len(pwl.constraint_names)`` instead of ``len(pwl.variables)`` — the
latter constructs a Variables view just to ask its length.
* Link-coord fallback for unnamed expressions is now ``f"_pwl_{i}"``
instead of ``str(i)``, so a user variable named e.g. ``"1"`` can't
collide with the synthetic coord.
* ``variables.py:Variable.__eq__`` ``# type: ignore`` narrowed back to
``# type: ignore[override]`` — the bare form was a regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): mypy clean up in TestLPEligibilityReasons / TestEvolvingAPIWarning
* ``_make_inputs`` return type narrowed from ``object`` to ``"_PwlInputs"``
(with ``TYPE_CHECKING`` import) so callers don't need
``# type: ignore`` to dispatch on it.
* ``test_reason_string`` ``kwargs: dict[str, Any]`` instead of bare
``dict`` — the previous ``# type: ignore[type-arg]`` is unused under
the project's mypy config.
* ``_reset_dedup`` autouse fixture annotated as
``Generator[None, None, None]`` since it ``yield``s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refac: rename pw repr function
doc: adjust figsize in notebook
* Fix piecewise validation edge cases
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Fabian <fab.hof@gmx.de>1 parent 620766d commit 94a38cd
20 files changed
Lines changed: 4483 additions & 2595 deletions
File tree
- dev-scripts
- doc
- examples
- linopy
- test
Lines changed: 348 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
22 | | - | |
| 21 | + | |
| 22 | + | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
| 26 | + | |
25 | 27 | | |
26 | 28 | | |
27 | 29 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
117 | | - | |
118 | 117 | | |
119 | 118 | | |
120 | 119 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
0 commit comments