Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
277cda8
feat(piecewise): add Slopes class for deferred breakpoint specs
FBumann May 6, 2026
c592b74
refactor(piecewise): remove slopes-mode of breakpoints() and slopes_t…
FBumann May 6, 2026
09a7869
docs(piecewise): migrate slopes examples to Slopes class
FBumann May 6, 2026
59e3454
feat(piecewise): custom Slopes repr that hides defaults and summarise…
FBumann May 6, 2026
7da32c1
test(piecewise): consolidate Slopes tests into focused, parametrised …
FBumann May 6, 2026
fd62ba8
chore: hoist _slopes_to_points test import + strip notebook execution…
FBumann May 6, 2026
a88325a
fix(notebook): restore em-dashes from — escapes to UTF-8
FBumann May 6, 2026
a4c041a
fix(notebook): restore unrelated unicode chars and Python version met…
FBumann May 6, 2026
2c00139
review fixes: emit Slopes warning, bound seq repr, harden dispatch test
FBumann May 6, 2026
f27ee49
fix(piecewise): three robustness issues in Slopes
FBumann May 6, 2026
63c4126
feat(piecewise): value-equality on Slopes via type-dispatched __eq__
FBumann May 6, 2026
2072014
feat(piecewise): summarise multi-dim ndarray Slopes values by shape
FBumann May 6, 2026
bb3db50
fix(piecewise): broaden Slopes equality, trim release-notes entry
FBumann May 6, 2026
9f9f3cc
docs(piecewise): trim notebook section 8 to match the surrounding shape
FBumann May 6, 2026
5338252
fix(piecewise): require exactly one non-Slopes tuple in add_piecewise…
FBumann May 6, 2026
196e532
test(piecewise): pin Slopes dispatch via assert_model_equal; widen nd…
FabianHofmann May 7, 2026
4bfee2f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 7, 2026
cc85185
refactor(piecewise): trim _values_equal and _summarise_breakslike
FabianHofmann May 7, 2026
c208ddd
fix(piecewise): TypeGuard on _is_numeric_scalar for mypy
FabianHofmann May 7, 2026
fbf3770
fix(piecewise): revert _values_equal equals-loop to explicit branches…
FabianHofmann May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ Creating a model
model.Model.add_objective
model.Model.add_piecewise_formulation
piecewise.PiecewiseFormulation
piecewise.Slopes
piecewise.breakpoints
piecewise.segments
piecewise.slopes_to_points
piecewise.tangent_lines
model.Model.linexpr
model.Model.remove_constraints
Expand Down
20 changes: 12 additions & 8 deletions doc/piecewise-linear-constraints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ Two factories with distinct geometric meaning:

linopy.breakpoints([0, 50, 100]) # connected
linopy.breakpoints({"gen1": [0, 50], "gen2": [0, 80]}, dim="gen") # per-entity
linopy.breakpoints(slopes=[1.2, 1.4], x_points=[0, 30, 60], y0=0) # from slopes
linopy.Slopes(
[1.2, 1.4], y0=0
) # from slopes (deferred — pairs with a sibling tuple)
linopy.segments([(0, 10), (50, 100)]) # two disjoint regions
linopy.segments({"gen1": [(0, 10)], "gen2": [(0, 80)]}, dim="gen")

Expand Down Expand Up @@ -240,21 +242,23 @@ Equivalent, but explicit about the DataArray construction:
From slopes
~~~~~~~~~~~

When you know marginal costs (slopes) rather than absolute values:
When you know marginal costs (slopes) rather than absolute values, wrap
them in :class:`linopy.Slopes`. The x grid is borrowed from the sibling
tuple — no need to repeat it:

.. code-block:: python

m.add_piecewise_formulation(
(power, [0, 50, 100, 150]),
(
cost,
linopy.breakpoints(
slopes=[1.1, 1.5, 1.9], x_points=[0, 50, 100, 150], y0=0
),
),
(cost, linopy.Slopes([1.1, 1.5, 1.9], y0=0)),
)
# cost breakpoints: [0, 55, 130, 225]

