Skip to content

Conversation

@FBumann
Copy link
Contributor

@FBumann FBumann commented Nov 26, 2025

Closes #444

Changes proposed in this Pull Request

This PR adds comprehensive quadratic constraints support to linopy, enabling optimization problems of the form x'Qx + a'x ≤ b.

New Features

Core quadratic constraint infrastructure:

  • QuadraticConstraint class for storing and manipulating quadratic constraints (linopy/constraints.py)
  • QuadraticConstraints container class with dict-like access
  • Model.add_quadratic_constraints() method for creating constraints
  • Model.quadratic_constraints property for accessing constraints
  • Model.has_quadratic_constraints property for checking if model contains QCs

Solver support:

  • Gurobi direct API integration for quadratic constraints
  • MOSEK direct API integration for quadratic constraints (convex only)
  • Centralized solver capability checking via quadratic_constraint_solvers and nonconvex_quadratic_constraint_solvers

File I/O:

  • LP file export with QCMATRIX sections for quadratic constraints
  • MPS file export with quadratic constraints (via Gurobi)
  • NetCDF I/O for models with quadratic constraints

Matrix representations:

  • Quadratic constraint matrix accessors for solver interfaces
  • Integration with existing matrix infrastructure

Additional:

  • Dual value retrieval for quadratic constraints (convex problems)
  • Example notebook demonstrating quadratic constraint usage

Before Merging:

Remove dev-scripts

The dev-scripts/ directory contains planning documents that were committed but should be removed before merging (per CLAUDE.md, dev-scripts is for non-tracked
temporary code). Would you like me to create a commit removing those files?

Review and documentation

As this is quite a big feature, i would be happy if someone that understands the codebase more deeply than i do goes into detail and verifies my code, especially the matrix computation. Also, including examples and explanations into the docs would be appropriate

Solver capabilities

Im not entirely sure if SCIP supports MIQCQP. In my tests i got an LP read error.
Dual values can only be retrived for convex models. For gurobi, we need to set QCPDual=1. Other solvers might need other settings. this is not set automatically.

Follow Ups

  1. Constraint modification methods (modify_rhs, modify_coeffs) - Medium priority
  2. Convexity checking - Low priority
  3. Documentation - Low priority

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.

FBumann and others added 27 commits November 25, 2025 19:31
* Add highs validation for quadratic constraints

* Multi-dimensional quadratic constraints - Fixed LP file and Gurobi direct API export to handle constraints with coordinates
* Add highs validation for quadratic constraints

* Multi-dimensional quadratic constraints - Fixed LP file and Gurobi direct API export to handle constraints with coordinates

* Add quadratic constraints to netcdf serialization

* add test_netcdf_roundtrip_multidimensional
* Add highs validation for quadratic constraints

* Multi-dimensional quadratic constraints - Fixed LP file and Gurobi direct API export to handle constraints with coordinates

* Add quadratic constraints to netcdf serialization

* add test_netcdf_roundtrip_multidimensional

* Added the following properties to MatrixAccessor in matrices.py:

  | Property  | Description                                             |
  |-----------|---------------------------------------------------------|
  | qclabels  | Vector of labels of all quadratic constraints           |
  | qc_sense  | Vector of senses (<=, >=, =)                            |
  | qc_rhs    | Vector of right-hand-side values                        |
  | Qc        | List of sparse Q matrices (one per constraint)          |
  | qc_linear | Sparse matrix of linear coefficients (n_qcons × n_vars) |

  The Q matrices follow the convention x'Qx where:
  - Diagonal terms are doubled (for x²)
  - Off-diagonal terms are symmetric (for xy)

* Fix imports
* Add highs validation for quadratic constraints

* Multi-dimensional quadratic constraints - Fixed LP file and Gurobi direct API export to handle constraints with coordinates

* Add quadratic constraints to netcdf serialization

* add test_netcdf_roundtrip_multidimensional

* Added the following properties to MatrixAccessor in matrices.py:

  | Property  | Description                                             |
  |-----------|---------------------------------------------------------|
  | qclabels  | Vector of labels of all quadratic constraints           |
  | qc_sense  | Vector of senses (<=, >=, =)                            |
  | qc_rhs    | Vector of right-hand-side values                        |
  | Qc        | List of sparse Q matrices (one per constraint)          |
  | qc_linear | Sparse matrix of linear coefficients (n_qcons × n_vars) |

  The Q matrices follow the convention x'Qx where:
  - Diagonal terms are doubled (for x²)
  - Off-diagonal terms are symmetric (for xy)

* Fix imports

* Add dual support
* Add highs validation for quadratic constraints

* Multi-dimensional quadratic constraints - Fixed LP file and Gurobi direct API export to handle constraints with coordinates

* Add quadratic constraints to netcdf serialization

* add test_netcdf_roundtrip_multidimensional

* Added the following properties to MatrixAccessor in matrices.py:

  | Property  | Description                                             |
  |-----------|---------------------------------------------------------|
  | qclabels  | Vector of labels of all quadratic constraints           |
  | qc_sense  | Vector of senses (<=, >=, =)                            |
  | qc_rhs    | Vector of right-hand-side values                        |
  | Qc        | List of sparse Q matrices (one per constraint)          |
  | qc_linear | Sparse matrix of linear coefficients (n_qcons × n_vars) |

  The Q matrices follow the convention x'Qx where:
  - Diagonal terms are doubled (for x²)
  - Off-diagonal terms are symmetric (for xy)

* Fix imports

* Add dual support

* Add Add mosek direct api

* qc_linear property now handles the case when there are no linear terms in the quadratic constraints (the vars column doesn't exist in
   that case)

* Remove separate quad checks

* Add tests for verification of solver
* Add highs validation for quadratic constraints

* Multi-dimensional quadratic constraints - Fixed LP file and Gurobi direct API export to handle constraints with coordinates

* Add quadratic constraints to netcdf serialization

* add test_netcdf_roundtrip_multidimensional

* Added the following properties to MatrixAccessor in matrices.py:

  | Property  | Description                                             |
  |-----------|---------------------------------------------------------|
  | qclabels  | Vector of labels of all quadratic constraints           |
  | qc_sense  | Vector of senses (<=, >=, =)                            |
  | qc_rhs    | Vector of right-hand-side values                        |
  | Qc        | List of sparse Q matrices (one per constraint)          |
  | qc_linear | Sparse matrix of linear coefficients (n_qcons × n_vars) |

  The Q matrices follow the convention x'Qx where:
  - Diagonal terms are doubled (for x²)
  - Off-diagonal terms are symmetric (for xy)

* Fix imports

* Add dual support

* Add Add mosek direct api

* qc_linear property now handles the case when there are no linear terms in the quadratic constraints (the vars column doesn't exist in
   that case)

* 1. generatevarnames: Changed np.arange(0, len(labels)) to np.arange(0, len(labels), dtype=np.int32) - MOSEK expects 32-bit integers for indices
  2. putarowslice: Added explicit type casts:
    - indptr → np.int64 (pointer arrays)
    - indices → np.int32 (column indices)
    - data → np.float64 (values)
  - <= constraints: Changed bl=0.0 to bl=-np.inf (inactive bound should be -∞)
  - >= constraints: Changed bu=0.0 to bu=np.inf (inactive bound should be +∞)
* Add defensive validation to new quadratic constraint code:
  1. linopy/matrices.py:185-238 - Uniqueness check for QC labels

  Added seen_labels tracking in both qc_sense and qc_rhs properties with assertions to detect duplicate quadratic constraint labels across different constraint objects.

  2. linopy/matrices.py:241-305 - Document Q-matrix assembly assumptions

  Extended the docstring for Qc property to document the assumption that flat_qcons stores each quadratic term exactly once, and the symmetrization logic applied.

  3. linopy/matrices.py:307-357 - Debug assertions for label consistency

  Added a skipped_rows counter in qc_linear with an assertion that fails if any linear terms are skipped due to missing variable or constraint labels.

  4. linopy/constants.py:203-213 - Expose qc_dual in Result repr

  Updated Result.__repr__ to show the count of qc_duals when present (e.g., "Solution: 2 primals, 1 duals, 3 qc_duals").

  5. examples/quadratic-constraints.ipynb - Strip notebook outputs

  Cleared all execution outputs and reset execution counts to None for all code cells.

  6. linopy/io.py:462-470 - Guard for missing label metadata

  Added an explicit check with a clear ValueError if a label from the flat representation is not found in the constraint metadata.

  Also fixed:

  - linopy/solvers.py:1213 - Removed the double assignment typo (solution = solution = ...).

* Add more tests

* Add section headers to lp file

* Adjust tests
* Add guard against mps export with quadratic constraints

* Add mps export support with gurobi

* Add roundtrip tests
…ict) type guards at lines 498-499 and 946-947
…ne and assert direct_obj is not None after all objective.value assignments to handle the Optional

  type
  1. linopy/expressions.py:1827 - Added missing from linopy import Model import to the doctest example in QuadraticExpression.to_constraint
@FabianHofmann
Copy link
Collaborator

wonderful initiative! let me know when I should take a first look

@FBumann
Copy link
Contributor Author

FBumann commented Nov 26, 2025

@FabianHofmann Its ready for an initial review. I would be happy if you give me some quick feedback on what functionality is missing (if some), so i wont get lost in details. I will revisit the new tests tomorrow.

@FBumann FBumann marked this pull request as ready for review November 26, 2025 10:46
@FBumann FBumann changed the title Quadratic constraints feat: Quadratic constraints Nov 26, 2025
@FBumann
Copy link
Contributor Author

FBumann commented Nov 26, 2025

From my analysis the QuadraticConstraint class is missing several wrapped xarray methods that Constraint has:

  • sel, isel, loc (for indexing)
  • where, fillna
  • rename, rename_dims, swap_dims, set_index
  • assign, assign_attrs, assign_coords
  • broadcast_like, chunk, drop_sel, drop_isel
  • expand_dims, shift, roll, reindex, reindex_like, stack

These are definitely a todo imo

FBumann and others added 8 commits November 26, 2025 15:26
  1. inequalities property - Returns a filtered QuadraticConstraints containing only inequality constraints (<= or >=)
  2. equalities property - Returns a filtered QuadraticConstraints containing only equality constraints (=)
  3. sanitize_zeros() method - Filters out terms with zero and close-to-zero coefficients from both quadratic and linear parts of constraints
  4. sanitize_missings() method - Sets constraint labels to -1 where all variables in both quadratic and linear parts are missing (-1)
  5. print_labels() method - Prints formatted representations of quadratic constraints by their labels, similar to the linear constraints version

  Added helper function (linopy/common.py):

  6. print_single_quadratic_constraint() - Formats a single quadratic constraint for display, handling both squared terms (x²) and cross-product terms (x·y)

  Added tests (test/test_quadratic_constraint.py):

  - TestQuadraticConstraintsContainerMethods class with 9 tests covering all new functionality

  These additions bring QuadraticConstraints closer to feature parity with the linear Constraints container.
…ations in test/test_quadratic_constraint.py:

Implemented QuadraticExpressionRolling and QuadraticExpressionGroupby
…d QuadraticExpression - both exclude internal dimensions (_term for linear, _term and

  _factor for quadratic)
@FBumann
Copy link
Contributor Author

FBumann commented Nov 26, 2025

From my analysis the QuadraticConstraint class is missing several wrapped xarray methods that Constraint has:

  • sel, isel, loc (for indexing)
  • where, fillna
  • rename, rename_dims, swap_dims, set_index
  • assign, assign_attrs, assign_coords
  • broadcast_like, chunk, drop_sel, drop_isel
  • expand_dims, shift, roll, reindex, reindex_like, stack

These are definitely a todo imo

resolved

@FBumann
Copy link
Contributor Author

FBumann commented Nov 26, 2025

@FabianHofmann My PR includes a "fix" regarding the expression.shape attribute. It excludes the _term dim. Please verify that this is actually correct. Else i need to adjust the quadratic_expression.shape

… LinearExpression

  2. QuadraticExpression now defines its own versions without overriding anything from BaseExpression
  3. No more Liskov substitution principle violations
  4. No # type: ignore hacks needed
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.

Quadratic Constraints and/or objective

2 participants