Python Interface for PDHCG

May 25, 2026 · View on GitHub

License PyPI version Publication arXiv

This is the Python interface to PDHCG, a GPU-accelerated first-order solver for large-scale Quadratic Programming (QP). It provides a high-level, Pythonic API for constructing, modifying, and solving QPs using NumPy and SciPy data structures.

Installation

Requirements

  • Python ≥ 3.8
  • NumPy ≥ 1.21
  • SciPy ≥ 1.8
  • An NVIDIA GPU with CUDA support (≥12.4 required)
  • A C/C++ toolchain with GCC and NVCC

!!! note "CUDA Version and SpMVOp" PDHCG automatically detects your CUDA version at compile time:

- **CUDA 13+**: Uses cuSPARSE **SpMVOp** for improved performance.
- **CUDA 12.x**: Falls back to the standard **SpMV** API. No manual intervention is required.

!!! note "Multi-GPU support" The Python interface currently supports single-GPU solving only. For multi-GPU distributed solving, build the C++ executable with -DPDHCG_COMPILE_DISTRIBUTED=ON and launch it via mpirun (see the main README).

Install

Install from PyPI:

pip install pdhcg

Or build from source:

git clone https://github.com/Lhongpei/PDHCG-II.git
cd PDHCG-II
pip install .

If your system has multiple CUDA installations or the default nvcc (typically in /usr/bin/nvcc) is outdated, you must explicitly point to your modern CUDA compiler using environment variables. This ensures the build system bypasses the system default.

# Replace '/your/path/to/nvcc' with your actual path
# Example: export CUDACXX=/usr/local/cuda-12.6/bin/nvcc
export CUDACXX=/your/path/to/nvcc
export SKBUILD_CMAKE_ARGS="-DCMAKE_CUDA_COMPILER=/your/path/to/nvcc"

pip install pdhcg

Quick Start

import numpy as np
import scipy.sparse as sp
from pdhcg import Model, PDHCG

# Example: minimize 0.5 * x'(Q + R^T D R)x + c'x
# subject to l <= A x <= u,  lb <= x <= ub
# (D defaults to identity, recovering 0.5 * x'Qx + 0.5 * ||Rx||^2.)

# 1. Define Standard QP terms
Q = sp.csc_matrix([[1.0, -1.0], [-1.0, 2.0]])
c = np.array([-2.0, -6.0])

# 2. Define Low-Rank Matrix R
# Let's add a term 0.5 * ||Rx||^2 where R = [[1, 0]]
# This effectively adds 0.5 * (x1)^2 to the objective
R = sp.csc_matrix([[1.0, 0.0]])

# 3. (Optional) Middle matrix D. Pass a 1-D numpy array for a diagonal D,
# a 2-D array for dense D, or a scipy.sparse matrix. Omit to use D = I.
# D = np.array([2.5])

# 4. Define Constraints
A = sp.csc_matrix([[1.0, 1.0], [-1.0, 2.0], [2.0, 1.0]])
l = np.array([-np.inf, -np.inf, -np.inf])
u = np.array([2.0, 2.0, 3.0])
lb = np.zeros(2)
ub = np.array([np.inf, np.inf])

# 5. Create QP model with Low-Rank term
m = Model(objective_matrix=Q,
          objective_matrix_low_rank=R,                # <--- Pass R here
          # objective_matrix_low_rank_middle=D,      # <--- and optionally D
          objective_vector=c,
          constraint_matrix=A,
          constraint_lower_bound=l,
          constraint_upper_bound=u,
          variable_lower_bound=lb,
          variable_upper_bound=ub)

# Set model sense
m.ModelSense = PDHCG.MINIMIZE

# Parameters can be set in multiple ways
m.Params.TimeLimit = 60        # attribute style
m.setParam("FeasibilityTol", 1e-6)
m.setParams(LogLevel=2, OptimalityTol=1e-8)

# Solve
m.optimize()

# Retrieve results
print("Status:", m.Status)
print("Objective:", m.ObjVal)
print("Primal solution:", m.X)
print("Dual solution:", m.Pi)

Modeling

The Model class represents a quadratic programming problem of the form:

min12x(Q+RDR)x+cx+c0s.t.  Axu,lbxub.\min \frac{1}{2} x^\top (Q + R^\top D R) x + c^\top x + c_0 \quad \text{s.t.} \; \ell \le A x \le u, \quad \text{lb} \le x \le \text{ub}.

