Skip to content

feat: PiecewiseFormulation return type, model repr, rename to add_piecewise_formulation#642

Draft
FBumann wants to merge 16 commits intoPyPSA:feat/piecewise-api-refactorfrom
FBumann:feat/piecewise-formulation-result
Draft

feat: PiecewiseFormulation return type, model repr, rename to add_piecewise_formulation#642
FBumann wants to merge 16 commits intoPyPSA:feat/piecewise-api-refactorfrom
FBumann:feat/piecewise-formulation-result

Conversation

@FBumann
Copy link
Copy Markdown
Collaborator

@FBumann FBumann commented Apr 1, 2026

I thought a bit about user ergonomics after creation for the piecewise stuff. I would like to keep the improvements surgical and only for inspecting, not for storage or behaviour. I decided that adding some sort of filtering in __repr__ methods and a basic registry on Model is probably the most surgical solution. I also added a dataclass PiecewiseFormulation as a return time for the renamed add_piecewise_formulation(), which makes it more prominent whats actually happening: Adding a complex formulation, not a regular constraint!.

This allows the user to inspect the created variables and constraints through a higher level object/view. This also feels quite usable for other Formulation concepts, like the existing internal SOS-Reformulations or others to come. One issue i see is that we categorize variables/constraints into 2 levels - the user defined one and the one created internally through Formulations. I like the concept and dont think there is anything wrong with it, but i think its an important sidenote.

Happy to hear your thoughts on it!

Summary

  • Add PiecewiseFormulation class as return type of add_piecewise_formulation() (renamed from add_piecewise_constraints)
  • Groups all auxiliary variables and constraints; variables/constraints properties return live views
  • Add _piecewise_formulations registry on Model with IO persistence (netcdf round-trip)
  • Model repr hides piecewise artifacts and shows a dedicated "Piecewise Formulations" section
  • Notebook updated to show reprs (last statement = add_piecewise_formulation)
  • Export PiecewiseFormulation from linopy

PiecewiseFormulation repr:

PiecewiseFormulation `pwl` [time: 3, gen: 2] — incremental
  Variables:
    * pwl_delta (time, gen, _breakpoint_seg)
    * pwl_order_binary (time, gen, _breakpoint_seg)
  Constraints:
    * pwl_delta_bound (time, gen, _breakpoint_seg)
    * pwl_fill_order (time, gen, _breakpoint_seg)
    * pwl_binary_order (time, gen, _breakpoint_seg)
    * pwl_link (_breakpoint, _pwl_var, gen, time)

Model repr:

Variables:
 * power (gen, time)
 * fuel (gen, time)

Piecewise Formulations:
 * pwl (time, gen) — incremental, 2 vars, 4 cons

Access:

pwl = m.add_piecewise_formulation(
    (power, [0, 30, 60, 100]),
    (fuel,  [0, 36, 84, 170]),
)
pwl.variables            # Variables view
pwl.constraints          # Constraints view
pwl.variables['pwl_delta']  # individual Variable
pwl.method               # "incremental" or "sos2"

Suffix renames

Internal PWL suffix constants renamed for clarity:

Old New What it does
_x_link / _y_link _link expr = weighted sum (N-var, single constraint)
_binary _segment_binary disjunctive segment selection
_inc_binary _order_binary incremental fill-ordering binary
_inc_link _delta_bound δ ≤ binary
_inc_order _binary_order binary_{i+1} ≤ δ_i
_fill _fill_order δ_{i+1} ≤ δ_i

Unchanged: _lambda, _convex, _delta, _select, _active_bound

Test plan

  • pytest test/test_piecewise_constraints.py — 108 passed
  • pytest test/test_model.py — 18 passed
  • mypy . --exclude build — clean
  • ruff check — clean

🤖 Generated with Claude Code

@FBumann FBumann changed the title feat: PiecewiseFormulation return type and model grouping refac: PiecewiseFormulation return type and model grouping Apr 1, 2026
@FBumann FBumann marked this pull request as draft April 1, 2026 17:24
@FBumann FBumann force-pushed the feat/piecewise-formulation-result branch 8 times, most recently from 7782b04 to 648bf24 Compare April 1, 2026 17:50
@FBumann FBumann changed the title refac: PiecewiseFormulation return type and model grouping feat: PiecewiseFormulation return type, model repr, rename to add_piecewise_formulation Apr 1, 2026
@FBumann FBumann force-pushed the feat/piecewise-formulation-result branch 2 times, most recently from e953159 to 8de5277 Compare April 1, 2026 18:14
FBumann and others added 14 commits April 1, 2026 20:25
…iecewise_formulation

