Skip to content

refac: Refactor piecewise API to a stateless construction layer + tangent_lines utility#638

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

refac: Refactor piecewise API to a stateless construction layer + tangent_lines utility#638
FBumann wants to merge 30 commits intomasterfrom
feat/piecewise-api-refactor

Conversation

@FBumann
Copy link
Copy Markdown
Collaborator

@FBumann FBumann commented Apr 1, 2026

Summary

Follows up on the discussion in #602. Replaces the descriptor pattern (PiecewiseExpression, PiecewiseConstraintDescriptor, piecewise()) with a stateless construction layer. Establishes a clean separation between piecewise equality constraints and mathematically very different tangent-line inequality bounds.

Final API

Each (expression, breakpoints) tuple links a variable to its breakpoints. All tuples share interpolation weights, coupling them on the same curve segment.

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

# With breakpoints() factory
m.add_piecewise_constraints(
    (power, linopy.breakpoints([0, 30, 60, 100])),
    (fuel,  linopy.breakpoints([0, 36, 84, 170])),
)

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

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

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

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

# Unit commitment with active parameter
m.add_piecewise_constraints(
    (power, x_pts),
    (fuel,  y_pts),
    active=commit,  # binary variable gates the PWL
)

# Inequality bounds (pure LP, no aux variables) — separate utility
t = linopy.tangent_lines(power, x_pts, y_pts)
m.add_constraints(fuel <= t)   # upper bound (concave f)
m.add_constraints(fuel >= t)   # lower bound (convex f)

Key changes

  • Remove piecewise() function and descriptor classes — no intermediate state objects
  • Tuple-based add_piecewise_constraints — each (expression, breakpoints) pair is a positional arg. 2-var and N-var are the same pattern.
  • tangent_lines() utility — computes tangent-line LinearExpression per segment. Pure LP, no auxiliary variables. Separate from piecewise equality.
  • sign parameter removed — piecewise only does equality linking. Inequality via tangent_lines + regular add_constraints.
  • N-variable support restored — link 3+ expressions through shared lambda weights (CHP use case from feat: add piecewise linear constraint API (SOS2, incremental, disjunctive) #576)
  • Variable names as link coords — the _pwl_var dimension shows power, fuel, heat instead of 0, 1, 2
  • Clean operator overloadsVariable.__le__, __ge__, __eq__ simplified (no more piecewise dispatch)

Design principles

  • API surface stays minimal: Model, Variables, Constraints, Expression — no new user-facing types
  • Piecewise is a construction layer that produces regular linopy objects
  • Equality linking (core formulation) is cleanly separated from inequality bounds (tangent lines)
  • tangent_lines creates zero variables — it returns a LinearExpression with one tangent line per segment

Notebook examples

# Example Feature demonstrated
1 Gas turbine SOS2 formulation
2 Coal plant Incremental formulation
3 Diesel generator Disjunctive (disconnected segments)
4 Concave bound tangent_lines for inequality
5 Slopes mode breakpoints(slopes=...)
6 Unit commitment active parameter
7 CHP plant N-variable linking (3 vars)
8 Generator fleet Per-entity breakpoints

Other API designs considered

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

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

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

B) Dict of expressions + shared breakpoints DataArray

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

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

C) Keyword-only 2-variable + dict N-variable (two separate forms)

m.add_piecewise_constraints(x=power, y=fuel, x_points=xp, y_points=yp)
m.add_piecewise_constraints(exprs={...}, breakpoints=bp)

Considered: Clear but maintains two separate code paths. The 2-var form doesn't generalize.

D) List of expressions + breakpoints array (name-matching)

m.add_piecewise_constraints(expressions=[power, fuel], breakpoints=bp)

Rejected: Magic name matching — breaks if variable names don't match breakpoint coordinates.

E) Chosen: Tuple-based

m.add_piecewise_constraints(
    (power, [0, 30, 60, 100]),
    (fuel,  [0, 40, 85, 160]),
)

