Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
5b01def
feat: add piecewise linear constraint API
FBumann Jan 25, 2026
ad61632
Fix lambda coords
FBumann Jan 25, 2026
c561760
rename to add_piecewise_constraints
FBumann Jan 25, 2026
4472548
rename to add_piecewise_constraints
FBumann Jan 25, 2026
32b10b0
fix types (mypy)
FBumann Jan 25, 2026
302d92b
linopy/constants.py — Added PWL_DELTA_SUFFIX = "_delta" and PWL_FIL…
FBumann Jan 30, 2026
6e76739
1. Step sizes: replaced manual loop + xr.concat with breakpoints.di…
FBumann Jan 30, 2026
36112e2
rewrite filling order constraint
FBumann Jan 30, 2026
ec4538b
Fix monotonicity check
FBumann Jan 30, 2026
59f92ae
Fix multiplication of constant-only LinearExpression (#568)
FabianHofmann Feb 6, 2026
36b15c5
perf: speed up LP file writing (2.5-3.9x on large models, no regressi…
FBumann Feb 6, 2026
9ce2005
Add auto_mask parameter to Model class (#555)
FBumann Feb 6, 2026
c9f83bb
update release notes
FabianHofmann Feb 9, 2026
e365258
Summary
FBumann Feb 9, 2026
5598e89
docs: add piecewise linear constraints documentation
FBumann Feb 9, 2026
0b41d3a
test: improve disjunctive piecewise linear test coverage
FBumann Feb 9, 2026
e4f4ee6
docs: Add notebook to showcase piecewise linear constraint
FBumann Feb 9, 2026
96eef89
Add cross reference to notebook
FBumann Feb 9, 2026
19651ed
Bugfix/fix readthedocs (#574)
RobbieKiwi Feb 9, 2026
7c539e7
Improve notebook
FBumann Feb 9, 2026
3f0fbaa
docs: add release notes and cross-reference for PWL constraints
FBumann Feb 9, 2026
fe72e1a
Merge remote-tracking branch 'origin/master' into feat/add-piecewise-…
FBumann Feb 9, 2026
a5a5a54
fix mypy issue in test
FBumann Feb 9, 2026
97ed0c0
fix polars dep lb (#578)
FBumann Feb 10, 2026
606a714
fix: revert np.where to xarray.where when adding vars/ constraints (#…
lkstrp Feb 10, 2026
ec6262b
test and future warning for #575 (#579)
lkstrp Feb 10, 2026
d7f5fe8
Improve docs about incremental
FBumann Feb 10, 2026
75c442c
Reinsert broadcasted mask (#580)
FabianHofmann Feb 10, 2026
45285ee
fix: add coords and dims to as_dataarray (#582)
FabianHofmann Feb 11, 2026
16d6f32
update release notes
FabianHofmann Feb 11, 2026
6655b54
fix: update HiGHS URLs and naming (#585)
FabianHofmann Feb 16, 2026
d5136e7
Add Knitro solver support (#532)
FabianHofmann Feb 18, 2026
1b08d2b
update release notes
FabianHofmann Feb 18, 2026
4c7a957
Merge branch 'master' into feat/add-piecewise-variants
FabianHofmann Feb 19, 2026
8b9d55d
refactor and add tests
FabianHofmann Feb 20, 2026
cbc2b88
fix: reject non-trailing NaN in incremental piecewise formulation
FabianHofmann Feb 20, 2026
841dcab
further refactor
FabianHofmann Feb 20, 2026
103cf69
extract piecewise linear logic into linopy/piecewise.py
FabianHofmann Feb 20, 2026
b7aba5f
feat: add sos reformulations into linopy to simplify adoption of new …
FBumann Feb 20, 2026
f629c2d
Merge branch 'master' into feat/add-piecewise-variants
FabianHofmann Feb 20, 2026
25eaefb
feat: allow broadcasted mask
FabianHofmann Feb 20, 2026
c201a6a
fix merge conflict in release notes
FabianHofmann Feb 20, 2026
c4f831c
refactor: remove link_dim from piecewise constraint API
FabianHofmann Feb 23, 2026
d403071
refactor: use LinExprLike type alias and consolidate piecewise valida…
FabianHofmann Feb 23, 2026
8b4b937
fix: resolve mypy errors in piecewise module
FabianHofmann Feb 23, 2026
2465f53
update release notes [skip ci]
FabianHofmann Feb 23, 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
639 changes: 639 additions & 0 deletions benchmark/benchmark_auto_mask.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Creating a model
model.Model.add_variables
model.Model.add_constraints
model.Model.add_objective
model.Model.add_piecewise_constraints
model.Model.add_disjunctive_piecewise_constraints
model.Model.linexpr
model.Model.remove_constraints

Expand Down
7 changes: 2 additions & 5 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import pkg_resources # part of setuptools
import linopy

# -- Project information -----------------------------------------------------

Expand All @@ -22,12 +22,9 @@
author = "Fabian Hofmann"

# The full version, including alpha/beta/rc tags
version = pkg_resources.get_distribution("linopy").version
version = linopy.__version__
release = "master" if "dev" in version else version

# For some reason is this needed, otherwise autosummary does fail on RTD but not locally
import linopy # noqa

# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
Expand Down
2 changes: 2 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ This package is published under MIT license.
creating-expressions
creating-constraints
sos-constraints
piecewise-linear-constraints
piecewise-linear-constraints-tutorial
manipulating-models
testing-framework
transport-tutorial
Expand Down
3 changes: 3 additions & 0 deletions doc/piecewise-linear-constraints-tutorial.nblink
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"path": "../examples/piecewise-linear-constraints.ipynb"
}
301 changes: 301 additions & 0 deletions doc/piecewise-linear-constraints.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
.. _piecewise-linear-constraints:

Piecewise Linear Constraints
============================

Piecewise linear (PWL) constraints approximate nonlinear functions as connected
linear segments, allowing you to model cost curves, efficiency curves, or
production functions within a linear programming framework.

Linopy provides two methods:

- :py:meth:`~linopy.model.Model.add_piecewise_constraints` -- for
**continuous** piecewise linear functions (segments connected end-to-end).
- :py:meth:`~linopy.model.Model.add_disjunctive_piecewise_constraints` -- for
**disconnected** segments (with gaps between them).

.. contents::
:local:
:depth: 2

Formulations
------------

SOS2 (Convex Combination)
~~~~~~~~~~~~~~~~~~~~~~~~~

Given breakpoints :math:`b_0, b_1, \ldots, b_n`, the SOS2 formulation
introduces interpolation variables :math:`\lambda_i` such that:

.. math::

\lambda_i \in [0, 1], \quad
\sum_{i=0}^{n} \lambda_i = 1, \quad
x = \sum_{i=0}^{n} \lambda_i \, b_i

The SOS2 constraint ensures that **at most two adjacent** :math:`\lambda_i` can
be non-zero, so :math:`x` is interpolated within one segment.

**Dict (multi-variable) case.** When multiple variables share the same lambdas,
breakpoints carry an extra *link* dimension :math:`v \in V` and linking becomes
:math:`x_v = \sum_i \lambda_i \, b_{v,i}` for all :math:`v`.

Incremental (Delta) Formulation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For **strictly monotonic** breakpoints :math:`b_0 < b_1 < \cdots < b_n`, the
incremental formulation is a pure LP (no SOS2 or binary variables):

.. math::

\delta_i \in [0, 1], \quad
\delta_{i+1} \le \delta_i, \quad
x = b_0 + \sum_{i=1}^{n} \delta_i \, (b_i - b_{i-1})

The filling-order constraints enforce that segment :math:`i+1` cannot be
partially filled unless segment :math:`i` is completely filled.

Disjunctive (Disaggregated Convex Combination)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For **disconnected segments** (with gaps), the disjunctive formulation selects
exactly one segment via binary indicators and applies SOS2 within it. No big-M
constants are needed, giving a tight LP relaxation.

Given :math:`K` segments, each with breakpoints :math:`b_{k,0}, \ldots, b_{k,n_k}`:

.. math::

y_k \in \{0, 1\}, \quad \sum_{k} y_k = 1

\lambda_{k,i} \in [0, 1], \quad
\sum_{i} \lambda_{k,i} = y_k, \quad
x = \sum_{k} \sum_{i} \lambda_{k,i} \, b_{k,i}

.. _choosing-a-formulation:

Choosing a Formulation
~~~~~~~~~~~~~~~~~~~~~~

.. list-table::
:header-rows: 1
:widths: 25 25 25 25

* - Property
- SOS2
- Incremental
- Disjunctive
* - Segments
- Connected
- Connected
- Disconnected (gaps allowed)
* - Breakpoint order
- Any
- Strictly monotonic
- Any (per segment)
* - Variable types
- Continuous + SOS2
- Continuous only (pure LP)
- Binary + SOS2
* - Best for
- General piecewise functions
- Monotonic curves
- Forbidden zones, discrete modes

Basic Usage
-----------

Single variable
~~~~~~~~~~~~~~~

.. code-block:: python

import linopy
import xarray as xr

m = linopy.Model()
x = m.add_variables(name="x")

breakpoints = xr.DataArray([0, 10, 50, 100], dims=["bp"])
m.add_piecewise_constraints(x, breakpoints, dim="bp")

Dict of variables
~~~~~~~~~~~~~~~~~~

Link multiple variables through shared interpolation weights. For example, a
turbine where power input determines power output (via a nonlinear efficiency
factor):

.. code-block:: python

m = linopy.Model()

power_in = m.add_variables(name="power_in")
power_out = m.add_variables(name="power_out")

# At 50 MW input the turbine produces 47.5 MW output (95% eff),
# at 100 MW input only 90 MW output (90% eff)
breakpoints = xr.DataArray(
[[0, 50, 100], [0, 47.5, 90]],
coords={"var": ["power_in", "power_out"], "bp": [0, 1, 2]},
)

m.add_piecewise_constraints(
{"power_in": power_in, "power_out": power_out},
breakpoints,
link_dim="var",
dim="bp",
)

Incremental method
~~~~~~~~~~~~~~~~~~~

.. code-block:: python

m.add_piecewise_constraints(x, breakpoints, dim="bp", method="incremental")

Pass ``method="auto"`` to automatically select incremental when breakpoints are
strictly monotonic, falling back to SOS2 otherwise.

Disjunctive (disconnected segments)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python

m = linopy.Model()
x = m.add_variables(name="x")

# Two disconnected segments: [0, 10] and [50, 100]
breakpoints = xr.DataArray(
[[0, 10], [50, 100]],
dims=["segment", "breakpoint"],
coords={"segment": [0, 1], "breakpoint": [0, 1]},
)

m.add_disjunctive_piecewise_constraints(x, breakpoints)

Method Signatures
-----------------

``add_piecewise_constraints``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python

Model.add_piecewise_constraints(
expr,
breakpoints,
link_dim=None,
dim="breakpoint",
mask=None,
name=None,
skip_nan_check=False,
method="sos2",
)

- ``expr`` -- ``Variable``, ``LinearExpression``, or ``dict`` of these.
- ``breakpoints`` -- ``xr.DataArray`` with breakpoint values. Must have ``dim``
as a dimension. For the dict case, must also have ``link_dim``.
- ``link_dim`` -- ``str``, optional. Dimension linking to different expressions.
- ``dim`` -- ``str``, default ``"breakpoint"``. Breakpoint-index dimension.
- ``mask`` -- ``xr.DataArray``, optional. Boolean mask for valid constraints.
- ``name`` -- ``str``, optional. Base name for generated variables/constraints.
- ``skip_nan_check`` -- ``bool``, default ``False``.
- ``method`` -- ``"sos2"`` (default), ``"incremental"``, or ``"auto"``.

``add_disjunctive_piecewise_constraints``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python

Model.add_disjunctive_piecewise_constraints(
expr,
breakpoints,
link_dim=None,
dim="breakpoint",
segment_dim="segment",
mask=None,
name=None,
skip_nan_check=False,
)

Same as above, plus:

- ``segment_dim`` -- ``str``, default ``"segment"``. Dimension indexing
segments. Use NaN in breakpoints to pad segments with fewer breakpoints.

Generated Variables and Constraints
------------------------------------

Given base name ``name``, the following objects are created:

**SOS2 method:**

.. list-table::
:header-rows: 1
:widths: 30 15 55

* - Name
- Type
- Description
* - ``{name}_lambda``
- Variable
- Interpolation weights :math:`\lambda_i \in [0, 1]` (SOS2).
* - ``{name}_convex``
- Constraint
- :math:`\sum_i \lambda_i = 1`.
* - ``{name}_link``
- Constraint
- :math:`x = \sum_i \lambda_i \, b_i`.

**Incremental method:**

.. list-table::
:header-rows: 1
:widths: 30 15 55

* - Name
- Type
- Description
* - ``{name}_delta``
- Variable
- Fill-fraction variables :math:`\delta_i \in [0, 1]`.
* - ``{name}_fill``
- Constraint
- :math:`\delta_{i+1} \le \delta_i` (only if 3+ breakpoints).
* - ``{name}_link``
- Constraint
- :math:`x = b_0 + \sum_i \delta_i \, s_i`.

**Disjunctive method:**

.. list-table::
:header-rows: 1
:widths: 30 15 55

* - Name
- Type
- Description
* - ``{name}_binary``
- Variable
- Segment indicators :math:`y_k \in \{0, 1\}`.
* - ``{name}_select``
- Constraint
- :math:`\sum_k y_k = 1`.
* - ``{name}_lambda``
- Variable
- Per-segment interpolation weights (SOS2).
* - ``{name}_convex``
- Constraint
- :math:`\sum_i \lambda_{k,i} = y_k`.
* - ``{name}_link``
- Constraint
- :math:`x = \sum_k \sum_i \lambda_{k,i} \, b_{k,i}`.

See Also
--------

- :doc:`piecewise-linear-constraints-tutorial` -- Worked examples with all three formulations
- :doc:`sos-constraints` -- Low-level SOS1/SOS2 constraint API
- :doc:`creating-constraints` -- General constraint creation
- :doc:`user-guide` -- Overall linopy usage patterns
19 changes: 18 additions & 1 deletion doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,26 @@ Release Notes
Upcoming Version
----------------

* Fix docs (pick highs solver)
* Add ``add_piecewise_constraints()`` for piecewise linear constraints with SOS2 and incremental (pure LP) formulations.
* Add ``add_disjunctive_piecewise_constraints()`` for disconnected piecewise linear segments (e.g. forbidden operating zones).
* Add the `sphinx-copybutton` to the documentation

Version 0.6.2
--------------

**Features**

* Add ``auto_mask`` parameter to ``Model`` class that automatically masks variables and constraints where bounds, coefficients, or RHS values contain NaN. This eliminates the need to manually create mask arrays when working with sparse or incomplete data.

**Performance**

* Speed up LP file writing by 2-2.7x on large models through Polars streaming engine, join-based constraint assembly, and reduced per-constraint overhead

**Bug Fixes**

* Fix multiplication of constant-only ``LinearExpression`` with other expressions
* Fix docs and Gurobi license handling

Version 0.6.1
--------------

Expand Down
5 changes: 5 additions & 0 deletions doc/sos-constraints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ Common Patterns
Piecewise Linear Cost Function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. note::

For a higher-level API that handles all the SOS2 bookkeeping automatically,
see :doc:`piecewise-linear-constraints`.

.. code-block:: python

def add_piecewise_cost(model, variable, breakpoints, costs):
Expand Down
Loading