Skip to content

feat: add active parameter to piecewise linear constraints#604

Merged
FabianHofmann merged 9 commits intopiecewise-followupfrom
feat/piecewise-linear-active
Mar 9, 2026
Merged

feat: add active parameter to piecewise linear constraints#604
FabianHofmann merged 9 commits intopiecewise-followupfrom
feat/piecewise-linear-active

Conversation

@FBumann
Copy link
Collaborator

@FBumann FBumann commented Mar 9, 2026

Changes proposed in this Pull Request

Add an active parameter to piecewise() that accepts a binary variable to gate piecewise linear functions on/off. This enables unit commitment formulations where a commitment binary controls the operating range of a unit.

  • When active=0, all auxiliary variables are forced to zero (x and y collapse to 0)
  • When active=1, the normal PWL domain [x₀, xₙ] is active

Motivation

In energy dispatch optimization, units have a binary on/off status and a variable efficiency curve (heat rate). The piecewise linear approximation of this curve needs to be gated by the commitment binary: when the unit is off, both power output and fuel consumption must be zero — not just "unconstrained".

This zero-forcing behavior is the only option that can be expressed with pure linear constraints. The alternative — selectively relaxing the PWL constraint (letting x and y float freely when off) — would require big-M formulations or solver-native indicator constraints, both of which are outside the scope of this feature.

How it works per method

Method Change Why it works
Incremental δ_i ≤ active + base terms × active Deltas forced to 0 → reconstruction collapses
SOS2 Σλ_i = active (instead of = 1) Lambdas forced to 0 → weighted sums collapse
Disjunctive Σz_k = active (instead of = 1) Segment binaries forced to 0 → lambdas collapse
LP Not supported (raises ValueError) See below

Why LP is excluded

The LP tangent-line formulation has no auxiliary variables — it directly constrains the x/y relationship via slope inequalities. This is its strength without active, but makes clean gating impossible:

  1. Big-M would be needed to relax the tangent lines when active=0
  2. Relaxing alone isn't enough — separate big-M constraints would also be needed to force x=0 and y=0
  3. The result would be more constraints, M-dependent numerical conditioning, and no structural advantage over SOS2/incremental where zero-forcing is free

Why active lives on piecewise() not add_piecewise_constraints()

The active binary is a property of the piecewise function itself (the mapping is gated), not of the constraint method used to implement it. It belongs with the breakpoints.

Naming

active was chosen over alternatives:

  • indicator — suggests constraint relaxation (Option B), not zero-forcing
  • binary — describes the type, not the role
  • commitment / on — too domain-specific for a general library

Open to discussion.

Usage

m = Model()
power = m.add_variables(lower=0, upper=100, name="power")
fuel = m.add_variables(name="fuel")
u = m.add_variables(binary=True, name="commit")

m.add_piecewise_constraints(
    piecewise(power, [20, 60, 100], [5, 20, 50], active=u) == fuel,
    method="incremental",
)

Checklist

  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in doc.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes doc/release_notes.rst of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

Test plan

  • Structural tests: verify correct constraints/variables are created for each method
  • active=None (default) produces identical formulation to before
  • active with method='lp' raises ValueError
  • Multi-dimensional breakpoints with active work correctly
  • Solver integration tests (active on/off, non-zero base, unit commitment pattern)
  • All 122 piecewise tests pass

🤖 Generated with Claude Code

Add an `active` parameter to the `piecewise()` function that accepts a
binary variable to gate piecewise linear functions on/off. This enables
unit commitment formulations where a commitment binary controls the
operating range.

The parameter modifies each formulation method as follows:
- Incremental: δ_i ≤ active (tightened bounds) + base terms × active
- SOS2: Σλ_i = active (instead of 1)
- Disjunctive: Σz_k = active (instead of 1)

When active=0, all auxiliary variables are forced to zero, collapsing
x and y to zero. When active=1, the normal PWL domain is active.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@FBumann FBumann changed the base branch from master to piecewise-followup March 9, 2026 08:54
@FBumann FBumann changed the title Feat/piecewise linear active feat: add active parameter to piecewise linear constraints Mar 9, 2026
@FBumann
Copy link
Collaborator Author

FBumann commented Mar 9, 2026

Addition to #602

FBumann and others added 7 commits March 9, 2026 09:59
Clarify that zero-forcing is the only linear formulation possible —
relaxing the constraint would require big-M or indicator constraints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Example 6 demonstrates the active parameter with a gas unit that
stays off at t=1 (low demand) and commits at t=2,3 (high demand),
showing power=0 and fuel=0 when the commitment binary is off.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tests for gaps identified in review:
- Inequality + active (incremental and SOS2, on and off)
- auto method selection + active (equality and auto-LP rejection)
- active with LinearExpression (not just Variable)
- active with NaN-masked breakpoints
- LP file output comparison (active vs plain)
- Multi-dimensional solver test (per-entity on/off)
- SOS2 non-zero base + active off
- SOS2 inequality + active off
- Disjunctive active on (solver)
- Fix: reject active when auto resolves to LP