- Add PiecewiseFormulation dataclass grouping all auxiliary variables
  and constraints created by a piecewise formulation
- Add _groups registry on Model to track grouped artifacts
- Model repr hides grouped items from Variables/Constraints sections
  and shows them in a new "Groups" section
- Rename add_piecewise_constraints -> add_piecewise_formulation
- Export PiecewiseFormulation from linopy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reorder cells so add_piecewise_formulation is the last statement,
letting Jupyter display the PiecewiseFormulation repr automatically.
Add print(m) cell to show the grouped model repr.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split tangent_lines cell so its LinearExpression repr is displayed.

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

PiecewiseFormulation now shows full dims (including internal) for each
variable and constraint. Model groups section shows "over (dim1, dim2)"
for user-facing dims only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Match style of Variables/Constraints containers which don't show counts.

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

Replace generic "Groups" with "Piecewise Formulations" in Model repr.
Rename internal registry and helper to match.

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>
Match Constraint repr pattern: `name` instead of 'name'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Match Constraint repr style: `name` [dim: size, ...] — method

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove jetTransient metadata and normalize cell format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PiecewiseFormulation now stores variable/constraint names as strings
with a model reference. Properties return live Views on access.
This makes serialization trivial — persist as JSON in netcdf attrs,
reconstruct on load.

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 force-pushed the feat/piecewise-formulation-result branch from dd60a4a to 0b482ee Compare April 1, 2026 18:26
- PWL_X_LINK_SUFFIX/_Y_LINK_SUFFIX → PWL_LINK_SUFFIX (N-var, single link)
- PWL_BINARY_SUFFIX → PWL_SEGMENT_BINARY_SUFFIX (disjunctive segment selection)
- PWL_INC_BINARY_SUFFIX → PWL_ORDER_BINARY_SUFFIX (incremental ordering)
- PWL_INC_LINK_SUFFIX → PWL_DELTA_BOUND_SUFFIX (δ ≤ binary)
- PWL_INC_ORDER_SUFFIX → PWL_BINARY_ORDER_SUFFIX (binary_{i+1} ≤ δ_i)
- PWL_FILL_SUFFIX → PWL_FILL_ORDER_SUFFIX (δ_{i+1} ≤ δ_i)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@FBumann FBumann force-pushed the feat/piecewise-formulation-result branch from e2e1a63 to 18af4d7 Compare April 1, 2026 18:45
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 2, 2026

More thoughts about the Formulation concept

On the Formulation concept

A Formulation in linopy is a modeling-layer concept: it decomposes a high-level mathematical relationship into standard variables and constraints. The solver layer never sees the formulation — it only sees the flat collection of variables, constraints, and objectives it already knows how to handle.

This is intentional. Solvers don't speak "formulations" — they speak variables, constraints, bounds. Even Gurobi's addGenConstrPWL is just a convenience that internally creates the same underlying structures. By decomposing once at the modeling layer, every solver backend works automatically without per-solver formulation code.

It also means formulations are composable: piecewise with active=commit works because commit is just a regular variable. And formulations are inspectable: users can examine the generated constraints, modify bounds, or add extra constraints on top.

Key design point: Formulation classes are never constructed by the user. They are result views returned by model.add_*() methods. The user calls model.add_piecewise_formulation(...) and gets back a PiecewiseFormulation — a lightweight object holding names and a model reference. All the actual work (creating variables, constraints, registering in the model) happens inside the add_* method. The formulation object is purely for inspection and access.

This pattern could generalize naturally to other model.add_*() methods that create auxiliary variables + constraints from a higher-level intent: absolute value, min/max, indicator constraints, etc. The question for each would be whether the complexity justifies a named Formulation return type, or whether a simpler return suffices. Piecewise clearly earns it (5+ aux vars/cons per call). Something like abs(x) (1 var, 2 cons) might not.

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