Skip to content

refactor(piecewise): per-tuple sign + categorized internal flow#664

Merged
FBumann merged 4 commits intofeat/piecewise-api-refactorfrom
feat/piecewise-per-tuple-sign
Apr 26, 2026
Merged

refactor(piecewise): per-tuple sign + categorized internal flow#664
FBumann merged 4 commits intofeat/piecewise-api-refactorfrom
feat/piecewise-per-tuple-sign

Conversation

@FBumann
Copy link
Copy Markdown
Collaborator

@FBumann FBumann commented Apr 26, 2026

Refactor of the unreleased add_piecewise_formulation API on feat/piecewise-api-refactor. Targets the in-progress branch, not master; no external users yet.

What changes

  • Per-tuple sign. Move the formulation-level sign= keyword onto the tuple it applies to: (y, y_pts, \"<=\") replaces sign=\"<=\". Each tuple carries an optional 3rd element (default \"==\"). The bounded tuple no longer has a special position — user tuple order is preserved end-to-end.
  • Validation. At most one tuple may carry a non-equality sign; 3+ tuples must all be \"==\". The N-input bounded case is rejected with an error message that invites a GitHub issue rather than promising a specific future API shape.
  • Categorized internal flow. New _PwlInputs dataclass carries categorized inputs (bounded_* vs pinned_*) through the dispatch chain (_build_links, _try_lp, _lp_eligibility, _add_continuous, _add_disjunctive). The "first tuple is special" positional convention is gone — every consumer reads the role explicitly.
  • Docs/notebooks. Rewrite the user-facing docs and example notebooks to leverage the new notation. Drop the "first-tuple convention" / "N−1 jointly pinned" scaffolding that existed only to teach the old positional rule. Restrictions reframed as invitations.

Before / after

# before
m.add_piecewise_formulation((y, y_pts), (x, x_pts), sign=\"<=\")

# after
m.add_piecewise_formulation((y, y_pts, \"<=\"), (x, x_pts))

The old sign= keyword now raises a TypeError with a one-line pointer to the new shape — convenient while updating the in-repo callers, not a migration aid for external users.

Future work this enables

The tuple shape is now the right primitive across three distinct piecewise regimes — same (expr, breaks, [sign]) call, no public-API redesign needed for any of them:

Regime Tuple form What's needed beyond this PR
2-var equality / inequality (y, y_pts), (x, x_pts) ± sign Nothing — current behaviour
3+ var equality (e.g. CHP) (power, p_pts), (fuel, f_pts), (heat, h_pts) Nothing — current behaviour
2-D triangulated z ≥ f(x, y) (z, z_pts, \">=\"), (x, x_pts), (y, y_pts), triangulation=tris Sibling triangulation= kwarg + new builders

For the 2-D case specifically: vertex coordinates stay as plain 1-D (_vertex,) DataArrays on each tuple — they survive slicing, broadcast, and use the existing coercion/masking machinery. The simplex adjacency rides along as one shared sibling kwarg, not embedded in the breakpoint slices (which would force fragile attrs or a non-DataArray wrapper class). Internally, _PwlInputs would grow a single optional field triangulation: DataArray | None; the dispatcher branches on it; the formulation builders for the triangulated path are new but consume the same input carrier.

The N≥3-inequality slot we reserved at the parser is what makes this fit naturally — (z, z_pts, \">=\"), (x, x_pts), (y, y_pts) reads as z ≥ f(x, y) without contortion, and the N≥3 + non-equality validation can loosen to allow it specifically when triangulation is set.

Other follow-ups this also unlocks

  • Friendly 2-arg alias (add_piecewise(y, x, x_pts, y_pts, sign=\"<=\")) — routes cleanly because role-categorization is explicit.
  • Multi-bounded relations (if a use case appears) — the "at most one bounded tuple" check is a single line; the dispatch chain already handles per-tuple roles.
  • API stabilization (drop EvolvingAPIWarning) — the shape we'd commit to is now structurally honest, with no positional convention to teach or maintain in docs.

Test plan

  • pytest test/test_piecewise_constraints.py test/test_piecewise_feasibility.py — 518 passed
  • pytest test/ --ignore=test/remote --ignore=test/test_optimization.py --ignore=test/test_solvers.py — 1578 passed
  • ruff check + ruff format --check — clean
  • mypy linopy/piecewise.py — clean
  • Both example notebooks execute end-to-end against Gurobi

🤖 Generated with Claude Code

FBumann and others added 4 commits April 26, 2026 18:40
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>
…s 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>
@FBumann FBumann merged commit a3e95f6 into feat/piecewise-api-refactor Apr 26, 2026
2 checks passed
@FBumann FBumann deleted the feat/piecewise-per-tuple-sign branch April 26, 2026 17:53
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