159 tests pass (was 122).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the active bound constraint name suffix to constants.py,
consistent with all other PWL suffix constants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Collaborator

@FabianHofmann FabianHofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice one, this is a great extension. found one issue. active is rejected for method="lp" explicitly, but it is still silently ignored when method="auto" resolves to the LP formulation for inequalities.

Concretely, something like

  m.add_piecewise_constraints(
      piecewise(x, [0, 50, 100], [0, 40, 60], active=u) >= y,
      method="auto",
  )

currently succeeds, but u is not used in the generated formulation at all.

Since auto is the default, this can lead to a model that appears gated but actually isn’t.

We probably want to raise here

@FabianHofmann
Copy link
Collaborator

mind if I quickly push this?

Keep only tests that exercise unique code paths or verify distinct
mathematical properties.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@FabianHofmann FabianHofmann marked this pull request as ready for review March 9, 2026 12:03
@FabianHofmann FabianHofmann merged commit 53ea09a into piecewise-followup Mar 9, 2026
18 checks passed
@FabianHofmann FabianHofmann deleted the feat/piecewise-linear-active branch March 9, 2026 12:19
FabianHofmann added a commit that referenced this pull request Mar 9, 2026
* Refactor piecewise constraints: add piecewise/segments/slopes_to_points API, LP formulation for convex/concave cases, and simplify tests

* piecewise: replace bp_dim/seg_dim params with constants, remove dead code, improve errors

* Fix piecewise linear constraints: add binary indicators to incremental formulation, add domain bounds to LP formulation

- Incremental method now uses binary indicator variables with link/order constraints to enforce proper segment filling order (Markowitz & Manne)
- LP method now adds x ∈ [min(xᵢ), max(xᵢ)] domain bound constraints to prevent extrapolation beyond breakpoints

* update signatures of breakpoints and segments, apply convexity check only where needed

* update doc

* Reject interior NaN and skip_nan_check+NaN in piecewise formulations

Validate trailing-NaN-only for SOS2 and disjunctive methods to prevent
corrupted adjacency. Fail fast when skip_nan_check=True but breakpoints
actually contain NaN.

* Allow piecewise() on either side of comparison operators

Support reversed syntax (y == piecewise(...)) via __le__/__ge__/__eq__
dispatch in BaseExpression and ScalarLinearExpression. Fix LP example
to use power == demand for more illustrative results.

* Fix mypy type errors for piecewise constraint types

- Add @overload to comparison operators (__le__, __ge__, __eq__) in
  BaseExpression and Variable to distinguish PiecewiseExpression from
  SideLike return types
- Update ConstraintLike type alias to include PiecewiseConstraintDescriptor
- Fix PiecewiseConstraintDescriptor.lhs type from object to LinExprLike
- Fix dict/sequence type mismatches in _dict_to_array, _dict_segments_to_array,
  _segments_list_to_array
- Remove unused type: ignore comments
- Narrow ScalarLinearExpression/ScalarVariable return types to not include
  PiecewiseConstraintDescriptor (impossible at runtime)

* rename header of jupyter notebook

* doc: rename notebook again

* feat: add active parameter to piecewise linear constraints (#604)

* feat: add `active` parameter to piecewise linear constraints

Add an `active` parameter to the `piecewise()` function that accepts a
binary variable to gate piecewise linear functions on/off. This enables
unit commitment formulations where a commitment binary controls the
operating range.

The parameter modifies each formulation method as follows:
- Incremental: δ_i ≤ active (tightened bounds) + base terms × active
- SOS2: Σλ_i = active (instead of 1)
- Disjunctive: Σz_k = active (instead of 1)

When active=0, all auxiliary variables are forced to zero, collapsing
x and y to zero. When active=1, the normal PWL domain is active.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: tighten active parameter docstrings

Clarify that zero-forcing is the only linear formulation possible —
relaxing the constraint would require big-M or indicator constraints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add active parameter to release notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve mypy type errors for x_base/y_base assignment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add unit commitment example to piecewise notebook

Example 6 demonstrates the active parameter with a gas unit that
stays off at t=1 (low demand) and commits at t=2,3 (high demand),
showing power=0 and fuel=0 when the commitment binary is off.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update notebook

* test: comprehensive active parameter test coverage

Add tests for gaps identified in review:
- Inequality + active (incremental and SOS2, on and off)
- auto method selection + active (equality and auto-LP rejection)
- active with LinearExpression (not just Variable)
- active with NaN-masked breakpoints
- LP file output comparison (active vs plain)
- Multi-dimensional solver test (per-entity on/off)
- SOS2 non-zero base + active off
- SOS2 inequality + active off
- Disjunctive active on (solver)
- Fix: reject active when auto resolves to LP

159 tests pass (was 122).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract PWL_ACTIVE_BOUND_SUFFIX constant

Move the active bound constraint name suffix to constants.py,
consistent with all other PWL suffix constants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: remove redundant active parameter tests

Keep only tests that exercise unique code paths or verify distinct
mathematical properties.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: FBumann <117816358+FBumann@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <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