Selected: Each pair collocates expression + breakpoints. No string keys, no separate dict, no name matching. 2-var and N-var are the same pattern. Minimal, explicit, composable.

Test plan

  • All 107 piecewise tests pass
  • Full test suite passes
  • Notebook executes cleanly
  • Ruff lint + format clean
  • Review with @FabianHofmann and @coroa

🤖 Generated with Claude Code

FBumann and others added 22 commits April 1, 2026 08:36
…on layer

Remove PiecewiseExpression, PiecewiseConstraintDescriptor, and the
piecewise() function. Replace with an overloaded add_piecewise_constraints()
that supports both a 2-variable positional API and an N-variable dict API
for linking 3+ expressions through shared lambda weights.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change add_piecewise_constraints() to use keyword-only parameters
(x=, y=, x_points=, y_points=) instead of positional args.  Add
detailed docstring documenting the mathematical meaning of equality
vs inequality constraints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The N-variable path was not broadcasting breakpoints to cover extra
dimensions from the expressions (e.g. time), resulting in shared
lambda variables across timesteps.  Also simplify CHP example to
use breakpoints() factory and add plot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The plotting helper now accepts a single breakpoints DataArray with a
"var" dimension, supporting both 2-variable and N-variable examples.
Replaces the inline CHP plot with a single function call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the N-variable core formulation with shared lambda weights,
explain how the 2-variable case maps to it, and detail the inequality
case (auxiliary variable + bound).  Remove all references to the
removed piecewise() function and descriptor classes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add linopy.piecewise_envelope() as a standalone linearization utility
that returns tangent-line LinearExpressions — no auxiliary variables.
Users combine it with regular add_constraints for inequality bounds.