Arguments

  • objective_matrix (Q, optional): Quadratic part of the objective function. Both dense (numpy.ndarray) and sparse (scipy.sparse.csr_matrix) inputs are supported.
  • objective_matrix_low_rank (R, optional): Low-rank factor matrix in the quadratic objective term. Both dense (numpy.ndarray) and sparse (scipy.sparse.csr_matrix) inputs are supported.
  • objective_matrix_low_rank_middle (D, optional): Middle matrix in RDRR^\top D R, rank×rank\text{rank}\times\text{rank}. Accepts a 1-D numpy array (diagonal DD), a 2-D numpy array (dense symmetric DD), or a scipy sparse matrix. May be indefinite — useful for quasi-Newton updates, kernel-based formulations, and weighted least squares. Defaults to identity, recovering RRR^\top R.
  • objective_vector (c): Linear part of the objective function.
  • constraint_matrix (A): Coefficient matrix for the constraints. Both dense (numpy.ndarray) and sparse (scipy.sparse.csr_matrix) inputs are supported.
  • constraint_lower_bound (l): Lower bounds for each constraint. Use -np.inf or None for no lower bound.
  • constraint_upper_bound (u): Upper bounds for each constraint. Use +np.inf or None for no upper bound.
  • variable_lower_bound (lb, optional): Lower bounds for the decision variables. Defaults to 0 for all variables if not provided.
  • variable_upper_bound (ub, optional): Upper bounds for the decision variables. Defaults to +np.inf for all variables if not provided.
  • objective_constant (c0, optional): Constant offset in the objective function. Defaults to 0.0.

To initialize a quadratic programming problem, you need to provide the objective matrix and vector, constraint matrix, and bounds on both constraints and variables.

Q = sp.csc_matrix([[1.0, -1.0], [-1.0, 2.0]])
c = np.array([-2.0, -6.0])
A = sp.csc_matrix([[1.0, 1.0], [-1.0, 2.0], [2.0, 1.0]])
l = np.array([-np.inf, -np.inf, -np.inf])
u = np.array([2.0, 2.0, 3.0])
lb = np.zeros(2)
ub = np.array([np.inf, np.inf])


# Create QP model
m = Model(objective_matrix=Q,
          objective_vector=c,
          constraint_matrix=A,
          constraint_lower_bound=l,
          constraint_upper_bound=u,
          variable_lower_bound=lb,
          variable_upper_bound=ub)

Model Sense

By default, pdhcg solves minimization problems.

To switch between minimization and maximization, set the attribute ModelSense:

# Set model sense
m.ModelSense = PDHCG.MAXIMIZE

Parameters

Solver parameters control termination criteria, logging, scaling, and restart behavior.

Below is a list of commonly used parameters, their internal keys, and descriptions.

AliasInternal KeyTypeDefaultDescription
TimeLimittime_sec_limitfloat3600.0Maximum wall-clock time in seconds. The solver terminates if the limit is reached.
IterationLimititeration_limitint2147483647Maximum number of iterations.
LogLevel, Verbosityverboseint1Verbosity level: 0 (Silent), 1 (Summary), or 2 (Detailed iteration info).
TermCheckFreqtermination_evaluation_frequencyint200Frequency (in iterations) at which termination conditions are evaluated.
OptimalityNormoptimality_normstring"l2"Norm for optimality criteria. Use "l2" for L2 norm or "linf" for infinity norm.
OptimalityToleps_optimal_relativefloat1e-4Relative tolerance for optimality gap. Solver stops if the relative primal-dual gap ≤ this value.
FeasibilityToleps_feasible_relativefloat1e-4Relative feasibility tolerance for primal/dual residuals.
RuizItersl_inf_ruiz_iterationsint10Number of iterations for L∞ Ruiz scaling. Improves numerical conditioning.
UsePCAlphahas_pock_chambolle_alphaboolTrueWhether to use the Pock–Chambolle α step size adjustment.
PCAlphapock_chambolle_alphafloat1.0Value of the Pock–Chambolle α parameter.
BoundObjRescalingbound_objective_rescalingboolTrueWhether to rescale the objective vector during preprocessing.
RestartArtificialThreshartificial_restart_thresholdfloat0.36Threshold for artificial restart.
RestartSufficientReductionsufficient_reduction_for_restartfloat0.2Sufficient reduction factor to justify a restart.
RestartNecessaryReductionnecessary_reduction_for_restartfloat0.5Necessary reduction factor required for a restart.
RestartKpk_pfloat0.99Proportional coefficient for PID-controlled primal weight updates.
ReflectionCoeffreflection_coefficientfloat1.0Reflection coefficient.
SVMaxItersv_max_iterint5000Maximum number of iterations for the power method.
SVTolsv_tolfloat1e-4Termination tolerance for the power method.
InnerIterLimitinner_iter_limitint1000Maximum number of iterations for the inner solver.
InnerInitTolinner_init_tolfloat1e-3Initial tolerance for the inner solver.
InnerMinTolinner_min_tolfloat1e-9Minimum tolerance for the inner solver.
DiagJacobiPreconddiag_jacobi_precondboolTrueWhether to use the Jacobi diagonal preconditioner in the inner subproblem. Set to False to disable.