For standalone resolution outside of ``add_piecewise_formulation``, call
:meth:`linopy.Slopes.to_breakpoints` with an explicit x grid::

bp = linopy.Slopes([1.1, 1.5, 1.9], y0=0).to_breakpoints([0, 50, 100, 150])

Per-entity breakpoints
~~~~~~~~~~~~~~~~~~~~~~

Expand Down
3 changes: 2 additions & 1 deletion doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ Upcoming Version
* Add unit-commitment gating via the ``active`` parameter on ``add_piecewise_formulation``: a binary variable that, when zero, forces all auxiliary variables (and thus the linked expressions) to zero. Works with the SOS2, incremental, and disjunctive methods.
* Surface formulation metadata on the returned ``PiecewiseFormulation``: ``.method`` (resolved method name) and ``.convexity`` (``"convex"`` / ``"concave"`` / ``"linear"`` / ``"mixed"`` when well-defined). Both persist across netCDF round-trip.
* Add ``tangent_lines()`` as a low-level helper that returns per-piece chord expressions as a ``LinearExpression`` — no variables created. Most users should prefer ``add_piecewise_formulation`` with a bounded tuple ``(y, y_pts, "<=")``, which builds on this helper and adds domain bounds and curvature validation.
* Add ``linopy.breakpoints()`` (lists/Series/DataFrame/DataArray/dict, plus a slopes-mode constructor), ``linopy.segments()`` (disjunctive operating regions), and ``slopes_to_points()`` (per-piece slopes → breakpoint y-coordinates) as breakpoint-construction helpers.
* Add ``linopy.breakpoints()`` (lists/Series/DataFrame/DataArray/dict) and ``linopy.segments()`` (disjunctive operating regions) as breakpoint-construction helpers.
* Add ``linopy.Slopes`` for specifying a piecewise curve by marginal costs / per-piece slopes instead of absolute y-values — ``(fuel, Slopes([1.2, 1.4, 1.7], y0=0))`` borrows the x grid from a sibling tuple in ``add_piecewise_formulation``.
* Add the `sphinx-copybutton` to the documentation
* Add SOS1 and SOS2 reformulations for solvers not supporting them.
* Add semi-continous variables for solvers that support them
Expand Down
30 changes: 30 additions & 0 deletions examples/piecewise-linear-constraints.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,36 @@
"m.solve(reformulate_sos=\"auto\")\n",
"m.solution[[\"power\", \"fuel\"]].to_dataframe()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 8. Specifying with slopes — `Slopes`\n",
"\n",
"When marginal costs (slopes) are more natural than absolute y-values, wrap them in `linopy.Slopes`. The x grid is borrowed from the sibling tuple — no need to repeat it. Same curve as section 1:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"m = linopy.Model()\n",
"power = m.add_variables(name=\"power\", lower=0, upper=100, coords=[time])\n",
"fuel = m.add_variables(name=\"fuel\", lower=0, coords=[time])\n",
"\n",
"m.add_piecewise_formulation(\n",
" (power, [0, 30, 60, 100]),\n",
" (fuel, linopy.Slopes([1.2, 1.6, 2.15], y0=0)),\n",
")\n",
"m.add_constraints(power == demand, name=\"demand\")\n",
"m.add_objective(fuel.sum())\n",
"m.solve(reformulate_sos=\"auto\")\n",
"\n",
"m.solution[[\"power\", \"fuel\"]].to_pandas()"
]
}
],
"metadata": {
Expand Down
4 changes: 2 additions & 2 deletions linopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
from linopy.objective import Objective
from linopy.piecewise import (
PiecewiseFormulation,
Slopes,
breakpoints,
segments,
slopes_to_points,
tangent_lines,
)
from linopy.remote import RemoteHandler
Expand All @@ -53,6 +53,7 @@
"PiecewiseFormulation",
"QuadraticExpression",
"RemoteHandler",
"Slopes",
"Variable",
"Variables",
"align",
Expand All @@ -62,6 +63,5 @@
"options",
"read_netcdf",
"segments",
"slopes_to_points",
"tangent_lines",
)
Loading
Loading