You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add sos reformulations into linopy to simplify adoption of new sos features (#549)
* The SOS constraint reformulation feature has been implemented successfully. Here's a summary:
Implementation Summary
New File: linopy/sos_reformulation.py
Core reformulation functions:
- validate_bounds_for_reformulation() - Validates that variables have finite bounds
- compute_big_m_values() - Computes Big-M values from variable bounds
- reformulate_sos1() - Reformulates SOS1 constraints using binary indicators and Big-M constraints
- reformulate_sos2() - Reformulates SOS2 constraints using segment indicators and adjacency constraints
- reformulate_all_sos() - Reformulates all SOS constraints in a model
Modified: linopy/model.py
- Added import for reformulate_all_sos
- Added reformulate_sos_constraints() method to Model class
- Added reformulate_sos: bool = False parameter to solve() method
- Updated SOS constraint check to automatically reformulate when reformulate_sos=True and solver doesn't support SOS natively
New Test File: test/test_sos_reformulation.py
36 comprehensive tests covering:
- Bound validation (finite/infinite)
- Big-M computation
- SOS1 reformulation (basic, negative bounds, multi-dimensional)
- SOS2 reformulation (basic, trivial cases, adjacency)
- Integration with solve() and HiGHS
- Equivalence with native Gurobi SOS support
- Edge cases (zero bounds, multiple SOS, custom prefix)
Usage Example
m = linopy.Model()
x = m.add_variables(lower=0, upper=1, coords=[pd.Index([0, 1, 2], name='i')], name='x')
m.add_sos_constraints(x, sos_type=1, sos_dim='i')
m.add_objective(x.sum(), sense='max')
# Works with HiGHS (which doesn't support SOS natively)
m.solve(solver_name='highs', reformulate_sos=True)
* Documentation Summary
New Section: "SOS Reformulation for Unsupported Solvers"
Added a comprehensive section (~300 lines) covering:
1. Enabling Reformulation - Shows reformulate_sos=True parameter and manual reformulate_sos_constraints() method
2. Requirements - Explains finite bounds requirement for Big-M method
3. Mathematical Formulation - Clear LaTeX math for both:
- SOS1: Binary indicators y_i, upper/lower linking constraints, cardinality constraint
- SOS2: Segment indicators z_j, first/middle/last element constraints, cardinality constraint
4. Interpretation - Explains how the constraints work intuitively with examples
5. Auxiliary Variables and Constraints - Documents the naming convention (_sos_reform_ prefix)
6. Multi-dimensional Variables - Shows how broadcasting works
7. Edge Cases Table - Lists all handled edge cases (single-element, zero bounds, all-positive, etc.)
8. Performance Considerations - Trade-offs between native SOS and reformulation
9. Complete Example - Piecewise linear approximation of x² with HiGHS
10. API Reference - Added method signatures for:
- Model.add_sos_constraints()
- Model.remove_sos_constraints()
- Model.reformulate_sos_constraints()
- Variables.sos property
* Added Tests for Multi-dimensional SOS
Unit Tests
- test_sos2_multidimensional: Tests that SOS2 reformulation with multi-dimensional variables (i, j) correctly creates:
- Segment indicators z with shape (i: n-1, j: m)
- Cardinality constraint preserves the j dimension
Integration Tests
- test_multidimensional_sos2_with_highs: Solves a multi-dimensional SOS2 problem with HiGHS and verifies:
- Optimal objective value (4 total - two adjacent non-zeros per column)
- SOS2 constraint satisfied for each j: at most 2 non-zeros, and if 2, they're adjacent
Test Results
test_sos1_multidimensional PASSED
test_sos2_multidimensional PASSED
test_multidimensional_sos1_with_highs PASSED
test_multidimensional_sos2_with_highs PASSED
The implementation correctly handles multi-dimensional variables by leveraging xarray's broadcasting - the SOS constraint is applied along the sos_dim for each combination of
the other dimensions.
* Add custom big_m parameter for SOS reformulation
Allow users to specify custom Big-M values in add_sos_constraints() for
tighter LP relaxations when variable bounds are conservative.
- Add big_m parameter: scalar or tuple(upper, lower)
- Store as variable attrs (big_m_upper, big_m_lower)
- Skip bound validation when custom big_m provided
- Scalar-only design ensures NetCDF persistence works correctly
For per-element Big-M values, users should adjust variable bounds directly.
* Add custom big_m parameter for SOS reformulation
Allow users to specify custom Big-M values in add_sos_constraints() for
tighter LP relaxations when variable bounds are conservative.
- Add big_m parameter: scalar or tuple(upper, lower)
- Store as variable attrs (big_m_upper, big_m_lower) for NetCDF persistence
- Use tighter of big_m and variable bounds: min() for upper, max() for lower
- Skip bound validation when custom big_m provided (allows infinite bounds)
Scalar-only design ensures NetCDF persistence works correctly. For
per-element Big-M values, users should adjust variable bounds directly.
* Simplification summary:
┌──────────────────────┬───────────┬───────────┬───────────┐
│ File │ Before │ After │ Reduction │
├──────────────────────┼───────────┼───────────┼───────────┤
│ sos_reformulation.py │ 377 lines │ 223 lines │ 41% │
├──────────────────────┼───────────┼───────────┼───────────┤
│ sos-constraints.rst │ 647 lines │ 164 lines │ 75% │
└──────────────────────┴───────────┴───────────┴───────────┘
Code changes:
- Merged validate_bounds_for_reformulation into compute_big_m_values
- Factored out add_linking_constraints helper in SOS2
- Used np.minimum/np.maximum instead of xr.where
- Kept proper docstrings with Parameters/Returns sections
Doc changes:
- Removed: Variable Representation, LP File Export, Common Patterns, Performance Considerations
- Trimmed: Examples to one each, Mathematical formulation to equations only
- Condensed: API reference, multi-dimensional explanation
* Revert some docs changes to be more surgical
* Add math to docs
* Improve docs
* Code simplifications:
1. sos_reformulation.py (230 → 203 lines):
- compute_big_m_values now returns single DataArray (not tuple)
- Removed all lower bound handling - only supports non-negative variables
- Removed add_linking_constraints helper function
- Simplified SOS1/SOS2 to only add upper linking constraints
2. model.py:
- Simplified big_m parameter from float | tuple[float, float] | None to float | None
- Removed big_m_lower attribute handling
3. Documentation (sos-constraints.rst):
- Updated big_m type signature
- Removed asymmetric Big-M example
- Added explicit requirement that variables must have non-negative lower bounds
4. Tests (46 → 38 tests):
- Removed tests for negative bounds
- Removed tests for tuple big_m
- Added tests for negative lower bound validation error
Rationale: The mathematical formulation in the docs assumes x ∈ ℝⁿ₊ (non-negative reals). This matches 99%+ of SOS use cases (selection indicators, piecewise linear weights).
The simplified code is now consistent with the documented formulation.
* Fix mypy
* Fix mypy
* Add constants for sos attr keys
* Add release notes
* Fix SOS reformulation: undo after solve, validate big_m, vectorize
- solve() now undoes SOS reformulation after solving, preserving model state
- Validate big_m > 0 in add_sos_constraints (fail fast)
- Vectorize SOS2 middle constraints, eliminate duplicate compute_big_m_values
- Warn when reformulate_sos=True is ignored for SOS-capable solvers
- Add tests for model immutability, double solve, big_m validation, undo
* tiny refac, plus uncovered test
* refac: move reformulating function to module
* Fix SOS reformulation: rollback, skipped attrs, undo in solve, sort coords
- Remove SOS attrs for skipped variables (size<=1, M==0) so solvers
don't see them as SOS constraints
- Wrap reformulation loop in try/except for transactional rollback
- Move undo into finally block in Model.solve() for exception safety
- Sort variables by coord values before building adjacency constraints
to match native SOS weight-based ordering
* update release notes [skip ci]
---------
Co-authored-by: Fabian Hofmann <fab.hof@gmx.de>
0 commit comments