Remove sign parameter, LP method, convexity detection, and all
inequality logic from add_piecewise_constraints. The piecewise API
now only does equality linking (the core formulation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
More accurate name — the function computes tangent lines per segment,
not necessarily a convex/concave envelope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single function doesn't justify a separate module. tangent_lines
lives next to breakpoints() and segments() — all stateless helpers
for the piecewise workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add prominent section explaining the fundamental difference:
- add_piecewise_constraints: exact equality, needs aux variables
- tangent_lines: one-sided bounds, pure LP, no aux variables
- tangent_lines with == is infeasible (overconstrained)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace keyword-only (x=, y=, x_points=, y_points=) and dict-based
(exprs=, breakpoints=) forms with a single tuple-based API:

    m.add_piecewise_constraints(
        (power, [0, 30, 60, 100]),
        (fuel,  [0, 36, 84, 170]),
    )

2-var and N-var are the same pattern — no separate convenience API.
Internally stacks all breakpoints along a link dimension and uses
a unified formulation path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The _pwl_var dimension now shows variable names (e.g. "power", "fuel")
instead of generic indices ("0", "1"), making generated constraints
easier to debug and inspect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… notebook

The piecewise() function was removed but api.rst still referenced it.
Also replace xr.concat with breakpoints() in plot cells to avoid
pandas StringDtype compatibility issue on newer xarray.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Silences xarray FutureWarning about default coords kwarg changing.
No behavior change — we concatenate along new dimensions where
coord handling is irrelevant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Example 8 (fleet of generators with per-entity breakpoints) to
the notebook.  Also drop scalar coordinates from breakpoints before
stacking to handle bp.sel(var="power") without MergeError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The descriptor API was never released, so for users this is all new.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@FBumann FBumann marked this pull request as ready for review April 1, 2026 11:53
@FBumann FBumann changed the title Refactor piecewise API: stateless construction layer + tangent_lines utility refac: Refactor piecewise API to be a stateless construction layer + tangent_lines utility Apr 1, 2026
@FBumann FBumann changed the title refac: Refactor piecewise API to be a stateless construction layer + tangent_lines utility refac: Refactor piecewise API to a stateless construction layer + tangent_lines utility Apr 1, 2026
@FBumann FBumann requested a review from FabianHofmann April 1, 2026 11:54
FBumann and others added 3 commits April 1, 2026 14:03
Reorder: Quick Start -> API -> When to Use What -> Breakpoint
Construction -> Formulation Methods -> Advanced Features.
Add per-entity, slopes, and N-variable examples. Deduplicate
code samples. Fold generated-variables tables into compact lists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 1, 2026

@FabianHofmann This is pretty urgent to me, as the current piecewise API is already on master and should NOT be included in the next release in my opinion.
But i understand that the CSR PR is more important atm.

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

FBumann and others added 3 commits April 1, 2026 14:32
…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: 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>
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 1, 2026

edit: did some refactoring anyway, grouped in #641

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

FBumann commented Apr 1, 2026

N-variable disjunctive + follow-up notes

Done: Refactored _add_disjunctive to use the same stacked N-variable pattern as _add_continuous. The 2-variable restriction is removed — disjunctive now works with any number of (expression, breakpoints) pairs, using a single _link constraint with a _pwl_var dimension (same as continuous SOS2/incremental).

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 1, 2026

Additional follow-ups

Single-variable support: All formulation methods (SOS2, incremental, disjunctive) should support a single (expression, breakpoints) pair. Use case: constraining a variable to lie on a set of discrete segments or breakpoints without linking to another variable. The stacking logic should handle len(pairs) == 1 naturally — just relax the len(pairs) < 2 check.

Segments with 2+ breakpoints: Currently segments() only supports pairs of points (start, end) per segment. It should support segments with 2 or more breakpoints, enabling piecewise-within-segment curves (e.g. a nonlinear segment approximated by multiple linear pieces within each disconnected region. Currently, a user could express this with "touching" segments, but its mathematically less efficient).

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 2, 2026

Comparison with JuMP's piecewise linear formulations

JuMP's approach (via PiecewiseLinearOpt.jl)

JuMP's core piecewiselinear() returns a single variable representing the PWL output. All auxiliary variables/constraints are added internally. The package offers 8 formulation methods that all share the same convex-combination (λ) core — they differ only in how SOS2 adjacency is enforced:

Method Binary vars Notes linopy
:SOS2 0 Delegates to solver's native SOS2 branching _add_sos2
:CC (Convex Combination) O(n) Same as SOS2 but adjacency enforced with explicit binaries instead of SOS2 set ✅ same formulation as _add_sos2but with our internal sos_reformulation
:MC (Multiple Choice) O(n) Disaggregated segment copies with binary selection ✅ essentially _add_disjunctive — and we extend it with gap support via segments()
:Incremental O(n) Delta chaining _add_incremental
:Logarithmic (default) O(log n) Gray code encoding ❌ not yet
:DisaggLogarithmic O(log n) Disaggregated + Gray codes ❌ not yet
:ZigZag O(log n) Zig-zag codes ❌ not yet
:ZigZagInteger 1 general int Most compact ❌ not yet

The default is :Logarithmicceil(log2(n-1)) binary variables, significantly more efficient for branch-and-bound with many breakpoints.

JuMP also supports bivariate PWL via triangulation (UnionJack, K1, Stencil, BestFit patterns) for z = f(x, y) with independent inputs.

Reference: Huchette & Vielma, "Nonconvex piecewise linear functions: Advanced formulations and simple modeling tools" (2017).

Two separate concerns: piecewise structure vs. SOS2 enforcement

JuMP mixes these into a single method parameter. We can do better by separating them:

Piecewise formulation structure (what add_piecewise_formulation handles):

Method What it does linopy
SOS2 λ weights + sum=1 + linking constraints. Delegates adjacency enforcement to add_sos_constraints. _add_sos2
Incremental Completely different structure — δ deltas + binary chaining. No λ, no SOS2 at all. _add_incremental
Disjunctive Per-segment λ + binary segment selection. Uses SOS2 within each segment, but segment decomposition is piecewise logic. _add_disjunctive

SOS2 adjacency enforcement (what add_sos_constraints / sos_reformulation.py handles — orthogonal to piecewise):

Method Binary vars Notes linopy
Native 0 Solver handles branching add_sos_constraints
CC / Big-M O(n) x[i] <= M[i] * (z[i-1] + z[i]) sos_reformulation.py (auto via reformulate_sos=True)
Logarithmic (Gray code) O(log n) Best trade-off for many breakpoints ❌ not yet, high value follow op ⭐
DisaggLogarithmic O(log n) Disaggregated + Gray codes ❌ not yet
ZigZag O(log n) Different code pattern, similar performance ❌ not yet
ZigZagInteger 1 general int Most compact ❌ not yet

This separation means:

  • Piecewise stays simple: 3 structural methods, already complete
  • SOS2 reformulations benefit all SOS constraints in the model, not just piecewise
  • The two choices compose: e.g. disjunctive piecewise + logarithmic SOS2 enforcement

JuMP bundles all 8 as piecewise methods. We separate structure from enforcement — cleaner architecture, broader applicability.

What linopy has on top of JuMP

Feature Description
Disconnected segments segments() factory + _add_disjunctive for forbidden operating zones with gaps (e.g. off or 50–80 MW). JuMP's MC handles contiguous segments only.
N-variable linking Link 3+ expressions through shared λ weights (CHP: power/fuel/heat). JuMP's piecewiselinear() is 1→1 only.
active parameter (unit commitment) Binary variable gates the entire PWL — when active=0, all aux variables are zero. In JuMP this requires manual formulation.
tangent_lines() utility Pure LP inequality bounds (no auxiliary variables). Returns a LinearExpression per segment for use with regular add_constraints.
Per-entity breakpoints Different curves per generator via breakpoints({"gas": [...], "coal": [...]}, dim="gen") with automatic xarray broadcasting.
Auto method selection method="auto" detects monotonicity and chooses incremental (faster) vs SOS2 (flexible). JuMP requires explicit method choice.

Why our architecture doesn't block any of these

The design in this PR is method-agnostic by construction:

  1. The dispatch is a simple if/else on a string in _add_continuous. The 3 structural methods are already complete.

  2. All formulation methods reduce to the same primitives: add_variables(continuous/binary/integer) + add_constraints + optionally add_sos_constraints. Every JuMP method uses exactly these building blocks.

  3. Result tracking is formulation-blind: PiecewiseFormulation snapshots which variable/constraint names were added via before/after diffing. It doesn't care how they were structured — any new method's artifacts get captured automatically.

  4. SOS2 reformulations are additive: Adding logarithmic/zigzag to sos_reformulation.py requires zero changes to the piecewise layer — it just gets faster SOS2 enforcement for free.

  5. No formulation-specific assumptions leak into the public API. The user writes method="sos2" and gets back the same PiecewiseFormulation with .variables and .constraints, regardless of how SOS2 is enforced.

Our N-variable linking vs. JuMP's triangulation

These solve different problems (complementary, not competing):

Our N-variable (1D curve) Triangulation (2D+ mesh)
Inputs 1 hidden parameter (e.g. load) 2+ independent variables (x, y)
Breakpoints 1D sequence 2D grid, triangulated into simplices
λ weights 2 adjacent points (SOS2 along 1 axis) 3 vertices of one triangle
Degrees of freedom 1 2+
Use case Coupled outputs of one parameter (CHP, efficiency curves) z = f(x, y) with independent inputs

Our 3-variable (power, fuel, heat) links three outputs coupled by a single operating parameter — you can't set power=60 and heat=25 independently; the curve determines heat given power. Triangulation handles z = f(x, y) where x and y move independently (e.g., cost as a function of temperature AND load).

Follow up: Triangulation

Triangulation is mathematically quite a bit more complex and needs different data. It takes a mesh instead of a 1D breakpoint sequence. (mesh). Its clearly a follow up, probably as a new Model.add_*()* method that can reuse the PiecewiseFormulation return type, name tracking, and model primitives.

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.

1 participant