Skip to content

feat: add m.dualize() and m.bounds_to_constraints() for LP dualization#626

Open
bobbyxng wants to merge 20 commits intomasterfrom
dual
Open

feat: add m.dualize() and m.bounds_to_constraints() for LP dualization#626
bobbyxng wants to merge 20 commits intomasterfrom
dual

Conversation

@bobbyxng
Copy link

Changes proposed in this Pull Request

  • Adds Model.dualize(), a method that constructs the LP dual of a linopy model, and Model.bounds_to_constraints(), a preprocessing step that converts variable bounds to explicit constraints so they are correctly reflected in the dual.
  • The dual is constructed following standard LP duality theory. For a primal minimization problem, the dual is a maximization problem with one dual variable per primal constraint. Variable bounds are converted to explicit constraints before dualization via bounds_to_constraints(), so that they appear in the constraint matrix and are correctly reflected in the dual.
  • Signs follows linopy's dual convention, allowing direct comparison between m2.variables[con_name].solution and m1.constraints[con_name].dual without sign adjustments.
  • Building on PR feat: add m.copy() method to create deep copy of model #623 which adds m.copy()

The motivation behind this was an automated LP dualizer (originally developed for adaptive robust optimization for energy system planning) which requires constructing an independent dual model without modifying the original. While similar implementations exist in other modelling frameworks, notably JuMP's Dualization.jl https://jump.dev/JuMP.jl/stable/packages/Dualization/, this feature was still missing in linopy.

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.

@bobbyxng bobbyxng requested review from FabianHofmann and Irieo March 18, 2026 14:43
@bobbyxng bobbyxng self-assigned this Mar 18, 2026
@bobbyxng bobbyxng added enhancement New feature or request help wanted Extra attention is needed model formulation discussion labels Mar 18, 2026
@bobbyxng
Copy link
Author

Here's a successful dualization tested on a 50-node PyPSA network
Notice the small deviations between primal and dual solution, given the nature/dual degeneracy of such energy system problems.

Primal objective: 95047351841.07184
Dual objective: 95047351841.07161
Abs. gap: 0.0002288818359375
Rel. gap: 2.4080821980207514e-15
image

Solution Generator-p
image

Solution StorageUnit-state_of_charge
image

@brynpickering brynpickering self-requested a review March 20, 2026 12:39
Copy link

@brynpickering brynpickering left a comment

Choose a reason for hiding this comment

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

@bobbyxng I can't comment on the actual dualisation so I'm focussing on cleanliness and efficiency of code. I've opened another PR (#629) in which I've made some suggestions (I started writing them in comments but they became a bit too verbose).

I feel like it shouldn't be necessary to have your lookup dictionary or rule and that you should be able to store your coefficients your mapping from constraint name to dual variables. I haven't investigated it in detail but the linear expression rule is probably reasonably slow to build compared to a vectorised array operation. I tried with a dummy model with 10000 timesteps and 2 active spatial nodes and it took 20 seconds to build. I can see dualisation therefore exploding on practical models, so more vectorisation is probably necessary.

You still also need:

  • unit tests
  • docs

BTW, for ease of review, it would be easier if you opened this PR with the copy branch as the base, rather than master. It will automatically revert to master when copy is merged.

logger.debug(f"Constraint '{name}' is fully masked, skipping.")
continue

if sign_vals[0] == "=":

Choose a reason for hiding this comment

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

Is it possible that a constraint has different signs for different array elements? If yes, you would need to use a ufunc to map the signs to lower and upper bound arrays of the same shape as con.labels, rather than just taking the first value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

discussion enhancement New feature or request help wanted Extra attention is needed model formulation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants