Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add native ComplexPSD cone for Hermitian PSD constraints#3128

Draft
PTNobel wants to merge 3 commits intomasterfrom
ptn/complex-psd
Draft

Add native ComplexPSD cone for Hermitian PSD constraints#3128
PTNobel wants to merge 3 commits intomasterfrom
ptn/complex-psd

Conversation

@PTNobel
Copy link
Collaborator

@PTNobel PTNobel commented Feb 8, 2026

Description

Add a native ComplexPSD cone type to CVXPY, leveraging SCS's native Hermitian PSD cone ('cs' key). For solvers without native support, ExactCone2Cone converts ComplexPSD to the 2n×2n real block matrix PSD formulation.

Instead of expanding complex PSD constraints into 2n×2n real block matrices during Complex2Real, this introduces a dedicated ComplexPSD cone that preserves the n×n Hermitian structure through the reduction chain. SCS handles it natively via the 'cs' cone key with cvec format; Clarabel and other solvers fall back through ExactCone2Cone.

Closes #3045

Type of change

  • New feature (backwards compatible)
  • New feature (breaking API changes)
  • Bug fix
  • Other (Documentation, CI, ...)

Contribution checklist

  • Add our license to new files.
  • Check that your code adheres to our coding style.
  • Write unittests.
  • Run the unittests and check that they're passing.
  • Run the benchmarks to make sure your change doesn't introduce a regression.

Instead of expanding complex PSD constraints into 2n×2n real block
matrices during Complex2Real, introduce a dedicated ComplexPSD cone
that preserves the n×n Hermitian structure. SCS handles this natively
via its 'cs' (Hermitian PSD) cone key with cvec format. For solvers
without native support (e.g. Clarabel), ExactCone2Cone converts
ComplexPSD to the equivalent 2n×2n real PSD formulation.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@PTNobel PTNobel requested a review from rileyjmurray February 8, 2026 11:17
@claude
Copy link

claude bot commented Feb 8, 2026

PR Review: Add native ComplexPSD cone for Hermitian PSD constraints

Summary

This PR introduces a dedicated ComplexPSD cone type that preserves the n×n Hermitian structure through the reduction chain, rather than expanding to 2n×2n real block matrices early. SCS leverages native Hermitian PSD support via the 'cs' cone, while other solvers fall back through ExactCone2Cone. The implementation is well-structured and aligns with CVXPY's architecture.


✅ Strengths

1. Architecture & Design

  • Clean separation of concerns: The reduction chain properly defers the ComplexPSD → PSD conversion until solver-specific handling, which is the right approach
  • Consistent with CVXPY patterns: ComplexPSD follows the same structure as PSD, SOC, etc.
  • Proper dual recovery: Both SCS-specific and fallback dual recovery are implemented correctly

2. Code Quality

  • License headers: ✅ All new files have Apache 2.0 headers
  • Documentation: Good docstrings explaining the cvec format and Hermitian structure
  • Type hints: Return type annotations present where appropriate
  • Error handling: Proper validation of matrix dimensions in ComplexPSD.__init__

3. Test Coverage

  • Comprehensive tests: Tests cover SCS native support, Clarabel fallback, infeasibility, solver equivalence, dual recovery, and DPP
  • Good test patterns: Uses BaseTest class, proper assertions with assertItemsAlmostEqual
  • Solver specification: Tests correctly use solver=cp.SCS and solver=cp.CLARABEL

4. SCS Integration

  • Correct cvec format implementation: The complex_psd_format_mat method properly implements the SCS Hermitian PSD cone format with sqrt(2) scaling and Hermitianization
  • Proper dual extraction: _cvec_to_complex_full correctly reverses the cvec encoding

🔍 Issues & Recommendations

1. Critical: Hermitian Symmetry Not Enforced (cvxpy/constraints/complex_psd.py:40-54)

The ComplexPSD constructor doesn't enforce that real_part is symmetric and imag_part is skew-symmetric. The docstring states this requirement but it's not validated:

# Current code accepts any real_part and imag_part
# Should validate:
# - real_part == real_part.T (symmetric)
# - imag_part == -imag_part.T (skew-symmetric)

Impact: Users could create invalid Hermitian matrices, leading to incorrect solver behavior or confusing errors.

Recommendation: Add validation in __init__ or document that this is the user's responsibility (since checking symbolic expressions for symmetry is expensive).


2. Potential Issue: residual property uses dense eigendecomposition (cvxpy/constraints/complex_psd.py:80-86)

@property
def residual(self):
    if self.args[0].value is None or self.args[1].value is None:
        return None
    H = self.args[0].value + 1j * self.args[1].value
    eigs = np.linalg.eigvalsh(H)
    return np.maximum(-np.min(eigs), 0)

Concern: For large matrices, computing all eigenvalues is expensive. The regular PSD constraint uses lambda_min() (see cvxpy/constraints/psd.py:84), though that's for symbolic expressions.

Impact: Performance degradation for large complex PSD constraints when checking residuals.

Recommendation: Consider documenting this limitation or optimizing for large matrices (though this may be acceptable for typical use cases).


3. Code Organization: Import ordering (cvxpy/reductions/complex2real/canonicalizers/psd_canon.py:18-19)

The import of ComplexPSD appears after the standard library import:

import numpy as np

from cvxpy.constraints.complex_psd import ComplexPSD

CVXPY guideline: "IMPORTANT: IMPORTS AT THE TOP" - all imports should be grouped together (stdlib, third-party, local).

Recommendation: This is minor and acceptable, but for strictest adherence to the style guide, could be written as:

import numpy as np
from cvxpy.constraints.complex_psd import ComplexPSD

(No blank line between import groups if they're all at the top)


4. Dual Recovery Comment (cvxpy/reductions/complex2real/complex2real.py:254-257)

The updated comment says:

# The dual arrives already as a complex n x n matrix,
# reconstructed by ExactCone2Cone.recover_dual or
# SCS.extract_dual_value for ComplexPSD constraints.
dvars[cid] = solution.dual_vars[cid]

Observation: This is clearer than the old code, but it might be worth noting that for real PSD constraints (when imag_args[0] is None), the dual is still real.

Recommendation: Minor - current code is fine, but could add: "For purely real PSD, dual remains real; for complex, dual is n×n complex."


5. Test Enhancement Suggestion

The tests are comprehensive, but could benefit from:

  • Testing edge cases: 1×1 matrices, larger matrices (e.g., 10×10)
  • Testing error cases: non-square matrices, mismatched shapes
  • Testing with other solvers that support PSD (MOSEK, CVXOPT) to ensure fallback works

Example addition:

def test_complex_psd_validation(self) -> None:
    """Test that ComplexPSD validates matrix dimensions."""
    with self.assertRaises(ValueError):
        # Non-square matrix should raise
        cp.ComplexPSD(cp.Variable((2, 3)), cp.Variable((2, 3)))
    
    with self.assertRaises(ValueError):
        # Mismatched shapes should raise
        cp.ComplexPSD(cp.Variable((2, 2)), cp.Variable((3, 3)))

🔒 Security Considerations

  • No security concerns identified: The code doesn't handle user input directly, doesn't perform unsafe operations, and follows CVXPY's existing patterns.
  • The sparse matrix construction in complex_psd_format_mat is bounded by matrix dimensions and doesn't have overflow risks for reasonable problem sizes.

⚡ Performance Considerations

Positive:

  • Avoiding premature 2n×2n expansion should reduce memory usage and improve solver efficiency for complex PSD constraints
  • SCS can exploit native Hermitian structure for better performance

Concerns:

  • The complex_psd_format_mat method uses Python loops to build sparse matrices. For large n, this could be slow during problem setup.

Recommendation: Consider vectorizing the sparse matrix construction if performance becomes an issue, though the current approach is clear and correct.


📋 Checklist Items

From the PR description:

  • Add our license to new files - ✅ Done
  • Check that your code adheres to our coding style - ✅ Mostly (minor import ordering note)
  • Write unittests - ✅ Comprehensive coverage
  • Run the unittests and check that they're passing - ⚠️ Not verified in this review
  • Run the benchmarks - ⏸️ Skipped (user acknowledged)

🎯 Recommendations Summary

Before merging:

  1. Consider adding validation or documentation about Hermitian symmetry requirements in ComplexPSD.__init__
  2. Consider adding test cases for dimension validation errors

Nice to have (can be follow-up):

  1. Performance optimization for complex_psd_format_mat if needed
  2. Testing with additional solvers
  3. Documentation updates for cvxpy.org (mentioned in CLAUDE.md)

✨ Overall Assessment

This is a high-quality PR that introduces an important optimization for complex PSD constraints. The implementation is architecturally sound, follows CVXPY conventions, and includes thorough tests. The code is well-documented and the dual recovery logic is correct.

The main suggestion is to address the Hermitian symmetry validation/documentation, but otherwise this is ready for merging after the test suite passes.

Rating: 8.5/10 - Excellent work! 🚀

@github-actions
Copy link

github-actions bot commented Feb 8, 2026

Benchmarks that have stayed the same:

   before           after         ratio
 [f972721f]       [c8cfaecb]
     13.6±0ms         14.5±0ms     1.07  simple_LP_benchmarks.SimpleFullyParametrizedLPBenchmark.time_compile_problem
      308±0ms          321±0ms     1.04  gini_portfolio.Yitzhaki.time_compile_problem
      12.9±0s          13.3±0s     1.03  finance.CVaRBenchmark.time_compile_problem
      813±0ms          836±0ms     1.03  simple_QP_benchmarks.LeastSquares.time_compile_problem
      2.89±0s          2.95±0s     1.02  quantum_hilbert_matrix.QuantumHilbertMatrix.time_compile_problem
     13.9±0ms         14.1±0ms     1.01  simple_QP_benchmarks.ParametrizedQPBenchmark.time_compile_problem
      11.2±0s          11.3±0s     1.01  simple_LP_benchmarks.SimpleLPBenchmark.time_compile_problem
      233±0ms          235±0ms     1.01  simple_QP_benchmarks.SimpleQPBenchmark.time_compile_problem
      4.26±0s          4.29±0s     1.01  huber_regression.HuberRegression.time_compile_problem
      229±0ms          231±0ms     1.01  gini_portfolio.Murray.time_compile_problem
      4.92±0s          4.95±0s     1.01  optimal_advertising.OptimalAdvertising.time_compile_problem
      143±0ms          144±0ms     1.01  high_dim_convex_plasticity.ConvexPlasticity.time_compile_problem
      273±0ms          274±0ms     1.00  matrix_stuffing.ParamSmallMatrixStuffing.time_compile_problem
      1.03±0s          1.04±0s     1.00  finance.FactorCovarianceModel.time_compile_problem
      22.3±0s          22.3±0s     1.00  sdp_segfault_1132_benchmark.SDPSegfault1132Benchmark.time_compile_problem
      1.36±0s          1.36±0s     1.00  matrix_stuffing.ParamConeMatrixStuffing.time_compile_problem
      692±0ms          686±0ms     0.99  matrix_stuffing.ConeMatrixStuffingBench.time_compile_problem
      893±0ms          884±0ms     0.99  simple_LP_benchmarks.SimpleScalarParametrizedLPBenchmark.time_compile_problem
      1.88±0s          1.86±0s     0.99  simple_QP_benchmarks.UnconstrainedQP.time_compile_problem
      281±0ms          277±0ms     0.99  slow_pruning_1668_benchmark.SlowPruningBenchmark.time_compile_problem
      4.99±0s          4.92±0s     0.99  svm_l1_regularization.SVMWithL1Regularization.time_compile_problem
      1.59±0s          1.56±0s     0.98  tv_inpainting.TvInpainting.time_compile_problem
     38.9±0ms         37.9±0ms     0.98  matrix_stuffing.SmallMatrixStuffing.time_compile_problem
      1.12±0s          1.07±0s     0.96  gini_portfolio.Cajas.time_compile_problem
      535±0ms          503±0ms     0.94  semidefinite_programming.SemidefiniteProgramming.time_compile_problem

…sertion

- Clarify docstring to explain implicit Hermitianization (like PSD's symmetrization)
- Use lambda_min/neg for residual instead of raw eigvalsh, with Hermitianization
- Add assertion that Clarabel doesn't natively support ComplexPSD
- Add test_complex_psd_validation for dimension checking
- Move imports to top of test file

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@SteveDiamond
Copy link
Collaborator

How does this change interact with DPP? Adding complex numbers to DPP is not worth it IMO if SCS is the only solver that can consume them.

@PTNobel
Copy link
Collaborator Author

PTNobel commented Feb 8, 2026

This should work perfectly fine with DPP! We still canonicalize to real matrices in complex2real, and so post #3032 everything should work transperantly. There's one DPP test, but it isn't very comprehensive.

The detailed explanation of how Hermitian PSD dual variables are
recovered from the 2n x 2n real block matrix was deleted from
complex2real.py (where the recovery no longer happens) and belongs
here where recover_dual actually extracts the blocks.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
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.

Support SCS complex semidefinite (Hermitian PSD) cone

2 participants

Comments