They can be set in multiple ways:

# Method 1: single parameter
m.setParam("TimeLimit", 300)
m.setParam("FeasibilityTol", 1e-6)

# Method 2: multiple parameters
m.setParams(TimeLimit=300, FeasibilityTol=1e-6)

# Method 3: attribute-style access
m.Params.TimeLimit = 300
m.Params.FeasibilityTol = 1e-6

Solution Attributes

After calling m.optimize(), the solver stores results in a set of read-only attributes. These attributes provide access to primal/dual solutions, objective values, residuals, and runtime statistics.

Attribute Reference

AttributeTypeDescription
StatusstrHuman-readable solver status ("OPTIMAL", "INFEASIBLE", "UNBOUNDED", "TIME_LIMIT", etc.).
StatusCodeintNumeric status code (OPTIMAL=1, INFEASIBLE=2, UNBOUNDED=3, ITERATION_LIMIT=4, TIME_LIMIT=5, UNSPECIFIED=-1).
ObjValfloatPrimal objective value at termination (sign-adjusted according to ModelSense).
DualObjfloatDual objective value at termination.
GapfloatAbsolute primal-dual gap.
RelGapfloatRelative primal-dual gap.
Xnumpy.ndarrayPrimal solution vector (x). May be None if no feasible solution was found.
Pinumpy.ndarrayDual solution vector (Lagrange multipliers).
IterCountintNumber of iterations performed.
RuntimefloatTotal wall-clock runtime in seconds.
RescalingTimefloatTime spent on preprocessing and rescaling (seconds).
RelPrimalResidualfloatRelative primal residual.
RelDualResidualfloatRelative dual residual.
MaxPrimalRayInfeasfloatMaximum primal ray infeasibility (indicator for infeasibility).
MaxDualRayInfeasfloatMaximum dual ray infeasibility.
PrimalRayLinObjfloatLinear objective value along a primal ray (used in infeasibility detection).
DualRayObjfloatObjective value along a dual ray (used in unboundedness detection).

All solution-related information can then be queried directly from the Model object:

m.optimize()

print("Status:", m.Status, "(code:", m.StatusCode, ")")
print("Primal objective:", m.ObjVal)
print("Dual objective:", m.DualObj)
print("Relative gap:", m.RelGap)
print("Iterations:", m.IterCount, " Runtime (s):", m.Runtime)

# Access solutions
print("Primal solution:", m.X)
print("Dual solution:", m.Pi)

# Check residuals
print("Primal residual:", m.RelPrimalResidual)
print("Dual residual:", m.RelDualResidual)

Warm Start

pdhcg supports warm starting from user-provided primal and/or dual solutions. This allows resuming from a previous iterate or reusing solutions from a related instance, often reducing iterations needed to reach optimality.

# Warm starting solution
x_init = [1.0, 2.0]
pi_init = [1.0, -1.0, 0.0]

# Set warm start
m.setWarmStart(primal=x_init, dual=pi_init)

# Solve
m.optimize()

Both primal and dual arguments are optional. You may specify only one of them if desired:

# Only provide primal start
m.setWarmStart(primal=x_init)

# Only provide dual start
m.setWarmStart(dual=pi_init)

If the warm-start vectors have incorrect dimensions, the solver automatically falls back to a cold start and issues a warning.

To clear existing warm-start values:

m.clearWarmStart()

or

m.setWarmStart(primal=None, dual=None)