diff --git a/.github/workflows/disabled/build.yml b/.github/workflows/disabled/build.yml index 03aba17..b8350d6 100644 --- a/.github/workflows/disabled/build.yml +++ b/.github/workflows/disabled/build.yml @@ -54,8 +54,8 @@ jobs: - name: Check executable run: | - test -f build/bin/pdhcg - ./build/bin/pdhcg --help || true + test -f build/pdhcg + ./build/pdhcg --help || true build-python: name: Build Python Package diff --git a/.gitignore b/.gitignore index b76a2c2..c2b5220 100644 --- a/.gitignore +++ b/.gitignore @@ -56,8 +56,11 @@ dkms.conf *.dwo # ignored files -build/* +build*/ test/* +!test/*.c +!test/*.cu +!test/*.h /.vscode /.venv /_b diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af2b8df..c34ec17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Pre-commit hooks for PDHCG-II +# Pre-commit hooks for PDHCG # Install: pip install pre-commit # Setup: pre-commit install # Run manually: pre-commit run --all-files diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d0eb1d..1aa4d05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_minimum_required(VERSION 3.20) project(pdhcg LANGUAGES C CXX CUDA) set(PDHCG_VERSION_MAJOR 0) -set(PDHCG_VERSION_MINOR 2) -set(PDHCG_VERSION_PATCH 1) +set(PDHCG_VERSION_MINOR 3) +set(PDHCG_VERSION_PATCH 0) set(PDHCG_VERSION "${PDHCG_VERSION_MAJOR}.${PDHCG_VERSION_MINOR}.${PDHCG_VERSION_PATCH}") add_compile_definitions(PDHCG_VERSION="${PDHCG_VERSION}") @@ -49,8 +49,12 @@ include(CMakeDependentOption) option(PDHCG_BUILD_STATIC_LIB "Build the PDHCG static library" ON) option(PDHCG_BUILD_SHARED_LIB "Build the PDHCG shared library" ON) -option(PDHCG_COMPILE_PSQP "Download and use PSQP for presolving" OFF) +option(PDHCG_COMPILE_PREFOS "Download and use PreFOS for presolving" OFF) +option(PDHCG_PREFOS_ENABLE_CUDA "Enable the PreFOS CUDA propagation backend" ON) option(PDHCG_COMPILE_DISTRIBUTED "Enable distributed computing with MPI" OFF) +option(PDHCG_ABSOLUTE_ONLY_TERMINATION + "Use absolute residuals and objective gap for optimal termination" + OFF) # format: cmake_dependent_option(OPTION "docstring" DEFAULT_VALUE "DEPENDENCY_VARIABLE" FORCE_OFF_VALUE) cmake_dependent_option(PDHCG_BUILD_PYTHON "Build the PDHCG Python bindings" OFF @@ -78,40 +82,42 @@ endif() include(FetchContent) # ----------------------------------------------------------------------------- -# PSQP (QP Presolver) Integration +# PreFOS Presolver Integration # ----------------------------------------------------------------------------- -if(PDHCG_COMPILE_PSQP) - # Use FetchContent to automatically download PSQP from git repository - set(PSQP_VERSION_TAG "main" CACHE STRING "PSQP git tag/branch to use") +if(PDHCG_COMPILE_PREFOS) + set(PREFOS_VERSION_TAG "v0.1.0" CACHE STRING "PreFOS git tag/branch to use") FetchContent_Declare( - psqp - GIT_REPOSITORY https://github.com/Lhongpei/PSQP.git - GIT_TAG ${PSQP_VERSION_TAG} + prefos + GIT_REPOSITORY https://github.com/Lhongpei/PreFOS.git + GIT_TAG ${PREFOS_VERSION_TAG} ) - # Check if PSQP has already been populated - FetchContent_GetProperties(psqp) - if(NOT psqp_POPULATED) - message(STATUS "Fetching PSQP from https://github.com/Lhongpei/PSQP.git (${PSQP_VERSION_TAG})") - FetchContent_MakeAvailable(psqp) - message(STATUS "PSQP populated at: ${psqp_SOURCE_DIR}") + # Embed PreFOS statically so installed PDHCG binaries have no additional + # presolver runtime dependency. The source override + # FETCHCONTENT_SOURCE_DIR_PREFOS remains available for local development. + set(_PDHCG_BUILD_SHARED_LIBS_WAS_DEFINED FALSE) + if(DEFINED BUILD_SHARED_LIBS) + set(_PDHCG_BUILD_SHARED_LIBS_WAS_DEFINED TRUE) + set(_PDHCG_SAVED_BUILD_SHARED_LIBS "${BUILD_SHARED_LIBS}") + endif() + set(BUILD_SHARED_LIBS OFF) + set(PREFOS_BUILD_TESTING OFF) + set(PREFOS_ENABLE_CUDA ${PDHCG_PREFOS_ENABLE_CUDA}) + FetchContent_MakeAvailable(prefos) + if(_PDHCG_BUILD_SHARED_LIBS_WAS_DEFINED) + set(BUILD_SHARED_LIBS "${_PDHCG_SAVED_BUILD_SHARED_LIBS}") + else() + unset(BUILD_SHARED_LIBS) endif() - # Set up PSQP include directories - if(TARGET PSQP) - target_include_directories(PSQP INTERFACE - $ - $ - ) - message(STATUS "PSQP target configured successfully") - # Add version definition - add_compile_definitions(PSQP_VERSION=\"${PSQP_VERSION_TAG}\") + if(TARGET PreFOS::PreFOS) + message(STATUS "PreFOS ${PREFOS_VERSION_TAG} configured successfully") else() - message(WARNING "PSQP target not found. Presolving features will be disabled.") + message(FATAL_ERROR "PreFOS target was not created") endif() else() - message(STATUS "PSQP integration disabled by user (PDHCG_COMPILE_PSQP=OFF).") + message(STATUS "PreFOS integration disabled (PDHCG_COMPILE_PREFOS=OFF).") endif() # ----------------------------------------------------------------------------- @@ -123,6 +129,7 @@ file(GLOB C_SOURCES ) file(GLOB CU_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cu" + "${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/*.cu" ) # Exclude cli.c from library builds @@ -139,6 +146,7 @@ if(PDHCG_COMPILE_DISTRIBUTED) file(GLOB DIST_C_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/distributed/*.c") file(GLOB DIST_CU_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/distributed/*.cu") list(REMOVE_ITEM DIST_C_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/distributed/distributed_ops_stub.c") + list(REMOVE_ITEM DIST_C_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/distributed/distributed_conic_stub.c") list(APPEND C_SOURCES ${DIST_C_SOURCES}) list(APPEND CU_SOURCES ${DIST_CU_SOURCES}) @@ -146,6 +154,7 @@ if(PDHCG_COMPILE_DISTRIBUTED) list(APPEND CORE_INCLUDE_DIRS PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/distributed) else() list(APPEND C_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/distributed/distributed_ops_stub.c") + list(APPEND C_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/distributed/distributed_conic_stub.c") list(APPEND CORE_INCLUDE_DIRS PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/distributed) endif() @@ -157,13 +166,20 @@ set(CORE_LINK_LIBS PUBLIC ZLIB::ZLIB ) -if(PDHCG_COMPILE_PSQP AND TARGET PSQP) - list(APPEND CORE_LINK_LIBS PUBLIC PSQP) - set(PDHCG_OPTIONAL_DEFINES "PSQP_AVAILABLE") +if(PDHCG_COMPILE_PREFOS AND TARGET PreFOS::PreFOS) + list(APPEND CORE_LINK_LIBS PUBLIC PreFOS::PreFOS) + set(PDHCG_OPTIONAL_DEFINES PREFOS_AVAILABLE) + if(PDHCG_PREFOS_ENABLE_CUDA) + list(APPEND PDHCG_OPTIONAL_DEFINES PDHCG_PREFOS_CUDA_ENABLED) + endif() else() set(PDHCG_OPTIONAL_DEFINES "") endif() +if(PDHCG_ABSOLUTE_ONLY_TERMINATION) + list(APPEND PDHCG_OPTIONAL_DEFINES PDHCG_ABSOLUTE_ONLY_TERMINATION) +endif() + if(PDHCG_COMPILE_DISTRIBUTED) find_package(MPI REQUIRED) add_compile_definitions(PDHCG_COMPILE_DISTRIBUTED) @@ -201,7 +217,7 @@ if(PDHCG_BUILD_STATIC_LIB) target_include_directories(pdhcg_core ${CORE_INCLUDE_DIRS}) target_link_libraries(pdhcg_core ${CORE_LINK_LIBS}) - # Add PSQP compile definition + # Add optional dependency compile definitions. target_compile_definitions(pdhcg_core PUBLIC ${PDHCG_OPTIONAL_DEFINES}) set_target_properties(pdhcg_core PROPERTIES @@ -222,7 +238,7 @@ if(PDHCG_BUILD_SHARED_LIB) target_include_directories(PDHCG_shared ${CORE_INCLUDE_DIRS}) target_link_libraries(PDHCG_shared ${CORE_LINK_LIBS}) - # Add PSQP compile definition + # Add optional dependency compile definitions. target_compile_definitions(PDHCG_shared PUBLIC ${PDHCG_OPTIONAL_DEFINES}) # Shared library must resolve device symbols as it is a final link point @@ -257,15 +273,11 @@ if(PDHCG_BUILD_CLI) # Link CLI to the static core library target_link_libraries(PDHCG_cli PRIVATE pdhcg_core) - # CLI is a final executable, it must resolve device symbols - # Set RPATH so that libPSQP.so can be found without LD_LIBRARY_PATH - # PSQP is in _deps/psqp-build when using FetchContent + # CLI is a final executable, so it must resolve device symbols. set_target_properties(PDHCG_cli PROPERTIES OUTPUT_NAME "pdhcg" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" CUDA_RESOLVE_DEVICE_SYMBOLS ON - INSTALL_RPATH "${CMAKE_BINARY_DIR}/_deps/psqp-build" - BUILD_WITH_INSTALL_RPATH TRUE ) endif() @@ -282,6 +294,12 @@ if(PDHCG_BUILD_TESTS) "${CMAKE_CURRENT_SOURCE_DIR}/test/*.c" "${CMAKE_CURRENT_SOURCE_DIR}/test/*.cu" ) + set(PDHCG_TEST_TOOLS + export_qcqp_socp + inspect_qcqp_exportability + qcqp_probe + solve_exported_qcqp_pdhcg + ) foreach(TEST_SRC ${TEST_SOURCES}) get_filename_component(TEST_NAME ${TEST_SRC} NAME_WE) @@ -297,23 +315,22 @@ if(PDHCG_BUILD_TESTS) PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/internal ) - # Set up RPATH to find PSQP if available - if(TARGET PSQP) - set(TEST_RPATH "${CMAKE_BINARY_DIR}/_deps/psqp-build") - else() - set(TEST_RPATH "") - endif() - # Tests are final executables, they must resolve device symbols set_target_properties(${TEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tests" CUDA_RESOLVE_DEVICE_SYMBOLS ON - INSTALL_RPATH "${TEST_RPATH}" - BUILD_WITH_INSTALL_RPATH TRUE ) - # Register with CTest - add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) + # Input-driven inspection/export tools are built but require explicit + # command-line data, so they are not zero-argument CTest cases. + if(TEST_NAME STREQUAL "test_distributed_conic" AND PDHCG_COMPILE_DISTRIBUTED) + add_test(NAME ${TEST_NAME} + COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 2 ${MPIEXEC_PREFLAGS} + $ ${MPIEXEC_POSTFLAGS}) + set_tests_properties(${TEST_NAME} PROPERTIES SKIP_RETURN_CODE 77 PROCESSORS 2 TIMEOUT 90) + elseif(NOT TEST_NAME IN_LIST PDHCG_TEST_TOOLS) + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) + endif() endforeach() endif() diff --git a/README.md b/README.md index a25cf15..fd296d1 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,33 @@ -# PDHCG-II: A GPU-Accelerated Solver for Quadratic Programming +# PDHCG: A First-Order Solver for Quadratic Conic Programming with Multi-GPU Acceleration -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![PyPI version](https://badge.fury.io/py/pdhcg.svg)](https://pypi.org/project/pdhcg/) [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://lhongpei.github.io/PDHCG-II) [![Publication](https://img.shields.io/badge/DOI-10.1287/ijoc.2024.0983-B31B1B.svg)](https://pubsonline.informs.org/doi/10.1287/ijoc.2024.0983) [![arXiv](https://img.shields.io/badge/arXiv-2602.23967-b31b1b.svg)](https://arxiv.org/abs/2602.23967) [![qpsolvers](https://img.shields.io/badge/qpsolvers-supported-brightgreen.svg)](https://github.com/qpsolvers/qpsolvers) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![PyPI version](https://badge.fury.io/py/pdhcg.svg)](https://pypi.org/project/pdhcg/) [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://lhongpei.github.io/PDHCG) [![Publication](https://img.shields.io/badge/DOI-10.1287/ijoc.2024.0983-B31B1B.svg)](https://pubsonline.informs.org/doi/10.1287/ijoc.2024.0983) [![arXiv](https://img.shields.io/badge/arXiv-2608.09159-b31b1b.svg)](https://arxiv.org/abs/2608.09159) [![qpsolvers](https://img.shields.io/badge/qpsolvers-supported-brightgreen.svg)](https://github.com/qpsolvers/qpsolvers) [![CVXPY](https://img.shields.io/badge/CVXPY-supported-brightgreen.svg)](https://www.cvxpy.org/) -**PDHCG** is a high-performance, GPU-accelerated implementation of the Primal-Dual Hybrid Gradient (PDHG) algorithm designed for solving large-scale Convex Quadratic Programming (QP) problems. +**PDHCG** is a high-performance, GPU-accelerated implementation of the Primal-Dual Hybrid Gradient (PDHG) algorithm for large-scale convex quadratic and quadratic conic programming. -For a detailed explanation of the methodology, please refer to our papers: [A Restarted Primal-Dual Hybrid Conjugate Gradient Method for Large-Scale Quadratic Programming](https://pubsonline.informs.org/doi/10.1287/ijoc.2024.0983) and [PDHCG-II: An Enhanced Version of PDHCG for Large-Scale Convex QP](https://arxiv.org/abs/2602.23967). +For a detailed explanation of the methodology, please refer to our papers: [A Restarted Primal-Dual Hybrid Conjugate Gradient Method for Large-Scale Quadratic Programming](https://pubsonline.informs.org/doi/10.1287/ijoc.2024.0983) and [GPU-Accelerated Conic Quadratic Programming with Local Linear Convergence under Strict Complementarity](https://arxiv.org/abs/2608.09159). --- ## Problem Formulation -PDHCG solves convex quadratic programs in the following form, which allows a flexible input of the quadratic objective matrix as a sparse component plus a structured low-rank component: +PDHCG solves convex quadratic conic programs in the following form, with a sparse quadratic objective component and an optional structured low-rank component: ```math \begin{aligned} \min_{x} \quad & \frac{1}{2}x^\top (Q + R^\top D R) x + c^\top x \\ \text{s.t.} \quad & \ell_c \le Ax \le u_c, \\ - & \ell_v \le x \le u_v. + & Fx + g \in \mathcal{K}_a, \\ + & \ell_v \le x \le u_v, \\ + & x_J \in \mathcal{K}_v \quad \text{for variable-cone blocks } J. \end{aligned} ``` - $Q$ is the sparse symmetric quadratic component (optional). - $R \in \mathbb{R}^{k\times n}$ is a tall low-rank factor (optional, $k$ = rank). - $D \in \mathbb{R}^{k\times k}$ is an optional middle matrix that scales / weights / signs the low-rank term. When omitted it defaults to the identity, recovering the standard $Q + R^\top R$ formulation. $D$ may be **diagonal, sparse, dense, or indefinite** — the backend auto-detects the cheapest runtime representation. +- Standard SOC, Rotated SOC, Exponential, and Power cones are supported both on variable blocks and through native affine constraints $Fx + g \in \mathcal{K}_a$. ## Installation (C++ Executable) @@ -39,17 +42,17 @@ To use the standalone C++ solver, you must compile the project using CMake. ### Build from Source Clone the repository and compile the project using CMake. ```bash -git clone https://github.com/Lhongpei/PDHCG-II.git -cd PDHCG-II +git clone https://github.com/Lhongpei/PDHCG.git +cd PDHCG cmake -S . -B build cmake --build build --clean-first ``` -This will create the solver binary at `./build/bin/pdhcg`. +This will create the solver binary at `./build/pdhcg`. If your system has multiple CUDA versions or the default nvcc is outdated (e.g., in `/usr/bin/nvcc`), you should explicitly specify the path to your modern CUDA compiler using the CUDACXX environment variable. ```bash -git clone https://github.com/Lhongpei/PDHCG-II.git -cd PDHCG-II +git clone https://github.com/Lhongpei/PDHCG.git +cd PDHCG # Replace '/your/path/to/nvcc' with the actual path, e.g., /usr/local/cuda-12.6/bin/nvcc CUDACXX=/your/path/to/nvcc cmake -S . -B build cmake --build build --clean-first @@ -71,14 +74,14 @@ This requires MPI and NCCL to be installed on your system. Run the solver from the command line: ```bash -./build/bin/pdhcg [OPTIONS] +./build/pdhcg [OPTIONS] ``` ### Command Line Arguments **Positional Arguments:** -1. ``: Path to the input QP (supports `.mps`, `.qps`, and `.mps.gz`). +1. ``: Path to the input problem file (supports `.mps`, `.qps`, `.cbf`, and gzip-compressed variants). 2. ``: Directory where solution files will be saved. Solver Parameters: @@ -90,20 +93,50 @@ Solver Parameters: | --iter_limit | int | Iteration limit. | 2147483647 | | --eps_opt | double | Relative optimality tolerance. | 1e-4 | | --eps_feas | double | Relative feasibility tolerance. | 1e-4 | -| --eps_infeas_detect | double | Infeasibility detection tolerance. | 1e-10 | +| --eps_infeas_detect | double | Infeasibility detection tolerance. | 1e-12 | +| --curtis_reid_iter | int | Iterations for Curtis-Reid log-domain matrix scaling; 0 disables it. | 0 | | --l_inf_ruiz_iter | int | Iterations for L-inf Ruiz rescaling. | 10 | | --pock_chambolle_alpha | double | Value for Pock-Chambolle step size parameter $\alpha$. | 1.0 | | --no_pock_chambolle | flag | Disable Pock-Chambolle rescaling (enabled by default). | false | | --no_bound_obj_rescaling | flag | Disable bound objective rescaling (enabled by default). | false | +| --no_cone_preserving_scaling | flag | Keep coordinate-wise scaling within cone blocks. | false | | --sv_max_iter | int | Max iterations for singular value estimation (Power Method). | 5000 | | --sv_tol | double | Tolerance for singular value estimation. | 1e-4 | | --eval_freq | int | Frequency of termination criteria evaluation (in iterations). | 200 | +| --artificial_restart_threshold | double | Threshold for artificial restart. | 0.36 | +| --sufficient_reduction_for_restart | double | Sufficient reduction factor to justify a restart. | 0.2 | +| --necessary_reduction_for_restart | double | Necessary reduction factor required for a restart. | 0.8 | | --opt_norm | string | Norm for optimality criteria (l2 or linf). | linf | | --inner_iter_limit | int | Max iterations for the inner solver. | 1000 | | --inner_init_tol | double | Initial tolerance for the inner solver. | 1e-3 | | --inner_min_tol | double | Minimum tolerance for the inner solver. | 1e-9 | -| --presolve | int | Enable (1) or disable (0) presolve. | 1 | | --no_diag_precond | flag | Disable the Jacobi diagonal preconditioner used in the inner subproblem (enabled by default). | false | +| --soc_form | string | Cone formulation for QCQP transformations: rotated or standard. | rotated | + +#### Cone scaling aggregation + +With the default cone-preserving scaling, +PDHCG first computes a positive candidate scale `d_j` for every coordinate, +then broadcasts one scale over each cone block `B`. Define + +\[ +d_{\max}=\max_{j\in B}d_j,\qquad +d_{\mathrm{rms}}=\sqrt{\frac{1}{|B|}\sum_{j\in B}d_j^2}. +\] + +The block scale is + +| Scaling phase | Block size <= 8 | Block size > 8 | +| :--- | :--- | :--- | +| Ruiz | `d_max` | `d_rms` | +| Pock-Chambolle | `d_rms` | `sqrt(d_max * d_rms)` | + +This preserves cone geometry while avoiding max-dominated scaling on large +blocks. The aggregation follows the +[`:phase_taper` strategy in HPR-SOCP](https://github.com/PolyU-IOR/HPR-SOCP/blob/0cccff309957e41225646a5e5d0bf811fe899daa/src/utils/scaling.jl#L462-L469), +with its GPU implementation in `src/kernels.jl` at the same commit. PDHCG +applies the rule to both variable and affine cone blocks. Setting +`--no_cone_preserving_scaling` bypasses block aggregation. **Distributed Options** (only available when built with `-DPDHCG_COMPILE_DISTRIBUTED=ON`): | Option | Type | Description | Default | @@ -120,10 +153,10 @@ When built with distributed support, the same binary automatically detects wheth ```bash # Multi-GPU on 4 GPUs -mpirun -n 4 ./build/bin/pdhcg problem.mps ./output +mpirun -n 4 ./build/pdhcg problem.mps ./output # Multi-GPU with a custom 2x2 process grid -mpirun -n 4 ./build/bin/pdhcg problem.mps ./output --grid_size 2,2 +mpirun -n 4 ./build/pdhcg problem.mps ./output --grid_size 2,2 ``` --- @@ -132,7 +165,7 @@ mpirun -n 4 ./build/bin/pdhcg problem.mps ./output --grid_size 2,2 > PDHCG is now officially supported as a backend in the popular [`qpsolvers`](https://github.com/qpsolvers/qpsolvers) ecosystem (v4.11.0+). -PDHCG provides a user-friendly Python interface that allows you to define, solve, and analyze QP problems using familiar libraries like NumPy and SciPy. +PDHCG provides a user-friendly Python interface for quadratic and quadratic conic problems using NumPy and SciPy. For detailed instructions on how to use the Python interface, including installation, modeling, and examples, please see the [Python Interface README](./python/README.md). @@ -192,17 +225,38 @@ if m.X is not None: print(f"Primal Solution: {m.X}") ``` +### CVXPY + +Install the optional dependency and import the backend once to register PDHCG: + +```bash +pip install "pdhcg[cvxpy]" +``` + +```python +import cvxpy as cp +import pdhcg.cvxpy_backend # Registers solver="PDHCG". + +x = cp.Variable() +problem = cp.Problem(cp.Minimize(x), [x >= 1]) +problem.solve(solver="PDHCG", eps=1e-6) +``` + +The backend supports quadratic objectives and CVXPY Zero, NonNeg, SOC, +ExpCone, and PowCone3D constraints. PSD and mixed-integer models are not +supported. + ## Citation If you use this software or method in your research, please cite our paper: ``` -@misc{li2026pdhcgiienhancedversionpdhcg, - title={PDHCG-II: An Enhanced Version of PDHCG for Large-Scale Convex QP}, +@misc{li2026gpuacceleratedconicquadraticprogramming, + title={GPU-Accelerated Conic Quadratic Programming with Local Linear Convergence under Strict Complementarity}, author={Hongpei Li and Yicheng Huang and Huikang Liu and Dongdong Ge and Yinyu Ye}, year={2026}, - eprint={2602.23967}, + eprint={2608.09159}, archivePrefix={arXiv}, primaryClass={math.OC}, - url={https://arxiv.org/abs/2602.23967}, + url={https://arxiv.org/abs/2608.09159}, } ``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..d73993c --- /dev/null +++ b/TODO.md @@ -0,0 +1,35 @@ +# Technical Debt + +## Distributed Model Transport + +Status: deferred + +`distribute_data_bcast_then_partition` currently broadcasts the complete unscaled +`working_problem`, then broadcasts `rescale_info`, whose payload contains another +complete copy in `scaled_problem`. Non-root ranks can therefore hold both global +models, the serialization buffer, and their local partitions at the same time. +This increases startup traffic and peak host memory, and can cause avoidable OOMs +on large instances. + +Follow-up direction: + +- Broadcast only the scaled model plus the small amount of original-model metadata + required for reporting, warm starts, cone metadata, and original norms. +- Recover original quantities from scaling factors where that is exact. +- Measure peak memory and distribution time before and after the change. + +## Processed Problem Representation + +Status: deferred + +`processed_qp_problem_t` mirrors most fields of `qp_problem_t` as borrowed pointers. +The repeated field lists in preprocessing, distributed partitioning, and cleanup +make ownership harder to audit and create maintenance work whenever `qp_problem_t` +changes. + +Follow-up direction: + +- Replace it with a small objective-specific derived representation containing only + quadratic type, diagonal data, and low-rank middle data. +- Pass the owning `qp_problem_t` separately wherever the original arrays are needed. +- Make owned and borrowed fields explicit in the type and its destructor. diff --git a/distributed/distributed_conic.cu b/distributed/distributed_conic.cu new file mode 100644 index 0000000..cce7b6e --- /dev/null +++ b/distributed/distributed_conic.cu @@ -0,0 +1,1029 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "distributed_conic.h" +#include "distributed_interface.h" +#include "distributed_types.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include + +#define DIST_CONE_THREADS 256 +#define PROJECTION_STATS 3 +#define RESIDUAL_STATS 10 + +enum +{ + RES_RV2 = 0, + RES_R0 = 1, + RES_R1 = 2, + RES_DOT = 3, + RES_N2 = 4, + RES_XN2 = 5, + RES_X0 = 6, + RES_D0 = 7, + RES_X1 = 8, + RES_D1 = 9 +}; + +enum +{ + RESIDUAL_MODE_ZERO = 0, + RESIDUAL_MODE_FIXED_VECTOR = 1, + RESIDUAL_MODE_FIXED_BALL = 2, + RESIDUAL_MODE_FREE_CONE = 3 +}; + +struct distributed_cone_split_s +{ + int num_cones; + int blocks_per_cone; + int *local_start; + int *local_first; + int *local_count; + int *v_dim; + cone_type_t *type; + unsigned char *fixed_mask; + double *stats; + double *complementarity_residual; +}; + +static __device__ inline void project_standard_soc_with_fixed_w( + double vector_norm2, double fixed_w, double input_z, double *vector_factor, double *projected_z) +{ + double fixed_norm2 = fixed_w * fixed_w; + if (input_z >= 0.0 && fixed_norm2 + vector_norm2 <= input_z * input_z) + { + *vector_factor = 1.0; + *projected_z = input_z; + return; + } + if (!(vector_norm2 > 0.0)) + { + *vector_factor = 1.0; + *projected_z = fmax(input_z, fabs(fixed_w)); + return; + } + if (fixed_w == 0.0) + { + double vector_norm = sqrt(vector_norm2); + if (vector_norm <= -input_z) + { + *vector_factor = 0.0; + *projected_z = 0.0; + } + else + { + double scale = (vector_norm + input_z) / (2.0 * vector_norm); + *vector_factor = scale; + *projected_z = scale * vector_norm; + } + return; + } + + double lower; + double upper; + bool lower_branch = input_z > 0.0; + if (input_z == 0.0) + { + *vector_factor = 0.5; + *projected_z = sqrt(fixed_norm2 + 0.25 * vector_norm2); + return; + } + if (lower_branch) + { + lower = 0.0; + upper = 1.0 - 1e-14; + } + else + { + lower = 1.0 + 1e-14; + upper = (1.0 + fabs(input_z) / fabs(fixed_w)) * (1.0 + 64.0 * DBL_EPSILON); + } + + for (int iteration = 0; iteration < 80; ++iteration) + { + double lambda = 0.5 * (lower + upper); + double scaled_norm2 = fixed_norm2 + vector_norm2 / ((1.0 + lambda) * (1.0 + lambda)); + double z = input_z / (1.0 - lambda); + double residual = scaled_norm2 - z * z; + if ((lower_branch && residual > 0.0) || (!lower_branch && residual < 0.0)) + lower = lambda; + else + upper = lambda; + if (upper - lower <= 1e-13 * (1.0 + upper + lower)) + break; + } + double lambda = 0.5 * (lower + upper); + *vector_factor = 1.0 / (1.0 + lambda); + *projected_z = input_z / (1.0 - lambda); +} + +static __global__ void collect_projection_stats_kernel(const double *__restrict__ primal, + const int *__restrict__ local_start, + const int *__restrict__ local_first, + const int *__restrict__ local_count, + const int *__restrict__ v_dim, + double *__restrict__ stats, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + + __shared__ double partial[DIST_CONE_THREADS]; + int first = local_first[cone]; + int count = local_count[cone]; + int start = local_start[cone]; + int k = v_dim[cone]; + double sum = 0.0; + int first_offset = (int)blockIdx.y * blockDim.x + threadIdx.x; + int stride = blockDim.x * gridDim.y; + + for (int offset = first_offset; offset < count; offset += stride) + { + int relative = first + offset; + double value = primal[start + offset]; + if (relative < k) + sum += value * value; + else if (relative == k) + stats[cone * PROJECTION_STATS + 1] = value; + else if (relative == k + 1) + stats[cone * PROJECTION_STATS + 2] = value; + } + + partial[threadIdx.x] = sum; + __syncthreads(); + for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) + { + if (threadIdx.x < stride) + partial[threadIdx.x] += partial[threadIdx.x + stride]; + __syncthreads(); + } + if (threadIdx.x == 0) + atomicAdd(&stats[cone * PROJECTION_STATS], partial[0]); +} + +static __global__ void apply_projection_kernel(double *__restrict__ primal, + const int *__restrict__ local_start, + const int *__restrict__ local_first, + const int *__restrict__ local_count, + const int *__restrict__ v_dim, + const cone_type_t *__restrict__ type, + const unsigned char *__restrict__ fixed_mask, + const double *__restrict__ stats, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + + const double INV_SQRT2 = 0.70710678118654752440; + int first = local_first[cone]; + int count = local_count[cone]; + int start = local_start[cone]; + int k = v_dim[cone]; + unsigned char fixed = fixed_mask[cone]; + double sum_v2 = stats[cone * PROJECTION_STATS]; + double aux0 = stats[cone * PROJECTION_STATS + 1]; + double aux1 = stats[cone * PROJECTION_STATS + 2]; + + double vector_factor = 1.0; + double new_aux0 = aux0; + double new_aux1 = aux1; + bool update_aux0 = false; + bool update_aux1 = false; + int first_offset = (int)blockIdx.y * blockDim.x + threadIdx.x; + int stride = blockDim.x * gridDim.y; + + if (type[cone] == CONE_STANDARD_SOC) + { + bool aux0_fixed = (fixed & PDHCG_DIST_CONE_FIXED_AUX0) != 0; + bool aux1_fixed = (fixed & PDHCG_DIST_CONE_FIXED_AUX1) != 0; + if (aux0_fixed && aux1_fixed) + { + double radius2 = aux1 * aux1 - aux0 * aux0; + if (!(radius2 > 0.0)) + vector_factor = 0.0; + else if (sum_v2 > radius2) + vector_factor = sqrt(radius2 / sum_v2); + } + else if (!aux0_fixed && aux1_fixed) + { + double norm2 = sum_v2 + aux0 * aux0; + double radius2 = aux1 * aux1; + if (!(radius2 > 0.0)) + vector_factor = 0.0; + else if (norm2 > radius2) + vector_factor = sqrt(radius2 / norm2); + new_aux0 = aux0 * vector_factor; + update_aux0 = true; + } + else if (aux0_fixed) + { + project_standard_soc_with_fixed_w(sum_v2, aux0, aux1, &vector_factor, &new_aux1); + update_aux1 = true; + } + else + { + double norm = sqrt(sum_v2 + aux0 * aux0); + update_aux0 = true; + update_aux1 = true; + if (norm <= aux1) + { + vector_factor = 1.0; + } + else if (norm <= -aux1) + { + vector_factor = 0.0; + new_aux0 = 0.0; + new_aux1 = 0.0; + } + else + { + double scale = (norm + aux1) / (2.0 * norm); + vector_factor = scale; + new_aux0 = scale * aux0; + new_aux1 = scale * norm; + } + } + } + else + { + bool both_fixed = fixed == (PDHCG_DIST_CONE_FIXED_AUX0 | PDHCG_DIST_CONE_FIXED_AUX1); + if (both_fixed) + { + double radius2 = 2.0 * aux0 * aux1; + if (!(radius2 > 0.0)) + vector_factor = 0.0; + else if (sum_v2 > radius2) + vector_factor = sqrt(radius2 / sum_v2); + } + else + { + double w = (aux0 - aux1) * INV_SQRT2; + double z = (aux0 + aux1) * INV_SQRT2; + double norm = sqrt(sum_v2 + w * w); + update_aux0 = true; + update_aux1 = true; + if (norm <= z) + { + vector_factor = 1.0; + } + else if (norm <= -z) + { + vector_factor = 0.0; + new_aux0 = 0.0; + new_aux1 = 0.0; + } + else + { + double scale = (norm + z) / (2.0 * norm); + double new_w = scale * w; + double new_z = scale * norm; + vector_factor = scale; + new_aux0 = (new_z + new_w) * INV_SQRT2; + new_aux1 = (new_z - new_w) * INV_SQRT2; + } + } + } + + for (int offset = first_offset; offset < count; offset += stride) + { + int relative = first + offset; + if (relative < k) + primal[start + offset] *= vector_factor; + else if (relative == k && update_aux0) + primal[start + offset] = new_aux0; + else if (relative == k + 1 && update_aux1) + primal[start + offset] = new_aux1; + } +} + +static __global__ void collect_residual_stats_kernel(const double *__restrict__ effective_objective, + const double *__restrict__ dual_product, + const double *__restrict__ primal, + const double *__restrict__ rescaling, + const int *__restrict__ local_start, + const int *__restrict__ local_first, + const int *__restrict__ local_count, + const int *__restrict__ v_dim, + double *__restrict__ stats, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + + __shared__ double partial_r2[DIST_CONE_THREADS]; + __shared__ double partial_dot[DIST_CONE_THREADS]; + __shared__ double partial_n2[DIST_CONE_THREADS]; + __shared__ double partial_xn2[DIST_CONE_THREADS]; + int first = local_first[cone]; + int count = local_count[cone]; + int start = local_start[cone]; + int k = v_dim[cone]; + double r2 = 0.0; + double dot = 0.0; + double n2 = 0.0; + double xn2 = 0.0; + int first_offset = (int)blockIdx.y * blockDim.x + threadIdx.x; + int stride = blockDim.x * gridDim.y; + + for (int offset = first_offset; offset < count; offset += stride) + { + int relative = first + offset; + int index = start + offset; + double r = effective_objective[index] - dual_product[index]; + double x = primal[index]; + double d = rescaling[index]; + if (relative < k) + { + double normal = x / (d * d); + r2 += r * r; + dot += r * normal; + n2 += normal * normal; + xn2 += (x / d) * (x / d); + } + else if (relative == k) + { + stats[cone * RESIDUAL_STATS + RES_R0] = r; + stats[cone * RESIDUAL_STATS + RES_X0] = x; + stats[cone * RESIDUAL_STATS + RES_D0] = d; + } + else if (relative == k + 1) + { + stats[cone * RESIDUAL_STATS + RES_R1] = r; + stats[cone * RESIDUAL_STATS + RES_X1] = x; + stats[cone * RESIDUAL_STATS + RES_D1] = d; + } + } + + partial_r2[threadIdx.x] = r2; + partial_dot[threadIdx.x] = dot; + partial_n2[threadIdx.x] = n2; + partial_xn2[threadIdx.x] = xn2; + __syncthreads(); + for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) + { + if (threadIdx.x < stride) + { + partial_r2[threadIdx.x] += partial_r2[threadIdx.x + stride]; + partial_dot[threadIdx.x] += partial_dot[threadIdx.x + stride]; + partial_n2[threadIdx.x] += partial_n2[threadIdx.x + stride]; + partial_xn2[threadIdx.x] += partial_xn2[threadIdx.x + stride]; + } + __syncthreads(); + } + if (threadIdx.x == 0) + { + atomicAdd(&stats[cone * RESIDUAL_STATS + RES_RV2], partial_r2[0]); + atomicAdd(&stats[cone * RESIDUAL_STATS + RES_DOT], partial_dot[0]); + atomicAdd(&stats[cone * RESIDUAL_STATS + RES_N2], partial_n2[0]); + atomicAdd(&stats[cone * RESIDUAL_STATS + RES_XN2], partial_xn2[0]); + } +} + +static __global__ void apply_residual_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ effective_objective, + const double *__restrict__ dual_product, + const double *__restrict__ primal, + const double *__restrict__ rescaling, + const int *__restrict__ local_start, + const int *__restrict__ local_first, + const int *__restrict__ local_count, + const int *__restrict__ v_dim, + const cone_type_t *__restrict__ type, + const unsigned char *__restrict__ fixed_mask, + const double *__restrict__ stats, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + + const double INV_SQRT2 = 0.70710678118654752440; + int first = local_first[cone]; + int count = local_count[cone]; + int start = local_start[cone]; + int k = v_dim[cone]; + unsigned char fixed = fixed_mask[cone]; + const double *cone_stats = stats + cone * RESIDUAL_STATS; + double rv2 = cone_stats[RES_RV2]; + double r0 = cone_stats[RES_R0]; + double r1 = cone_stats[RES_R1]; + double x0 = cone_stats[RES_X0]; + double d0 = cone_stats[RES_D0]; + double x1 = cone_stats[RES_X1]; + double d1 = cone_stats[RES_D1]; + + double vector_factor = 0.0; + double endpoint_residual0 = 0.0; + double endpoint_residual1 = 0.0; + double lambda = 0.0; + double complementarity = 0.0; + int mode = RESIDUAL_MODE_ZERO; + int first_offset = (int)blockIdx.y * blockDim.x + threadIdx.x; + int stride = blockDim.x * gridDim.y; + + if (type[cone] == CONE_STANDARD_SOC) + { + bool aux0_fixed = (fixed & PDHCG_DIST_CONE_FIXED_AUX0) != 0; + bool aux1_fixed = (fixed & PDHCG_DIST_CONE_FIXED_AUX1) != 0; + if (aux0_fixed && aux1_fixed) + { + double w = x0 / d0; + double z = x1 / d1; + double radius2 = z * z - w * w; + if (radius2 > 0.0) + { + double dot = cone_stats[RES_DOT]; + double n2 = cone_stats[RES_N2]; + lambda = (dot < 0.0 && n2 > 0.0) ? -dot / n2 : 0.0; + complementarity = lambda * fmax(radius2 - cone_stats[RES_XN2], 0.0) / (2.0 * sqrt(radius2)); + mode = RESIDUAL_MODE_FIXED_VECTOR; + } + } + else if (!aux0_fixed && aux1_fixed) + { + double normal0 = x0 / (d0 * d0); + double dot = cone_stats[RES_DOT] + r0 * normal0; + double n2 = cone_stats[RES_N2] + normal0 * normal0; + double xnorm2 = cone_stats[RES_XN2] + (x0 / d0) * (x0 / d0); + double radius = x1 / d1; + double radius2 = radius * radius; + if (radius2 > 0.0) + { + lambda = (dot < 0.0 && n2 > 0.0) ? -dot / n2 : 0.0; + complementarity = lambda * fmax(radius2 - xnorm2, 0.0) / (2.0 * sqrt(radius2)); + mode = RESIDUAL_MODE_FIXED_BALL; + } + } + else if (aux0_fixed) + { + /* The projected-gradient mapping handles this reduced SOC section. */ + mode = RESIDUAL_MODE_ZERO; + } + else + { + double norm = sqrt(rv2 + r0 * r0); + double projected0; + double projected1; + if (norm <= r1) + { + vector_factor = 0.0; + projected0 = r0; + projected1 = r1; + } + else if (norm <= -r1) + { + vector_factor = 1.0; + projected0 = 0.0; + projected1 = 0.0; + } + else + { + double scale = (r1 + norm) / (2.0 * norm); + vector_factor = 1.0 - scale; + projected0 = scale * r0; + projected1 = scale * norm; + } + endpoint_residual0 = (r0 - projected0) * d0; + endpoint_residual1 = (r1 - projected1) * d1; + mode = RESIDUAL_MODE_FREE_CONE; + } + } + else + { + bool both_fixed = fixed == (PDHCG_DIST_CONE_FIXED_AUX0 | PDHCG_DIST_CONE_FIXED_AUX1); + if (both_fixed) + { + double s = x0 / d0; + double t = x1 / d1; + double radius2 = 2.0 * s * t; + if (radius2 > 0.0) + { + double dot = cone_stats[RES_DOT]; + double n2 = cone_stats[RES_N2]; + lambda = (dot < 0.0 && n2 > 0.0) ? -dot / n2 : 0.0; + complementarity = lambda * fmax(radius2 - cone_stats[RES_XN2], 0.0) / (2.0 * sqrt(radius2)); + mode = RESIDUAL_MODE_FIXED_VECTOR; + } + } + else + { + double rw = (r0 - r1) * INV_SQRT2; + double rz = (r0 + r1) * INV_SQRT2; + double norm = sqrt(rv2 + rw * rw); + double projected_s; + double projected_t; + if (norm <= rz) + { + vector_factor = 0.0; + projected_s = r0; + projected_t = r1; + } + else if (norm <= -rz) + { + vector_factor = 1.0; + projected_s = 0.0; + projected_t = 0.0; + } + else + { + double scale = (rz + norm) / (2.0 * norm); + double projected_w = scale * rw; + double projected_z = scale * norm; + vector_factor = 1.0 - scale; + projected_s = (projected_z + projected_w) * INV_SQRT2; + projected_t = (projected_z - projected_w) * INV_SQRT2; + } + endpoint_residual0 = (r0 - projected_s) * d0; + endpoint_residual1 = (r1 - projected_t) * d1; + mode = RESIDUAL_MODE_FREE_CONE; + } + } + + if (first_offset == 0) + complementarity_residual[cone] = complementarity; + + for (int offset = first_offset; offset < count; offset += stride) + { + int relative = first + offset; + int index = start + offset; + double r = effective_objective[index] - dual_product[index]; + double d = rescaling[index]; + if (relative < k) + { + if (mode == RESIDUAL_MODE_FIXED_VECTOR || mode == RESIDUAL_MODE_FIXED_BALL) + { + double normal = primal[index] / (d * d); + double residual = (r + lambda * normal) * d; + dual_residual[index] = residual; + } + else if (mode == RESIDUAL_MODE_ZERO) + dual_residual[index] = 0.0; + else + dual_residual[index] = r * vector_factor * d; + } + else if (relative == k) + { + if (mode == RESIDUAL_MODE_FIXED_BALL) + { + double normal = primal[index] / (d * d); + dual_residual[index] = (r + lambda * normal) * d; + } + else if (mode == RESIDUAL_MODE_FREE_CONE) + dual_residual[index] = endpoint_residual0; + else + dual_residual[index] = 0.0; + } + else if (relative == k + 1) + { + dual_residual[index] = (mode == RESIDUAL_MODE_FREE_CONE) ? endpoint_residual1 : 0.0; + } + } +} + +static __global__ void recompute_reflected_kernel(double *__restrict__ reflected, + const double *__restrict__ primal, + const double *__restrict__ current, + const int *__restrict__ local_start, + const int *__restrict__ local_count, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + int start = local_start[cone]; + int count = local_count[cone]; + int first_offset = (int)blockIdx.y * blockDim.x + threadIdx.x; + int stride = blockDim.x * gridDim.y; + for (int offset = first_offset; offset < count; offset += stride) + reflected[start + offset] = 2.0 * primal[start + offset] - current[start + offset]; +} + +static __global__ void set_dual_slack_kernel(double *__restrict__ dual_slack, + const double *__restrict__ effective_objective, + const double *__restrict__ dual_product, + const int *__restrict__ local_start, + const int *__restrict__ local_count, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + int start = local_start[cone]; + int count = local_count[cone]; + int first_offset = (int)blockIdx.y * blockDim.x + threadIdx.x; + int stride = blockDim.x * gridDim.y; + for (int offset = first_offset; offset < count; offset += stride) + dual_slack[start + offset] = effective_objective[start + offset] - dual_product[start + offset]; +} + +static __global__ void prepare_affine_residuals_kernel(double *__restrict__ projection_point, + const double *__restrict__ primal_product, + const double *__restrict__ affine_cone_offset, + const double *__restrict__ dual_solution, + const int *__restrict__ local_start, + const int *__restrict__ local_count, + double *__restrict__ dot_products, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + + __shared__ double partial[DIST_CONE_THREADS]; + int start = local_start[cone]; + int count = local_count[cone]; + double dot = 0.0; + int first_offset = (int)blockIdx.y * blockDim.x + threadIdx.x; + int stride = blockDim.x * gridDim.y; + for (int offset = first_offset; offset < count; offset += stride) + { + int index = start + offset; + double dual = dual_solution[index]; + projection_point[index] = -dual; + dot += dual * (primal_product[index] + affine_cone_offset[index]); + } + partial[threadIdx.x] = dot; + __syncthreads(); + for (int reduction_stride = blockDim.x / 2; reduction_stride > 0; reduction_stride >>= 1) + { + if (threadIdx.x < reduction_stride) + partial[threadIdx.x] += partial[threadIdx.x + reduction_stride]; + __syncthreads(); + } + if (threadIdx.x == 0) + atomicAdd(dot_products + cone, partial[0]); +} + +static __global__ void finalize_affine_complementarity_kernel(double *__restrict__ complementarity_residual, + double constraint_bound_rescaling, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone < num_cones) + complementarity_residual[cone] = fabs(complementarity_residual[cone]) / constraint_bound_rescaling; +} + +static void copy_int_array(int **device, const int *host, int count) +{ + CUDA_CHECK(cudaMalloc(device, (size_t)count * sizeof(int))); + CUDA_CHECK(cudaMemcpy(*device, host, (size_t)count * sizeof(int), cudaMemcpyHostToDevice)); +} + +static distributed_cone_split_t *allocate_split_runtime(pdhg_solver_state_t *state, + const distributed_cone_partition_t *partition, + const double *coordinate_rescaling, + MPI_Comm communicator, + const char *axis_name) +{ + if (!partition || partition->num_cones <= 0) + return NULL; + + int K = partition->num_cones; + distributed_cone_split_t *split = (distributed_cone_split_t *)safe_calloc(1, sizeof(distributed_cone_split_t)); + split->num_cones = K; + int max_local_count = 0; + for (int cone = 0; cone < K; ++cone) + if (partition->local_count[cone] > max_local_count) + max_local_count = partition->local_count[cone]; + + int device = 0; + cudaDeviceProp properties; + CUDA_CHECK(cudaGetDevice(&device)); + CUDA_CHECK(cudaGetDeviceProperties(&properties, device)); + int desired_blocks = (max_local_count + DIST_CONE_THREADS - 1) / DIST_CONE_THREADS; + int block_budget = (4 * properties.multiProcessorCount) / K; + desired_blocks = desired_blocks > 0 ? desired_blocks : 1; + block_budget = block_budget > 0 ? block_budget : 1; + block_budget = block_budget < 64 ? block_budget : 64; + split->blocks_per_cone = desired_blocks < block_budget ? desired_blocks : block_budget; + + copy_int_array(&split->local_start, partition->local_start, K); + copy_int_array(&split->local_first, partition->local_first, K); + copy_int_array(&split->local_count, partition->local_count, K); + copy_int_array(&split->v_dim, partition->v_dim, K); + CUDA_CHECK(cudaMalloc(&split->type, (size_t)K * sizeof(cone_type_t))); + CUDA_CHECK(cudaMemcpy(split->type, partition->type, (size_t)K * sizeof(cone_type_t), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMalloc(&split->fixed_mask, (size_t)K * sizeof(unsigned char))); + CUDA_CHECK(cudaMemcpy( + split->fixed_mask, partition->fixed_mask, (size_t)K * sizeof(unsigned char), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMalloc(&split->stats, (size_t)K * RESIDUAL_STATS * sizeof(double))); + CUDA_CHECK(cudaMalloc(&split->complementarity_residual, (size_t)K * sizeof(double))); + CUDA_CHECK(cudaMemset(split->complementarity_residual, 0, (size_t)K * sizeof(double))); + + double *local_min = (double *)malloc((size_t)K * sizeof(double)); + double *local_max = (double *)malloc((size_t)K * sizeof(double)); + double *global_min = (double *)malloc((size_t)K * sizeof(double)); + double *global_max = (double *)malloc((size_t)K * sizeof(double)); + for (int cone = 0; cone < K; ++cone) + { + local_min[cone] = DBL_MAX; + local_max[cone] = 0.0; + int start = partition->local_start[cone]; + for (int slot = 0; slot < partition->local_count[cone]; ++slot) + { + double d = coordinate_rescaling[start + slot]; + local_min[cone] = fmin(local_min[cone], d); + local_max[cone] = fmax(local_max[cone], d); + } + } + MPI_Allreduce(local_min, global_min, K, MPI_DOUBLE, MPI_MIN, communicator); + MPI_Allreduce(local_max, global_max, K, MPI_DOUBLE, MPI_MAX, communicator); + for (int cone = 0; cone < K; ++cone) + { + if (!(global_min[cone] > 0.0) || fabs(global_max[cone] - global_min[cone]) > 1e-12 * (1.0 + global_max[cone])) + { + fprintf(stderr, + "Error: split %s cone %d has heterogeneous coordinate scaling; keep it on one GPU or disable " + "the split.\n", + axis_name, + cone); + MPI_Abort(state->grid_context->comm_global, EXIT_FAILURE); + } + } + free(local_min); + free(local_max); + free(global_min); + free(global_max); + return split; +} + +void initialize_split_cones(pdhg_solver_state_t *state, const rescale_info_t *rescale_info) +{ + state->cones.split = NULL; + state->affine_cones.split = NULL; + if (!state->grid_context) + return; + + const distributed_cone_partition_t *partition = &state->grid_context->split_cones; + int K = partition->num_cones; + distributed_cone_split_t *split = + allocate_split_runtime(state, partition, rescale_info->var_rescale, state->grid_context->comm_row, "variable"); + state->cones.split = split; + + if (split && rescale_info->processed_problem && rescale_info->processed_problem->quad_type == PDHCG_DIAG_Q) + { + double local_max_q = 0.0; + const double *diag = rescale_info->processed_problem->diagonal_quad_objective; + for (int cone = 0; cone < K; ++cone) + { + int start = partition->local_start[cone]; + for (int slot = 0; slot < partition->local_count[cone]; ++slot) + local_max_q = fmax(local_max_q, fabs(diag[start + slot])); + } + double global_max_q = 0.0; + MPI_Allreduce(&local_max_q, &global_max_q, 1, MPI_DOUBLE, MPI_MAX, state->grid_context->comm_row); + if (global_max_q != 0.0) + { + fprintf(stderr, "Error: a split cone has a nonzero diagonal quadratic objective coefficient.\n"); + MPI_Abort(state->grid_context->comm_global, EXIT_FAILURE); + } + } + + const double INV_SQRT2 = 0.70710678118654752440; + for (int cone = 0; split && cone < K; ++cone) + { + int first = partition->local_first[cone]; + int count = partition->local_count[cone]; + int local_start = partition->local_start[cone]; + int k = partition->v_dim[cone]; + unsigned char fixed = partition->fixed_mask[cone]; + for (int endpoint = 0; endpoint < 2; ++endpoint) + { + int relative = k + endpoint; + if (relative < first || relative >= first + count) + continue; + bool pinned = + endpoint == 0 ? (fixed & PDHCG_DIST_CONE_FIXED_AUX0) != 0 : (fixed & PDHCG_DIST_CONE_FIXED_AUX1) != 0; + if (pinned) + continue; + int index = local_start + relative - first; + double value = 0.0; + if (partition->type[cone] == CONE_STANDARD_SOC) + { + value = (endpoint == 0 ? -INV_SQRT2 : INV_SQRT2) * rescale_info->con_bound_rescale * + rescale_info->var_rescale[index]; + } + else if (endpoint == 1) + { + value = rescale_info->con_bound_rescale * rescale_info->var_rescale[index]; + } + double *destinations[] = {state->initial_primal_solution, + state->current_primal_solution, + state->pdhg_primal_solution, + state->reflected_primal_solution}; + for (double *destination : destinations) + CUDA_CHECK(cudaMemcpy(destination + index, &value, sizeof(double), cudaMemcpyHostToDevice)); + } + } + + const distributed_cone_partition_t *affine_partition = &state->grid_context->split_affine_cones; + state->affine_cones.split = allocate_split_runtime( + state, affine_partition, rescale_info->con_rescale, state->grid_context->comm_col, "affine"); +} + +static void free_split_runtime(distributed_cone_split_t *split) +{ + if (!split) + return; + CUDA_CHECK(cudaFree(split->local_start)); + CUDA_CHECK(cudaFree(split->local_first)); + CUDA_CHECK(cudaFree(split->local_count)); + CUDA_CHECK(cudaFree(split->v_dim)); + CUDA_CHECK(cudaFree(split->type)); + CUDA_CHECK(cudaFree(split->fixed_mask)); + CUDA_CHECK(cudaFree(split->stats)); + CUDA_CHECK(cudaFree(split->complementarity_residual)); + free(split); +} + +void free_split_cones(pdhg_solver_state_t *state) +{ + if (!state) + return; + free_split_runtime(state->cones.split); + free_split_runtime(state->affine_cones.split); + state->cones.split = NULL; + state->affine_cones.split = NULL; +} + +void project_split_cones(pdhg_solver_state_t *state, cone_runtime_t *runtime, double *vector) +{ + distributed_cone_split_t *split = runtime ? runtime->split : NULL; + if (!split || split->num_cones <= 0) + return; + int K = split->num_cones; + dim3 grid((unsigned int)K, (unsigned int)split->blocks_per_cone); + CUDA_CHECK(cudaMemset(split->stats, 0, (size_t)K * PROJECTION_STATS * sizeof(double))); + collect_projection_stats_kernel<<>>( + vector, split->local_start, split->local_first, split->local_count, split->v_dim, split->stats, K); + CUDA_CHECK(cudaGetLastError()); + pdhcg_comm_scope_t scope = runtime->axis == CONE_AXIS_VARIABLE ? PDHCG_SCOPE_ROW : PDHCG_SCOPE_COL; + pdhcg_all_reduce_array(state->grid_context, split->stats, K * PROJECTION_STATS, PDHCG_OP_SUM, scope, 0); + apply_projection_kernel<<>>(vector, + split->local_start, + split->local_first, + split->local_count, + split->v_dim, + split->type, + split->fixed_mask, + split->stats, + K); + CUDA_CHECK(cudaGetLastError()); +} + +void recompute_split_cone_reflected(pdhg_solver_state_t *state, + double *reflected_primal, + const double *pdhg_primal, + const double *current_primal) +{ + distributed_cone_split_t *split = state->cones.split; + if (!split || split->num_cones <= 0) + return; + dim3 grid((unsigned int)split->num_cones, (unsigned int)split->blocks_per_cone); + recompute_reflected_kernel<<>>( + reflected_primal, pdhg_primal, current_primal, split->local_start, split->local_count, split->num_cones); + CUDA_CHECK(cudaGetLastError()); +} + +void compute_split_cone_dual_residual(pdhg_solver_state_t *state, const double *effective_objective) +{ + distributed_cone_split_t *split = state->cones.split; + if (!split || split->num_cones <= 0) + return; + int K = split->num_cones; + dim3 grid((unsigned int)K, (unsigned int)split->blocks_per_cone); + CUDA_CHECK(cudaMemset(split->stats, 0, (size_t)K * RESIDUAL_STATS * sizeof(double))); + CUDA_CHECK(cudaMemset(split->complementarity_residual, 0, (size_t)K * sizeof(double))); + collect_residual_stats_kernel<<>>(effective_objective, + state->dual_product, + state->pdhg_primal_solution, + state->variable_rescaling, + split->local_start, + split->local_first, + split->local_count, + split->v_dim, + split->stats, + K); + CUDA_CHECK(cudaGetLastError()); + pdhcg_all_reduce_array(state->grid_context, split->stats, K * RESIDUAL_STATS, PDHCG_OP_SUM, PDHCG_SCOPE_ROW, 0); + apply_residual_kernel<<>>(state->dual_residual, + split->complementarity_residual, + effective_objective, + state->dual_product, + state->pdhg_primal_solution, + state->variable_rescaling, + split->local_start, + split->local_first, + split->local_count, + split->v_dim, + split->type, + split->fixed_mask, + split->stats, + K); + CUDA_CHECK(cudaGetLastError()); +} + +double get_split_cone_complementarity_norm(pdhg_solver_state_t *state, norm_type_t norm) +{ + distributed_cone_split_t *split = state->cones.split; + if (!split || split->num_cones <= 0 || !state->grid_context || state->grid_context->coords[1] != 0) + return 0.0; + + if (norm == NORM_TYPE_L_INF) + return get_vector_inf_norm(state->blas_handle, split->num_cones, split->complementarity_residual); + + double residual_norm = 0.0; + CUBLAS_CHECK( + cublasDnrm2_v2_64(state->blas_handle, split->num_cones, split->complementarity_residual, 1, &residual_norm)); + return residual_norm; +} + +void finalize_split_affine_cone_complementarity(pdhg_solver_state_t *state) +{ + distributed_cone_split_t *split = state->affine_cones.split; + if (!split || split->num_cones <= 0) + return; + + int K = split->num_cones; + pdhcg_all_reduce_array(state->grid_context, split->complementarity_residual, K, PDHCG_OP_SUM, PDHCG_SCOPE_COL, 0); + int blocks = (K + DIST_CONE_THREADS - 1) / DIST_CONE_THREADS; + finalize_affine_complementarity_kernel<<>>( + split->complementarity_residual, state->constraint_bound_rescaling, K); + CUDA_CHECK(cudaGetLastError()); +} + +void prepare_split_affine_cone_residuals(pdhg_solver_state_t *state, + double *projection_point, + const double *primal_product, + const double *affine_cone_offset, + const double *dual_solution) +{ + distributed_cone_split_t *split = state->affine_cones.split; + if (!split || split->num_cones <= 0) + return; + int K = split->num_cones; + dim3 grid((unsigned int)K, (unsigned int)split->blocks_per_cone); + /* Keep the dot products separate from stats, which split projection reuses. */ + CUDA_CHECK(cudaMemset(split->complementarity_residual, 0, (size_t)K * sizeof(double))); + prepare_affine_residuals_kernel<<>>(projection_point, + primal_product, + affine_cone_offset, + dual_solution, + split->local_start, + split->local_count, + split->complementarity_residual, + K); + CUDA_CHECK(cudaGetLastError()); +} + +double get_split_affine_cone_complementarity_norm(pdhg_solver_state_t *state, norm_type_t norm) +{ + distributed_cone_split_t *split = state->affine_cones.split; + if (!split || split->num_cones <= 0 || !state->grid_context || state->grid_context->coords[0] != 0) + return 0.0; + + if (norm == NORM_TYPE_L_INF) + return get_vector_inf_norm(state->blas_handle, split->num_cones, split->complementarity_residual); + + double residual_norm = 0.0; + CUBLAS_CHECK( + cublasDnrm2_v2_64(state->blas_handle, split->num_cones, split->complementarity_residual, 1, &residual_norm)); + return residual_norm; +} + +void set_split_cone_dual_slack(pdhg_solver_state_t *state, + double *dual_slack, + const double *effective_objective, + const double *dual_product) +{ + distributed_cone_split_t *split = state->cones.split; + if (!split || split->num_cones <= 0) + return; + dim3 grid((unsigned int)split->num_cones, (unsigned int)split->blocks_per_cone); + set_dual_slack_kernel<<>>( + dual_slack, effective_objective, dual_product, split->local_start, split->local_count, split->num_cones); + CUDA_CHECK(cudaGetLastError()); +} diff --git a/distributed/distributed_conic.h b/distributed/distributed_conic.h new file mode 100644 index 0000000..529347f --- /dev/null +++ b/distributed/distributed_conic.h @@ -0,0 +1,33 @@ +#pragma once + +#include "internal_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + void initialize_split_cones(pdhg_solver_state_t *state, const rescale_info_t *rescale_info); + void free_split_cones(pdhg_solver_state_t *state); + void project_split_cones(pdhg_solver_state_t *state, cone_runtime_t *runtime, double *vector); + void recompute_split_cone_reflected(pdhg_solver_state_t *state, + double *reflected_primal, + const double *pdhg_primal, + const double *current_primal); + void compute_split_cone_dual_residual(pdhg_solver_state_t *state, const double *effective_objective); + double get_split_cone_complementarity_norm(pdhg_solver_state_t *state, norm_type_t norm); + void prepare_split_affine_cone_residuals(pdhg_solver_state_t *state, + double *projection_point, + const double *primal_product, + const double *affine_cone_offset, + const double *dual_solution); + void finalize_split_affine_cone_complementarity(pdhg_solver_state_t *state); + double get_split_affine_cone_complementarity_norm(pdhg_solver_state_t *state, norm_type_t norm); + void set_split_cone_dual_slack(pdhg_solver_state_t *state, + double *dual_slack, + const double *effective_objective, + const double *dual_product); + +#ifdef __cplusplus +} +#endif diff --git a/distributed/distributed_conic_stub.c b/distributed/distributed_conic_stub.c new file mode 100644 index 0000000..6fa7821 --- /dev/null +++ b/distributed/distributed_conic_stub.c @@ -0,0 +1,95 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "distributed_conic.h" + +void initialize_split_cones(pdhg_solver_state_t *state, const rescale_info_t *rescale_info) +{ + (void)state; + (void)rescale_info; +} + +void free_split_cones(pdhg_solver_state_t *state) +{ + (void)state; +} + +void project_split_cones(pdhg_solver_state_t *state, cone_runtime_t *runtime, double *vector) +{ + (void)state; + (void)runtime; + (void)vector; +} + +void recompute_split_cone_reflected(pdhg_solver_state_t *state, + double *reflected_primal, + const double *pdhg_primal, + const double *current_primal) +{ + (void)state; + (void)reflected_primal; + (void)pdhg_primal; + (void)current_primal; +} + +void compute_split_cone_dual_residual(pdhg_solver_state_t *state, const double *effective_objective) +{ + (void)state; + (void)effective_objective; +} + +double get_split_cone_complementarity_norm(pdhg_solver_state_t *state, norm_type_t norm) +{ + (void)state; + (void)norm; + return 0.0; +} + +void prepare_split_affine_cone_residuals(pdhg_solver_state_t *state, + double *projection_point, + const double *primal_product, + const double *affine_cone_offset, + const double *dual_solution) +{ + (void)state; + (void)projection_point; + (void)primal_product; + (void)affine_cone_offset; + (void)dual_solution; +} + +void finalize_split_affine_cone_complementarity(pdhg_solver_state_t *state) +{ + (void)state; +} + +double get_split_affine_cone_complementarity_norm(pdhg_solver_state_t *state, norm_type_t norm) +{ + (void)state; + (void)norm; + return 0.0; +} + +void set_split_cone_dual_slack(pdhg_solver_state_t *state, + double *dual_slack, + const double *effective_objective, + const double *dual_product) +{ + (void)state; + (void)dual_slack; + (void)effective_objective; + (void)dual_product; +} diff --git a/distributed/distributed_ops_real.cu b/distributed/distributed_ops_real.cu index ce7da69..e046c73 100644 --- a/distributed/distributed_ops_real.cu +++ b/distributed/distributed_ops_real.cu @@ -15,6 +15,7 @@ limitations under the License. */ #include "distributed_interface.h" +#include "distributed_solver.h" #include "distributed_types.h" #include "distributed_utils.h" #include @@ -91,3 +92,28 @@ int pdhcg_get_grid_row_coord(struct grid_context_s *ctx) { return ctx ? ctx->coords[0] : 0; } + +int pdhcg_get_global_num_variables(grid_context_t *ctx) +{ + return ctx ? ctx->global_num_variables : 0; +} + +int pdhcg_get_variable_start(grid_context_t *ctx) +{ + return ctx ? ctx->n_start : 0; +} + +int pdhcg_get_global_num_cones(grid_context_t *ctx) +{ + return ctx ? ctx->global_num_cones : 0; +} + +int pdhcg_get_global_num_affine_cones(grid_context_t *ctx) +{ + return ctx ? ctx->global_num_affine_cones : 0; +} + +pdhcg_result_t *pdhcg_distributed_optimize(const pdhg_parameters_t *params, const qp_problem_t *original_problem) +{ + return distributed_optimize(params, original_problem); +} diff --git a/distributed/distributed_ops_stub.c b/distributed/distributed_ops_stub.c index 0829bf0..410dc16 100644 --- a/distributed/distributed_ops_stub.c +++ b/distributed/distributed_ops_stub.c @@ -16,6 +16,7 @@ limitations under the License. #include "distributed_interface.h" #include +#include void pdhcg_all_reduce_array( grid_context_t *ctx, double *buf, int count, pdhcg_reduce_op_t op, pdhcg_comm_scope_t scope, void *stream) @@ -49,3 +50,35 @@ int pdhcg_get_grid_row_coord(struct grid_context_s *ctx) (void)ctx; return 0; } + +int pdhcg_get_global_num_variables(grid_context_t *ctx) +{ + (void)ctx; + return 0; +} + +int pdhcg_get_variable_start(grid_context_t *ctx) +{ + (void)ctx; + return 0; +} + +int pdhcg_get_global_num_cones(grid_context_t *ctx) +{ + (void)ctx; + return 0; +} + +int pdhcg_get_global_num_affine_cones(grid_context_t *ctx) +{ + (void)ctx; + return 0; +} + +pdhcg_result_t *pdhcg_distributed_optimize(const pdhg_parameters_t *params, const qp_problem_t *original_problem) +{ + (void)params; + (void)original_problem; + fprintf(stderr, "[interface] distributed support is not enabled in this build.\n"); + return NULL; +} diff --git a/distributed/distributed_solver.cu b/distributed/distributed_solver.cu index 959dfaf..caef8c4 100644 --- a/distributed/distributed_solver.cu +++ b/distributed/distributed_solver.cu @@ -24,6 +24,7 @@ limitations under the License. #include "permute.h" #include "preconditioner.h" #include "presolve_wrapper.h" +#include "qcqp_transform.h" #include "solver.h" #include "solver_state.h" #include "spmv_backend.h" @@ -289,6 +290,7 @@ static pdhcg_result_t *distributed_optimize_core(const pdhg_parameters_t *params pdhg_solver_state_t *state = initialize_solver_state(params, local_working_problem, local_rescale_info, grid_context); + qp_problem_free(local_working_problem); allreduce_obj_bound_norm(state, params); @@ -315,7 +317,8 @@ static pdhcg_result_t *distributed_optimize_core(const pdhg_parameters_t *params { compute_residual(state, params->optimality_norm); - if (state->is_this_major_iteration && state->total_count < 3 * params->termination_evaluation_frequency) + if (!state->has_variable_cones && state->grid_context->global_num_affine_cones == 0 && + state->is_this_major_iteration && state->total_count < 3 * params->termination_evaluation_frequency) { compute_infeasibility_information(state); } @@ -382,7 +385,64 @@ static pdhcg_result_t *distributed_optimize_core(const pdhg_parameters_t *params pdhcg_result_t *distributed_optimize(const pdhg_parameters_t *params, const qp_problem_t *original_problem) { + pdhg_parameters_t default_params; + if (!params) + { + set_default_parameters(&default_params); + params = &default_params; + } pdhg_parameters_t sub_params = *params; + int rank_global = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank_global); + + int fixed_section_invalid = 0; + if (rank_global == 0 && pdhcg_validate_fixed_cone_sections(original_problem) != 0) + fixed_section_invalid = 1; + MPI_Bcast(&fixed_section_invalid, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (fixed_section_invalid) + return NULL; + + const qp_problem_t *input_problem = original_problem; + qp_problem_t *transformed = NULL; + int transform_failed = 0; + if (rank_global == 0) + { + if (!original_problem) + { + fprintf(stderr, "Error: rank 0 did not provide a problem.\n"); + transform_failed = 1; + } + else if (original_problem->num_quadratic_constraints > 0) + { + transformed = qcqp_to_socp_qp(original_problem, params->default_cone_type); + if (!transformed) + { + fprintf(stderr, "Error: distributed QCQP -> SOCP transformation failed.\n"); + transform_failed = 1; + } + else + { + original_problem = transformed; + if (params->verbose >= 1) + { + const char *form_name = params->default_cone_type == CONE_STANDARD_SOC ? "standard" : "rotated"; + fprintf(stderr, + "[QCQP] %d quadratic constraint(s) reformulated as %d %s SOC block(s) before " + "distributed partitioning.\n", + input_problem->num_quadratic_constraints, + transformed->cones.num_cones, + form_name); + } + } + } + } + MPI_Bcast(&transform_failed, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (transform_failed) + { + if (transformed) + qp_problem_free(transformed); + return NULL; + } select_valid_grid_size(params, original_problem, &sub_params); @@ -403,7 +463,7 @@ pdhcg_result_t *distributed_optimize(const pdhg_parameters_t *params, const qp_p if (grid_context.rank_global == 0) { - if (params->presolve && pdhcg_presolve_available()) + if (params->presolve && original_problem->affine_cones.num_cones == 0 && pdhcg_presolve_available()) { presolve_info = pdhcg_presolve(original_problem, params); if (presolve_info) @@ -433,16 +493,10 @@ pdhcg_result_t *distributed_optimize(const pdhg_parameters_t *params, const qp_p row_perm = (int *)malloc(working_problem->num_constraints * sizeof(int)); col_perm = (int *)malloc(working_problem->num_variables * sizeof(int)); - if (params->permute_method == FULL_RANDOM_PERMUTATION) - { - generate_random_permutation(working_problem->num_variables, col_perm); - generate_random_permutation(working_problem->num_constraints, row_perm); - } - else if (params->permute_method == BLOCK_RANDOM_PERMUTATION) - { - generate_block_permutation(working_problem->num_variables, params->permute_block_size, col_perm); - generate_block_permutation(working_problem->num_constraints, params->permute_block_size, row_perm); - } + generate_cone_aware_permutation( + working_problem, params->permute_method, params->permute_block_size, col_perm); + generate_affine_cone_aware_row_permutation( + working_problem, params->permute_method, params->permute_block_size, row_perm); permuted_problem = permute_problem_return_new(working_problem, row_perm, col_perm); working_problem = permuted_problem; @@ -456,12 +510,17 @@ pdhcg_result_t *distributed_optimize(const pdhg_parameters_t *params, const qp_p if (grid_context.rank_global == 0) { result = pdhcg_create_result_from_presolve(presolve_info, original_problem); + restore_qcqp_result_dimensions(result, transformed ? input_problem : NULL); if (result) pdhg_final_log(result, params); if (presolve_info) pdhcg_presolve_info_free(presolve_info); + if (transformed) + qp_problem_free(transformed); + destroy_parallel_context(&grid_context); return result; } + destroy_parallel_context(&grid_context); return NULL; } @@ -488,14 +547,31 @@ pdhcg_result_t *distributed_optimize(const pdhg_parameters_t *params, const qp_p qp_problem_free(dummy_problem); } + restore_qcqp_result_dimensions(result, transformed ? input_problem : NULL); pdhg_final_log(result, params); if (presolve_info) pdhcg_presolve_info_free(presolve_info); + if (transformed) + qp_problem_free(transformed); } else if (grid_context.rank_global != 0) { result = NULL; } + else + { + free(row_perm); + free(col_perm); + if (permuted_problem) + qp_problem_free(permuted_problem); + if (dummy_problem) + qp_problem_free(dummy_problem); + if (presolve_info) + pdhcg_presolve_info_free(presolve_info); + if (transformed) + qp_problem_free(transformed); + } + destroy_parallel_context(&grid_context); return result; } diff --git a/distributed/distributed_types.h b/distributed/distributed_types.h index 766bace..d03ccc9 100644 --- a/distributed/distributed_types.h +++ b/distributed/distributed_types.h @@ -16,9 +16,28 @@ limitations under the License. #pragma once +#include "pdhcg_types.h" #include #include +typedef struct distributed_cone_partition_s +{ + int num_cones; + int *v_dim; + cone_type_t *type; + unsigned char *fixed_mask; + int *local_start; + int *local_first; + int *local_count; +} distributed_cone_partition_t; + +enum +{ + PDHCG_DIST_CONE_FIXED_AUX0 = 1, + PDHCG_DIST_CONE_FIXED_AUX1 = 2, + PDHCG_DIST_CONE_FIXED_VECTOR = 4 +}; + struct grid_context_s { MPI_Comm comm_global; @@ -31,11 +50,12 @@ struct grid_context_s int coords[2]; int dims[2]; int global_num_variables; + int global_num_cones; + int global_num_affine_cones; int n_start; + int n_end; + int *variable_cuts; + int *constraint_cuts; + distributed_cone_partition_t split_cones; + distributed_cone_partition_t split_affine_cones; }; - -typedef struct -{ - MPI_Request *reqs; - int num_reqs; -} big_request_t; diff --git a/distributed/distributed_utils.cu b/distributed/distributed_utils.cu index ba86354..c5dcddd 100644 --- a/distributed/distributed_utils.cu +++ b/distributed/distributed_utils.cu @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +#include "cone_utils.h" #include "distributed_types.h" #include "distributed_utils.h" #include "internal_types.h" +#include "partition_utils.h" #include "solver_state.h" #include "utils.h" #include @@ -51,6 +53,7 @@ extern "C" grid_context_t initialize_parallel_context(int P_row, int P_col) { grid_context_t grid; + memset(&grid, 0, sizeof(grid)); int initialized; int world_size; @@ -139,6 +142,40 @@ extern "C" return grid; } } + +static void free_distributed_cone_partition(distributed_cone_partition_t *partition) +{ + if (!partition) + return; + free(partition->v_dim); + free(partition->type); + free(partition->fixed_mask); + free(partition->local_start); + free(partition->local_first); + free(partition->local_count); + memset(partition, 0, sizeof(*partition)); +} + +void destroy_parallel_context(grid_context_t *grid) +{ + if (!grid) + return; + + free(grid->variable_cuts); + free(grid->constraint_cuts); + free_distributed_cone_partition(&grid->split_cones); + free_distributed_cone_partition(&grid->split_affine_cones); + + NCCL_CHECK(ncclCommDestroy(grid->nccl_row)); + NCCL_CHECK(ncclCommDestroy(grid->nccl_col)); + NCCL_CHECK(ncclCommDestroy(grid->nccl_global)); + if (grid->comm_row != MPI_COMM_NULL) + MPI_Comm_free(&grid->comm_row); + if (grid->comm_col != MPI_COMM_NULL) + MPI_Comm_free(&grid->comm_col); + memset(grid, 0, sizeof(*grid)); +} + int *get_balanced_cuts(const int *weights, int total_dim, int num_partitions) { int *cuts = (int *)malloc((num_partitions + 1) * sizeof(int)); @@ -178,6 +215,291 @@ int *get_balanced_cuts(const int *weights, int total_dim, int num_partitions) return cuts; } +static unsigned char distributed_cone_fixed_mask(const qp_problem_t *problem, int cone) +{ + if (!problem->cones.is_fixed) + return 0; + + int start = problem->cones.start_idx[cone]; + int k = problem->cones.v_dim[cone]; + int length = cone_block_length(&problem->cones, cone); + unsigned char mask = 0; + for (int slot = 0; slot < length - 2; ++slot) + if (problem->cones.is_fixed[start + slot]) + mask |= PDHCG_DIST_CONE_FIXED_VECTOR; + if (problem->cones.is_fixed[start + k]) + mask |= PDHCG_DIST_CONE_FIXED_AUX0; + if (problem->cones.is_fixed[start + k + 1]) + mask |= PDHCG_DIST_CONE_FIXED_AUX1; + return mask; +} + +static bool objective_is_pure_diagonal(const qp_problem_t *problem) +{ + if (problem->num_rank_lowrank_obj > 0 || !problem->objective_sparse_matrix) + return problem->num_rank_lowrank_obj == 0; + for (int row = 0; row < problem->num_variables; ++row) + { + for (int nz = problem->objective_sparse_matrix->row_ptr[row]; + nz < problem->objective_sparse_matrix->row_ptr[row + 1]; + ++nz) + { + if (problem->objective_sparse_matrix->col_ind[nz] != row && + problem->objective_sparse_matrix->val[nz] != 0.0) + return false; + } + } + return true; +} + +static bool cone_has_diagonal_objective(const qp_problem_t *problem, int cone) +{ + if (!problem->objective_sparse_matrix || !objective_is_pure_diagonal(problem)) + return false; + int start = problem->cones.start_idx[cone]; + int end = start + cone_block_length(&problem->cones, cone); + for (int row = start; row < end; ++row) + { + for (int nz = problem->objective_sparse_matrix->row_ptr[row]; + nz < problem->objective_sparse_matrix->row_ptr[row + 1]; + ++nz) + { + if (problem->objective_sparse_matrix->col_ind[nz] == row && + problem->objective_sparse_matrix->val[nz] != 0.0) + return true; + } + } + return false; +} + +static bool cone_can_span_gpus(const qp_problem_t *problem, int cone, const pdhg_parameters_t *params) +{ + cone_type_t type = problem->cones.type[cone]; + if (type != CONE_STANDARD_SOC && type != CONE_ROTATED_SOC) + return false; + if (!params->use_cone_preserving_scaling || cone_has_diagonal_objective(problem, cone)) + return false; + + unsigned char fixed = distributed_cone_fixed_mask(problem, cone); + if (fixed & PDHCG_DIST_CONE_FIXED_VECTOR) + return false; + if (type == CONE_STANDARD_SOC) + return fixed == 0 || fixed == PDHCG_DIST_CONE_FIXED_AUX0 || fixed == PDHCG_DIST_CONE_FIXED_AUX1 || + fixed == (PDHCG_DIST_CONE_FIXED_AUX0 | PDHCG_DIST_CONE_FIXED_AUX1); + return fixed == 0 || fixed == (PDHCG_DIST_CONE_FIXED_AUX0 | PDHCG_DIST_CONE_FIXED_AUX1); +} + +static bool affine_cone_can_span_gpus(const qp_problem_t *problem, int cone, const pdhg_parameters_t *params) +{ + cone_type_t type = problem->affine_cones.type[cone]; + return (type == CONE_STANDARD_SOC || type == CONE_ROTATED_SOC) && params->use_cone_preserving_scaling; +} + +static int *get_uniform_cuts(int total_dim, int num_partitions) +{ + int *cuts = (int *)malloc((size_t)(num_partitions + 1) * sizeof(int)); + cuts[0] = 0; + cuts[num_partitions] = total_dim; + int chunk = total_dim / num_partitions; + for (int part = 1; part < num_partitions; ++part) + cuts[part] = part * chunk; + return cuts; +} + +static int find_partition(const int *cuts, int num_partitions, int index) +{ + for (int part = 0; part < num_partitions; ++part) + if (index >= cuts[part] && index < cuts[part + 1]) + return part; + return num_partitions - 1; +} + +static bool cone_can_span_partition(const qp_problem_t *problem, int cone, bool affine, const pdhg_parameters_t *params) +{ + return affine ? affine_cone_can_span_gpus(problem, cone, params) : cone_can_span_gpus(problem, cone, params); +} + +static bool adjust_cuts_for_cones(const qp_problem_t *problem, + const cone_blocks_t *cones, + int total_dim, + int num_partitions, + bool affine, + const pdhg_parameters_t *params, + int *cuts) +{ + int target_size = (total_dim + num_partitions - 1) / num_partitions; + int *forbidden_starts = cones->num_cones > 0 ? (int *)safe_malloc((size_t)cones->num_cones * sizeof(int)) : NULL; + int *forbidden_ends = cones->num_cones > 0 ? (int *)safe_malloc((size_t)cones->num_cones * sizeof(int)) : NULL; + int num_intervals = 0; + for (int cone = 0; cone < cones->num_cones; ++cone) + { + int start = cones->start_idx[cone]; + int end = start + cone_block_length(cones, cone); + bool may_split = cone_can_span_partition(problem, cone, affine, params) && end - start > target_size; + if (!may_split && end - start > 1) + { + forbidden_starts[num_intervals] = start + 1; + forbidden_ends[num_intervals] = end - 1; + ++num_intervals; + } + } + bool success = + optimize_partition_cuts(total_dim, num_partitions, forbidden_starts, forbidden_ends, num_intervals, cuts); + free(forbidden_starts); + free(forbidden_ends); + return success; +} + +static void build_split_partition(const qp_problem_t *problem, + const cone_blocks_t *cones, + const int *cuts, + int num_partitions, + int local_partition, + bool affine, + const pdhg_parameters_t *params, + grid_context_t *grid, + distributed_cone_partition_t *partition) +{ + free_distributed_cone_partition(partition); + for (int cone = 0; cone < cones->num_cones; ++cone) + { + int start = cones->start_idx[cone]; + int end = start + cone_block_length(cones, cone); + if (find_partition(cuts, num_partitions, start) != find_partition(cuts, num_partitions, end - 1)) + ++partition->num_cones; + } + + int count = partition->num_cones; + if (count == 0) + return; + partition->v_dim = (int *)safe_malloc((size_t)count * sizeof(int)); + partition->type = (cone_type_t *)safe_malloc((size_t)count * sizeof(cone_type_t)); + partition->fixed_mask = (unsigned char *)safe_calloc((size_t)count, sizeof(unsigned char)); + partition->local_start = (int *)safe_malloc((size_t)count * sizeof(int)); + partition->local_first = (int *)safe_malloc((size_t)count * sizeof(int)); + partition->local_count = (int *)safe_malloc((size_t)count * sizeof(int)); + + int local_begin = cuts[local_partition]; + int local_end = cuts[local_partition + 1]; + int out = 0; + for (int cone = 0; cone < cones->num_cones; ++cone) + { + int start = cones->start_idx[cone]; + int end = start + cone_block_length(cones, cone); + if (find_partition(cuts, num_partitions, start) == find_partition(cuts, num_partitions, end - 1)) + continue; + if (!cone_can_span_partition(problem, cone, affine, params)) + { + fprintf(stderr, + "Error: unsupported %scone %d crossed a GPU %s partition boundary.\n", + affine ? "affine " : "", + cone, + affine ? "row" : "column"); + MPI_Abort(grid->comm_global, EXIT_FAILURE); + } + + int intersection_start = start > local_begin ? start : local_begin; + int intersection_end = end < local_end ? end : local_end; + int local_count = intersection_end > intersection_start ? intersection_end - intersection_start : 0; + partition->v_dim[out] = cones->v_dim[cone]; + partition->type[out] = cones->type[cone]; + if (!affine) + partition->fixed_mask[out] = distributed_cone_fixed_mask(problem, cone); + partition->local_start[out] = local_count > 0 ? intersection_start - local_begin : 0; + partition->local_first[out] = local_count > 0 ? intersection_start - start : 0; + partition->local_count[out] = local_count; + ++out; + } +} + +void configure_partition_metadata(const qp_problem_t *problem, grid_context_t *grid, const pdhg_parameters_t *params) +{ + int n = problem->num_variables; + int m = problem->num_constraints; + int P_cols = grid->dims[1]; + int P_rows = grid->dims[0]; + grid->global_num_cones = problem->cones.num_cones; + grid->global_num_affine_cones = problem->affine_cones.num_cones; + + free(grid->variable_cuts); + free(grid->constraint_cuts); + grid->variable_cuts = NULL; + grid->constraint_cuts = NULL; + + if (params->partition_method == NNZ_BALANCE_PARTITION) + { + int *col_weights = (int *)calloc((size_t)n, sizeof(int)); + int *row_weights = (int *)calloc((size_t)m, sizeof(int)); + if (problem->constraint_matrix) + { + for (int row = 0; row < m; ++row) + { + row_weights[row] = + problem->constraint_matrix->row_ptr[row + 1] - problem->constraint_matrix->row_ptr[row]; + for (int nz = problem->constraint_matrix->row_ptr[row]; + nz < problem->constraint_matrix->row_ptr[row + 1]; + ++nz) + ++col_weights[problem->constraint_matrix->col_ind[nz]]; + } + } + grid->variable_cuts = get_balanced_cuts(col_weights, n, P_cols); + grid->constraint_cuts = get_balanced_cuts(row_weights, m, P_rows); + free(col_weights); + free(row_weights); + } + else + { + grid->variable_cuts = get_uniform_cuts(n, P_cols); + grid->constraint_cuts = get_uniform_cuts(m, P_rows); + } + + bool variable_cuts_valid = + adjust_cuts_for_cones(problem, &problem->cones, n, P_cols, false, params, grid->variable_cuts); + bool constraint_cuts_valid = + adjust_cuts_for_cones(problem, &problem->affine_cones, m, P_rows, true, params, grid->constraint_cuts); + + int empty_variable_partition = 0; + int empty_constraint_partition = 0; + for (int part = 0; part < P_cols; ++part) + empty_variable_partition |= grid->variable_cuts[part] == grid->variable_cuts[part + 1]; + for (int part = 0; part < P_rows; ++part) + empty_constraint_partition |= grid->constraint_cuts[part] == grid->constraint_cuts[part + 1]; + int invalid_variable_partition = !variable_cuts_valid || empty_variable_partition; + int invalid_constraint_partition = !constraint_cuts_valid || empty_constraint_partition; + if (invalid_variable_partition || invalid_constraint_partition) + { + if (grid->rank_global == 0) + { + fprintf(stderr, + "Error: the requested %d x %d process grid creates an empty %s partition " + "(problem dimensions %d x %d). Use fewer row/column tiles; zero-width local " + "partitions are not supported.\n", + P_rows, + P_cols, + invalid_variable_partition ? "variable" : "constraint", + m, + n); + } + MPI_Abort(grid->comm_global, EXIT_FAILURE); + } + + int my_col = grid->coords[1]; + grid->n_start = grid->variable_cuts[my_col]; + grid->n_end = grid->variable_cuts[my_col + 1]; + int my_row = grid->coords[0]; + build_split_partition( + problem, &problem->cones, grid->variable_cuts, P_cols, my_col, false, params, grid, &grid->split_cones); + build_split_partition(problem, + &problem->affine_cones, + grid->constraint_cuts, + P_rows, + my_row, + true, + params, + grid, + &grid->split_affine_cones); +} + CsrComponent * extract_csr_component(int row_start, int row_end, int col_start, int col_end, const CsrComponent *src, int *out_nnz) { @@ -245,13 +567,48 @@ double *copy_slice(const double *src, int start, int count) return dst; } +static void extract_local_cone_blocks(cone_blocks_t *local, const cone_blocks_t *global, int range_start, int range_end) +{ + for (int cone = 0; cone < global->num_cones; ++cone) + { + int start = global->start_idx[cone]; + int end = start + cone_block_length(global, cone); + if (start >= range_start && end <= range_end) + ++local->num_cones; + } + + int count = local->num_cones; + if (count == 0) + return; + local->start_idx = (int *)safe_malloc((size_t)count * sizeof(int)); + local->v_dim = (int *)safe_malloc((size_t)count * sizeof(int)); + local->type = (cone_type_t *)safe_malloc((size_t)count * sizeof(cone_type_t)); + if (global->power_alpha) + local->power_alpha = (double *)safe_malloc((size_t)count * sizeof(double)); + + int out = 0; + for (int cone = 0; cone < global->num_cones; ++cone) + { + int start = global->start_idx[cone]; + int end = start + cone_block_length(global, cone); + if (start < range_start || end > range_end) + continue; + local->start_idx[out] = start - range_start; + local->v_dim[out] = global->v_dim[cone]; + local->type[out] = global->type[cone]; + if (local->power_alpha) + local->power_alpha[out] = global->power_alpha[cone]; + ++out; + } +} + qp_problem_t *partition_qp_problem(const qp_problem_t *global_qp, const grid_context_t *grid, partition_method_t method, int *out_n_start, int *out_m_start) { - qp_problem_t *loc = (qp_problem_t *)calloc(1, sizeof(qp_problem_t)); + qp_problem_t *loc = (qp_problem_t *)safe_calloc(1, sizeof(qp_problem_t)); int my_row_idx = grid->coords[0]; int my_col_idx = grid->coords[1]; @@ -262,49 +619,48 @@ qp_problem_t *partition_qp_problem(const qp_problem_t *global_qp, int m_total = global_qp->num_constraints; int n_start, n_end, m_start, m_end; - if (method == NNZ_BALANCE_PARTITION) + int *owned_col_cuts = NULL; + int *owned_row_cuts = NULL; + const int *col_cuts = grid->variable_cuts; + const int *row_cuts = grid->constraint_cuts; + + if (!col_cuts || !row_cuts) { - int *col_weights = (int *)calloc(n_total, sizeof(int)); - if (global_qp->constraint_matrix) + if (method == NNZ_BALANCE_PARTITION) { - for (int i = 0; i < global_qp->constraint_matrix_num_nonzeros; i++) + int *col_weights = (int *)calloc((size_t)n_total, sizeof(int)); + int *row_weights = (int *)calloc((size_t)m_total, sizeof(int)); + if (global_qp->constraint_matrix) { - int c = global_qp->constraint_matrix->col_ind[i]; - if (c < n_total) - col_weights[c]++; + for (int i = 0; i < m_total; ++i) + { + row_weights[i] = + global_qp->constraint_matrix->row_ptr[i + 1] - global_qp->constraint_matrix->row_ptr[i]; + for (int nz = global_qp->constraint_matrix->row_ptr[i]; + nz < global_qp->constraint_matrix->row_ptr[i + 1]; + ++nz) + ++col_weights[global_qp->constraint_matrix->col_ind[nz]]; + } } + owned_col_cuts = get_balanced_cuts(col_weights, n_total, P_cols); + owned_row_cuts = get_balanced_cuts(row_weights, m_total, P_rows); + free(col_weights); + free(row_weights); } - int *col_cuts = get_balanced_cuts(col_weights, n_total, P_cols); - n_start = col_cuts[my_col_idx]; - n_end = col_cuts[my_col_idx + 1]; - free(col_weights); - free(col_cuts); - - int *row_weights = (int *)malloc(m_total * sizeof(int)); - if (global_qp->constraint_matrix) + else { - for (int i = 0; i < m_total; i++) - { - row_weights[i] = - global_qp->constraint_matrix->row_ptr[i + 1] - global_qp->constraint_matrix->row_ptr[i]; - } + owned_col_cuts = get_uniform_cuts(n_total, P_cols); + owned_row_cuts = get_uniform_cuts(m_total, P_rows); } - int *row_cuts = get_balanced_cuts(row_weights, m_total, P_rows); - m_start = row_cuts[my_row_idx]; - m_end = row_cuts[my_row_idx + 1]; - free(row_weights); - free(row_cuts); - } - else - { - int n_chunk = n_total / P_cols; - n_start = my_col_idx * n_chunk; - n_end = (my_col_idx == P_cols - 1) ? n_total : (my_col_idx + 1) * n_chunk; - int m_chunk = m_total / P_rows; - m_start = my_row_idx * m_chunk; - m_end = (my_row_idx == P_rows - 1) ? m_total : (my_row_idx + 1) * m_chunk; + col_cuts = owned_col_cuts; + row_cuts = owned_row_cuts; } + n_start = col_cuts[my_col_idx]; + n_end = col_cuts[my_col_idx + 1]; + m_start = row_cuts[my_row_idx]; + m_end = row_cuts[my_row_idx + 1]; + if (out_n_start) *out_n_start = n_start; if (out_m_start) @@ -356,12 +712,33 @@ qp_problem_t *partition_qp_problem(const qp_problem_t *global_qp, loc->variable_upper_bound = copy_slice(global_qp->variable_upper_bound, n_start, loc->num_variables); loc->constraint_lower_bound = copy_slice(global_qp->constraint_lower_bound, m_start, loc->num_constraints); loc->constraint_upper_bound = copy_slice(global_qp->constraint_upper_bound, m_start, loc->num_constraints); + loc->affine_cone_offset = copy_slice(global_qp->affine_cone_offset, m_start, loc->num_constraints); if (global_qp->primal_start) loc->primal_start = copy_slice(global_qp->primal_start, n_start, loc->num_variables); if (global_qp->dual_start) loc->dual_start = copy_slice(global_qp->dual_start, m_start, loc->num_constraints); + loc->num_original_variables = 0; + if (global_qp->num_original_variables > n_start) + { + int original_end = global_qp->num_original_variables < n_end ? global_qp->num_original_variables : n_end; + loc->num_original_variables = original_end - n_start; + } + + extract_local_cone_blocks(&loc->cones, &global_qp->cones, n_start, n_end); + if (global_qp->cones.is_fixed) + { + loc->cones.fixed_mask_size = loc->num_variables; + loc->cones.is_fixed = (char *)malloc((size_t)loc->num_variables * sizeof(char)); + memcpy(loc->cones.is_fixed, global_qp->cones.is_fixed + n_start, (size_t)loc->num_variables * sizeof(char)); + } + + extract_local_cone_blocks(&loc->affine_cones, &global_qp->affine_cones, m_start, m_end); + + free(owned_col_cuts); + free(owned_row_cuts); + return loc; } @@ -447,222 +824,275 @@ rescale_info_t *partition_rescale_info(rescale_info_t *global_info, return loc_info; } -size_t get_qp_problem_size(const qp_problem_t *qp) +typedef struct { - if (!qp) - return 0; - size_t size = 0; - - size += sizeof(int) * 6 + sizeof(double); - - size += sizeof(double) * qp->num_variables * 3; - size += sizeof(double) * qp->num_constraints * 2; - -#define ADD_CSR_SIZE(csr, num_rows, nnz) \ - { \ - size += sizeof(int); \ - if (csr) \ - { \ - int safe_nnz = (nnz) > 0 ? (nnz) : 1; \ - size += sizeof(int) * ((num_rows) + 1); \ - size += sizeof(int) * safe_nnz; \ - size += sizeof(double) * safe_nnz; \ - } \ - } + char *cursor; + size_t size; +} buffer_writer_t; - ADD_CSR_SIZE(qp->constraint_matrix, qp->num_constraints, qp->constraint_matrix_num_nonzeros); - ADD_CSR_SIZE(qp->objective_sparse_matrix, qp->num_variables, qp->objective_sparse_matrix_num_nonzeros); - ADD_CSR_SIZE(qp->objective_lowrank_matrix, qp->num_rank_lowrank_obj, qp->objective_lowrank_matrix_num_nonzeros); +typedef struct +{ + const char *cursor; +} buffer_reader_t; - size += sizeof(int); - ADD_CSR_SIZE(qp->objective_lowrank_middle_matrix, - qp->num_rank_lowrank_obj, - qp->objective_lowrank_middle_matrix_num_nonzeros); +static void buffer_writer_write(buffer_writer_t *writer, const void *source, size_t bytes) +{ + if (bytes == 0) + return; + if (writer->cursor) + { + memcpy(writer->cursor, source, bytes); + writer->cursor += bytes; + } + writer->size += bytes; +} - size += sizeof(int) * 2; - if (qp->primal_start) - size += sizeof(double) * qp->num_variables; - if (qp->dual_start) - size += sizeof(double) * qp->num_constraints; +static void buffer_reader_read(buffer_reader_t *reader, void *destination, size_t bytes) +{ + if (bytes == 0) + return; + memcpy(destination, reader->cursor, bytes); + reader->cursor += bytes; +} - return size; +static void *buffer_reader_alloc(buffer_reader_t *reader, size_t count, size_t element_size) +{ + size_t bytes = count * element_size; + if (bytes == 0) + return NULL; + void *destination = safe_malloc(bytes); + buffer_reader_read(reader, destination, bytes); + return destination; } -#define S_CSR(csr, num_rows, nnz) \ - { \ - int has_csr = (csr != NULL); \ - S_COPY(has_csr, int); \ - if (has_csr) \ - { \ - S_ARR(csr->row_ptr, num_rows + 1, int); \ - S_ARR(csr->col_ind, nnz > 0 ? nnz : 1, int); \ - S_ARR(csr->val, nnz > 0 ? nnz : 1, double); \ - } \ +static void write_cone_blocks(buffer_writer_t *writer, const cone_blocks_t *blocks) +{ + buffer_writer_write(writer, &blocks->num_cones, sizeof(int)); + if (blocks->num_cones > 0) + { + size_t count = (size_t)blocks->num_cones; + buffer_writer_write(writer, blocks->start_idx, count * sizeof(int)); + buffer_writer_write(writer, blocks->v_dim, count * sizeof(int)); + buffer_writer_write(writer, blocks->type, count * sizeof(cone_type_t)); } -void serialize_qp_problem_to_ptr(const qp_problem_t *qp, char **ptr_ref) -{ - char *ptr = *ptr_ref; + int has_power_alpha = blocks->power_alpha != NULL; + buffer_writer_write(writer, &has_power_alpha, sizeof(int)); + if (has_power_alpha) + buffer_writer_write(writer, blocks->power_alpha, (size_t)blocks->num_cones * sizeof(double)); -#define S_COPY(val, type) \ - { \ - *((type *)ptr) = val; \ - ptr += sizeof(type); \ - } -#define S_ARR(arr, count, type) \ - { \ - memcpy(ptr, arr, sizeof(type) * (count)); \ - ptr += sizeof(type) * (count); \ + buffer_writer_write(writer, &blocks->fixed_mask_size, sizeof(int)); + if (blocks->fixed_mask_size > 0) + buffer_writer_write(writer, blocks->is_fixed, (size_t)blocks->fixed_mask_size * sizeof(char)); +} + +static void read_cone_blocks(buffer_reader_t *reader, cone_blocks_t *blocks) +{ + buffer_reader_read(reader, &blocks->num_cones, sizeof(int)); + if (blocks->num_cones > 0) + { + size_t count = (size_t)blocks->num_cones; + blocks->start_idx = (int *)buffer_reader_alloc(reader, count, sizeof(int)); + blocks->v_dim = (int *)buffer_reader_alloc(reader, count, sizeof(int)); + blocks->type = (cone_type_t *)buffer_reader_alloc(reader, count, sizeof(cone_type_t)); } - S_COPY(qp->num_variables, int); - S_COPY(qp->num_constraints, int); - S_COPY(qp->num_rank_lowrank_obj, int); - S_COPY(qp->constraint_matrix_num_nonzeros, int); - S_COPY(qp->objective_sparse_matrix_num_nonzeros, int); - S_COPY(qp->objective_lowrank_matrix_num_nonzeros, int); - S_COPY(qp->objective_constant, double); - - S_ARR(qp->objective_vector, qp->num_variables, double); - S_ARR(qp->variable_lower_bound, qp->num_variables, double); - S_ARR(qp->variable_upper_bound, qp->num_variables, double); - S_ARR(qp->constraint_lower_bound, qp->num_constraints, double); - S_ARR(qp->constraint_upper_bound, qp->num_constraints, double); - - S_CSR(qp->constraint_matrix, qp->num_constraints, qp->constraint_matrix_num_nonzeros); - S_CSR(qp->objective_sparse_matrix, qp->num_variables, qp->objective_sparse_matrix_num_nonzeros); - S_CSR(qp->objective_lowrank_matrix, qp->num_rank_lowrank_obj, qp->objective_lowrank_matrix_num_nonzeros); - - S_COPY(qp->objective_lowrank_middle_matrix_num_nonzeros, int); - S_CSR(qp->objective_lowrank_middle_matrix, - qp->num_rank_lowrank_obj, - qp->objective_lowrank_middle_matrix_num_nonzeros); - - int has_primal = (qp->primal_start != NULL); - int has_dual = (qp->dual_start != NULL); - S_COPY(has_primal, int); - S_COPY(has_dual, int); - if (has_primal) - S_ARR(qp->primal_start, qp->num_variables, double); - if (has_dual) - S_ARR(qp->dual_start, qp->num_constraints, double); + int has_power_alpha = 0; + buffer_reader_read(reader, &has_power_alpha, sizeof(int)); + if (has_power_alpha) + blocks->power_alpha = (double *)buffer_reader_alloc(reader, (size_t)blocks->num_cones, sizeof(double)); - *ptr_ref = ptr; + buffer_reader_read(reader, &blocks->fixed_mask_size, sizeof(int)); + if (blocks->fixed_mask_size > 0) + blocks->is_fixed = (char *)buffer_reader_alloc(reader, (size_t)blocks->fixed_mask_size, sizeof(char)); } -#define D_CSR(target_ptr, num_rows, nnz) \ - { \ - int has_csr; \ - D_VAL(has_csr, int); \ - if (has_csr) \ - { \ - target_ptr = (CsrComponent *)malloc(sizeof(CsrComponent)); \ - int safe_nnz = nnz > 0 ? nnz : 1; \ - D_ARR(target_ptr->row_ptr, num_rows + 1, int); \ - D_ARR(target_ptr->col_ind, safe_nnz, int); \ - D_ARR(target_ptr->val, safe_nnz, double); \ - } \ - } -qp_problem_t *deserialize_qp_problem_from_ptr(const char **ptr_ref) +static void write_csr_component(buffer_writer_t *writer, const CsrComponent *csr, int num_rows, int num_nonzeros) { - const char *ptr = *ptr_ref; - qp_problem_t *qp = (qp_problem_t *)calloc(1, sizeof(qp_problem_t)); + int has_csr = csr && csr->row_ptr; + buffer_writer_write(writer, &has_csr, sizeof(int)); + if (!has_csr) + return; -#define D_VAL(var, type) \ - { \ - var = *((type *)ptr); \ - ptr += sizeof(type); \ + buffer_writer_write(writer, csr->row_ptr, (size_t)(num_rows + 1) * sizeof(int)); + if (num_nonzeros > 0) + { + buffer_writer_write(writer, csr->col_ind, (size_t)num_nonzeros * sizeof(int)); + buffer_writer_write(writer, csr->val, (size_t)num_nonzeros * sizeof(double)); } -#define D_ARR(dest, count, type) \ - { \ - dest = (type *)malloc(sizeof(type) * (count)); \ - memcpy(dest, ptr, sizeof(type) * (count)); \ - ptr += sizeof(type) * (count); \ + else + { + const int zero_index = 0; + const double zero_value = 0.0; + buffer_writer_write(writer, &zero_index, sizeof(int)); + buffer_writer_write(writer, &zero_value, sizeof(double)); } +} + +static CsrComponent *read_csr_component(buffer_reader_t *reader, int num_rows, int num_nonzeros) +{ + int has_csr = 0; + buffer_reader_read(reader, &has_csr, sizeof(int)); + if (!has_csr) + return NULL; + + CsrComponent *csr = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + int stored_nonzeros = num_nonzeros > 0 ? num_nonzeros : 1; + csr->row_ptr = (int *)buffer_reader_alloc(reader, (size_t)num_rows + 1, sizeof(int)); + csr->col_ind = (int *)buffer_reader_alloc(reader, (size_t)stored_nonzeros, sizeof(int)); + csr->val = (double *)buffer_reader_alloc(reader, (size_t)stored_nonzeros, sizeof(double)); + return csr; +} - D_VAL(qp->num_variables, int); - D_VAL(qp->num_constraints, int); - D_VAL(qp->num_rank_lowrank_obj, int); - D_VAL(qp->constraint_matrix_num_nonzeros, int); - D_VAL(qp->objective_sparse_matrix_num_nonzeros, int); - D_VAL(qp->objective_lowrank_matrix_num_nonzeros, int); - D_VAL(qp->objective_constant, double); - - D_ARR(qp->objective_vector, qp->num_variables, double); - D_ARR(qp->variable_lower_bound, qp->num_variables, double); - D_ARR(qp->variable_upper_bound, qp->num_variables, double); - D_ARR(qp->constraint_lower_bound, qp->num_constraints, double); - D_ARR(qp->constraint_upper_bound, qp->num_constraints, double); - - D_CSR(qp->constraint_matrix, qp->num_constraints, qp->constraint_matrix_num_nonzeros); - D_CSR(qp->objective_sparse_matrix, qp->num_variables, qp->objective_sparse_matrix_num_nonzeros); - D_CSR(qp->objective_lowrank_matrix, qp->num_rank_lowrank_obj, qp->objective_lowrank_matrix_num_nonzeros); - - D_VAL(qp->objective_lowrank_middle_matrix_num_nonzeros, int); - qp->objective_lowrank_middle_matrix = NULL; - D_CSR(qp->objective_lowrank_middle_matrix, - qp->num_rank_lowrank_obj, - qp->objective_lowrank_middle_matrix_num_nonzeros); - - int has_primal, has_dual; - D_VAL(has_primal, int); - D_VAL(has_dual, int); +static void write_qp_problem_fields(buffer_writer_t *writer, const qp_problem_t *qp) +{ + buffer_writer_write(writer, &qp->num_variables, sizeof(int)); + buffer_writer_write(writer, &qp->num_constraints, sizeof(int)); + buffer_writer_write(writer, &qp->num_rank_lowrank_obj, sizeof(int)); + buffer_writer_write(writer, &qp->constraint_matrix_num_nonzeros, sizeof(int)); + buffer_writer_write(writer, &qp->objective_sparse_matrix_num_nonzeros, sizeof(int)); + buffer_writer_write(writer, &qp->objective_lowrank_matrix_num_nonzeros, sizeof(int)); + buffer_writer_write(writer, &qp->objective_constant, sizeof(double)); + buffer_writer_write(writer, &qp->num_original_variables, sizeof(int)); + + write_cone_blocks(writer, &qp->cones); + write_cone_blocks(writer, &qp->affine_cones); + + size_t variable_bytes = (size_t)qp->num_variables * sizeof(double); + size_t constraint_bytes = (size_t)qp->num_constraints * sizeof(double); + buffer_writer_write(writer, qp->objective_vector, variable_bytes); + buffer_writer_write(writer, qp->variable_lower_bound, variable_bytes); + buffer_writer_write(writer, qp->variable_upper_bound, variable_bytes); + buffer_writer_write(writer, qp->constraint_lower_bound, constraint_bytes); + buffer_writer_write(writer, qp->constraint_upper_bound, constraint_bytes); + buffer_writer_write(writer, qp->affine_cone_offset, constraint_bytes); + + write_csr_component(writer, qp->constraint_matrix, qp->num_constraints, qp->constraint_matrix_num_nonzeros); + write_csr_component( + writer, qp->objective_sparse_matrix, qp->num_variables, qp->objective_sparse_matrix_num_nonzeros); + write_csr_component( + writer, qp->objective_lowrank_matrix, qp->num_rank_lowrank_obj, qp->objective_lowrank_matrix_num_nonzeros); + + buffer_writer_write(writer, &qp->objective_lowrank_middle_matrix_num_nonzeros, sizeof(int)); + write_csr_component(writer, + qp->objective_lowrank_middle_matrix, + qp->num_rank_lowrank_obj, + qp->objective_lowrank_middle_matrix_num_nonzeros); + + int has_primal = qp->primal_start != NULL; + int has_dual = qp->dual_start != NULL; + buffer_writer_write(writer, &has_primal, sizeof(int)); + buffer_writer_write(writer, &has_dual, sizeof(int)); if (has_primal) - D_ARR(qp->primal_start, qp->num_variables, double); + buffer_writer_write(writer, qp->primal_start, variable_bytes); if (has_dual) - D_ARR(qp->dual_start, qp->num_constraints, double); + buffer_writer_write(writer, qp->dual_start, constraint_bytes); +} - *ptr_ref = ptr; +static qp_problem_t *read_qp_problem_fields(buffer_reader_t *reader) +{ + qp_problem_t *qp = (qp_problem_t *)safe_calloc(1, sizeof(qp_problem_t)); + buffer_reader_read(reader, &qp->num_variables, sizeof(int)); + buffer_reader_read(reader, &qp->num_constraints, sizeof(int)); + buffer_reader_read(reader, &qp->num_rank_lowrank_obj, sizeof(int)); + buffer_reader_read(reader, &qp->constraint_matrix_num_nonzeros, sizeof(int)); + buffer_reader_read(reader, &qp->objective_sparse_matrix_num_nonzeros, sizeof(int)); + buffer_reader_read(reader, &qp->objective_lowrank_matrix_num_nonzeros, sizeof(int)); + buffer_reader_read(reader, &qp->objective_constant, sizeof(double)); + buffer_reader_read(reader, &qp->num_original_variables, sizeof(int)); + + read_cone_blocks(reader, &qp->cones); + read_cone_blocks(reader, &qp->affine_cones); + + qp->objective_vector = (double *)buffer_reader_alloc(reader, (size_t)qp->num_variables, sizeof(double)); + qp->variable_lower_bound = (double *)buffer_reader_alloc(reader, (size_t)qp->num_variables, sizeof(double)); + qp->variable_upper_bound = (double *)buffer_reader_alloc(reader, (size_t)qp->num_variables, sizeof(double)); + qp->constraint_lower_bound = (double *)buffer_reader_alloc(reader, (size_t)qp->num_constraints, sizeof(double)); + qp->constraint_upper_bound = (double *)buffer_reader_alloc(reader, (size_t)qp->num_constraints, sizeof(double)); + qp->affine_cone_offset = (double *)buffer_reader_alloc(reader, (size_t)qp->num_constraints, sizeof(double)); + + qp->constraint_matrix = read_csr_component(reader, qp->num_constraints, qp->constraint_matrix_num_nonzeros); + qp->objective_sparse_matrix = + read_csr_component(reader, qp->num_variables, qp->objective_sparse_matrix_num_nonzeros); + qp->objective_lowrank_matrix = + read_csr_component(reader, qp->num_rank_lowrank_obj, qp->objective_lowrank_matrix_num_nonzeros); + + buffer_reader_read(reader, &qp->objective_lowrank_middle_matrix_num_nonzeros, sizeof(int)); + qp->objective_lowrank_middle_matrix = + read_csr_component(reader, qp->num_rank_lowrank_obj, qp->objective_lowrank_middle_matrix_num_nonzeros); + + int has_primal = 0; + int has_dual = 0; + buffer_reader_read(reader, &has_primal, sizeof(int)); + buffer_reader_read(reader, &has_dual, sizeof(int)); + if (has_primal) + qp->primal_start = (double *)buffer_reader_alloc(reader, (size_t)qp->num_variables, sizeof(double)); + if (has_dual) + qp->dual_start = (double *)buffer_reader_alloc(reader, (size_t)qp->num_constraints, sizeof(double)); return qp; } -size_t get_rescale_info_size(const rescale_info_t *info) +size_t get_qp_problem_size(const qp_problem_t *qp) { - if (!info) + if (!qp) return 0; - size_t size = 0; - size += sizeof(double) * 3; - int n = info->scaled_problem->num_variables; - int m = info->scaled_problem->num_constraints; - size += sizeof(double) * (n + m); - - size += get_qp_problem_size(info->scaled_problem); + buffer_writer_t writer = {NULL, 0}; + write_qp_problem_fields(&writer, qp); + return writer.size; +} - return size; +void serialize_qp_problem_to_ptr(const qp_problem_t *qp, char **ptr_ref) +{ + buffer_writer_t writer = {*ptr_ref, 0}; + write_qp_problem_fields(&writer, qp); + *ptr_ref = writer.cursor; } -void serialize_rescale_info(const rescale_info_t *info, char *buffer) +qp_problem_t *deserialize_qp_problem_from_ptr(const char **ptr_ref) { - char *ptr = buffer; + buffer_reader_t reader = {*ptr_ref}; + qp_problem_t *qp = read_qp_problem_fields(&reader); + *ptr_ref = reader.cursor; + return qp; +} - S_COPY(info->con_bound_rescale, double); - S_COPY(info->obj_vec_rescale, double); - S_COPY(info->rescaling_time_sec, double); +static void write_rescale_info_fields(buffer_writer_t *writer, const rescale_info_t *info) +{ + buffer_writer_write(writer, &info->con_bound_rescale, sizeof(double)); + buffer_writer_write(writer, &info->obj_vec_rescale, sizeof(double)); + buffer_writer_write(writer, &info->rescaling_time_sec, sizeof(double)); + write_qp_problem_fields(writer, info->scaled_problem); + buffer_writer_write(writer, info->var_rescale, (size_t)info->scaled_problem->num_variables * sizeof(double)); + buffer_writer_write(writer, info->con_rescale, (size_t)info->scaled_problem->num_constraints * sizeof(double)); +} - serialize_qp_problem_to_ptr(info->scaled_problem, &ptr); +size_t get_rescale_info_size(const rescale_info_t *info) +{ + if (!info) + return 0; + buffer_writer_t writer = {NULL, 0}; + write_rescale_info_fields(&writer, info); + return writer.size; +} - int n = info->scaled_problem->num_variables; - int m = info->scaled_problem->num_constraints; - S_ARR(info->var_rescale, n, double); - S_ARR(info->con_rescale, m, double); +void serialize_rescale_info(const rescale_info_t *info, char *buffer) +{ + buffer_writer_t writer = {buffer, 0}; + write_rescale_info_fields(&writer, info); } rescale_info_t *deserialize_rescale_info(const char *buffer) { - const char *ptr = buffer; - rescale_info_t *info = (rescale_info_t *)calloc(1, sizeof(rescale_info_t)); - - D_VAL(info->con_bound_rescale, double); - D_VAL(info->obj_vec_rescale, double); - D_VAL(info->rescaling_time_sec, double); - - info->scaled_problem = deserialize_qp_problem_from_ptr(&ptr); - - int n = info->scaled_problem->num_variables; - int m = info->scaled_problem->num_constraints; - D_ARR(info->var_rescale, n, double); - D_ARR(info->con_rescale, m, double); - + buffer_reader_t reader = {buffer}; + rescale_info_t *info = (rescale_info_t *)safe_calloc(1, sizeof(rescale_info_t)); + buffer_reader_read(&reader, &info->con_bound_rescale, sizeof(double)); + buffer_reader_read(&reader, &info->obj_vec_rescale, sizeof(double)); + buffer_reader_read(&reader, &info->rescaling_time_sec, sizeof(double)); + info->scaled_problem = read_qp_problem_fields(&reader); + info->var_rescale = + (double *)buffer_reader_alloc(&reader, (size_t)info->scaled_problem->num_variables, sizeof(double)); + info->con_rescale = + (double *)buffer_reader_alloc(&reader, (size_t)info->scaled_problem->num_constraints, sizeof(double)); return info; } @@ -697,78 +1127,6 @@ void big_bcast_bytes(void **buffer_ptr, size_t *size_ptr, int root, MPI_Comm com } } -void big_send_bytes(const void *buffer, size_t size, int dest, MPI_Comm comm) -{ - unsigned long long total_len = size; - MPI_Send(&total_len, 1, MPI_UNSIGNED_LONG_LONG, dest, 0, comm); - - const char *buf = (const char *)buffer; - size_t offset = 0; - while (offset < total_len) - { - size_t remaining = total_len - offset; - int current_chunk = (remaining > CHUNK_SIZE) ? CHUNK_SIZE : (int)remaining; - MPI_Send(buf + offset, current_chunk, MPI_BYTE, dest, 1, comm); - offset += current_chunk; - } -} - -void big_recv_bytes(void **buffer_ptr, size_t *size_ptr, int source, MPI_Comm comm) -{ - unsigned long long total_len = 0; - MPI_Recv(&total_len, 1, MPI_UNSIGNED_LONG_LONG, source, 0, comm, MPI_STATUS_IGNORE); - - *size_ptr = (size_t)total_len; - *buffer_ptr = malloc((size_t)total_len); - - char *buf = (char *)(*buffer_ptr); - size_t offset = 0; - while (offset < total_len) - { - size_t remaining = total_len - offset; - int current_chunk = (remaining > CHUNK_SIZE) ? CHUNK_SIZE : (int)remaining; - MPI_Recv(buf + offset, current_chunk, MPI_BYTE, source, 1, comm, MPI_STATUS_IGNORE); - offset += current_chunk; - } -} - -big_request_t big_isend_bytes(const void *buffer, size_t size, int dest, MPI_Comm comm) -{ - big_request_t breq; - int num_chunks = (size + CHUNK_SIZE - 1) / CHUNK_SIZE; - breq.num_reqs = 1 + num_chunks; - breq.reqs = (MPI_Request *)malloc(breq.num_reqs * sizeof(MPI_Request)); - - unsigned long long *p_len = (unsigned long long *)malloc(sizeof(unsigned long long)); - *p_len = size; - - MPI_Isend(p_len, 1, MPI_UNSIGNED_LONG_LONG, dest, 0, comm, &breq.reqs[0]); - - const char *buf = (const char *)buffer; - size_t offset = 0; - int req_idx = 1; - while (offset < size) - { - size_t remaining = size - offset; - int current_chunk = (remaining > CHUNK_SIZE) ? CHUNK_SIZE : (int)remaining; - MPI_Isend(buf + offset, current_chunk, MPI_BYTE, dest, 1, comm, &breq.reqs[req_idx++]); - offset += current_chunk; - } - return breq; -} - -void big_wait_bytes(big_request_t *breq, unsigned long long *p_len) -{ - if (breq->num_reqs > 0) - { - MPI_Waitall(breq->num_reqs, breq->reqs, MPI_STATUSES_IGNORE); - free(breq->reqs); - breq->num_reqs = 0; - } - if (p_len) - free(p_len); -} - void distribute_data_bcast_then_partition(const qp_problem_t *working_problem, rescale_info_t *rescale_info, grid_context_t *grid_context, @@ -803,6 +1161,7 @@ void distribute_data_bcast_then_partition(const qp_problem_t *working_problem, } grid_context->global_num_variables = current_working_problem->num_variables; + configure_partition_metadata(current_working_problem, grid_context, params); { char *buf = NULL; @@ -828,6 +1187,7 @@ void distribute_data_bcast_then_partition(const qp_problem_t *working_problem, partition_rescale_info(current_rescale_info, grid_context, params->partition_method, &real_n_start, NULL); *out_local_qp = partition_qp_problem(current_working_problem, grid_context, params->partition_method, NULL, NULL); grid_context->n_start = real_n_start; + grid_context->n_end = real_n_start + (*out_local_qp)->num_variables; if (grid_context->rank_global != 0) { @@ -842,112 +1202,6 @@ void distribute_data_bcast_then_partition(const qp_problem_t *working_problem, } } -void distribute_data_partition_then_send(const qp_problem_t *working_problem, - rescale_info_t *rescale_info, - grid_context_t *grid_context, - const pdhg_parameters_t *params, - qp_problem_t **out_local_qp, - rescale_info_t **out_local_resc) -{ - double t_start = MPI_Wtime(); - int world_size; - MPI_Comm_size(grid_context->comm_global, &world_size); - - if (grid_context->rank_global == 0) - { - char *prev_buf_lp = NULL; - char *prev_buf_resc = NULL; - big_request_t req_lp = {NULL, 0}; - big_request_t req_resc = {NULL, 0}; - unsigned long long *prev_len_lp = NULL; - unsigned long long *prev_len_resc = NULL; - - for (int r = 0; r < world_size; ++r) - { - grid_context_t target_grid = *grid_context; - target_grid.rank_global = r; - target_grid.coords[0] = r / grid_context->dims[1]; - target_grid.coords[1] = r % grid_context->dims[1]; - - qp_problem_t *sub_qp = - partition_qp_problem(working_problem, &target_grid, params->partition_method, NULL, NULL); - rescale_info_t *sub_rescale = - partition_rescale_info(rescale_info, &target_grid, params->partition_method, NULL, NULL); - - if (r == 0) - { - *out_local_qp = sub_qp; - *out_local_resc = sub_rescale; - continue; - } - - size_t sz_lp = get_qp_problem_size(sub_qp); - char *buf_lp = (char *)malloc(sz_lp); - char *ptr_lp = buf_lp; - serialize_qp_problem_to_ptr(sub_qp, &ptr_lp); - - size_t sz_resc = get_rescale_info_size(sub_rescale); - char *buf_resc = (char *)malloc(sz_resc); - serialize_rescale_info(sub_rescale, buf_resc); - - qp_problem_free(sub_qp); - rescale_info_free(sub_rescale); - - if (req_lp.num_reqs > 0 || req_resc.num_reqs > 0) - { - big_wait_bytes(&req_lp, prev_len_lp); - big_wait_bytes(&req_resc, prev_len_resc); - free(prev_buf_lp); - free(prev_buf_resc); - } - - prev_len_lp = (unsigned long long *)malloc(sizeof(unsigned long long)); - *prev_len_lp = sz_lp; - req_lp = big_isend_bytes(buf_lp, sz_lp, r, grid_context->comm_global); - - prev_len_resc = (unsigned long long *)malloc(sizeof(unsigned long long)); - *prev_len_resc = sz_resc; - req_resc = big_isend_bytes(buf_resc, sz_resc, r, grid_context->comm_global); - - prev_buf_lp = buf_lp; - prev_buf_resc = buf_resc; - } - - if (req_lp.num_reqs > 0 || req_resc.num_reqs > 0) - { - big_wait_bytes(&req_lp, prev_len_lp); - big_wait_bytes(&req_resc, prev_len_resc); - free(prev_buf_lp); - free(prev_buf_resc); - } - - rescale_info_free(rescale_info); - } - else - { - char *buf_lp = NULL; - size_t sz_lp = 0; - big_recv_bytes((void **)&buf_lp, &sz_lp, 0, grid_context->comm_global); - const char *ptr_lp = buf_lp; - *out_local_qp = deserialize_qp_problem_from_ptr(&ptr_lp); - free(buf_lp); - - char *buf_resc = NULL; - size_t sz_resc = 0; - big_recv_bytes((void **)&buf_resc, &sz_resc, 0, grid_context->comm_global); - *out_local_resc = deserialize_rescale_info(buf_resc); - free(buf_resc); - } - - double t_end = MPI_Wtime(); - if (params->verbose && grid_context->rank_global == 0) - { - printf("[Timer] Data Distribution (Partition -> P2P Send) took %.3f " - "seconds.\n", - t_end - t_start); - } -} - double compute_global_norm(cublasHandle_t blas_handle, int m_local, double *d_vec, MPI_Comm comm) { double local_norm_sq = 0.0; diff --git a/distributed/distributed_utils.h b/distributed/distributed_utils.h index fa0b19d..64d940c 100644 --- a/distributed/distributed_utils.h +++ b/distributed/distributed_utils.h @@ -36,6 +36,9 @@ extern "C" } \ } while (0) grid_context_t initialize_parallel_context(int P_row, int P_col); + void destroy_parallel_context(grid_context_t *grid); + void + configure_partition_metadata(const qp_problem_t *problem, grid_context_t *grid, const pdhg_parameters_t *params); rescale_info_t *partition_rescale_info(rescale_info_t *global_info, const grid_context_t *grid, partition_method_t method, @@ -54,22 +57,12 @@ extern "C" void serialize_qp_problem_to_ptr(const qp_problem_t *qp, char **ptr_ref); size_t get_qp_problem_size(const qp_problem_t *qp); void big_bcast_bytes(void **buffer_ptr, size_t *size_ptr, int root, MPI_Comm comm); - void big_send_bytes(const void *buffer, size_t size, int dest, MPI_Comm comm); - void big_recv_bytes(void **buffer_ptr, size_t *size_ptr, int source, MPI_Comm comm); - big_request_t big_isend_bytes(const void *buffer, size_t size, int dest, MPI_Comm comm); - void big_wait_bytes(big_request_t *breq, unsigned long long *p_len); void distribute_data_bcast_then_partition(const qp_problem_t *working_problem, rescale_info_t *rescale_info, grid_context_t *grid_context, const pdhg_parameters_t *params, qp_problem_t **out_local_qp, rescale_info_t **out_local_resc); - void distribute_data_partition_then_send(const qp_problem_t *working_problem, - rescale_info_t *rescale_info, - grid_context_t *grid_context, - const pdhg_parameters_t *params, - qp_problem_t **out_local_qp, - rescale_info_t **out_local_resc); void gather_distributed_vector( double *d_local_vec, int local_len, MPI_Comm comm_check, MPI_Comm comm_gather, double **result_ptr); void print_distributed_params(const pdhg_parameters_t *params); diff --git a/docs/C_API.md b/docs/C_API.md index 4199b48..3b0148f 100644 --- a/docs/C_API.md +++ b/docs/C_API.md @@ -1,7 +1,7 @@ ### C Interface -PDHCG-II provides a C API for directly solving QPs in memory, defined in header file `include/pdhcg.h`. +PDHCG provides a C API for directly solving QPs in memory, defined in header file `include/pdhcg.h`. #### Functions and Parameters @@ -18,7 +18,13 @@ qp_problem_t *create_qp_problem( const double *con_ub, // constraint upper bounds (length m) const double *var_lb, // variable lower bounds (length n) const double *var_ub, // variable upper bounds (length n) - const double *objective_constant // scalar objective offset + const double *objective_constant, // scalar objective offset + int num_var_cones, // number of variable cone blocks + const cone_spec_t *var_cones, // variable cone descriptors + const matrix_desc_t *affine_cone_matrix_desc, // affine matrix F (p x n) + const double *affine_cone_offset, // affine offset g (length p) + int num_affine_cones, // number of affine cone blocks + const cone_spec_t *affine_cones // descriptors indexing rows of F ); pdhcg_result_t* solve_qp_problem( @@ -40,6 +46,14 @@ The objective minimized is `0.5 * x^T (Q + R^T D R) x + c^T x + c0`. `Q`, `R`, a - `var_lb`: Variable lower bounds. If `NULL`, defaults to all `-INFINITY`. - `var_ub`: Variable upper bounds. If `NULL`, defaults to all `+INFINITY`. - `objective_constant`: Scalar constant term added to the objective value. If `NULL`, defaults to `0.0`. +- `num_var_cones`, `var_cones`: Optional conic variable blocks. Supported types are SOC, rotated SOC, exponential, and power cones. Pass `0` and `NULL` when no variable cones are present. See [Types](c/types.md) for slot layouts and `set_cone_fixed` in [Functions](c/functions.md) for pinning individual slots. +- `affine_cone_matrix_desc`: Matrix descriptor for `F` in the native constraint `F x + affine_cone_offset in K`. Pass `NULL` when no affine cone rows are present. +- `affine_cone_offset`: Offset vector with one entry per row of `F`. Pass `NULL` for zero offsets. +- `num_affine_cones`, `affine_cones`: Cone blocks covering every row of `F`. Each `start_idx` is relative to `F`; blocks must be disjoint. Affine cone descriptors must set `is_fixed = NULL`. + +Affine cone constraints are handled directly without introducing slack variables. +Internally, PDHCG appends the affine rows after scalar rows and returns duals in +the order `[dual_A, dual_F]`. `solve_qp_problem` parameters: @@ -57,7 +71,9 @@ pdhcg_result_t* solve_qp_problem_distributed( ); ``` -This requires PDHCG to be compiled with `-DPDHCG_COMPILE_DISTRIBUTED=ON` and launched via `mpirun`. +Distributed execution requires PDHCG to be compiled with +`-DPDHCG_COMPILE_DISTRIBUTED=ON` and launched via `mpirun`. A non-distributed +build keeps the function symbol but returns `NULL` with an explanatory error. #### Example: Solving a Small QP ```c @@ -108,7 +124,7 @@ int main() { // 6. Build the QP problem // Note: We pass NULL for R_desc (low-rank factor), D_desc (middle matrix), - // and objective_constant. + // and objective_constant. Both cone counts are zero for a plain QP. qp_problem_t* prob = create_qp_problem( c, // objective_c &Q_desc, // Q_desc @@ -119,7 +135,13 @@ int main() { u, // con_ub lb, // var_lb ub, // var_ub - NULL // objective_constant + NULL, // objective_constant + 0, // num_var_cones + NULL, // var_cones + NULL, // affine_cone_matrix_desc + NULL, // affine_cone_offset + 0, // num_affine_cones + NULL // affine_cones ); // 7. Solve (NULL → use default parameters) diff --git a/docs/c/functions.md b/docs/c/functions.md index 00d3751..a46e79a 100644 --- a/docs/c/functions.md +++ b/docs/c/functions.md @@ -11,12 +11,21 @@ qp_problem_t *create_qp_problem( const matrix_desc_t *A_desc, const double *con_lb, const double *con_ub, const double *var_lb, const double *var_ub, - const double *objective_constant + const double *objective_constant, + int num_var_cones, + const cone_spec_t *var_cones, + const matrix_desc_t *affine_cone_matrix_desc, + const double *affine_cone_offset, + int num_affine_cones, + const cone_spec_t *affine_cones ); ``` Creates a QP problem of the form -`min 0.5 * x^T (Q + R^T D R) x + c^T x` subject to `con_lb <= A x <= con_ub` and `var_lb <= x <= var_ub`. +`min 0.5 * x^T (Q + R^T D R) x + c^T x` subject to +`con_lb <= A x <= con_ub`, `F x + affine_cone_offset in K`, +`var_lb <= x <= var_ub`, and optional variable cone blocks. The affine cone +blocks must be disjoint and cover every row of `F`. **Parameters:** @@ -32,6 +41,12 @@ Creates a QP problem of the form | `var_lb` | Variable lower bounds (size n) | | `var_ub` | Variable upper bounds (size n) | | `objective_constant` | Constant term in objective (can be NULL) | +| `num_var_cones` | Number of variable cone blocks | +| `var_cones` | Array of variable `cone_spec_t` descriptors, or NULL when the count is zero | +| `affine_cone_matrix_desc` | Matrix `F` in the native affine cone constraint; NULL when no affine cones are present | +| `affine_cone_offset` | Offset vector with one entry per row of `F`; NULL means zero | +| `num_affine_cones` | Number of affine cone blocks covering `F` | +| `affine_cones` | Array of affine `cone_spec_t` descriptors, or NULL when the count is zero | **Returns:** Pointer to allocated `qp_problem_t`, or NULL on error. @@ -47,7 +62,9 @@ void set_start_values( ); ``` -Sets initial primal and dual solutions for warm starting. +Sets initial primal and dual solutions for warm starting. Passing `NULL` clears +the corresponding warm start, while values pinned by `set_cone_fixed` remain +part of the model and are preserved. **Parameters:** @@ -55,7 +72,38 @@ Sets initial primal and dual solutions for warm starting. |-----------|-------------| | `prob` | QP problem pointer | | `primal` | Primal solution vector (size n, can be NULL) | -| `dual` | Dual solution vector (size m, can be NULL) | +| `dual` | Dual solution vector (size `m + p`, ordered as `[dual_A, dual_F]`; can be NULL) | + +Rejects a primal warm start that changes a value already pinned by +`set_cone_fixed`. + +--- + +## set_cone_fixed + +```c +int set_cone_fixed( + qp_problem_t *prob, + int cone_idx, + int slot, + double value +); +``` + +Pins one slot of cone `cone_idx` to `value`. Allocates the `is_fixed` flag array on first use and also writes `primal_start[start_idx + slot] = value` so the projection sees the constant. During preprocessing, that slot is also converted to equal lower and upper bounds. Typical use: fix the `y` slot of an exponential cone (e.g. Fisher-market entropy term with `y = 1`). + +Variable SOC, rotated-SOC, exponential, and power cones support every fixed-slot pattern whose intersection with the cone is nonempty. The solver validates the section before preprocessing and rejects empty or non-finite sections. Projection and stationarity residuals use the same weighted fixed-section operator, including diagonal quadratic objectives and large SOC/rotated-SOC blocks. + +**Parameters:** + +| Parameter | Description | +|-----------|-------------| +| `prob` | QP problem pointer | +| `cone_idx` | Cone index in `[0, num_cones)` | +| `slot` | Slot offset within the cone (0-based) | +| `value` | Fixed value | + +**Returns:** 0 on success, nonzero on error (bad indices or no cones). --- @@ -93,7 +141,9 @@ pdhcg_result_t *solve_qp_problem_distributed( Solves the QP problem using the distributed multi-GPU PDHCG algorithm. !!! note "Availability" - This function is only available when PDHCG is compiled with `-DPDHCG_COMPILE_DISTRIBUTED=ON`. + Distributed execution requires `-DPDHCG_COMPILE_DISTRIBUTED=ON`. In a + non-distributed build the API remains available but returns `NULL` with an + explanatory error. **Parameters:** diff --git a/docs/c/overview.md b/docs/c/overview.md index 6764266..ab76de0 100644 --- a/docs/c/overview.md +++ b/docs/c/overview.md @@ -49,7 +49,8 @@ int main() { // Create problem (NULL for Q, R, and D -> linear problem) qp_problem_t *prob = create_qp_problem( c, NULL, NULL, NULL, &A_desc, - con_lb, con_ub, var_lb, var_ub, NULL + con_lb, con_ub, var_lb, var_ub, NULL, + 0, NULL, NULL, NULL, 0, NULL ); // Set parameters @@ -75,14 +76,17 @@ int main() { ## Distributed / Multi-GPU Solving -PDHCG supports distributed solving across multiple GPUs via MPI and NCCL. When compiled with `-DPDHCG_COMPILE_DISTRIBUTED=ON`, the public header `pdhcg.h` conditionally declares: +PDHCG supports distributed solving across multiple GPUs via MPI and NCCL. The public header declares: ```c pdhcg_result_t *solve_qp_problem_distributed(const pdhg_parameters_t *params, const qp_problem_t *original_problem); ``` -Use `solve_qp_problem_distributed()` in place of `solve_qp_problem()`, and launch your program with `mpirun` (or `mpiexec`). +Use `solve_qp_problem_distributed()` in place of `solve_qp_problem()`, compile with +`-DPDHCG_COMPILE_DISTRIBUTED=ON`, and launch your program with `mpirun` (or +`mpiexec`). A non-distributed build keeps the symbol for API compatibility and +returns `NULL` with an explanatory error. See the [C API Functions](functions.md) reference for details, and the [Examples](../examples.md) page for usage examples. @@ -99,13 +103,26 @@ qp_problem_t *create_qp_problem( const matrix_desc_t *A_desc, const double *con_lb, const double *con_ub, const double *var_lb, const double *var_ub, - const double *objective_constant + const double *objective_constant, + int num_var_cones, + const cone_spec_t *var_cones, + const matrix_desc_t *affine_cone_matrix_desc, + const double *affine_cone_offset, + int num_affine_cones, + const cone_spec_t *affine_cones ); ``` Creates a QP problem of the form -`min 0.5 * x^T (Q + R^T D R) x + c^T x s.t. con_lb <= A x <= con_ub, var_lb <= x <= var_ub` -from matrix descriptors. `Q_desc` (sparse quadratic), `R_desc` (low-rank factor, shape `k x n`), and `D_desc` (rank-by-rank middle matrix in `R^T D R`) are all optional — pass `NULL` to omit any of them. `D_desc` defaults to identity, recovering the standard `Q + R^T R` formulation; it may be diagonal, sparse, dense, or indefinite, and the runtime auto-detects the cheapest representation. +`min 0.5 * x^T (Q + R^T D R) x + c^T x s.t. con_lb <= A x <= con_ub, var_lb <= x <= var_ub`, +with optional variable cones and native affine constraints +`F x + affine_cone_offset in K`. Affine cone starts are relative to rows of +`F`, and the blocks must cover `F` completely. The problem is built from matrix +descriptors. `Q_desc` (sparse quadratic), `R_desc` (low-rank factor, shape +`k x n`), and `D_desc` (rank-by-rank middle matrix in `R^T D R`) are all +optional — pass `NULL` to omit any of them. `D_desc` defaults to identity, +recovering the standard `Q + R^T R` formulation; it may be diagonal, sparse, +dense, or indefinite, and the runtime auto-detects the cheapest representation. ### Setting Start Values @@ -117,7 +134,8 @@ void set_start_values( ); ``` -Sets initial primal and dual solutions for warm starting. +Sets initial primal and dual solutions for warm starting. A `NULL` primal clears +free-coordinate warm starts but preserves values pinned by `set_cone_fixed`. ### Solving diff --git a/docs/c/types.md b/docs/c/types.md index b986048..301b2eb 100644 --- a/docs/c/types.md +++ b/docs/c/types.md @@ -96,6 +96,61 @@ typedef struct { } matrix_desc_t; ``` +## Cone Type + +```c +typedef enum { + CONE_ROTATED_SOC = 0, + CONE_STANDARD_SOC = 1, + CONE_EXPONENTIAL = 2, + CONE_POWER = 3 +} cone_type_t; +``` + +| Value | Constraint | Slot layout (length) | +|-------|------------|----------------------| +| `CONE_STANDARD_SOC` | `\|\|v\|\|^2 + w^2 <= z^2`, `z >= 0` | `v` (`v_dim`), `w`, `z` | +| `CONE_ROTATED_SOC` | `\|\|v\|\|^2 <= 2 s t`, `s, t >= 0` | `v` (`v_dim`), `s`, `t` | +| `CONE_EXPONENTIAL` | `y * exp(x / y) <= z`, `y > 0` | `x`, `y`, `z` (`v_dim` must be 1) | +| `CONE_POWER` | `x^alpha * y^(1-alpha) >= \|z\|`, `x,y >= 0` | `x`, `y`, `z` (`v_dim` must be 1) | + +## Cone Spec + +```c +typedef struct { + cone_type_t type; + int start_idx; + int v_dim; + double power_alpha; + const char *is_fixed; +} cone_spec_t; +``` + +Input descriptor for a single cone block. In `var_cones`, `start_idx` indexes +the variable vector; in `affine_cones`, it indexes rows of the separately +supplied affine matrix `F`. The +slot count is `v_dim + 2` for SOC/RSOC and `3` for +exponential/power cones. Power cones require `power_alpha` in `(0,1)`. +Variable cones may provide an `is_fixed` array of `slot_count` bytes. Every +mathematically nonempty fixed-slot pattern is supported for all four cone +types. Affine cones must set `is_fixed` to NULL. + +## Cone Blocks + +```c +typedef struct { + int num_cones; + int *start_idx; /* [num_cones] */ + int *v_dim; /* [num_cones] */ + cone_type_t *type; /* [num_cones] */ + double *power_alpha; /* [num_cones], or NULL */ + int fixed_mask_size; /* number of entries in is_fixed */ + char *is_fixed; /* ambient-coordinate flags, or NULL */ +} cone_blocks_t; +``` + +Storage form held inside `qp_problem_t`. Built from the user-supplied `cone_spec_t` array by `create_qp_problem`; users normally do not touch this struct directly. + ## Quadratic Objective Type ```c @@ -134,12 +189,21 @@ typedef struct { double *constraint_lower_bound; double *constraint_upper_bound; + double *affine_cone_offset; + cone_blocks_t affine_cones; double *primal_start; double *dual_start; } qp_problem_t; ``` +`create_qp_problem` canonicalizes the public `A` and `F` inputs into one +internal constraint matrix `[A; F]`. Scalar rows come first and retain their +lower and upper bounds. Affine rows follow with infinite scalar bounds; +`affine_cone_offset` is zero on scalar rows, and stored affine cone `start_idx` +values are global internal row indices. Consequently, `dual_start` and returned +dual solutions have length `rows(A) + rows(F)` and order `[dual_A, dual_F]`. + ## Restart Parameters ```c @@ -223,10 +287,12 @@ Describes the 2D process grid for distributed solving. If `decided` is `false`, ```c typedef struct { + int curtis_reid_iterations; int l_inf_ruiz_iterations; bool has_pock_chambolle_alpha; double pock_chambolle_alpha; bool bound_objective_rescaling; + bool use_cone_preserving_scaling; int verbose; int termination_evaluation_frequency; int sv_max_iter; diff --git a/docs/citation.md b/docs/citation.md index 5e7d7b1..681f55b 100644 --- a/docs/citation.md +++ b/docs/citation.md @@ -1,18 +1,18 @@ # Citation -If you use PDHCG-II in your research, please cite the following papers: +If you use PDHCG in your research, please cite the following papers: ## Main Paper ```bibtex -@misc{li2026pdhcgiienhancedversionpdhcg, - title={PDHCG-II: An Enhanced Version of PDHCG for Large-Scale Convex QP}, +@misc{li2026gpuacceleratedconicquadraticprogramming, + title={GPU-Accelerated Conic Quadratic Programming with Local Linear Convergence under Strict Complementarity}, author={Hongpei Li and Yicheng Huang and Huikang Liu and Dongdong Ge and Yinyu Ye}, year={2026}, - eprint={2602.23967}, + eprint={2608.09159}, archivePrefix={arXiv}, primaryClass={math.OC}, - url={https://arxiv.org/abs/2602.23967}, + url={https://arxiv.org/abs/2608.09159}, } ``` @@ -32,9 +32,9 @@ If you use PDHCG-II in your research, please cite the following papers: ## Links -- [arXiv Preprint](https://arxiv.org/abs/2602.23967) +- [arXiv Preprint](https://arxiv.org/abs/2608.09159) - [INFORMS Journal on Computing](https://pubsonline.informs.org/doi/10.1287/ijoc.2024.0983) -- [GitHub Repository](https://github.com/Lhongpei/PDHCG-II) +- [GitHub Repository](https://github.com/Lhongpei/PDHCG) ## Acknowledgments diff --git a/docs/examples.md b/docs/examples.md index 3bc1cb2..e7f6cec 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -123,20 +123,20 @@ m.optimize() ### Reading from MPS File ```bash -./build/bin/pdhcg problem.mps ./output --time_limit 3600 --eps_opt 1e-6 +./build/pdhcg problem.mps ./output --time_limit 3600 --eps_opt 1e-6 ``` ### Command Line Options ```bash # Silent mode, tight tolerance -./build/bin/pdhcg problem.mps ./output -v 0 --eps_opt 1e-8 --eps_feas 1e-8 +./build/pdhcg problem.mps ./output -v 0 --eps_opt 1e-8 --eps_feas 1e-8 # With iteration limit -./build/bin/pdhcg problem.mps ./output --iter_limit 100000 +./build/pdhcg problem.mps ./output --iter_limit 100000 # Disable Pock-Chambolle rescaling -./build/bin/pdhcg problem.mps ./output --no_pock_chambolle +./build/pdhcg problem.mps ./output --no_pock_chambolle ``` ## Multi-GPU Distributed Examples @@ -147,7 +147,7 @@ These examples require the solver to be built with `-DPDHCG_COMPILE_DISTRIBUTED= ```bash # Run on 4 GPUs -mpirun -n 4 ./build/bin/pdhcg problem.mps ./output +mpirun -n 4 ./build/pdhcg problem.mps ./output ``` ### Custom Process Grid @@ -156,20 +156,165 @@ By default, the solver attempts to infer a square-ish 2D process grid. You can e ```bash # Use a 2x4 grid (8 GPUs total) -mpirun -n 8 ./build/bin/pdhcg problem.mps ./output --grid_size 2,4 +mpirun -n 8 ./build/pdhcg problem.mps ./output --grid_size 2,4 ``` ### Partition and Permutation Options ```bash # Uniform row partitioning with block permutation -mpirun -n 4 ./build/bin/pdhcg problem.mps ./output \ +mpirun -n 4 ./build/pdhcg problem.mps ./output \ --partition_method uniform \ --permute_method block \ --permute_block_size 512 # Nonzero-balanced partitioning with random permutation -mpirun -n 4 ./build/bin/pdhcg problem.mps ./output \ +mpirun -n 4 ./build/pdhcg problem.mps ./output \ --partition_method nnz \ --permute_method random ``` + +## Conic Examples + +The conic interface accepts a columnar `ConeSpec` through `Model` or the +lower-level `solve_once` entry. See the [Python model API](python/model.md#cone-constraints) +for its fields and the [C API](c/functions.md) for cone layouts. + +### Standard SOC + +Minimise `z` over `(v, w, z) in K_soc` with `v = 3`, `w = 4`. Optimum recovers +`(3, 4, 5)` (the Euclidean norm `||(3,4)||_2`). + +```python +import numpy as np +import scipy.sparse as sp +from pdhcg import ConeSpec, ConeType +from pdhcg._core import solve_once + +A = sp.csr_matrix([[1.0, 0.0, 0.0], + [0.0, 1.0, 0.0]]) +c = np.array([0.0, 0.0, 1.0]) +con_lb = np.array([3.0, 4.0]); con_ub = con_lb.copy() +INF = np.full(3, 1e30) + +info = solve_once( + None, None, A, c, 0.0, + -INF, INF, con_lb, con_ub, + cones=ConeSpec(ConeType.SOC, np.array([0], dtype=np.int32)), +) +print(info["X"]) # [3., 4., 5.] +``` + +Expected output: + +``` +Status: OPTIMAL +X: [3.0 4.0 5.0] +PrimalObj: 5.0 +``` + +### SPARSE_Q coupled to SOC + +Off-diagonal `Q` on the non-cone block `(a, b)` linearly coupled into a SOC +cone `(v, w, z)`. Variables are `(a, b, v, w, z)`; constraints pin `a = v`, +`b = w`; `Q = [[1, 0.5], [0.5, 1]]` on the `(a,b)` block. Optimum: +`a = 3, b = 4, v = 3, w = 4, z = 5`. + +```python +import numpy as np +import scipy.sparse as sp +from pdhcg import ConeSpec, ConeType +from pdhcg._core import solve_once + +# (a, b, v, w, z) with a - v = 0, b - w = 0 +A = sp.csr_matrix([[1, 0, -1, 0, 0], + [0, 1, 0, -1, 0]], dtype=float) +Q = sp.csr_matrix([[1.0, 0.5, 0, 0, 0], + [0.5, 1.0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]) +c = np.array([-3.0, -4.0, 0.0, 0.0, 1.0]) +con_lb = np.zeros(2); con_ub = np.zeros(2) +INF = np.full(5, 1e30) + +info = solve_once( + Q, None, A, c, 0.0, + -INF, INF, con_lb, con_ub, + cones=ConeSpec(ConeType.SOC, np.array([2], dtype=np.int32)), +) +print(info["X"]) # [3, 4, 3, 4, 5] +print(info["PrimalObj"]) # -1.5 +``` + +Expected output: + +``` +Status: OPTIMAL +X: [3.0 4.0 3.0 4.0 5.0] +PrimalObj: -1.5 +``` + +### Exponential cone with fixed `y` (Fisher-style) + +Quasi-linear Fisher market, 2 buyers `×` 3 goods, hand-set utilities `u_ij` +and budgets `w_i`. Variables: `x_ij` (allocations), `v_i` (slack), and per +buyer an exp triple `(z_i, y_i, t_i)` with `y_i` pinned to 1. Cone +feasibility at the solution: `y_i * exp(z_i / y_i) <= t_i`. + +```python +import numpy as np +import scipy.sparse as sp +from pdhcg import ConeSpec, ConeType +from pdhcg._core import solve_once + +u = np.array([[0.5, 1.0, 0.2], [0.3, 0.7, 1.0]]) # 2 buyers, 3 goods +w = np.array([1.0, 1.5]) # budgets +b = np.array([1.0, 1.0, 1.0]) # supplies +n, m = u.shape +nx, N = n*m, n*m + n + 3*n # x | v | (z,y,t) +v0, c0 = nx, nx + n + +# Build A row by row: m supply rows, then n budget rows. +rows = sp.lil_matrix((m + n, N)) +for j in range(m): + rows[j, [i*m + j for i in range(n)]] = 1.0 # sum_i x_ij = b_j +for i in range(n): + rows[m+i, i*m:(i+1)*m] = -u[i] # -u_i^T x_i - v_i + t_i = 0 + rows[m+i, v0+i] = -1.0 + rows[m+i, c0+3*i+2] = 1.0 +A = rows.tocsr() + +c = np.zeros(N) +for i in range(n): + c[v0+i] = 1.0 + c[c0+3*i] = -w[i] # min sum v_i - w_i z_i +lb = np.full(N, -1e30); ub = np.full(N, 1e30) +lb[:nx] = 0.0; lb[v0:v0+n] = 0.0 +con_b = np.concatenate([b, np.zeros(n)]) + +primal_start = np.zeros(N) +primal_start[c0+1::3] = 1.0 # pin y_i = 1 +fixed_mask = np.zeros(N, dtype=np.uint8) +fixed_mask[c0+1::3] = 1 +cones = ConeSpec( + ConeType.EXP, + c0 + 3 * np.arange(n, dtype=np.int32), + fixed_mask=fixed_mask, +) + +info = solve_once(None, None, A, c, 0.0, lb, ub, con_b, con_b.copy(), + primal_start=primal_start, cones=cones) +x = info["X"] +for i in range(n): + z, y, t = x[c0+3*i:c0+3*i+3] + print(f"buyer {i}: y={y:.4f} exp(z/y)={np.exp(z/y):.4f} t={t:.4f}") +``` + +Expected output: + +``` +Status: OPTIMAL +buyer 0: y=1.0000 exp(z/y)=... t=... # y*exp(z/y) <= t +buyer 1: y=1.0000 exp(z/y)=... t=... +``` diff --git a/docs/index.md b/docs/index.md index 759b918..faf5621 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,16 +1,18 @@ -# PDHCG-II +# PDHCG -PDHCG-II is a high-performance, GPU-accelerated implementation of the Primal-Dual Hybrid Gradient (PDHG) algorithm designed for solving large-scale Convex Quadratic Programming (QP) problems. +PDHCG is a high-performance, GPU-accelerated implementation of the Primal-Dual Hybrid Gradient (PDHG) algorithm for large-scale convex quadratic and quadratic conic programming. ## Problem Formulation -PDHCG solves quadratic programs in the following form: +PDHCG solves quadratic conic programs in the following form: $$ \begin{aligned} \min_{x} \quad & \frac{1}{2}x^\top (Q + R^\top D R) x + c^\top x \\ \text{s.t.} \quad & \ell_c \le Ax \le u_c, \\ - & \ell_v \le x \le u_v. + & Fx + g \in \mathcal{K}_a, \\ + & \ell_v \le x \le u_v, \\ + & x_J \in \mathcal{K}_v \quad \text{for variable-cone blocks } J. \end{aligned} $$ @@ -20,14 +22,17 @@ Where: - $R \in \mathbb{R}^{k\times n}$ is a low-rank factor of rank $k$ (optional) - $D \in \mathbb{R}^{k\times k}$ is an optional middle matrix; defaults to the identity, recovering the standard $Q + R^\top R$ form. May be diagonal, sparse, dense, or indefinite — the backend auto-detects the cheapest representation - $A$ is the constraint matrix +- $F$ and $g$ define the native affine-cone map - $c$ is the linear objective vector - $\ell_c, u_c$ are constraint bounds - $\ell_v, u_v$ are variable bounds +- $\mathcal{K}_a$ and $\mathcal{K}_v$ are products of Standard SOC, Rotated SOC, Exponential, or Power cones ## Key Features - **GPU Acceleration**: Fully leverages NVIDIA CUDA for extreme-scale QP problems - **Flexible Problem Structure**: Supports sparse, low-rank, and middle-weighted low-rank ($R^\top D R$) quadratic terms — alone or combined +- **Conic constraints**: fully GPU-accelerated SOC, Rotated SOC, Exponential, and Power cone projection on variable blocks or native affine maps $Fx + g$ - **High Performance**: Competitive with commercial solvers on large-scale problems - **SpMVOp Auto-Detection**: Automatically uses cuSPARSE SpMVOp on CUDA 13+ while falling back to standard SpMV on CUDA 12.x - **Multi-GPU Distributed Solving**: Supports parallel solving across multiple GPUs via MPI and NCCL (optional, enabled at compile time) @@ -38,20 +43,21 @@ Where: - [Python API Reference](python/quickstart.md) - [C API Reference](c/overview.md) - [Examples](examples.md) +- [Conic constraints usage](examples.md#conic-examples) ## Citation If you use this software in your research, please cite: ```bibtex -@misc{li2026pdhcgiienhancedversionpdhcg, - title={PDHCG-II: An Enhanced Version of PDHCG for Large-Scale Convex QP}, +@misc{li2026gpuacceleratedconicquadraticprogramming, + title={GPU-Accelerated Conic Quadratic Programming with Local Linear Convergence under Strict Complementarity}, author={Hongpei Li and Yicheng Huang and Huikang Liu and Dongdong Ge and Yinyu Ye}, year={2026}, - eprint={2602.23967}, + eprint={2608.09159}, archivePrefix={arXiv}, primaryClass={math.OC}, - url={https://arxiv.org/abs/2602.23967}, + url={https://arxiv.org/abs/2608.09159}, } ``` diff --git a/docs/installation.md b/docs/installation.md index 2e1b2e7..f16c9ff 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -20,13 +20,13 @@ Clone the repository and compile the project using CMake: ```bash -git clone https://github.com/Lhongpei/PDHCG-II.git -cd PDHCG-II +git clone https://github.com/Lhongpei/PDHCG.git +cd PDHCG cmake -S . -B build cmake --build build --clean-first ``` -This will create the solver binary at `./build/bin/pdhcg`. +This will create the solver binary at `./build/pdhcg`. ### Specifying CUDA Compiler @@ -63,8 +63,8 @@ pip install pdhcg ### From Source ```bash -git clone https://github.com/Lhongpei/PDHCG-II.git -cd PDHCG-II +git clone https://github.com/Lhongpei/PDHCG.git +cd PDHCG pip install . ``` @@ -73,8 +73,8 @@ pip install . For development with editable install: ```bash -git clone https://github.com/Lhongpei/PDHCG-II.git -cd PDHCG-II +git clone https://github.com/Lhongpei/PDHCG.git +cd PDHCG pip install -e ".[test]" ``` @@ -94,7 +94,7 @@ pip install pdhcg ### C++ Executable ```bash -./build/bin/pdhcg --help +./build/pdhcg --help ``` ### Python Package diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..c29c0d0 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,94 @@ +# Migrating to 0.3 + +Version 0.3 adds quadratic conic models and changes the C and Python model +construction APIs. This page covers the source changes needed by existing 0.2 +callers. + +## C API + +`create_qp_problem` has six new trailing arguments: + +```c +qp_problem_t *create_qp_problem( + const double *objective_c, + const matrix_desc_t *Q_desc, + const matrix_desc_t *R_desc, + const matrix_desc_t *D_desc, + const matrix_desc_t *A_desc, + const double *con_lb, + const double *con_ub, + const double *var_lb, + const double *var_ub, + const double *objective_constant, + int num_var_cones, + const cone_spec_t *var_cones, + const matrix_desc_t *affine_cone_matrix_desc, + const double *affine_cone_offset, + int num_affine_cones, + const cone_spec_t *affine_cones); +``` + +An existing QP with no cones only needs the six neutral arguments appended: + +```c +qp_problem_t *problem = create_qp_problem( + c, Q, R, D, A, con_lb, con_ub, var_lb, var_ub, objective_constant, + 0, NULL, NULL, NULL, 0, NULL); +``` + +For conic models, use `cone_spec_t` arrays as described in the +[C API overview](c/overview.md). Variable-cone indices refer to variables; +affine-cone indices refer to rows of the separately supplied +`affine_cone_matrix_desc` (`F`), and `affine_cone_offset` has one entry per row +of `F`. Affine cone blocks must cover every row of `F`. + +`pdhcg_postsolve` now returns nonzero after a complete primal-dual recovery and +zero when postsolve fails or full dual recovery is unavailable: + +```c +if (!pdhcg_postsolve(info, result, original_problem)) { + /* Handle postsolve failure. */ +} +``` + +`qp_problem_t` and `pdhg_parameters_t` gained conic fields. Do not depend on +their old binary layout. Recompile downstream code and initialize parameters +through `set_default_parameters` before overriding individual fields. + +## Python API + +Cone metadata is now columnar. Replace a list of dictionaries with one +`ConeSpec`: + +```python +import numpy as np +from pdhcg import ConeSpec, ConeType + +cones = ConeSpec( + types=np.array([ConeType.SOC, ConeType.POWER], dtype=np.int32), + starts=np.array([0, 4], dtype=np.int32), + v_dims=np.array([2, 1], dtype=np.int32), + power_alphas=np.array([0.0, 0.4]), +) +``` + +Pass this object as `variable_cones` or `affine_cones` when constructing a +`Model`. Legacy `list[dict]` inputs intentionally raise `TypeError`. + +CVXPY support is optional: + +```bash +pip install "pdhcg[cvxpy]" +``` + +Import the backend once before selecting PDHCG as the solver: + +```python +import cvxpy as cp +import pdhcg.cvxpy_backend # Registers solver="PDHCG". +``` + +## Executable Location + +A source build places the command-line executable at `build/pdhcg`. Installed +packages place it in the installation prefix's `bin` directory. diff --git a/docs/python/model.md b/docs/python/model.md index a10f289..669cfe1 100644 --- a/docs/python/model.md +++ b/docs/python/model.md @@ -17,6 +17,8 @@ - setConstraintMatrix - setConstraintLowerBound - setConstraintUpperBound + - setVariableCones + - setAffineConeConstraints - setVariableLowerBound - setVariableUpperBound - setWarmStart @@ -25,3 +27,67 @@ - setParams - getParam - optimize + +## Cone constraints + +Use the columnar `ConeSpec` API for both variable and affine cones. Its metadata +is stored in contiguous NumPy arrays, so even millions of cone blocks do not +require one Python dict per cone. + +| Field | Type | Notes | +|---|---|---| +| `types` | scalar or `int32[K]` | `ConeType.SOC`, `RSOC`, `EXP`, or `POWER`. | +| `starts` | `int32[K]` | First variable index or affine row of each block. | +| `v_dims` | scalar or `int32[K]` | Length of `v`; defaults to 1. | +| `power_alphas` | scalar or `float64[K]` | Required in `(0, 1)` for power cones. | +| `fixed_mask` | optional `uint8[N]` | Ambient-coordinate mask for fixed variable slots. Values come from the primal warm start. | + +Slot layout per cone: + +- `soc`: `v[0..v_dim-1], w, z` with `||v||^2 + w^2 <= z^2`, `z >= 0`. +- `rsoc`: `v[0..v_dim-1], s, t` with `||v||^2 <= 2 s t`, `s, t >= 0`. +- `exp`: `x, y, z` with `y * exp(x / y) <= z`, `y > 0`. +- `power`: `x, y, z` with `x^alpha * y^(1-alpha) >= |z|`, `x, y >= 0`. + +```python +import numpy as np +from pdhcg import ConeSpec, ConeType, Model + +cones = ConeSpec( + types=[ConeType.SOC, ConeType.EXP], + starts=np.array([0, 4], dtype=np.int32), + v_dims=[2, 1], +) +model = Model(objective_vector=c, constraint_matrix=A, variable_cones=cones) +``` + +Scalar metadata broadcasts. For example, one million adjacent exponential +cones can be described without a Python loop: + +```python +num_cones = 1_000_000 +cones = ConeSpec( + ConeType.EXP, + 3 * np.arange(num_cones, dtype=np.int32), +) +``` + +`solve_once(..., cones=cones)` accepts the same object. Cone arguments accept +`ConeSpec` only; the former per-cone `list[dict]` input is not supported. + +See [quickstart](quickstart.md#quick-start-with-cone-constraints) for a runnable example. + +Native affine cone constraints `F x + g in K` are available directly on +`Model` through the `affine_cone_matrix`, `affine_cone_offset`, and +`affine_cones` constructor arguments, or `setAffineConeConstraints`. Here, +`ConeSpec.starts` indexes rows of `F`, and the blocks must cover every row of +`F` exactly once. + +```python +model = Model( + objective_vector=c, + affine_cone_matrix=F, + affine_cone_offset=g, + affine_cones=ConeSpec(ConeType.SOC, np.array([0], dtype=np.int32)), +) +``` diff --git a/docs/python/parameters.md b/docs/python/parameters.md index 6e6238f..7f27935 100644 --- a/docs/python/parameters.md +++ b/docs/python/parameters.md @@ -21,16 +21,18 @@ m.setParams(TimeLimit=3600, LogLevel=1) | `IterationLimit` | int | 2147483647 | Maximum number of iterations | | `OptTol` | float | 1e-4 | Relative optimality tolerance | | `FeasTol` | float | 1e-4 | Relative feasibility tolerance | -| `InfeasTol` | float | 1e-10 | Infeasibility detection tolerance | +| `InfeasTol` | float | 1e-12 | Infeasibility detection tolerance | ### Algorithm Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| +| `CurtisReidIters` | int | 0 | Iterations for Curtis-Reid log-domain matrix scaling; 0 disables it | | `RuizIterations` | int | 10 | Iterations for L-inf Ruiz rescaling | | `PockChambolleAlpha` | float | 1.0 | Pock-Chambolle step size parameter | | `UsePockChambolle` | bool | True | Enable Pock-Chambolle rescaling | | `UseBoundObjectiveRescaling` | bool | True | Enable bound objective rescaling | +| `UseConePreservingScaling` | bool | True | Broadcast one scaling value over every cone block | | `EvalFrequency` | int | 200 | Frequency of termination criteria evaluation | ### Inner Solver Parameters @@ -60,3 +62,8 @@ m.setParams(TimeLimit=3600, LogLevel=1) | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `OptNorm` | str | "linf" | Norm for optimality: "l2" or "linf" | + +!!! note "Cone constraints" + Cone specs are not solver parameters; they are part of the problem and are + passed as a columnar `ConeSpec` to `Model(variable_cones=...)` or the + `cones=` kwarg of `solve_once`. See [model.md](model.md#cone-constraints). diff --git a/docs/python/quickstart.md b/docs/python/quickstart.md index 2d9097b..166cb4f 100644 --- a/docs/python/quickstart.md +++ b/docs/python/quickstart.md @@ -1,6 +1,6 @@ # Python Quick Start -PDHCG provides a user-friendly Python interface that allows you to define, solve, and analyze QP problems using familiar libraries like NumPy and SciPy. +PDHCG provides a user-friendly Python interface for quadratic and quadratic conic problems using familiar NumPy and SciPy data structures. ## Basic Usage @@ -55,14 +55,64 @@ if m.X is not None: print(f"Primal Solution: {m.X}") ``` +## Quick start with cone constraints + +Conic constraints use a columnar `ConeSpec`, whose arrays can describe many +blocks without allocating one Python object per cone. See +[model.md](model.md#cone-constraints). + +```python +import numpy as np +import scipy.sparse as sp +from pdhcg import ConeSpec, ConeType, Model + +# min z s.t. v = 3, w = 4, (v, w, z) in K_soc => z = sqrt(v^2 + w^2) = 5 +A = sp.csr_matrix([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) +model = Model( + objective_vector=np.array([0.0, 0.0, 1.0]), + constraint_matrix=A, + constraint_lower_bound=np.array([3.0, 4.0]), + constraint_upper_bound=np.array([3.0, 4.0]), + variable_cones=ConeSpec( + ConeType.SOC, + np.array([0], dtype=np.int32), + v_dims=1, + ), +) +model.optimize() +print(model.Status, model.X) +``` + +## CVXPY + +Install the optional integration with `pip install "pdhcg[cvxpy]"`, then +import the backend once in each process: + +```python +import cvxpy as cp +import pdhcg.cvxpy_backend # Registers solver="PDHCG". + +x = cp.Variable() +problem = cp.Problem(cp.Minimize(x), [x >= 1]) +value = problem.solve(solver="PDHCG", eps=1e-6) + +print(problem.status, value, x.value) +``` + +The backend preserves CVXPY's primal and dual conventions. It supports +quadratic objectives and Zero, NonNeg, SOC, ExpCone, and PowCone3D +constraints. PSD and mixed-integer models are not supported. + ## Model Creation -The `Model` class is the core interface for defining QP problems. The problem formulation is: +The `Model` class is the core interface for defining quadratic conic problems. The problem formulation is: $$ \begin{aligned} \min_{x} \quad & \frac{1}{2}x^\top (Q + R^\top D R) x + c^\top x \\ \text{s.t.} \quad & \ell_c \le Ax \le u_c, \\ + & Fx + g \in \mathcal{K}_a, \\ + & x_J \in \mathcal{K}_v \quad \text{for variable-cone blocks } J, \\ & \ell_v \le x \le u_v. \end{aligned} $$ @@ -79,6 +129,10 @@ $$ - `constraint_matrix` ($A$): Linear constraint matrix - `constraint_lower_bound` ($\ell_c$): Constraint lower bounds - `constraint_upper_bound` ($u_c$): Constraint upper bounds +- `affine_cone_matrix` ($F$): Matrix in the native affine-cone constraint $Fx + g \in \mathcal{K}_a$ +- `affine_cone_offset` ($g$): Affine-cone offset; defaults to zero +- `affine_cones`: `ConeSpec` covering every row of $F$ +- `variable_cones`: `ConeSpec` describing cone blocks embedded in $x$ - `variable_lower_bound` ($\ell_v$): Variable lower bounds (default: $-\infty$) - `variable_upper_bound` ($u_v$): Variable upper bounds (default: $+\infty$) - `objective_constant`: Constant term in objective diff --git a/include/cbf_parser.h b/include/cbf_parser.h new file mode 100644 index 0000000..a1e6303 --- /dev/null +++ b/include/cbf_parser.h @@ -0,0 +1,30 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include "pdhcg_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + qp_problem_t *read_cbf_file(const char *filename); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/mps_parser.h b/include/mps_parser.h index a5be4c1..60d87b1 100644 --- a/include/mps_parser.h +++ b/include/mps_parser.h @@ -19,4 +19,13 @@ limitations under the License. #include "pdhcg_types.h" -qp_problem_t *read_mps_file(const char *filename); \ No newline at end of file +#ifdef __cplusplus +extern "C" +{ +#endif + + qp_problem_t *read_mps_file(const char *filename); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/pdhcg.h b/include/pdhcg.h index 5b096d7..ea9573b 100644 --- a/include/pdhcg.h +++ b/include/pdhcg.h @@ -24,8 +24,10 @@ extern "C" { #endif - // create an qp_problem_t from matrix descriptors. - // pass NULL for the default D = I. + /* Pass NULL for any optional matrix descriptor (defaults: Q=0, R=0, D=I). + A models scalar rows con_lb <= A*x <= con_ub. affine_cone_matrix_desc + models F*x + affine_cone_offset in K; affine cone indices refer to rows + of F, which must be fully covered by the supplied cone blocks. */ qp_problem_t *create_qp_problem(const double *objective_c, const matrix_desc_t *Q_desc, const matrix_desc_t *R_desc, @@ -35,18 +37,26 @@ extern "C" const double *con_ub, const double *var_lb, const double *var_ub, - const double *objective_constant); + const double *objective_constant, + int num_var_cones, + const cone_spec_t *var_cones, + const matrix_desc_t *affine_cone_matrix_desc, + const double *affine_cone_offset, + int num_affine_cones, + const cone_spec_t *affine_cones); - // Set up initial primal and dual solution for an qp_problem_t + /* dual has rows(A) + rows(F) entries ordered as [dual_A, dual_F]. */ void set_start_values(qp_problem_t *prob, const double *primal, const double *dual); + int set_cone_fixed(qp_problem_t *prob, int cone_idx, int slot, double value); + + qp_problem_t *qcqp_to_socp_qp(const qp_problem_t *orig_qcqp, cone_type_t default_type); + // solve the LP problem using PDHG pdhcg_result_t *solve_qp_problem(const qp_problem_t *prob, const pdhg_parameters_t *params); -#ifdef PDHCG_COMPILE_DISTRIBUTED // solve the QP problem using distributed multi-GPU PDHG pdhcg_result_t *solve_qp_problem_distributed(const pdhg_parameters_t *params, const qp_problem_t *original_problem); -#endif // parameter void set_default_parameters(pdhg_parameters_t *params); diff --git a/include/pdhcg_types.h b/include/pdhcg_types.h index 1ac91ad..3152397 100644 --- a/include/pdhcg_types.h +++ b/include/pdhcg_types.h @@ -60,6 +60,35 @@ extern "C" PDHCG_NON_Q } quad_obj_type_t; + typedef enum + { + CONE_ROTATED_SOC = 0, + CONE_STANDARD_SOC = 1, + CONE_EXPONENTIAL = 2, + CONE_POWER = 3, /* 3-dim: x^alpha * y^(1-alpha) >= |z|, x,y >= 0 */ + NUM_CONE_TYPES = 4 + } cone_type_t; + + typedef struct + { + int num_cones; + int *start_idx; /* [num_cones] */ + int *v_dim; /* [num_cones] */ + cone_type_t *type; /* [num_cones] */ + double *power_alpha; /* [num_cones]; alpha in (0,1) for CONE_POWER, else unused */ + int fixed_mask_size; /* number of entries in is_fixed; zero when no mask is stored */ + char *is_fixed; + } cone_blocks_t; + + typedef struct + { + cone_type_t type; + int start_idx; /* variable index, or row of F for affine cones */ + int v_dim; + double power_alpha; /* required for CONE_POWER (in (0,1)); ignored otherwise */ + const char *is_fixed; /* variable cones only; must be NULL for affine cones */ + } cone_spec_t; + typedef struct { int num_variables; @@ -85,6 +114,19 @@ extern "C" double *constraint_lower_bound; double *constraint_upper_bound; + /* Internal canonical rows [A; F]. affine_cone_offset is zero on the + scalar A rows, and affine_cones use global canonical row indices. */ + double *affine_cone_offset; + cone_blocks_t affine_cones; + + int num_quadratic_constraints; + int *quadratic_constraint_row_indices; + CsrComponent **quadratic_constraint_matrices; + int *quadratic_constraint_matrix_num_nonzeros; + + cone_blocks_t cones; + int num_original_variables; + double *primal_start; double *dual_start; @@ -139,10 +181,12 @@ extern "C" typedef struct { + int curtis_reid_iterations; int l_inf_ruiz_iterations; bool has_pock_chambolle_alpha; double pock_chambolle_alpha; bool bound_objective_rescaling; + bool use_cone_preserving_scaling; int verbose; int termination_evaluation_frequency; int sv_max_iter; @@ -155,6 +199,7 @@ extern "C" inner_solver_parameters_t inner_solver_parameters; bool presolve; bool diag_jacobi_precond; + cone_type_t default_cone_type; partition_method_t partition_method; permute_method_t permute_method; grid_size_t grid_size; diff --git a/include/presolve_wrapper.h b/include/presolve_wrapper.h index 4a34b82..c384a83 100644 --- a/include/presolve_wrapper.h +++ b/include/presolve_wrapper.h @@ -1,8 +1,20 @@ /* - * PDHCG-II PSQP Presolve Wrapper Header - * - * This header provides the interface for using PSQP presolver within PDHCG-II. - */ +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* PDHCG optional PreFOS presolve adapter. */ #ifndef PDHCG_PRESOLVE_WRAPPER_H #define PDHCG_PRESOLVE_WRAPPER_H @@ -16,107 +28,37 @@ extern "C" { #endif - /* Presolve information structure (similar to cuPDLPx) - * Internal PSQP types are hidden using void* to avoid exposing PSQP headers */ + typedef enum + { + PDHCG_PRESOLVE_STATUS_UNCHANGED = 0, + PDHCG_PRESOLVE_STATUS_REDUCED, + PDHCG_PRESOLVE_STATUS_PRIMAL_INFEASIBLE, + PDHCG_PRESOLVE_STATUS_ERROR, + PDHCG_PRESOLVE_STATUS_NOT_AVAILABLE + } pdhcg_presolve_status_t; + + /* PreFOS types stay private to the adapter implementation. */ typedef struct { - void *presolver; /* Actually Presolver* */ - void *settings; /* Actually Settings* */ + void *presolver; qp_problem_t *reduced_problem; bool problem_solved_during_presolve; double presolve_time; - int presolve_status; + pdhcg_presolve_status_t presolve_status; + int prefos_original_rows; + double postsolve_tolerance; } pdhcg_presolve_info_t; - /* Data structure for presolved problem */ - typedef struct - { - int success; - int infeasible; - int unbounded; - - size_t m; - size_t n; - size_t nnz; - - double *Ax; - int *Ai; - int *Ap; - double *lhs; - double *rhs; - double *c; - double *lbs; - double *ubs; - double obj_offset; - - int has_quad_qr; - double *Qx; - int *Qi; - int *Qp; - size_t Qnnz; - double *Rx; - int *Ri; - int *Rp; - size_t Rnnz; - size_t k; - - void *presolver_handle; - } PDHCG_PresolvedData; - - /* Presolve standard QP with P matrix */ - PDHCG_PresolvedData *pdhcg_presolve_qp(const double *Ax, - const int *Ai, - const int *Ap, - size_t m, - size_t n, - size_t nnz, - const double *lhs, - const double *rhs, - const double *lbs, - const double *ubs, - const double *c, - const double *Px, - const int *Pi, - const int *Pp, - size_t Pnnz); - - /* Presolve QP in QR format: P = Q + R^T R. - * Note: PSQP does not support the optional middle matrix D from - * Q + R^T D R; the solver auto-disables presolve when D != I. */ - PDHCG_PresolvedData *pdhcg_presolve_qr(const double *Ax, - const int *Ai, - const int *Ap, - size_t m, - size_t n, - size_t nnz, - const double *lhs, - const double *rhs, - const double *lbs, - const double *ubs, - const double *c, - const double *Qx, - const int *Qi, - const int *Qp, - size_t Qnnz, - const double *Rx, - const int *Ri, - const int *Rp, - size_t Rnnz, - size_t k); - - /* Cleanup presolved data */ - void pdhcg_presolve_cleanup(PDHCG_PresolvedData *data); - - /* Get PSQP version string */ + /* Get the configured presolver version string. */ const char *pdhcg_presolve_version(void); - /* Check if PSQP is available */ + /* Check whether PDHCG was compiled with PreFOS. */ int pdhcg_presolve_available(void); /* Get presolve status string */ const char *pdhcg_get_presolve_status_str(int status); - /* Main presolve function (similar to cuPDLPx's pslp_presolve) */ + /* Presolve the unified LP/QP/conic model. */ pdhcg_presolve_info_t *pdhcg_presolve(const qp_problem_t *original_prob, const pdhg_parameters_t *params); /* Create result from presolve (when problem is solved during presolve) */ @@ -124,7 +66,7 @@ extern "C" const qp_problem_t *original_prob); /* Postsolve to recover original solution */ - void pdhcg_postsolve(const pdhcg_presolve_info_t *info, pdhcg_result_t *result, const qp_problem_t *original_prob); + int pdhcg_postsolve(const pdhcg_presolve_info_t *info, pdhcg_result_t *result, const qp_problem_t *original_prob); /* Free presolve info */ void pdhcg_presolve_info_free(pdhcg_presolve_info_t *info); diff --git a/internal/cone_dispatch.h b/internal/cone_dispatch.h new file mode 100644 index 0000000..fa9e8b8 --- /dev/null +++ b/internal/cone_dispatch.h @@ -0,0 +1,38 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include "internal_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + void project_cone_runtime(pdhg_solver_state_t *state, cone_runtime_t *runtime, double *vector, double *warm_start); + + void project_cone_runtime_diag_q(pdhg_solver_state_t *state, cone_runtime_t *runtime, double primal_step_size); + + void compute_cone_dual_residual(pdhg_solver_state_t *state, const double *effective_objective); + + void recompute_cone_reflection(pdhg_solver_state_t *state); + + void set_cone_dual_slack(pdhg_solver_state_t *state, const double *effective_objective); + +#ifdef __cplusplus +} +#endif diff --git a/internal/cone_section_projection.cuh b/internal/cone_section_projection.cuh new file mode 100644 index 0000000..ad614cb --- /dev/null +++ b/internal/cone_section_projection.cuh @@ -0,0 +1,746 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include +#include + +__device__ static inline double +cone_section_weight(const double *rescaling, const double *q_diag, double tau, int index) +{ + double metric = q_diag ? 1.0 + tau * q_diag[index] : 1.0; + double d = rescaling[index]; + return fmax(metric * d * d, DBL_MIN); +} + +__device__ static inline bool cone_section_has_fixed(const char *is_fixed, int start, int length) +{ + if (!is_fixed) + return false; + for (int slot = 0; slot < length; ++slot) + if (is_fixed[start + slot]) + return true; + return false; +} + +__device__ static inline double cone_section_actual(const double *point, const double *rescaling, int index) +{ + return point[index] / rescaling[index]; +} + +/* For the negative scalar branch of a weighted SOC projection, return a + multiplier at which the root residual is nonnegative. */ +__device__ static inline double cone_section_negative_soc_upper( + double singular_metric, double endpoint_polar, double fixed_norm2, double polar_norm2, double max_vector_metric) +{ + double upper; + if (fixed_norm2 > 0.0) + { + upper = singular_metric + endpoint_polar / sqrt(fixed_norm2); + } + else + { + double polar_norm = sqrt(polar_norm2); + double gap = polar_norm - endpoint_polar; + if (!(gap > 0.0)) + return NAN; + upper = (polar_norm / gap) * singular_metric + (endpoint_polar / gap) * max_vector_metric; + } + return upper * (1.0 + 64.0 * DBL_EPSILON); +} + +/* Map a weighted rotated SOC to a weighted standard SOC in sum/difference + endpoint coordinates, then reuse its negative-branch bracket. */ +__device__ static inline double cone_section_negative_rsoc_upper(double omega_s, + double omega_t, + double s, + double t, + double fixed_norm2, + double polar_norm2, + double max_vector_metric) +{ + const double inv_sqrt2 = 0.70710678118654752440; + double sqrt_omega_s = sqrt(omega_s); + double sqrt_omega_t = sqrt(omega_t); + double root_metric = sqrt_omega_s * sqrt_omega_t; + double scaled_s = sqrt_omega_s * s; + double scaled_t = sqrt_omega_t * t; + double transformed_w = (scaled_s - scaled_t) * inv_sqrt2; + double endpoint_polar = -(scaled_s + scaled_t) * inv_sqrt2; + double transformed_fixed_norm2 = root_metric * fixed_norm2; + double transformed_polar_norm2 = polar_norm2 / root_metric + transformed_w * transformed_w; + double transformed_max_metric = fmax(1.0, max_vector_metric / root_metric); + double transformed_upper = cone_section_negative_soc_upper( + 1.0, endpoint_polar, transformed_fixed_norm2, transformed_polar_norm2, transformed_max_metric); + return root_metric * transformed_upper; +} + +/* Weighted projection onto an arbitrary nonempty fixed section of + { (u,z) : ||u||_2 <= z }. The first k+1 coordinates form u. */ +__device__ static inline void project_standard_soc_section_serial(double *point, + const double *rescaling, + const double *q_diag, + double tau, + double *warm_start, + int start, + int k, + const char *is_fixed) +{ + int u_length = k + 1; + int z_index = start + u_length; + bool fixed_z = is_fixed[z_index] != 0; + double fixed_norm2 = 0.0; + double free_norm2 = 0.0; + double polar_norm2 = 0.0; + double max_omega = 0.0; + int free_count = 0; + for (int slot = 0; slot < u_length; ++slot) + { + int index = start + slot; + double value = cone_section_actual(point, rescaling, index); + if (is_fixed[index]) + fixed_norm2 += value * value; + else + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + free_norm2 += value * value; + polar_norm2 += (omega * value) * (omega * value); + max_omega = fmax(max_omega, omega); + ++free_count; + } + } + + double z_input = cone_section_actual(point, rescaling, z_index); + if (fixed_z) + { + double radius2 = fmax(0.0, z_input * z_input - fixed_norm2); + if (free_count == 0 || free_norm2 <= radius2) + return; + if (!(radius2 > 0.0)) + { + for (int slot = 0; slot < u_length; ++slot) + if (!is_fixed[start + slot]) + point[start + slot] = 0.0; + return; + } + + double lo = 0.0; + double hi = sqrt(polar_norm2) / sqrt(radius2) * (1.0 + 64.0 * DBL_EPSILON); + if (!(hi > 0.0) || !isfinite(hi)) + { + hi = warm_start && *warm_start > 0.0 && isfinite(*warm_start) ? *warm_start : 1.0; + for (int expansion = 0; expansion < 100; ++expansion) + { + double norm2 = 0.0; + for (int slot = 0; slot < u_length; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + hi); + norm2 += value * value; + } + if (norm2 <= radius2) + break; + hi *= 2.0; + } + } + for (int iteration = 0; iteration < 80; ++iteration) + { + double lambda = 0.5 * (lo + hi); + double norm2 = 0.0; + for (int slot = 0; slot < u_length; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + lambda); + norm2 += value * value; + } + if (norm2 > radius2) + lo = lambda; + else + hi = lambda; + if ((hi - lo) <= 1e-13 * (1.0 + hi + lo)) + break; + } + double lambda = 0.5 * (lo + hi); + if (warm_start) + *warm_start = lambda; + for (int slot = 0; slot < u_length; ++slot) + { + int index = start + slot; + if (!is_fixed[index]) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + point[index] *= omega / (omega + lambda); + } + } + return; + } + + double total_norm2 = fixed_norm2 + free_norm2; + if (z_input >= 0.0 && total_norm2 <= z_input * z_input) + return; + if (free_count == 0) + { + double projected_z = fmax(z_input, sqrt(fixed_norm2)); + point[z_index] = projected_z * rescaling[z_index]; + return; + } + + double omega_z = cone_section_weight(rescaling, q_diag, tau, z_index); + if (fixed_norm2 == 0.0) + { + if (-omega_z * z_input >= sqrt(polar_norm2)) + { + for (int slot = 0; slot < u_length; ++slot) + if (!is_fixed[start + slot]) + point[start + slot] = 0.0; + point[z_index] = 0.0; + return; + } + } + + double lambda; + if (z_input == 0.0) + { + lambda = omega_z; + double norm2 = fixed_norm2; + for (int slot = 0; slot < u_length; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + lambda); + norm2 += value * value; + } + point[z_index] = sqrt(norm2) * rescaling[z_index]; + } + else + { + bool lower_branch = z_input > 0.0; + double lo; + double hi; + if (lower_branch) + { + lo = 0.0; + hi = omega_z * (1.0 - 1e-14); + } + else + { + lo = omega_z * (1.0 + 1e-14); + hi = cone_section_negative_soc_upper(omega_z, -omega_z * z_input, fixed_norm2, polar_norm2, max_omega); + if (!(hi > lo) || !isfinite(hi)) + { + hi = 2.0 * omega_z; + for (int expansion = 0; expansion < 100; ++expansion) + { + double norm2 = fixed_norm2; + for (int slot = 0; slot < u_length; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + hi); + norm2 += value * value; + } + double z = omega_z * z_input / (omega_z - hi); + if (norm2 >= z * z) + break; + hi *= 2.0; + } + } + } + + if (warm_start && *warm_start > lo && *warm_start < hi && isfinite(*warm_start)) + { + double norm2 = fixed_norm2; + for (int slot = 0; slot < u_length; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + *warm_start); + norm2 += value * value; + } + double z = omega_z * z_input / (omega_z - *warm_start); + double f = norm2 - z * z; + if ((lower_branch && f > 0.0) || (!lower_branch && f < 0.0)) + lo = *warm_start; + else + hi = *warm_start; + } + + for (int iteration = 0; iteration < 80; ++iteration) + { + double trial = 0.5 * (lo + hi); + double norm2 = fixed_norm2; + for (int slot = 0; slot < u_length; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + trial); + norm2 += value * value; + } + double z = omega_z * z_input / (omega_z - trial); + double f = norm2 - z * z; + if ((lower_branch && f > 0.0) || (!lower_branch && f < 0.0)) + lo = trial; + else + hi = trial; + if ((hi - lo) <= 1e-13 * (1.0 + hi + lo)) + break; + } + lambda = 0.5 * (lo + hi); + point[z_index] *= omega_z / (omega_z - lambda); + } + + if (warm_start) + *warm_start = lambda; + for (int slot = 0; slot < u_length; ++slot) + { + int index = start + slot; + if (!is_fixed[index]) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + point[index] *= omega / (omega + lambda); + } + } +} + +__device__ static inline double rotated_soc_smooth_objective(const double *point, + const double *rescaling, + const double *q_diag, + double tau, + int start, + int k, + const char *is_fixed, + double lambda, + double s, + double t) +{ + double objective = 0.0; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double input = cone_section_actual(point, rescaling, index); + double value = input * omega / (omega + lambda); + double delta = value - input; + objective += omega * delta * delta; + } + int s_index = start + k; + int t_index = s_index + 1; + double omega_s = cone_section_weight(rescaling, q_diag, tau, s_index); + double omega_t = cone_section_weight(rescaling, q_diag, tau, t_index); + double ds = s - cone_section_actual(point, rescaling, s_index); + double dt = t - cone_section_actual(point, rescaling, t_index); + return objective + omega_s * ds * ds + omega_t * dt * dt; +} + +/* Weighted projection onto an arbitrary nonempty fixed section of + { (v,s,t) : ||v||_2^2 <= 2 s t, s >= 0, t >= 0 }. */ +__device__ static inline void project_rotated_soc_section_serial(double *point, + const double *rescaling, + const double *q_diag, + double tau, + double *warm_start, + int start, + int k, + const char *is_fixed) +{ + int s_index = start + k; + int t_index = s_index + 1; + bool fixed_s = is_fixed[s_index] != 0; + bool fixed_t = is_fixed[t_index] != 0; + double s_input = cone_section_actual(point, rescaling, s_index); + double t_input = cone_section_actual(point, rescaling, t_index); + double fixed_norm2 = 0.0; + double free_norm2 = 0.0; + double polar_norm2 = 0.0; + double max_omega = 0.0; + int free_count = 0; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + double value = cone_section_actual(point, rescaling, index); + if (is_fixed[index]) + fixed_norm2 += value * value; + else + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + free_norm2 += value * value; + polar_norm2 += (omega * value) * (omega * value); + max_omega = fmax(max_omega, omega); + ++free_count; + } + } + + if (fixed_s && fixed_t) + { + double radius2 = fmax(0.0, 2.0 * s_input * t_input - fixed_norm2); + if (free_count == 0 || free_norm2 <= radius2) + return; + if (!(radius2 > 0.0)) + { + for (int slot = 0; slot < k; ++slot) + if (!is_fixed[start + slot]) + point[start + slot] = 0.0; + return; + } + + double lo = 0.0; + double hi = sqrt(polar_norm2) / sqrt(radius2) * (1.0 + 64.0 * DBL_EPSILON); + if (!(hi > 0.0) || !isfinite(hi)) + { + hi = warm_start && *warm_start > 0.0 && isfinite(*warm_start) ? *warm_start : 1.0; + for (int expansion = 0; expansion < 100; ++expansion) + { + double norm2 = 0.0; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + hi); + norm2 += value * value; + } + if (norm2 <= radius2) + break; + hi *= 2.0; + } + } + for (int iteration = 0; iteration < 80; ++iteration) + { + double lambda = 0.5 * (lo + hi); + double norm2 = 0.0; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + lambda); + norm2 += value * value; + } + if (norm2 > radius2) + lo = lambda; + else + hi = lambda; + if ((hi - lo) <= 1e-13 * (1.0 + hi + lo)) + break; + } + double lambda = 0.5 * (lo + hi); + if (warm_start) + *warm_start = lambda; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (!is_fixed[index]) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + point[index] *= omega / (omega + lambda); + } + } + return; + } + + if (fixed_s || fixed_t) + { + int free_endpoint_index = fixed_s ? t_index : s_index; + double fixed_endpoint = fixed_s ? s_input : t_input; + double free_endpoint_input = fixed_s ? t_input : s_input; + double omega_endpoint = cone_section_weight(rescaling, q_diag, tau, free_endpoint_index); + if (!(fixed_endpoint > 0.0)) + { + for (int slot = 0; slot < k; ++slot) + if (!is_fixed[start + slot]) + point[start + slot] = 0.0; + point[free_endpoint_index] = fmax(free_endpoint_input, 0.0) * rescaling[free_endpoint_index]; + return; + } + if (free_endpoint_input >= 0.0 && fixed_norm2 + free_norm2 <= 2.0 * fixed_endpoint * free_endpoint_input) + return; + if (free_count == 0) + { + double lower_bound = fixed_norm2 / (2.0 * fixed_endpoint); + point[free_endpoint_index] = fmax(free_endpoint_input, lower_bound) * rescaling[free_endpoint_index]; + return; + } + + double lo = 0.0; + double violation = fixed_norm2 + free_norm2 - 2.0 * fixed_endpoint * free_endpoint_input; + double hi = omega_endpoint * violation / (2.0 * fixed_endpoint * fixed_endpoint); + hi *= 1.0 + 64.0 * DBL_EPSILON; + if (!(hi > 0.0) || !isfinite(hi)) + { + hi = warm_start && *warm_start > 0.0 && isfinite(*warm_start) ? *warm_start : omega_endpoint; + for (int expansion = 0; expansion < 100; ++expansion) + { + double norm2 = fixed_norm2; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + hi); + norm2 += value * value; + } + double endpoint = free_endpoint_input + hi * fixed_endpoint / omega_endpoint; + if (norm2 <= 2.0 * fixed_endpoint * endpoint) + break; + hi *= 2.0; + } + } + for (int iteration = 0; iteration < 80; ++iteration) + { + double lambda = 0.5 * (lo + hi); + double norm2 = fixed_norm2; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + lambda); + norm2 += value * value; + } + double endpoint = free_endpoint_input + lambda * fixed_endpoint / omega_endpoint; + if (norm2 > 2.0 * fixed_endpoint * endpoint) + lo = lambda; + else + hi = lambda; + if ((hi - lo) <= 1e-13 * (1.0 + hi + lo)) + break; + } + double lambda = 0.5 * (lo + hi); + if (warm_start) + *warm_start = lambda; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (!is_fixed[index]) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + point[index] *= omega / (omega + lambda); + } + } + point[free_endpoint_index] = + (free_endpoint_input + lambda * fixed_endpoint / omega_endpoint) * rescaling[free_endpoint_index]; + return; + } + + double total_norm2 = fixed_norm2 + free_norm2; + if (s_input >= 0.0 && t_input >= 0.0 && total_norm2 <= 2.0 * s_input * t_input) + return; + + double omega_s = cone_section_weight(rescaling, q_diag, tau, s_index); + double omega_t = cone_section_weight(rescaling, q_diag, tau, t_index); + if (fixed_norm2 == 0.0) + { + double bs = omega_s * s_input; + double bt = omega_t * t_input; + if (bs <= 0.0 && bt <= 0.0 && polar_norm2 <= 2.0 * bs * bt) + { + for (int slot = 0; slot < k; ++slot) + if (!is_fixed[start + slot]) + point[start + slot] = 0.0; + point[s_index] = 0.0; + point[t_index] = 0.0; + return; + } + } + + double root_metric = sqrt(omega_s) * sqrt(omega_t); + double balance = sqrt(omega_s) * s_input + sqrt(omega_t) * t_input; + double balance_scale = 1.0 + fabs(sqrt(omega_s) * s_input) + fabs(sqrt(omega_t) * t_input); + double lambda = root_metric; + double projected_s = 0.0; + double projected_t = 0.0; + bool smooth_valid = true; + + if (fabs(balance) <= 64.0 * DBL_EPSILON * balance_scale) + { + double norm2 = fixed_norm2; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + lambda); + norm2 += value * value; + } + double product = 0.5 * root_metric * norm2; + double delta = sqrt(omega_s) * s_input; + double scaled_t = 0.5 * (-delta + sqrt(fmax(0.0, delta * delta + 4.0 * product))); + double scaled_s = scaled_t + delta; + projected_s = scaled_s / sqrt(omega_s); + projected_t = scaled_t / sqrt(omega_t); + smooth_valid = projected_s >= 0.0 && projected_t >= 0.0; + } + else + { + bool lower_branch = balance > 0.0; + double lo = lower_branch ? 0.0 : root_metric * (1.0 + 1e-14); + double hi = lower_branch ? root_metric * (1.0 - 1e-14) : 2.0 * root_metric; + + if (!lower_branch) + { + hi = cone_section_negative_rsoc_upper( + omega_s, omega_t, s_input, t_input, fixed_norm2, polar_norm2, max_omega); + if (!(hi > lo) || !isfinite(hi)) + { + hi = 2.0 * root_metric; + for (int expansion = 0; expansion < 100; ++expansion) + { + double determinant = omega_s * omega_t - hi * hi; + double s = omega_t * (omega_s * s_input + hi * t_input) / determinant; + double t = omega_s * (omega_t * t_input + hi * s_input) / determinant; + double f = INFINITY; + if (s >= 0.0 && t >= 0.0) + { + double norm2 = fixed_norm2; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + hi); + norm2 += value * value; + } + f = norm2 - 2.0 * s * t; + } + if (f >= 0.0) + break; + hi *= 2.0; + } + } + } + + for (int iteration = 0; iteration < 90; ++iteration) + { + double trial = 0.5 * (lo + hi); + double determinant = omega_s * omega_t - trial * trial; + double s = omega_t * (omega_s * s_input + trial * t_input) / determinant; + double t = omega_s * (omega_t * t_input + trial * s_input) / determinant; + double f = INFINITY; + if (s >= 0.0 && t >= 0.0) + { + double norm2 = fixed_norm2; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index) * omega / (omega + trial); + norm2 += value * value; + } + f = norm2 - 2.0 * s * t; + } + if ((lower_branch && f > 0.0) || (!lower_branch && f < 0.0)) + lo = trial; + else + hi = trial; + if ((hi - lo) <= 1e-13 * (1.0 + hi + lo)) + break; + } + lambda = 0.5 * (lo + hi); + double determinant = omega_s * omega_t - lambda * lambda; + projected_s = omega_t * (omega_s * s_input + lambda * t_input) / determinant; + projected_t = omega_s * (omega_t * t_input + lambda * s_input) / determinant; + smooth_valid = isfinite(projected_s) && isfinite(projected_t) && projected_s >= 0.0 && projected_t >= 0.0; + } + + double best_objective = smooth_valid + ? rotated_soc_smooth_objective( + point, rescaling, q_diag, tau, start, k, is_fixed, lambda, projected_s, projected_t) + : INFINITY; + int mode = smooth_valid ? 0 : 1; + if (fixed_norm2 == 0.0) + { + double vector_objective = 0.0; + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (!is_fixed[index]) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = cone_section_actual(point, rescaling, index); + vector_objective += omega * value * value; + } + } + double s_axis = fmax(s_input, 0.0); + double s_axis_objective = + vector_objective + omega_s * (s_axis - s_input) * (s_axis - s_input) + omega_t * t_input * t_input; + if (s_axis_objective < best_objective) + { + best_objective = s_axis_objective; + projected_s = s_axis; + projected_t = 0.0; + mode = 1; + } + double t_axis = fmax(t_input, 0.0); + double t_axis_objective = + vector_objective + omega_s * s_input * s_input + omega_t * (t_axis - t_input) * (t_axis - t_input); + if (t_axis_objective < best_objective) + { + projected_s = 0.0; + projected_t = t_axis; + mode = 1; + } + } + + if (mode == 0) + { + for (int slot = 0; slot < k; ++slot) + { + int index = start + slot; + if (!is_fixed[index]) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + point[index] *= omega / (omega + lambda); + } + } + if (warm_start) + *warm_start = lambda; + } + else + { + for (int slot = 0; slot < k; ++slot) + if (!is_fixed[start + slot]) + point[start + slot] = 0.0; + if (warm_start) + *warm_start = 0.0; + } + point[s_index] = projected_s * rescaling[s_index]; + point[t_index] = projected_t * rescaling[t_index]; +} diff --git a/internal/cone_utils.h b/internal/cone_utils.h new file mode 100644 index 0000000..97e10e1 --- /dev/null +++ b/internal/cone_utils.h @@ -0,0 +1,39 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include "pdhcg_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + int cone_length(cone_type_t type, int v_dim); + int cone_block_length(const cone_blocks_t *blocks, int block); + int cone_blocks_init_from_specs(cone_blocks_t *blocks, + int num_cones, + const cone_spec_t *specs, + int ambient_dimension, + bool allow_fixed, + const char *label); + void cone_blocks_clone(cone_blocks_t *dst, const cone_blocks_t *src); + void cone_blocks_free(cone_blocks_t *blocks); + +#ifdef __cplusplus +} +#endif diff --git a/internal/distributed_interface.h b/internal/distributed_interface.h index 16908fe..5c8d727 100644 --- a/internal/distributed_interface.h +++ b/internal/distributed_interface.h @@ -1,6 +1,7 @@ #ifndef PDHCG_DISTRIBUTED_INTERFACE_H #define PDHCG_DISTRIBUTED_INTERFACE_H +#include "pdhcg_types.h" #include #ifdef __cplusplus @@ -34,6 +35,16 @@ extern "C" int pdhcg_get_grid_row_coord(struct grid_context_s *ctx); + int pdhcg_get_global_num_variables(grid_context_t *ctx); + + int pdhcg_get_variable_start(grid_context_t *ctx); + + int pdhcg_get_global_num_cones(grid_context_t *ctx); + + int pdhcg_get_global_num_affine_cones(grid_context_t *ctx); + + pdhcg_result_t *pdhcg_distributed_optimize(const pdhg_parameters_t *params, const qp_problem_t *original_problem); + #ifdef __cplusplus } #endif diff --git a/internal/internal_types.h b/internal/internal_types.h index ccf97c8..92babac 100644 --- a/internal/internal_types.h +++ b/internal/internal_types.h @@ -48,7 +48,6 @@ typedef struct cusparseDnVecDescr_t vec_primal_obj_prod; - // Low rank Component cu_sparse_matrix_csr_t *objective_lowrank_matrix; cu_sparse_matrix_csr_t *objective_lowrank_matrix_t; pdhcg_spmv_ctx_t *spmv_ctx_R; @@ -65,7 +64,6 @@ typedef struct double *d_middle_dense; double *Rx_buffer; - // Buffer for Distributed Version double *global_primal_obj_product; cusparseDnVecDescr_t vec_global_primal_obj_prod; } quadratic_objective_term_t; @@ -96,6 +94,36 @@ typedef struct int has_inner_loop; } inner_solver_t; +typedef struct distributed_cone_split_s distributed_cone_split_t; +struct cone_bucket_s; + +typedef enum +{ + CONE_AXIS_VARIABLE = 0, + CONE_AXIS_CONSTRAINT = 1 +} cone_axis_t; + +typedef struct +{ + cone_axis_t axis; + int num_blocks; + int *start_idx; /* permuted by bucket */ + int *v_dim; /* permuted by bucket */ + double *power_alpha; /* permuted by bucket; NULL if no power cones */ + char *is_fixed; /* NULL if no fixes */ + double *projection_warm_start; /* device [PDHCG_CONE_WORKSPACE_STRIDE * num_blocks] */ + double *residual_warm_start; /* device [PDHCG_CONE_WORKSPACE_STRIDE * num_blocks] */ + double *complementarity_residual; /* device [num_blocks] */ + double *power_violation_workspace; /* device [2 * num_blocks], variable side only */ + double *coordinate_rescaling; /* device [num_constraints], affine side only */ + double *effective_objective_gradient; /* device [num_variables] */ + double *bb_primal_snapshot; /* device [num_variables] */ + struct cone_bucket_s *buckets; + int num_buckets; + bool has_power_cones; + distributed_cone_split_t *split; +} cone_runtime_t; + typedef enum { LP, @@ -117,6 +145,7 @@ typedef struct cu_sparse_matrix_csr_t *constraint_matrix_t; double *constraint_lower_bound; double *constraint_upper_bound; + double *affine_cone_offset; int num_blocks_primal; int num_blocks_dual; int num_blocks_primal_dual; @@ -188,8 +217,8 @@ typedef struct cusparseDnVecDescr_t vec_primal_prod; cusparseDnVecDescr_t vec_dual_prod; - double *ones_primal_d; - double *ones_dual_d; + double *ones_primal; + double *ones_dual; double feasibility_polishing_time; int feasibility_iteration; @@ -197,8 +226,36 @@ typedef struct problem_type_t problem_type; inner_solver_t *inner_solver; grid_context_t *grid_context; + + bool has_variable_cones; + cone_runtime_t cones; + cone_runtime_t affine_cones; + int num_original_variables; } pdhg_solver_state_t; +typedef enum +{ + PROJ_METHOD_THREAD = 0, + PROJ_METHOD_WARP = 1, + PROJ_METHOD_BLOCK = 2, + PROJ_METHOD_GRID = 3, + PROJ_METHOD_GRID_WEIGHTED = 4, + NUM_PROJ_METHODS = 5 +} cone_proj_method_t; + +#define PDHCG_LARGE_CONE_MIN_VDIM 32768 +#define PDHCG_LARGE_CONE_BLOCKS_PER_CONE 128 +#define PDHCG_CONE_WORKSPACE_STRIDE 8 +#define PDHCG_CONE_GRID_ROOT_ITERATIONS 40 + +typedef struct cone_bucket_s +{ + cone_type_t type; + cone_proj_method_t method; + int offset; /* start within permuted cone arrays */ + int count; +} cone_bucket_t; + typedef enum { PDHCG_D_NONE = 0, diff --git a/internal/partition_utils.h b/internal/partition_utils.h new file mode 100644 index 0000000..d0179e7 --- /dev/null +++ b/internal/partition_utils.h @@ -0,0 +1,35 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + bool optimize_partition_cuts(int total_dimension, + int num_partitions, + const int *forbidden_starts, + const int *forbidden_ends, + int num_forbidden_intervals, + int *cuts); + +#ifdef __cplusplus +} +#endif diff --git a/internal/pdhcg_kernels.cuh b/internal/pdhcg_kernels.cuh index 64592ff..23ddc90 100644 --- a/internal/pdhcg_kernels.cuh +++ b/internal/pdhcg_kernels.cuh @@ -34,6 +34,35 @@ extern "C" __global__ void element_wise_mul_inplace_kernel(double *__restrict__ x, const double *__restrict__ d, int n); + __global__ void vector_sub_kernel(double *__restrict__ direction, + const double *__restrict__ a, + const double *__restrict__ b, + int n); + + __global__ void + vector_add_kernel(const double *__restrict__ a, const double *__restrict__ b, double *__restrict__ out, int n); + + __global__ void project_primal_onto_bounds_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_lower_bound, + const double *__restrict__ variable_upper_bound, + int num_variables); + + __global__ void prepare_projected_gradient_point_kernel(double *__restrict__ projected_point, + const double *__restrict__ primal_solution, + const double *__restrict__ effective_objective, + const double *__restrict__ dual_product, + const double *__restrict__ variable_lower_bound, + const double *__restrict__ variable_upper_bound, + double step_size, + int num_variables); + + __global__ void augment_projected_gradient_residual_kernel(double *__restrict__ dual_residual, + const double *__restrict__ primal_solution, + const double *__restrict__ projected_point, + const double *__restrict__ variable_rescaling, + double step_size, + int num_variables); + // ====================================================================== // Advanced Metrics & Reduced Costs // ====================================================================== @@ -98,8 +127,9 @@ extern "C" __global__ void compute_next_pdhg_dual_solution_kernel(const double *current_dual, double *reflected_dual, const double *primal_product, - const double *const_lb, - const double *const_ub, + const double *affine_cone_offset, + const double *constraint_lower_bound, + const double *constraint_upper_bound, int n, double step_size); @@ -107,11 +137,30 @@ extern "C" double *pdhg_dual, double *reflected_dual, const double *primal_product, - const double *const_lb, - const double *const_ub, + const double *affine_cone_offset, + const double *constraint_lower_bound, + const double *constraint_upper_bound, int n, double step_size); + __global__ void prepare_constraint_dual_update_kernel(const double *current_dual, + const double *primal_product, + const double *affine_cone_offset, + const double *constraint_lower_bound, + const double *constraint_upper_bound, + double *projected_constraint_value, + int n, + double step_size); + + __global__ void finish_constraint_dual_update_kernel(const double *current_dual, + const double *primal_product, + const double *affine_cone_offset, + const double *projected_constraint_value, + double *pdhg_dual, + double *reflected_dual, + int n, + double step_size); + // ====================================================================== // Halpern & Solution Management // ====================================================================== @@ -258,6 +307,7 @@ extern "C" __global__ void compute_lp_residual_kernel(double *primal_residual, const double *primal_product, + const double *affine_cone_offset, const double *constraint_lower_bound, const double *constraint_upper_bound, const double *dual_solution, @@ -267,14 +317,17 @@ extern "C" const double *objective_vector, const double *constraint_rescaling, const double *variable_rescaling, + double *affine_dual_membership, double *dual_obj_contribution, const double *const_lb_finite, const double *const_ub_finite, + bool defer_constraint_projection, int num_constraints, int num_variables); __global__ void compute_qp_residual_kernel(double *primal_residual, const double *primal_product, + const double *affine_cone_offset, const double *primal_obj_product, const double *primal_solution, const double *constraint_lower_bound, @@ -288,13 +341,47 @@ extern "C" const double *objective_vector, const double *constraint_rescaling, const double *variable_rescaling, + double *affine_dual_membership, double *dual_obj_contribution, const double *const_lb_finite, const double *const_ub_finite, const double step_size, + bool defer_constraint_projection, int num_constraints, int num_variables); + __global__ void finish_affine_cone_residuals_kernel(double *primal_residual, + const double *primal_product, + const double *affine_cone_offset, + const double *constraint_rescaling, + double *dual_membership, + const double *dual_membership_rescaling, + int n); + + __global__ void prepare_affine_cone_residuals_kernel(double *projection_point, + double *complementarity_residual, + const double *primal_product, + const double *affine_cone_offset, + const double *dual_solution, + const int *start_idx, + const int *v_dim, + double constraint_bound_rescaling, + int num_cones); + + __global__ void prepare_affine_cone_residuals_grid_kernel(double *projection_point, + double *complementarity_accumulator, + const double *primal_product, + const double *affine_cone_offset, + const double *dual_solution, + const int *start_idx, + const int *v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void finish_affine_cone_complementarity_kernel(double *complementarity_residual, + double constraint_bound_rescaling, + int num_cones); + __global__ void recover_primal_obj_dual_product(double *dual_product, double *primal_obj_product, const double *variable_rescaling, @@ -327,6 +414,7 @@ extern "C" __global__ void dual_solution_dual_objective_contribution_kernel(const double *constraint_lower_bound_finite_val, const double *constraint_upper_bound_finite_val, + const double *affine_cone_offset, const double *dual_solution, int num_constraints, double *dual_objective_dual_solution_contribution_array); @@ -349,6 +437,485 @@ extern "C" const double *__restrict__ variable_upper_bound, int n_vars); + __global__ void project_rotated_soc_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void project_rotated_soc_warp_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void project_rotated_soc_block_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void initialize_rotated_soc_grid_weighted_kernel(const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone); + + __global__ void + finalize_rotated_soc_grid_weighted_initialization_kernel(const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones); + + __global__ void reduce_rotated_soc_grid_weighted_root_kernel(const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone); + + __global__ void finalize_rotated_soc_grid_weighted_root_kernel(const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones); + + __global__ void reduce_rotated_soc_grid_axis_objective_kernel(const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone); + + __global__ void finalize_rotated_soc_grid_axis_objective_kernel(const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones); + + __global__ void apply_rotated_soc_grid_weighted_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone); + + __global__ void project_rotated_soc_grid_reduce_kernel(double *__restrict__ primal_solution, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void project_rotated_soc_grid_finalize_kernel(double *__restrict__ primal_solution, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones); + + __global__ void project_rotated_soc_grid_apply_kernel(double *__restrict__ primal_solution, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void project_standard_soc_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void project_standard_soc_warp_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void project_standard_soc_block_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void initialize_standard_soc_grid_weighted_kernel(const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone); + + __global__ void + finalize_standard_soc_grid_weighted_initialization_kernel(const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones); + + __global__ void reduce_standard_soc_grid_weighted_root_kernel(const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone); + + __global__ void finalize_standard_soc_grid_weighted_root_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones); + + __global__ void apply_standard_soc_grid_weighted_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone); + + __global__ void project_standard_soc_grid_reduce_kernel(double *__restrict__ primal_solution, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void project_standard_soc_grid_finalize_kernel(double *__restrict__ primal_solution, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones); + + __global__ void project_standard_soc_grid_apply_kernel(double *__restrict__ primal_solution, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void compute_cone_dual_residual_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void compute_cone_dual_residual_warp_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void compute_cone_dual_residual_grid_reduce_kernel(const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void compute_cone_dual_residual_grid_finalize_kernel(double *__restrict__ dual_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones); + + __global__ void compute_cone_dual_residual_grid_apply_kernel(double *__restrict__ dual_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void compute_cone_dual_residual_standard_warp_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void compute_cone_dual_residual_standard_grid_reduce_kernel(const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void + compute_cone_dual_residual_standard_grid_finalize_kernel(double *__restrict__ dual_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones); + + __global__ void compute_cone_dual_residual_standard_grid_apply_kernel(double *__restrict__ dual_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void project_exp_cone_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void compute_cone_dual_residual_exp_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void project_power_cone_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const double *__restrict__ power_alpha, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void compute_cone_dual_residual_power_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const double *__restrict__ power_alpha, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void compute_power_cone_primal_violation_kernel(double *__restrict__ absolute_violation, + double *__restrict__ relative_violation, + const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const int *__restrict__ start_idx, + const double *__restrict__ power_alpha, + double homogeneous_scale, + int num_blocks); + + __global__ void project_power_cone_diag_q_kernel(double *__restrict__ pdhg_primal, + double *__restrict__ reflected_primal, + const double *__restrict__ current_primal, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const double *__restrict__ power_alpha, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void compute_cone_dual_residual_standard_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void set_cone_dual_slack_kernel(double *__restrict__ dual_slack, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_blocks); + + __global__ void set_cone_dual_slack_warp_kernel(double *__restrict__ dual_slack, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones); + + __global__ void set_cone_dual_slack_grid_kernel(double *__restrict__ dual_slack, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void recompute_reflected_at_cone_kernel(double *__restrict__ reflected_primal, + const double *__restrict__ pdhg_primal, + const double *__restrict__ current_primal, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_blocks); + + __global__ void recompute_reflected_at_cone_warp_kernel(double *__restrict__ reflected_primal, + const double *__restrict__ pdhg_primal, + const double *__restrict__ current_primal, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones); + + __global__ void recompute_reflected_at_cone_block_kernel(double *__restrict__ reflected_primal, + const double *__restrict__ pdhg_primal, + const double *__restrict__ current_primal, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones); + + __global__ void recompute_reflected_at_cone_grid_kernel(double *__restrict__ reflected_primal, + const double *__restrict__ pdhg_primal, + const double *__restrict__ current_primal, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void clear_cone_residual_grid_kernel(double *__restrict__ dual_residual, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone); + + __global__ void project_rotated_soc_diag_q_kernel(double *__restrict__ pdhg_primal, + double *__restrict__ reflected_primal, + const double *__restrict__ current_primal, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void project_standard_soc_diag_q_kernel(double *__restrict__ pdhg_primal, + double *__restrict__ reflected_primal, + const double *__restrict__ current_primal, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + + __global__ void project_exp_cone_diag_q_kernel(double *__restrict__ pdhg_primal, + double *__restrict__ reflected_primal, + const double *__restrict__ current_primal, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks); + #ifdef __cplusplus } #endif diff --git a/internal/permute.h b/internal/permute.h index 4d33ce3..e10fee2 100644 --- a/internal/permute.h +++ b/internal/permute.h @@ -28,24 +28,17 @@ extern "C" { #endif - typedef struct - { - int new_col; - double val; - } permute_tuple_t; - - void generate_random_permutation(int n, int *perm); - - void permute_problem(qp_problem_t *qp, int *row_perm, int *col_perm); - - void randomly_permute_problem(qp_problem_t *qp, int **out_row_perm, int **out_col_perm); + bool permute_problem(qp_problem_t *qp, int *row_perm, int *col_perm); qp_problem_t *permute_problem_return_new(const qp_problem_t *qp, int *row_perm, int *col_perm); - void generate_block_permutation(int n, int block_size, int *perm); - void generate_random_permutation(int n, int *perm); - void compute_inv_perm(int n, const int *perm, int *inv_perm); - void permute_double_array(double *arr, int n, const int *perm); + void generate_cone_aware_permutation(const qp_problem_t *qp, permute_method_t method, int block_size, int *perm); + void generate_affine_cone_aware_row_permutation(const qp_problem_t *qp, + permute_method_t method, + int block_size, + int *perm); + bool validate_cone_permutation(const qp_problem_t *qp, const int *col_perm); + bool validate_affine_cone_row_permutation(const qp_problem_t *qp, const int *row_perm); void repermute_solution(pdhcg_result_t *result, int *row_perm, int *col_perm); #ifdef __cplusplus } diff --git a/internal/qcqp_transform.h b/internal/qcqp_transform.h new file mode 100644 index 0000000..37eae67 --- /dev/null +++ b/internal/qcqp_transform.h @@ -0,0 +1,30 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include "pdhcg_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + void restore_qcqp_result_dimensions(pdhcg_result_t *result, const qp_problem_t *original); + +#ifdef __cplusplus +} +#endif diff --git a/internal/solver.h b/internal/solver.h index 9159541..afd1595 100644 --- a/internal/solver.h +++ b/internal/solver.h @@ -20,11 +20,13 @@ limitations under the License. #include "pdhcg_types.h" #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif -pdhcg_result_t *optimize(const pdhg_parameters_t *params, - const qp_problem_t *original_problem); + pdhcg_result_t *optimize(const pdhg_parameters_t *params, const qp_problem_t *original_problem); + + int pdhcg_validate_fixed_cone_sections(const qp_problem_t *problem); #ifdef __cplusplus } diff --git a/mkdocs.yml b/mkdocs.yml index a80e50f..3440d7e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,10 +1,11 @@ -site_name: PDHCG-II Documentation -site_description: GPU-Accelerated Solver for Quadratic Programming +site_name: PDHCG Documentation +site_description: GPU-Accelerated Solver for Quadratic Conic Programming site_author: Hongpei Li -site_url: https://lhongpei.github.io/PDHCG-II +site_url: https://lhongpei.github.io/PDHCG +exclude_docs: README.md -repo_name: Lhongpei/PDHCG-II -repo_url: https://github.com/Lhongpei/PDHCG-II +repo_name: Lhongpei/PDHCG +repo_url: https://github.com/Lhongpei/PDHCG nav: - Home: index.md @@ -17,7 +18,9 @@ nav: - Overview: c/overview.md - Types: c/types.md - Functions: c/functions.md + - Combined Reference: C_API.md - Examples: examples.md + - Migrating to 0.3: migration.md - Citation: citation.md theme: @@ -101,4 +104,4 @@ extra_javascript: extra: social: - icon: fontawesome/brands/github - link: https://github.com/Lhongpei/PDHCG-II + link: https://github.com/Lhongpei/PDHCG diff --git a/pdhcg/_pdhcg_core.pyi b/pdhcg/_pdhcg_core.pyi index fee1491..53d30cb 100644 --- a/pdhcg/_pdhcg_core.pyi +++ b/pdhcg/_pdhcg_core.pyi @@ -3,10 +3,14 @@ pdhcg core bindings (auto-detect dense/CSR/CSC/COO; initialize default params he """ from __future__ import annotations import typing -__all__: list[str] = ['get_default_params', 'solve_once'] +__all__: list[str] = ['get_default_params', 'read_problem_file', 'solve_once'] def get_default_params() -> dict: """ Return default PDHG parameters as a dict """ -def solve_once(Q: typing.Any, R: typing.Any, A: typing.Any, objective_vector: typing.Any, objective_constant: typing.Any = None, variable_lower_bound: typing.Any = None, variable_upper_bound: typing.Any = None, constraint_lower_bound: typing.Any = None, constraint_upper_bound: typing.Any = None, zero_tolerance: typing.SupportsFloat | typing.SupportsIndex = 0.0, params: typing.Any = None, primal_start: typing.Any = None, dual_start: typing.Any = None, D: typing.Any = None) -> dict: +def read_problem_file(path: str) -> dict: + """ + Read an MPS or CBF file (.mps/.mps.gz/.cbf/.cbf.gz) and return a dict with c, obj_const, Q, A, constr_lb, constr_ub, var_lb, var_ub, cones, affine_F, affine_g, affine_cones, and primal_start. + """ +def solve_once(Q: typing.Any, R: typing.Any, A: typing.Any, objective_vector: typing.Any, objective_constant: typing.Any = None, variable_lower_bound: typing.Any = None, variable_upper_bound: typing.Any = None, constraint_lower_bound: typing.Any = None, constraint_upper_bound: typing.Any = None, zero_tolerance: typing.SupportsFloat | typing.SupportsIndex = 0.0, params: typing.Any = None, primal_start: typing.Any = None, dual_start: typing.Any = None, D: typing.Any = None, cones: typing.Any = None, affine_F: typing.Any = None, affine_g: typing.Any = None, affine_cones: typing.Any = None) -> dict: ... diff --git a/pyproject.toml b/pyproject.toml index 530d245..e7d9c98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "pdhcg" -version = "0.2.1" +version = "0.3.0" description = "Python bindings for PDHCG, GPU accelerated first order solver for Quadratic Programming" readme = "README.md" license = { text = "Apache-2.0" } @@ -23,7 +23,7 @@ build-dir = "build/{wheel_tag}" cmake.version = ">=3.20" cmake.build-type = "Release" wheel.packages = ["python/pdhcg"] -sdist.include = ["tests/**", "pyproject.toml", "README.md", "LICENSE"] +sdist.include = ["tests/**", "CHANGELOG.md", "pyproject.toml", "README.md", "LICENSE"] [tool.scikit-build.cmake.define] CMAKE_CUDA_ARCHITECTURES = "all" @@ -38,9 +38,13 @@ PDHCG_BUILD_TESTS = "OFF" test = [ "pytest>=8", "pytest-cov>=4", + "cvxpy>=1.6; python_version >= '3.9'", "numpy", "scipy" ] +cvxpy = [ + "cvxpy>=1.6; python_version >= '3.9'", +] dev = [ "ruff>=0.9.0", "pre-commit>=4.0.0", diff --git a/python/README.md b/python/README.md index b7c2924..8f18967 100644 --- a/python/README.md +++ b/python/README.md @@ -3,10 +3,10 @@ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![PyPI version](https://badge.fury.io/py/pdhcg.svg)](https://pypi.org/project/pdhcg/) [![Publication](https://img.shields.io/badge/DOI-10.1287/ijoc.2024.0983-B31B1B.svg)](https://pubsonline.informs.org/doi/10.1287/ijoc.2024.0983) -[![arXiv](https://img.shields.io/badge/arXiv-2405.16160-b31b1b.svg)](https://arxiv.org/abs/2405.16160) +[![arXiv](https://img.shields.io/badge/arXiv-2608.09159-b31b1b.svg)](https://arxiv.org/abs/2608.09159) -This is the Python interface to **[`PDHCG`](../README.md)**, 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. +This is the Python interface to **[`PDHCG`](../README.md)**, a GPU-accelerated first-order solver for large-scale quadratic and quadratic conic programming. +It provides a high-level, Pythonic API using NumPy and SciPy data structures. ## Installation @@ -37,8 +37,8 @@ pip install pdhcg Or build from source: ```bash -git clone https://github.com/Lhongpei/PDHCG-II.git -cd PDHCG-II +git clone https://github.com/Lhongpei/PDHCG.git +cd PDHCG pip install . ``` @@ -113,14 +113,58 @@ print("Primal solution:", m.X) print("Dual solution:", m.Pi) ``` +## Cone Constraints + +Second-order, rotated second-order, exponential, and power cone constraints use +the columnar `ConeSpec` API: + +```python +import numpy as np +from pdhcg import ConeSpec, ConeType, Model + +cones = ConeSpec(ConeType.EXP, 3 * np.arange(num_cones, dtype=np.int32)) +model = Model(objective_vector=c, constraint_matrix=A, variable_cones=cones) +``` + +The same object can be passed as `solve_once(..., cones=cones)`. See +[docs/python/quickstart.md](../docs/python/quickstart.md#quick-start-with-cone-constraints) +for a runnable example and [docs/python/model.md](../docs/python/model.md#cone-constraints) +for all fields and affine-cone input. + +## CVXPY + +Install PDHCG with the optional CVXPY dependency: + +```bash +pip install "pdhcg[cvxpy]" +``` + +Import the backend once before solving: + +```python +import cvxpy as cp +import pdhcg.cvxpy_backend # Registers solver="PDHCG". + +x = cp.Variable() +problem = cp.Problem(cp.Minimize(x), [x >= 1]) +problem.solve(solver="PDHCG", eps=1e-6) +``` + +Quadratic objectives and Zero, NonNeg, SOC, ExpCone, and PowCone3D +constraints are supported. PSD and mixed-integer models are not supported. + ## Modeling -The `Model` class represents a quadratic programming problem of the form: +The `Model` class represents a quadratic conic programming problem of the form: $$ -\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}. +\begin{aligned} +\min_x \quad & \frac{1}{2} x^\top (Q + R^\top D R) x + c^\top x + c_0 \\ +\text{s.t.} \quad & \ell \le A x \le u, \\ + & Fx + g \in \mathcal{K}_a, \\ + & x_J \in \mathcal{K}_v \quad \text{for variable-cone blocks } J, \\ + & \text{lb} \le x \le \text{ub}. +\end{aligned} $$ ### Arguments @@ -132,6 +176,10 @@ $$ - **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. +- **affine_cone_matrix** (`F`, optional): Matrix in the native affine-cone constraint $Fx + g \in \mathcal{K}_a$. +- **affine_cone_offset** (`g`, optional): Affine-cone offset. Defaults to zero. +- **affine_cones** (optional): `ConeSpec` covering every row of `F`. +- **variable_cones** (optional): `ConeSpec` describing cone blocks embedded in `x`. - **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`. @@ -182,16 +230,18 @@ Below is a list of commonly used parameters, their internal keys, and descriptio | `IterationLimit` | `iteration_limit` | int | `2147483647` | Maximum number of iterations. | | `LogLevel`, `Verbosity` | `verbose` | int | `1` | Verbosity level: `0` (Silent), `1` (Summary), or `2` (Detailed iteration info). | | `TermCheckFreq` | `termination_evaluation_frequency` | int | `200` | Frequency (in iterations) at which termination conditions are evaluated. | -| `OptimalityNorm` | `optimality_norm` | string | `"l2"` | Norm for optimality criteria. Use `"l2"` for L2 norm or `"linf"` for infinity norm. | +| `OptimalityNorm` | `optimality_norm` | string | `"linf"` | Norm for optimality criteria. Use `"l2"` for L2 norm or `"linf"` for infinity norm. | | `OptimalityTol` | `eps_optimal_relative` | float | `1e-4` | Relative tolerance for optimality gap. Solver stops if the relative primal-dual gap ≤ this value. | | `FeasibilityTol` | `eps_feasible_relative` | float | `1e-4` | Relative feasibility tolerance for primal/dual residuals. | +| `CurtisReidIters` | `curtis_reid_iterations` | int | `0` | Number of Curtis-Reid log-domain scaling iterations. Set to `0` to disable. | | `RuizIters` | `l_inf_ruiz_iterations` | int | `10` | Number of iterations for L∞ Ruiz scaling. Improves numerical conditioning. | | `UsePCAlpha` | `has_pock_chambolle_alpha` | bool | `True` | Whether to use the Pock–Chambolle α step size adjustment. | | `PCAlpha` | `pock_chambolle_alpha` | float | `1.0` | Value of the Pock–Chambolle α parameter. | | `BoundObjRescaling` | `bound_objective_rescaling` | bool | `True` | Whether to rescale the objective vector during preprocessing. | +| `UseConePreservingScaling` | `use_cone_preserving_scaling` | bool | `True` | Whether to broadcast one scaling value over every cone block. | | `RestartArtificialThresh` | `artificial_restart_threshold` | float | `0.36` | Threshold for artificial restart. | | `RestartSufficientReduction` | `sufficient_reduction_for_restart` | float | `0.2` | Sufficient reduction factor to justify a restart. | -| `RestartNecessaryReduction` | `necessary_reduction_for_restart` | float | `0.5` | Necessary reduction factor required for a restart. | +| `RestartNecessaryReduction` | `necessary_reduction_for_restart` | float | `0.8` | Necessary reduction factor required for a restart. | | `RestartKp` | `k_p` | float | `0.99` | Proportional coefficient for PID-controlled primal weight updates. | | `ReflectionCoeff` | `reflection_coefficient` | float | `1.0` | Reflection coefficient. | | `SVMaxIter` | `sv_max_iter` | int | `5000` | Maximum number of iterations for the power method. | diff --git a/python/pdhcg/PDHCG.py b/python/pdhcg/PDHCG.py index 3057211..90357ee 100644 --- a/python/pdhcg/PDHCG.py +++ b/python/pdhcg/PDHCG.py @@ -41,10 +41,12 @@ "OptimalityTol": "eps_optimal_relative", "FeasibilityTol": "eps_feasible_relative", # scaling / step size + "CurtisReidIters": "curtis_reid_iterations", "RuizIters": "l_inf_ruiz_iterations", "UsePCAlpha": "has_pock_chambolle_alpha", "PCAlpha": "pock_chambolle_alpha", "BoundObjRescaling": "bound_objective_rescaling", + "UseConePreservingScaling": "use_cone_preserving_scaling", # restarts "RestartArtificialThresh": "artificial_restart_threshold", "RestartSufficientReduction": "sufficient_reduction_for_restart", diff --git a/python/pdhcg/__init__.py b/python/pdhcg/__init__.py index 08754ae..c4b85b8 100644 --- a/python/pdhcg/__init__.py +++ b/python/pdhcg/__init__.py @@ -28,16 +28,16 @@ Links ----- -* Repository: https://github.com/Lhongpei/PDHCG-II +* Repository: https://github.com/Lhongpei/PDHCG """ -from .model import Model -from . import PDHCG +from importlib.metadata import PackageNotFoundError, version -__all__ = ["Model"] +from . import PDHCG +from .cones import ConeSpec, ConeType +from .model import Model -# versioning -from importlib.metadata import version, PackageNotFoundError +__all__ = ["ConeSpec", "ConeType", "Model", "PDHCG"] # get version from package metadata (toml file) try: diff --git a/python/pdhcg/_core.py b/python/pdhcg/_core.py index b6f8b15..cf40b93 100644 --- a/python/pdhcg/_core.py +++ b/python/pdhcg/_core.py @@ -13,4 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ._pdhcg_core import solve_once, get_default_params +from ._pdhcg_core import get_default_params, read_problem_file, solve_once + +__all__ = ["get_default_params", "read_problem_file", "solve_once"] diff --git a/python/pdhcg/cones.py b/python/pdhcg/cones.py new file mode 100644 index 0000000..a0d6a76 --- /dev/null +++ b/python/pdhcg/cones.py @@ -0,0 +1,200 @@ +# Copyright 2026 Hongpei Li +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from enum import IntEnum +from numbers import Integral +from typing import Any, Mapping, Optional + +import numpy as np + + +class ConeType(IntEnum): + """Cone type codes shared with the C API.""" + + RSOC = 0 + SOC = 1 + EXP = 2 + POWER = 3 + + ROTATED_SOC = RSOC + STANDARD_SOC = SOC + EXPONENTIAL = EXP + + +_CONE_TYPE_NAMES = { + "rsoc": ConeType.RSOC, + "soc": ConeType.SOC, + "exp": ConeType.EXP, + "power": ConeType.POWER, +} + + +def _cone_type_code(value: Any) -> int: + if isinstance(value, str): + try: + return int(_CONE_TYPE_NAMES[value.lower()]) + except KeyError as exc: + raise ValueError("cone type must be 'soc', 'rsoc', 'exp', or 'power'") from exc + if not isinstance(value, Integral): + raise TypeError("cone type must be a ConeType, integer code, or string") + code = int(value) + if code < int(ConeType.RSOC) or code > int(ConeType.POWER): + raise ValueError(f"invalid cone type code {code}") + return code + + +def _as_i32_vector(value: Any, count: int, name: str, default: Optional[int] = None) -> np.ndarray: + if value is None: + if default is None: + raise ValueError(f"{name} is required") + return np.full(count, default, dtype=np.int32) + + array = np.asarray(value) + if array.dtype.kind not in "iu": + raise TypeError(f"{name} must contain integers") + if array.ndim == 0: + scalar = int(array) + if scalar < np.iinfo(np.int32).min or scalar > np.iinfo(np.int32).max: + raise OverflowError(f"{name} value {scalar} does not fit int32") + return np.full(count, scalar, dtype=np.int32) + if array.ndim != 1 or array.size != count: + raise ValueError(f"{name} must be a scalar or a 1D array of length {count}") + if array.size and ( + np.min(array) < np.iinfo(np.int32).min or np.max(array) > np.iinfo(np.int32).max + ): + raise OverflowError(f"{name} contains a value that does not fit int32") + return np.ascontiguousarray(array, dtype=np.int32) + + +def _as_f64_vector(value: Any, count: int, name: str, default: float) -> np.ndarray: + if value is None: + return np.full(count, default, dtype=np.float64) + array = np.asarray(value) + if array.ndim == 0: + return np.full(count, float(array), dtype=np.float64) + if array.ndim != 1 or array.size != count: + raise ValueError(f"{name} must be a scalar or a 1D array of length {count}") + return np.ascontiguousarray(array, dtype=np.float64) + + +def _as_type_vector(value: Any, count: int) -> np.ndarray: + if isinstance(value, (str, ConeType, int, np.integer)): + return np.full(count, _cone_type_code(value), dtype=np.int32) + array = np.asarray(value) + if array.ndim != 1 or array.size != count: + raise ValueError(f"types must be a scalar or a 1D array of length {count}") + if array.dtype.kind in "iu": + if array.size and (array.min() < int(ConeType.RSOC) or array.max() > int(ConeType.POWER)): + raise ValueError("types contains an invalid cone type code") + return np.ascontiguousarray(array, dtype=np.int32) + return np.fromiter((_cone_type_code(item) for item in array), dtype=np.int32, count=count) + + +class ConeSpec: + """Columnar description of one or more cone blocks. + + ``types``, ``v_dims``, and ``power_alphas`` may be scalars and are then + broadcast to all entries in ``starts``. For variable cones, ``starts`` are + variable indices. For affine cones, they are rows of the separately supplied + affine map ``F``. + """ + + __slots__ = ("types", "starts", "v_dims", "power_alphas", "fixed_mask") + + def __init__( + self, + types: Any, + starts: Any, + v_dims: Any = 1, + power_alphas: Any = 0.0, + fixed_mask: Optional[Any] = None, + ) -> None: + starts_array = np.asarray(starts) + if starts_array.ndim != 1: + raise ValueError("starts must be a 1D array") + count = int(starts_array.size) + + self.starts = _as_i32_vector(starts_array, count, "starts") + self.types = _as_type_vector(types, count) + self.v_dims = _as_i32_vector(v_dims, count, "v_dims", default=1) + self.power_alphas = _as_f64_vector(power_alphas, count, "power_alphas", 0.0) + + if np.any(self.starts < 0): + raise ValueError("starts must be nonnegative") + if np.any(self.v_dims <= 0): + raise ValueError("v_dims must be positive") + three_dimensional = (self.types == int(ConeType.EXP)) | (self.types == int(ConeType.POWER)) + if np.any(self.v_dims[three_dimensional] != 1): + raise ValueError("EXP and POWER cones require v_dim == 1") + power = self.types == int(ConeType.POWER) + if np.any( + ~np.isfinite(self.power_alphas[power]) + | (self.power_alphas[power] <= 0.0) + | (self.power_alphas[power] >= 1.0) + ): + raise ValueError("POWER cone alphas must lie in (0, 1)") + + if fixed_mask is None: + self.fixed_mask = None + else: + mask = np.asarray(fixed_mask) + if mask.ndim != 1: + raise ValueError("fixed_mask must be a 1D ambient-coordinate mask") + self.fixed_mask = np.ascontiguousarray(mask, dtype=np.uint8) + + def __len__(self) -> int: + return int(self.starts.size) + + def validate_ambient( + self, + ambient_dimension: int, + *, + allow_fixed: bool, + require_cover: bool = False, + ) -> None: + """Validate ranges against the variable or affine-row dimension.""" + ambient_dimension = int(ambient_dimension) + if ambient_dimension < 0: + raise ValueError("ambient_dimension must be nonnegative") + lengths = np.where( + (self.types == int(ConeType.EXP)) | (self.types == int(ConeType.POWER)), + 3, + self.v_dims.astype(np.int64) + 2, + ) + ends = self.starts.astype(np.int64) + lengths + if np.any(ends > ambient_dimension): + raise ValueError("a cone block extends beyond the ambient dimension") + if require_cover and int(lengths.sum()) != ambient_dimension: + raise ValueError("affine cones must cover every row of F") + if self.fixed_mask is not None: + if not allow_fixed: + raise ValueError("affine cones do not support fixed slots") + if self.fixed_mask.size != ambient_dimension: + raise ValueError( + "fixed_mask length " + f"{self.fixed_mask.size} != ambient dimension {ambient_dimension}" + ) + + @classmethod + def from_columnar(cls, payload: Mapping[str, Any]) -> "ConeSpec": + """Construct from the compact payload returned by ``read_problem_file``.""" + return cls( + payload["types"], + payload["starts"], + payload["v_dims"], + payload["power_alphas"], + payload.get("fixed_mask"), + ) diff --git a/python/pdhcg/cvxpy_backend.py b/python/pdhcg/cvxpy_backend.py new file mode 100644 index 0000000..dbfdd20 --- /dev/null +++ b/python/pdhcg/cvxpy_backend.py @@ -0,0 +1,371 @@ +# Copyright 2026 Hongpei Li +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +"""CVXPY conic-solver backend for PDHCG. + +Import this module (``import pdhcg.cvxpy_backend``) once per process; it will +register ``PDHCG`` under ``cvxpy.settings.SOLVER_MAP_CONIC`` so that +``problem.solve(solver='PDHCG')`` works. + +Supported CVXPY constraints: Zero, NonNeg, SOC, ExpCone, PowCone3D. +Not supported: PSD, integer variables. +""" + +from __future__ import annotations + +import warnings +from typing import Any + +import cvxpy.settings as _cvx_s +import numpy as np +import scipy.sparse as sp +from cvxpy.constraints import SOC, ExpCone, NonNeg, PowCone3D, Zero +from cvxpy.reductions.solution import Solution, failure_solution +from cvxpy.reductions.solvers import utilities +from cvxpy.reductions.solvers.conic_solvers.conic_solver import ConicSolver + +from ._core import solve_once +from .cones import ConeSpec, ConeType + +_STATUS_MAP = { + "OPTIMAL": _cvx_s.OPTIMAL, + "PRIMAL_INFEASIBLE": _cvx_s.INFEASIBLE, + "DUAL_INFEASIBLE": _cvx_s.UNBOUNDED, + "INFEASIBLE_OR_UNBOUNDED": _cvx_s.INFEASIBLE_OR_UNBOUNDED, + "TIME_LIMIT": _cvx_s.USER_LIMIT, + "ITERATION_LIMIT": _cvx_s.USER_LIMIT, + "FEAS_POLISH_SUCCESS": _cvx_s.OPTIMAL, + "UNSPECIFIED": _cvx_s.SOLVER_ERROR, +} + + +class PDHCG(ConicSolver): + """PDHCG conic-solver plugin for CVXPY.""" + + MIP_CAPABLE = False + SUPPORTED_CONSTRAINTS = [Zero, NonNeg, SOC, ExpCone, PowCone3D] + + # CVXPY's ExpCone convention is (x, y, z) with z >= y * exp(x/y), y > 0. + # PDHCG's internal exp cone convention is (r1, r2, r3) with r3 >= r2 * exp(r1/r2). + # Direct mapping: cvxpy(x,y,z) -> internal(r1,r2,r3). + EXP_CONE_ORDER = [0, 1, 2] + + def name(self): + return "PDHCG" + + def import_solver(self) -> None: + import pdhcg # noqa: F401 + + def supports_quad_obj(self) -> bool: + return True + + def cite(self, data): + return ( + "@misc{pdhcg,\n" + " title = {PDHCG: GPU-accelerated Primal-Dual Hybrid Conjugate Gradient QP solver},\n" + " author = {Li, Hongpei and collaborators},\n" + " year = {2026},\n" + " url = {https://github.com/Lhongpei/PDHCG}\n" + "}" + ) + + def invert(self, solution, inverse_data): + status = _STATUS_MAP.get(solution.get("status", "UNSPECIFIED"), _cvx_s.SOLVER_ERROR) + attr: dict[str, Any] = { + _cvx_s.SOLVE_TIME: solution.get("solve_time", 0.0), + _cvx_s.NUM_ITERS: solution.get("iterations", 0), + } + if status in _cvx_s.SOLUTION_PRESENT: + opt_val = solution["value"] + inverse_data[_cvx_s.OFFSET] + primal_vars = {inverse_data[self.VAR_ID]: solution["primal"]} + eq_dual = utilities.get_dual_values( + solution["eq_dual"], + utilities.extract_dual_value, + inverse_data[self.EQ_CONSTR], + ) + ineq_dual = utilities.get_dual_values( + solution["ineq_dual"], + utilities.extract_dual_value, + inverse_data[self.NEQ_CONSTR], + ) + dual_vars = {**eq_dual, **ineq_dual} + return Solution(status, opt_val, primal_vars, dual_vars, attr) + return failure_solution(status, attr) + + def solve_via_data( + self, data, warm_start: bool, verbose: bool, solver_opts: dict, solver_cache=None + ): + A_cvx = sp.csr_matrix(data[_cvx_s.A]) + b_cvx = np.asarray(data[_cvx_s.B], dtype=np.float64).ravel() + c = np.asarray(data[_cvx_s.C], dtype=np.float64).ravel() + P = data.get(_cvx_s.P, None) + + cone_dims = data[ConicSolver.DIMS] + n_zero = int(cone_dims.zero) + n_nonneg = int(cone_dims.nonneg) + soc_dims = list(cone_dims.soc) + n_exp = int(cone_dims.exp) + pow_alphas = list(cone_dims.p3d) + if cone_dims.psd: + raise ValueError("PDHCG does not support PSD constraints.") + + n = c.size + soc_total = sum(soc_dims) + exp_total = 3 * n_exp + pow_total = 3 * len(pow_alphas) + n_cone_rows = soc_total + exp_total + pow_total + n_total_rows = n_zero + n_nonneg + n_cone_rows + + assert A_cvx.shape == (n_total_rows, n), ( + f"A shape {A_cvx.shape} != expected ({n_total_rows}, {n})" + ) + + # Internal slack layout: one SOC needs (v_dim + 2) slots = (k - 1) + 2 = k + 1 + # (extra "phantom" w-slot pinned to 0). EXP and POWER need 3 slots each. + n_soc_blocks = len(soc_dims) + n_pow_blocks = len(pow_alphas) + n_slack = soc_total + n_soc_blocks + 3 * n_exp + 3 * n_pow_blocks + n_vars_total = n + n_slack + if n_vars_total > np.iinfo(np.int32).max or n_total_rows > np.iinfo(np.int32).max: + raise ValueError("PDHCG dimensions must fit signed 32-bit indices.") + + # Every CVXPY cone row maps to exactly one internal slack slot. Store the + # column map directly; row indices are simply arange(n_cone_rows). + S_cols = np.empty(n_cone_rows, dtype=np.int64) + + n_cones = n_soc_blocks + n_exp + n_pow_blocks + cone_types = np.empty(n_cones, dtype=np.int32) + cone_starts = np.empty(n_cones, dtype=np.int32) + cone_v_dims = np.ones(n_cones, dtype=np.int32) + cone_alphas = np.zeros(n_cones, dtype=np.float64) + is_fixed_mask = np.zeros(n_slack, dtype=np.uint8) + slack_off = 0 + cvx_row_off = 0 + cone_idx = 0 + + # --- SOC blocks --- + for k in soc_dims: + # cvxpy layout: (top, tail_0..tail_{k-2}) at rows cvx_row_off..cvx_row_off+k-1 + # internal layout: [v_0..v_{k-2}, w, z] at slots slack_off..slack_off+k + mapped_slots = S_cols[cvx_row_off : cvx_row_off + k] + mapped_slots[0] = slack_off + k # z + mapped_slots[1:] = np.arange(slack_off, slack_off + k - 1, dtype=np.int64) + is_fixed_mask[slack_off + (k - 1)] = 1 # phantom w always pinned + + cone_types[cone_idx] = int(ConeType.SOC) + cone_starts[cone_idx] = n + slack_off + cone_v_dims[cone_idx] = k - 1 + cone_idx += 1 + slack_off += k + 1 + cvx_row_off += k + + # --- EXP blocks --- + if n_exp: + cone_slice = slice(cone_idx, cone_idx + n_exp) + cone_types[cone_slice] = int(ConeType.EXP) + cone_starts[cone_slice] = n + slack_off + 3 * np.arange(n_exp, dtype=np.int64) + row_count = 3 * n_exp + mapped_slots = np.arange(slack_off, slack_off + row_count, dtype=np.int64) + S_cols[cvx_row_off : cvx_row_off + row_count] = mapped_slots + cone_idx += n_exp + slack_off += row_count + cvx_row_off += row_count + + # --- POWER3D blocks --- + # cvxpy PowCone3D: x^alpha * y^(1-alpha) >= |z|, x,y >= 0. Direct 1-1 mapping. + if n_pow_blocks: + cone_slice = slice(cone_idx, cone_idx + n_pow_blocks) + cone_types[cone_slice] = int(ConeType.POWER) + cone_starts[cone_slice] = n + slack_off + 3 * np.arange(n_pow_blocks, dtype=np.int64) + cone_alphas[cone_slice] = np.asarray(pow_alphas, dtype=np.float64) + row_count = 3 * n_pow_blocks + mapped_slots = np.arange(slack_off, slack_off + row_count, dtype=np.int64) + S_cols[cvx_row_off : cvx_row_off + row_count] = mapped_slots + cone_idx += n_pow_blocks + slack_off += row_count + cvx_row_off += row_count + + assert cone_idx == n_cones + assert slack_off == n_slack + assert cvx_row_off == n_cone_rows + + # --- Assemble A_new = [A_x, mapping matrix M(cvx rows -> internal slack cols)] --- + # Zero + nonneg rows: no slack (absorbed into row bounds). + # Cone rows: A_cvx (x-part) + M (identity-like row-to-slot mapping). + n_row_lp = n_zero + n_nonneg + A_top_x = A_cvx[:n_row_lp, :] # LP rows: x-part + A_bot_x = A_cvx[n_row_lp:, :] # cone rows: x-part + + M = sp.csr_matrix( + ( + np.ones(n_cone_rows, dtype=np.float64), + (np.arange(n_cone_rows, dtype=np.int64), S_cols), + ), + shape=(n_cone_rows, n_slack), + ) + + # LP rows have zero on slack columns; cone rows have M + zeros_top = sp.csr_matrix((n_row_lp, n_slack)) + A_new = sp.vstack( + [ + sp.hstack([A_top_x, zeros_top]), + sp.hstack([A_bot_x, M]), + ], + format="csr", + ) + + # Row bounds + row_lb = np.full(n_total_rows, -np.inf, dtype=np.float64) + row_ub = np.full(n_total_rows, np.inf, dtype=np.float64) + # Zero rows: A x = b_cvx (interpret A_cvx*x + s = b_cvx with s = 0) + row_lb[:n_zero] = b_cvx[:n_zero] + row_ub[:n_zero] = b_cvx[:n_zero] + # Nonneg rows: A x + s = b_cvx, s >= 0 => A x <= b_cvx + row_ub[n_zero : n_zero + n_nonneg] = b_cvx[n_zero : n_zero + n_nonneg] + # Cone rows: A x + M*s_slack = b_cvx (equality) + row_lb[n_row_lp:] = b_cvx[n_row_lp:] + row_ub[n_row_lp:] = b_cvx[n_row_lp:] + + # Objective: c and P are on x-part; slack has zero coeff. + c_full = np.concatenate([c, np.zeros(n_slack, dtype=np.float64)]) + if P is not None and sp.issparse(P) and P.nnz > 0: + P_full = sp.block_diag( + [sp.csr_matrix(P), sp.csr_matrix((n_slack, n_slack))], format="csr" + ) + else: + P_full = None + + # Variable bounds: x is unbounded; slack is unbounded (cone-slot semantics). + var_lb = np.full(n_vars_total, -np.inf, dtype=np.float64) + var_ub = np.full(n_vars_total, np.inf, dtype=np.float64) + + # Assemble is_fixed / primal_start on the full [x; slack] vector. + is_fixed_full = np.zeros(n_vars_total, dtype=np.uint8) + primal_start_full = np.zeros(n_vars_total, dtype=np.float64) + is_fixed_full[n:] = is_fixed_mask + + cones_spec = ( + ConeSpec( + cone_types, + cone_starts, + cone_v_dims, + cone_alphas, + fixed_mask=is_fixed_full if n_soc_blocks else None, + ) + if n_cones + else None + ) + + # Merge solver_opts into params dict (accepted keys are the PDHCG params). + params_dict = _translate_opts(solver_opts, verbose) + + info = solve_once( + Q=P_full, + R=None, + A=A_new, + objective_vector=c_full, + objective_constant=None, + variable_lower_bound=var_lb, + variable_upper_bound=var_ub, + constraint_lower_bound=row_lb, + constraint_upper_bound=row_ub, + zero_tolerance=0.0, + params=params_dict, + primal_start=primal_start_full if n_soc_blocks else None, + dual_start=None, + D=None, + cones=cones_spec, + ) + + # Build the solution dict expected by our invert(). + status = info.get("Status", "UNSPECIFIED") + x_full = np.asarray(info.get("X"), dtype=np.float64) if info.get("X") is not None else None + y_full = ( + np.asarray(info.get("Pi"), dtype=np.float64) if info.get("Pi") is not None else None + ) + + # Extract primal for original x (first n entries). + primal = x_full[:n] if x_full is not None else None + # PDHCG's row multiplier convention is the negative of CVXPY's canonical + # A*x + s = b convention. Convert once before splitting Zero and inequality + # cone duals so equality, NonNeg, SOC, Exp, and Power duals agree with CVXPY. + cvxpy_dual = -y_full if y_full is not None else None + eq_dual = cvxpy_dual[:n_zero] if cvxpy_dual is not None else None + ineq_dual = cvxpy_dual[n_zero:] if cvxpy_dual is not None else None + + return { + "status": status, + "value": float(info.get("PrimalObj", 0.0)), + "primal": primal, + "eq_dual": eq_dual, + "ineq_dual": ineq_dual, + "solve_time": float(info.get("RuntimeSec", 0.0)), + "iterations": int(info.get("Iterations", 0)), + } + + +# Map cvxpy solver_opts to pdhcg's params dict. Common cvxpy option names get +# translated; anything else is passed through if it matches a pdhcg param key. +_OPT_ALIASES = { + "time_limit": "time_sec_limit", + "max_iter": "iteration_limit", + "iter_limit": "iteration_limit", + "eps": "eps_optimal_relative", + "eps_abs": "eps_optimal_relative", + "eps_rel": "eps_optimal_relative", + "feas_tol": "eps_feasible_relative", + "opt_tol": "eps_optimal_relative", + "verbose": "verbose", +} + + +def _translate_opts(solver_opts: dict, verbose: bool) -> dict: + params = {"verbose": int(verbose)} + # cvxpy inserts use_quad_obj to control canonicalization; ignore. + solver_opts = dict(solver_opts or {}) + solver_opts.pop("use_quad_obj", None) + for k, v in solver_opts.items(): + key = _OPT_ALIASES.get(k, k) + params[key] = v + return params + + +# --- Register with cvxpy at import time ----------------------------------- +def _register() -> None: + import contextlib + + from cvxpy.reductions.solvers import defines + + inst = PDHCG() + try: + _ = inst.is_installed() + except Exception: + contextlib.suppress(Exception) + defines.SOLVER_MAP_CONIC[inst.name()] = inst + if inst.name() not in defines.CONIC_SOLVERS: + # Insert near the front so it's preferred when explicitly requested. + defines.CONIC_SOLVERS.append(inst.name()) + if inst.name() not in defines.INSTALLED_CONIC_SOLVERS: + defines.INSTALLED_CONIC_SOLVERS.append(inst.name()) + if inst.name() not in defines.INSTALLED_SOLVERS: + defines.INSTALLED_SOLVERS.append(inst.name()) + # cvxpy.settings mirrors these; update if present. + for attr in ( + "SOLVER_MAP_CONIC", + "CONIC_SOLVERS", + "INSTALLED_CONIC_SOLVERS", + "INSTALLED_SOLVERS", + ): + if hasattr(_cvx_s, attr): + setattr(_cvx_s, attr, getattr(defines, attr)) + # Add the solver constant string on cvxpy.settings so `cvxpy.PDHCG` works. + if not hasattr(_cvx_s, "PDHCG"): + _cvx_s.PDHCG = inst.name() + + +try: + _register() +except Exception as exc: # pragma: no cover — registration is best-effort + warnings.warn(f"PDHCG cvxpy backend failed to register: {exc}") diff --git a/python/pdhcg/model.py b/python/pdhcg/model.py index 87da0e5..8b03b31 100644 --- a/python/pdhcg/model.py +++ b/python/pdhcg/model.py @@ -22,7 +22,8 @@ import scipy.sparse as sp from . import PDHCG -from ._core import get_default_params, solve_once +from ._core import get_default_params, read_problem_file, solve_once +from .cones import ConeSpec # array-like type ArrayLike = Union[np.ndarray, list, tuple] @@ -89,6 +90,7 @@ class Model: ``` minimize 1/2 x^T (Q + R^T D R) x + c^T x subject to l_c <= A x <= u_c + F x + g in K l_v <= x <= u_v ``` @@ -122,6 +124,10 @@ def __init__( variable_lower_bound: Optional[ArrayLike] = None, variable_upper_bound: Optional[ArrayLike] = None, objective_constant: float = 0.0, + affine_cone_matrix: Optional[Union[np.ndarray, sp.spmatrix]] = None, + affine_cone_offset: Optional[ArrayLike] = None, + affine_cones: Optional[ConeSpec] = None, + variable_cones: Optional[ConeSpec] = None, ): """ Initialize the Model with the given parameters. @@ -139,6 +145,11 @@ def __init__( variable_lower_bound: Lower bounds for the decision variables. variable_upper_bound: Upper bounds for the decision variables. objective_constant: Constant term in the objective function. + affine_cone_matrix: Matrix F in the native constraint F x + g in K. + affine_cone_offset: Offset g, with one entry per row of F. Defaults to zero. + affine_cones: Compact cone metadata covering all rows of F. + variable_cones: Compact metadata for cone blocks in the variable vector. + Must be a :class:`pdhcg.ConeSpec`. Note: If variable bounds are not provided, they default to -inf and +inf respectively. @@ -146,6 +157,7 @@ def __init__( # problem dimensions self.num_vars = 0 self.num_constrs = 0 + self.num_affine_constrs = 0 # Check A if constraint_matrix is not None: @@ -186,11 +198,31 @@ def __init__( f"objective_matrix_low_rank dimensions mismatch variables ({self.num_vars})" ) + # Check F (if A, Q, and R were None, infer n from the affine cone map). + if affine_cone_matrix is not None: + if not hasattr(affine_cone_matrix, "shape") or len(affine_cone_matrix.shape) != 2: + raise ValueError( + "affine_cone_matrix must be a 2D numpy.ndarray or scipy.sparse matrix." + ) + if self.num_vars == 0: + self.num_vars = int(affine_cone_matrix.shape[1]) + elif affine_cone_matrix.shape[1] != self.num_vars: + raise ValueError( + f"affine_cone_matrix dimensions mismatch variables ({self.num_vars})" + ) + + if self.num_vars == 0 and objective_vector is not None: + objective_shape = np.asarray(objective_vector).shape + if len(objective_shape) != 1: + raise ValueError(f"objective_vector must be 1D, got shape {objective_shape}") + self.num_vars = int(objective_shape[0]) + if ( self.num_vars == 0 and constraint_matrix is None and objective_matrix is None and objective_matrix_low_rank is None + and affine_cone_matrix is None ): return None @@ -210,6 +242,12 @@ def __init__( self.setConstraintUpperBound(constraint_upper_bound) self.setVariableLowerBound(variable_lower_bound) self.setVariableUpperBound(variable_upper_bound) + self._variable_cones: Optional[ConeSpec] = None + self.affine_F = None + self.affine_g = None + self._affine_cones: Optional[ConeSpec] = None + self.setVariableCones(variable_cones) + self.setAffineConeConstraints(affine_cone_matrix, affine_cone_offset, affine_cones) # initialize warm start values self._primal_start: Optional[np.ndarray] = None # warm start primal solution self._dual_start: Optional[np.ndarray] = None # warm start dual solution @@ -232,6 +270,51 @@ def __init__( self._p_ray_lin_obj: Optional[float] = None # primal ray linear objective self._d_ray_obj: Optional[float] = None # dual ray objective + @classmethod + def read_file(cls, path: str) -> Model: + """ + Read a problem file (.mps/.mps.gz/.cbf/.cbf.gz) and construct a Model. + Cones (SOC/RSOC/EXP/POWER) present in the file are preserved and passed + through to the solver. + """ + raw = read_problem_file(path) + + def _to_csr(d): + if d is None: + return None + return sp.csr_matrix( + (d["data"], d["indices"], d["indptr"]), + shape=d["shape"], + ) + + Q = _to_csr(raw.get("Q")) + A = _to_csr(raw.get("A")) + affine_F = _to_csr(raw.get("affine_F")) + m = cls( + objective_vector=raw["c"], + constraint_matrix=A, + constraint_lower_bound=raw["constr_lb"] if A is not None else None, + constraint_upper_bound=raw["constr_ub"] if A is not None else None, + objective_matrix=Q, + variable_lower_bound=raw["var_lb"], + variable_upper_bound=raw["var_ub"], + objective_constant=raw.get("obj_const", 0.0), + affine_cone_matrix=affine_F, + affine_cone_offset=raw.get("affine_g"), + affine_cones=( + ConeSpec.from_columnar(raw["affine_cones"]) + if raw.get("affine_cones") is not None + else None + ), + variable_cones=( + ConeSpec.from_columnar(raw["cones"]) if raw.get("cones") is not None else None + ), + ) + ps = raw.get("primal_start") + if ps is not None: + m._primal_start = np.asarray(ps, dtype=np.float64) + return m + def setObjectiveVector(self, c: ArrayLike) -> None: """ Overwrite the linear objective vector c. @@ -447,6 +530,74 @@ def setConstraintUpperBound(self, constr_ub: Optional[ArrayLike]) -> None: # clear cached solution self._clear_solution_cache() + def setAffineConeConstraints( + self, + F_like: Optional[Union[np.ndarray, sp.spmatrix]], + g: Optional[ArrayLike], + cones: Optional[ConeSpec], + ) -> None: + """Set the native affine cone constraint ``F x + g in K``.""" + if F_like is None: + if g is not None or cones is not None: + raise ValueError( + "affine_cone_matrix is required when affine offset or cones are provided" + ) + self.affine_F = None + self.affine_g = None + self._affine_cones = None + self.num_affine_constrs = 0 + self._clear_solution_cache() + return + if not isinstance(F_like, (np.ndarray, sp.spmatrix)): + raise TypeError( + "setAffineConeConstraints: F must be a numpy.ndarray or scipy.sparse matrix" + ) + if len(F_like.shape) != 2 or F_like.shape[1] != self.num_vars: + raise ValueError( + f"setAffineConeConstraints: F shape {F_like.shape} must have {self.num_vars} columns" + ) + if not isinstance(cones, ConeSpec): + raise TypeError("setAffineConeConstraints: cones must be a ConeSpec") + if len(cones) == 0: + raise ValueError("setAffineConeConstraints: cones must cover every row of F") + affine_F = _as_csr_f64_i32(F_like) if sp.issparse(F_like) else _as_dense_f64_c(F_like) + num_affine_constrs = int(F_like.shape[0]) + if g is None: + affine_g = None + else: + affine_g = _as_dense_f64_c(g).ravel() + if affine_g.size != num_affine_constrs: + raise ValueError( + "setAffineConeConstraints: affine offset length " + f"{affine_g.size} != rows {num_affine_constrs}" + ) + cones.validate_ambient( + num_affine_constrs, + allow_fixed=False, + require_cover=True, + ) + self.affine_F = affine_F + self.affine_g = affine_g + self.num_affine_constrs = num_affine_constrs + self._affine_cones = cones + self._clear_solution_cache() + + def setVariableCones(self, cones: Optional[ConeSpec]) -> None: + """Set cone blocks embedded directly in the variable vector.""" + if cones is None: + self._variable_cones = None + self._clear_solution_cache() + return + if not isinstance(cones, ConeSpec): + raise TypeError("setVariableCones: cones must be a ConeSpec") + if len(cones) == 0: + self._variable_cones = None + self._clear_solution_cache() + return + cones.validate_ambient(self.num_vars, allow_fixed=True) + self._variable_cones = cones + self._clear_solution_cache() + def setVariableLowerBound(self, lb: Optional[ArrayLike]) -> None: """ Overwrite the decision variable lower bounds. @@ -509,11 +660,12 @@ def setWarmStart( # set dual warm start if dual is not None: dual_arr = _as_dense_f64_c(dual).ravel() - if dual_arr.size == self.num_constrs: # otherwise default to None + expected_dual_size = self.num_constrs + self.num_affine_constrs + if dual_arr.size == expected_dual_size: # otherwise default to None self._dual_start = dual_arr else: warnings.warn( - f"Warm start dual size mismatch (expected {self.num_constrs}, got {dual_arr.size}).", + f"Warm start dual size mismatch (expected {expected_dual_size}, got {dual_arr.size}).", RuntimeWarning, ) # clear dual warm start @@ -577,6 +729,10 @@ def optimize(self): primal_start=self._primal_start, dual_start=self._dual_start, D=getattr(self, "D", None), + cones=self._variable_cones, + affine_F=self.affine_F, + affine_g=self.affine_g, + affine_cones=self._affine_cones, ) # solutions self._x = np.asarray(info.get("X")) if info.get("X") is not None else None diff --git a/python_bindings/CMakeLists.txt b/python_bindings/CMakeLists.txt index b59e35f..5705982 100644 --- a/python_bindings/CMakeLists.txt +++ b/python_bindings/CMakeLists.txt @@ -11,6 +11,9 @@ pybind11_add_module(_pdhcg_core MODULE ${PYBIND_SOURCES}) target_link_libraries(_pdhcg_core PRIVATE pdhcg_core ) +target_include_directories(_pdhcg_core PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../internal +) # set rpath so that the module can find the shared libraries at runtime # Include PSQP build directory in RPATH if PSQP is available diff --git a/python_bindings/_core_bindings.cpp b/python_bindings/_core_bindings.cpp index db81950..818875b 100644 --- a/python_bindings/_core_bindings.cpp +++ b/python_bindings/_core_bindings.cpp @@ -15,7 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +#include "cbf_parser.h" +#include "cone_utils.h" +#include "mps_parser.h" #include "pdhcg.h" +#include +#include #include #include #include @@ -264,10 +269,12 @@ static py::dict get_default_params_py() d["iteration_limit"] = p.termination_criteria.iteration_limit; // rescaling + d["curtis_reid_iterations"] = p.curtis_reid_iterations; d["l_inf_ruiz_iterations"] = p.l_inf_ruiz_iterations; d["has_pock_chambolle_alpha"] = p.has_pock_chambolle_alpha; d["pock_chambolle_alpha"] = p.pock_chambolle_alpha; d["bound_objective_rescaling"] = p.bound_objective_rescaling; + d["use_cone_preserving_scaling"] = p.use_cone_preserving_scaling; // restart d["artificial_restart_threshold"] = p.restart_params.artificial_restart_threshold; @@ -353,10 +360,12 @@ static void parse_params_from_python(py::object params_obj, pdhg_parameters_t *p geti("iteration_limit", p->termination_criteria.iteration_limit); // rescaling + geti("curtis_reid_iterations", p->curtis_reid_iterations); geti("l_inf_ruiz_iterations", p->l_inf_ruiz_iterations); getb("has_pock_chambolle_alpha", p->has_pock_chambolle_alpha); getf("pock_chambolle_alpha", p->pock_chambolle_alpha); getb("bound_objective_rescaling", p->bound_objective_rescaling); + getb("use_cone_preserving_scaling", p->use_cone_preserving_scaling); // restart getf("artificial_restart_threshold", p->restart_params.artificial_restart_threshold); @@ -480,26 +489,168 @@ static PyMatrixView get_matrix_from_python(py::object A, double zero_tol) throw std::invalid_argument("Unsupported matrix A: expected numpy.ndarray or " "scipy.sparse (csr/csc/coo)"); } -// solve function +static py::dict +cone_blocks_to_columnar(const cone_blocks_t *blocks, int ambient_dimension, const std::vector *starts = nullptr) +{ + int count = blocks ? blocks->num_cones : 0; + if (starts && (int)starts->size() != count) + throw std::logic_error("compact cone starts have the wrong length"); + + py::array_t types({count}); + py::array_t start_indices({count}); + py::array_t v_dims({count}); + py::array_t power_alphas({count}); + int32_t *type_data = types.mutable_data(); + int32_t *start_data = start_indices.mutable_data(); + int32_t *v_dim_data = v_dims.mutable_data(); + double *alpha_data = power_alphas.mutable_data(); + for (int cone = 0; cone < count; ++cone) + { + type_data[cone] = static_cast(blocks->type[cone]); + start_data[cone] = starts ? (*starts)[cone] : blocks->start_idx[cone]; + v_dim_data[cone] = blocks->v_dim[cone]; + alpha_data[cone] = blocks->type[cone] == CONE_POWER && blocks->power_alpha ? blocks->power_alpha[cone] : 0.0; + } + + py::dict result; + result["types"] = types; + result["starts"] = start_indices; + result["v_dims"] = v_dims; + result["power_alphas"] = power_alphas; + if (blocks && blocks->is_fixed) + { + if (blocks->fixed_mask_size != ambient_dimension) + throw std::logic_error("cone fixed mask has the wrong ambient dimension"); + py::array_t fixed_mask({ambient_dimension}); + uint8_t *fixed_data = fixed_mask.mutable_data(); + for (int index = 0; index < ambient_dimension; ++index) + fixed_data[index] = blocks->is_fixed[index] ? 1 : 0; + result["fixed_mask"] = fixed_mask; + } + else + { + result["fixed_mask"] = py::none(); + } + return result; +} + +struct ParsedConeSpecs +{ + std::vector specs; + std::vector owners; +}; + +static bool has_columnar_cone_fields(const py::object &cones) +{ + return py::hasattr(cones, "types") && py::hasattr(cones, "starts") && py::hasattr(cones, "v_dims") && + py::hasattr(cones, "power_alphas"); +} + +static py::object cone_field(const py::object &cones, const char *name, bool required = true) +{ + if (py::hasattr(cones, name)) + return cones.attr(name); + if (required) + throw std::invalid_argument(std::string("columnar cone metadata requires '") + name + "'"); + return py::none(); +} + +static ParsedConeSpecs parse_columnar_cone_specs(py::object cones, bool affine, int ambient_dimension) +{ + using IntArray = py::array_t; + using DoubleArray = py::array_t; + + IntArray types(cone_field(cones, "types")); + IntArray starts(cone_field(cones, "starts")); + IntArray v_dims(cone_field(cones, "v_dims")); + DoubleArray power_alphas(cone_field(cones, "power_alphas")); + if (types.ndim() != 1 || starts.ndim() != 1 || v_dims.ndim() != 1 || power_alphas.ndim() != 1) + throw std::invalid_argument("columnar cone fields must be one-dimensional arrays"); + py::ssize_t count = starts.size(); + if (types.size() != count || v_dims.size() != count || power_alphas.size() != count) + throw std::invalid_argument("columnar cone fields must have the same length"); + if (count > std::numeric_limits::max()) + throw std::invalid_argument("too many cone blocks"); + + ParsedConeSpecs out; + out.specs.resize((size_t)count); + + const uint8_t *fixed_data = nullptr; + py::object fixed_field = cone_field(cones, "fixed_mask", false); + if (!fixed_field.is_none()) + { + if (affine) + throw std::invalid_argument("affine cones do not support fixed slots"); + py::array_t fixed_mask(fixed_field); + if (fixed_mask.ndim() != 1 || fixed_mask.size() != ambient_dimension) + throw std::invalid_argument("fixed_mask length must equal the variable dimension"); + fixed_data = fixed_mask.data(); + out.owners.push_back(std::move(fixed_mask)); + } + + const char *kind = affine ? "affine cone" : "cone"; + const int32_t *type_data = types.data(); + const int32_t *start_data = starts.data(); + const int32_t *v_dim_data = v_dims.data(); + const double *alpha_data = power_alphas.data(); + for (py::ssize_t cone = 0; cone < count; ++cone) + { + int type_code = type_data[cone]; + if (type_code < CONE_ROTATED_SOC || type_code > CONE_POWER) + throw std::invalid_argument(std::string(kind) + " has an invalid type code"); + cone_spec_t &spec = out.specs[(size_t)cone]; + spec.type = static_cast(type_code); + spec.start_idx = start_data[cone]; + spec.v_dim = v_dim_data[cone]; + spec.power_alpha = alpha_data[cone]; + if (spec.v_dim <= 0) + throw std::invalid_argument(std::string(kind) + " v_dim must be positive"); + if ((spec.type == CONE_EXPONENTIAL || spec.type == CONE_POWER) && spec.v_dim != 1) + throw std::invalid_argument(std::string(kind) + " EXP and POWER blocks require v_dim == 1"); + if (spec.type == CONE_POWER && + !(spec.power_alpha > 0.0 && spec.power_alpha < 1.0 && std::isfinite(spec.power_alpha))) + throw std::invalid_argument(std::string(kind) + " power alpha must be in (0,1)"); + int length = cone_length(spec.type, spec.v_dim); + if (spec.start_idx < 0 || length <= 0 || (long long)spec.start_idx + length > (long long)ambient_dimension) + throw std::invalid_argument(std::string(kind) + " range exceeds the ambient dimension"); + spec.is_fixed = fixed_data ? reinterpret_cast(fixed_data + spec.start_idx) : nullptr; + } + return out; +} + +static ParsedConeSpecs parse_cone_specs(py::object cones, bool affine, int ambient_dimension) +{ + ParsedConeSpecs out; + if (cones.is_none()) + return out; + if (!has_columnar_cone_fields(cones)) + throw std::invalid_argument("cones must be a pdhcg.ConeSpec"); + return parse_columnar_cone_specs(cones, affine, ambient_dimension); +} + static py::dict solve_once(py::object Q, py::object R, py::object A, - py::object objective_vector, // c - py::object objective_constant, // c0 - py::object variable_lower_bound, // lb - py::object variable_upper_bound, // ub - py::object constraint_lower_bound, // l - py::object constraint_upper_bound, // u + py::object objective_vector, + py::object objective_constant, + py::object variable_lower_bound, + py::object variable_upper_bound, + py::object constraint_lower_bound, + py::object constraint_upper_bound, double zero_tolerance = 0.0, py::object params = py::none(), py::object primal_start = py::none(), py::object dual_start = py::none(), - py::object D = py::none()) + py::object D = py::none(), + py::object cones = py::none(), + py::object affine_F = py::none(), + py::object affine_g = py::none(), + py::object affine_cones = py::none()) { static std::once_flag cuda_init_flag; std::call_once(cuda_init_flag, []() { cudaFree(0); }); - PyMatrixView view_a, view_q, view_r; + PyMatrixView view_a, view_q, view_r, view_f; if (!A.is_none()) { view_a = get_matrix_from_python(A, zero_tolerance); @@ -515,6 +666,11 @@ static py::dict solve_once(py::object Q, view_r = get_matrix_from_python(R, zero_tolerance); } + if (!affine_F.is_none()) + { + view_f = get_matrix_from_python(affine_F, zero_tolerance); + } + int n = 0; int m = 0; @@ -524,10 +680,17 @@ static py::dict solve_once(py::object Q, n = view_q.desc.n; else if (view_r.desc.n > 0) n = view_r.desc.n; + else if (view_f.desc.n > 0) + n = view_f.desc.n; if (view_a.desc.m > 0) m = view_a.desc.m; + if (!affine_F.is_none() && view_f.desc.n != n) + throw std::invalid_argument("affine_F column count must match the number of variables"); + if (affine_F.is_none() && (!affine_g.is_none() || !affine_cones.is_none())) + throw std::invalid_argument("affine_F is required when affine_g or affine_cones is provided"); + view_a.keep.owners.insert(view_a.keep.owners.end(), view_q.keep.owners.begin(), view_q.keep.owners.end()); view_a.keep.owners.insert(view_a.keep.owners.end(), view_r.keep.owners.begin(), view_r.keep.owners.end()); @@ -554,6 +717,7 @@ static py::dict solve_once(py::object Q, const matrix_desc_t *q_desc_ptr = Q.is_none() ? nullptr : &view_q.desc; const matrix_desc_t *r_desc_ptr = R.is_none() ? nullptr : &view_r.desc; const matrix_desc_t *a_desc_ptr = A.is_none() ? nullptr : &view_a.desc; + const matrix_desc_t *f_desc_ptr = affine_F.is_none() ? nullptr : &view_f.desc; PyMatrixView view_d; std::vector d_diag_rp, d_diag_ci; @@ -622,19 +786,46 @@ static py::dict solve_once(py::object Q, } } - qp_problem_t *prob = - create_qp_problem(c_ptr, q_desc_ptr, r_desc_ptr, d_desc_ptr, a_desc_ptr, l_ptr, u_ptr, lb_ptr, ub_ptr, c0_ptr); + int num_affine_rows = affine_F.is_none() ? 0 : view_f.desc.m; + ParsedConeSpecs parsed_cones = parse_cone_specs(cones, false, n); + ParsedConeSpecs parsed_affine_cones = parse_cone_specs(affine_cones, true, num_affine_rows); + std::vector &cones_vec = parsed_cones.specs; + std::vector &affine_cones_vec = parsed_affine_cones.specs; + const double *affine_g_ptr = nullptr; + if (!affine_F.is_none()) + { + if (affine_cones_vec.empty()) + throw std::invalid_argument("affine_cones must describe every row of affine_F"); + ensure_len_or_null(affine_g, "affine_g", num_affine_rows); + affine_g_ptr = get_arr_ptr_f64_or_null(affine_g, "affine_g", view_f.keep); + } + + qp_problem_t *prob = create_qp_problem(c_ptr, + q_desc_ptr, + r_desc_ptr, + d_desc_ptr, + a_desc_ptr, + l_ptr, + u_ptr, + lb_ptr, + ub_ptr, + c0_ptr, + (int)cones_vec.size(), + cones_vec.empty() ? nullptr : cones_vec.data(), + f_desc_ptr, + affine_g_ptr, + (int)affine_cones_vec.size(), + affine_cones_vec.empty() ? nullptr : affine_cones_vec.data()); if (!prob) { throw std::runtime_error("create_qp_problem failed."); } - // set warm start values if provided if ((primal_start && !primal_start.is_none()) || (dual_start && !dual_start.is_none())) { // validate dimensions and get pointers ensure_len_or_null(primal_start, "primal_start", n); - ensure_len_or_null(dual_start, "dual_start", m); + ensure_len_or_null(dual_start, "dual_start", m + num_affine_rows); const double *primal_ptr = get_arr_ptr_f64_or_null(primal_start, "primal_start", view_a.keep); const double *dual_ptr = get_arr_ptr_f64_or_null(dual_start, "dual_start", view_a.keep); @@ -714,7 +905,185 @@ static py::dict solve_once(py::object Q, return info; } -// module +/* Convert a CsrComponent + shape (m, n, nnz) into a Python dict suitable for + passing to scipy.sparse.csr_matrix(...). Returns None if the matrix is empty. */ +static py::object csr_to_py(const CsrComponent *csr, int rows, int cols, int nnz) +{ + if (!csr || nnz <= 0 || !csr->row_ptr) + return py::none(); + py::array_t indptr({rows + 1}); + py::array_t indices({nnz}); + py::array_t vals({nnz}); + std::memcpy(indptr.request().ptr, csr->row_ptr, sizeof(int) * (rows + 1)); + std::memcpy(indices.request().ptr, csr->col_ind, sizeof(int) * nnz); + std::memcpy(vals.request().ptr, csr->val, sizeof(double) * nnz); + py::dict d; + d["indptr"] = indptr; + d["indices"] = indices; + d["data"] = vals; + d["shape"] = py::make_tuple(rows, cols); + return d; +} + +static py::object csr_selected_rows_to_py(const CsrComponent *csr, const std::vector &selected_rows, int cols) +{ + if (!csr || !csr->row_ptr || selected_rows.empty()) + return py::none(); + int nnz = 0; + for (int row : selected_rows) + nnz += csr->row_ptr[row + 1] - csr->row_ptr[row]; + py::array_t indptr({(int)selected_rows.size() + 1}); + py::array_t indices({nnz}); + py::array_t vals({nnz}); + int32_t *indptr_data = indptr.mutable_data(); + int32_t *indices_data = indices.mutable_data(); + double *values_data = vals.mutable_data(); + indptr_data[0] = 0; + int cursor = 0; + for (size_t out_row = 0; out_row < selected_rows.size(); ++out_row) + { + int row = selected_rows[out_row]; + int begin = csr->row_ptr[row]; + int count = csr->row_ptr[row + 1] - begin; + if (count > 0) + { + std::memcpy(indices_data + cursor, csr->col_ind + begin, sizeof(int) * count); + std::memcpy(values_data + cursor, csr->val + begin, sizeof(double) * count); + } + cursor += count; + indptr_data[out_row + 1] = cursor; + } + py::dict d; + d["indptr"] = indptr; + d["indices"] = indices; + d["data"] = vals; + d["shape"] = py::make_tuple((int)selected_rows.size(), cols); + return d; +} + +/* Read an MPS or CBF problem file. Dispatches on file extension (.cbf/.cbf.gz -> CBF, + otherwise MPS). Affine cone rows are returned separately as affine_F, affine_g, + and affine_cones. Sparse matrices use {indptr, indices, data, shape} payloads. */ +static py::dict read_problem_file_py(const std::string &path) +{ + qp_problem_t *prob = nullptr; + size_t n = path.size(); + bool is_cbf = false; + size_t stem_end = n; + if (n > 3 && path.compare(n - 3, 3, ".gz") == 0) + stem_end = n - 3; + if (stem_end >= 4 && path.compare(stem_end - 4, 4, ".cbf") == 0) + is_cbf = true; + + prob = is_cbf ? read_cbf_file(path.c_str()) : read_mps_file(path.c_str()); + if (!prob) + throw std::runtime_error("failed to read problem file: " + path); + + /* QCQP files: transform quadratic constraints to SOCP cones so the extracted + problem is directly solvable via solve_once. Default to rotated SOC form. */ + if (prob->num_quadratic_constraints > 0) + { + qp_problem_t *lifted = qcqp_to_socp_qp(prob, CONE_ROTATED_SOC); + qp_problem_free(prob); + if (!lifted) + throw std::runtime_error("QCQP -> SOCP transform failed for: " + path); + prob = lifted; + } + + py::dict out; + int n_var = prob->num_variables; + int m_con = prob->num_constraints; + std::vector is_cone_row((size_t)m_con, 0); + std::vector affine_rows; + for (int cone = 0; cone < prob->affine_cones.num_cones; ++cone) + { + int length = cone_block_length(&prob->affine_cones, cone); + int start = prob->affine_cones.start_idx[cone]; + for (int slot = 0; slot < length; ++slot) + { + is_cone_row[start + slot] = 1; + affine_rows.push_back(start + slot); + } + } + std::vector scalar_rows; + for (int row = 0; row < m_con; ++row) + if (!is_cone_row[row]) + scalar_rows.push_back(row); + int m_scalar = (int)scalar_rows.size(); + int m_affine = (int)affine_rows.size(); + + py::array_t c({n_var}); + std::memcpy(c.request().ptr, prob->objective_vector, sizeof(double) * n_var); + out["c"] = c; + out["obj_const"] = prob->objective_constant; + + out["Q"] = csr_to_py(prob->objective_sparse_matrix, n_var, n_var, prob->objective_sparse_matrix_num_nonzeros); + out["A"] = csr_selected_rows_to_py(prob->constraint_matrix, scalar_rows, n_var); + + py::array_t constr_lb({m_scalar}); + py::array_t constr_ub({m_scalar}); + py::array_t var_lb({n_var}); + py::array_t var_ub({n_var}); + if (m_scalar > 0) + { + double *lower = constr_lb.mutable_data(); + double *upper = constr_ub.mutable_data(); + for (int i = 0; i < m_scalar; ++i) + { + int row = scalar_rows[i]; + double constant = prob->affine_cone_offset[row]; + lower[i] = prob->constraint_lower_bound[row] - constant; + upper[i] = prob->constraint_upper_bound[row] - constant; + } + } + std::memcpy(var_lb.request().ptr, prob->variable_lower_bound, sizeof(double) * n_var); + std::memcpy(var_ub.request().ptr, prob->variable_upper_bound, sizeof(double) * n_var); + out["constr_lb"] = constr_lb; + out["constr_ub"] = constr_ub; + out["var_lb"] = var_lb; + out["var_ub"] = var_ub; + + if (prob->cones.num_cones > 0) + out["cones"] = cone_blocks_to_columnar(&prob->cones, n_var); + else + out["cones"] = py::none(); + + if (m_affine > 0) + { + out["affine_F"] = csr_selected_rows_to_py(prob->constraint_matrix, affine_rows, n_var); + py::array_t affine_g({m_affine}); + double *affine_g_data = affine_g.mutable_data(); + for (int row = 0; row < m_affine; ++row) + affine_g_data[row] = prob->affine_cone_offset[affine_rows[row]]; + out["affine_g"] = affine_g; + int compact_start = 0; + std::vector compact_starts; + compact_starts.reserve((size_t)prob->affine_cones.num_cones); + for (int i = 0; i < prob->affine_cones.num_cones; ++i) + { + compact_starts.push_back(compact_start); + compact_start += cone_block_length(&prob->affine_cones, i); + } + out["affine_cones"] = cone_blocks_to_columnar(&prob->affine_cones, m_affine, &compact_starts); + } + else + { + out["affine_F"] = py::none(); + out["affine_g"] = py::none(); + out["affine_cones"] = py::none(); + } + + if (prob->primal_start) + { + py::array_t ps({n_var}); + std::memcpy(ps.request().ptr, prob->primal_start, sizeof(double) * n_var); + out["primal_start"] = ps; + } + + qp_problem_free(prob); + return out; +} + PYBIND11_MODULE(_pdhcg_core, m) { m.doc() = "pdhcg core bindings (auto-detect dense/CSR/CSC/COO; initialize " @@ -722,6 +1091,13 @@ PYBIND11_MODULE(_pdhcg_core, m) m.def("get_default_params", &get_default_params_py, "Return default PDHG parameters as a dict"); + m.def("read_problem_file", + &read_problem_file_py, + py::arg("path"), + "Read an MPS or CBF file (.mps/.mps.gz/.cbf/.cbf.gz) and return a dict with " + "c, obj_const, Q, A, constr_lb, constr_ub, var_lb, var_ub, cones, affine_F, " + "affine_g, affine_cones, and primal_start."); + m.def("solve_once", &solve_once, py::arg("Q"), @@ -737,5 +1113,9 @@ PYBIND11_MODULE(_pdhcg_core, m) py::arg("params") = py::none(), py::arg("primal_start") = py::none(), py::arg("dual_start") = py::none(), - py::arg("D") = py::none()); + py::arg("D") = py::none(), + py::arg("cones") = py::none(), + py::arg("affine_F") = py::none(), + py::arg("affine_g") = py::none(), + py::arg("affine_cones") = py::none()); } diff --git a/src/cbf_parser.c b/src/cbf_parser.c new file mode 100644 index 0000000..7dc9862 --- /dev/null +++ b/src/cbf_parser.c @@ -0,0 +1,1286 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* MOSEK Conic Benchmark Format (CBF) parser. + Supports variable-side cones L=, L+, L-, F, Q, QR, EXP; + constraint-side cones L=, L+, L-, Q, QR, EXP. + Nonlinear constraint-side cones are represented natively as A x + b in K. + Rejects: PSDVAR, PSDCON, HCOORD, DCOORD, FCOORD, OBJFCOORD, INT, + POW, POW*, EXP*, CHANGE blocks. */ + +#include "cbf_parser.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include + +#define LINE_BUF_SIZE 8192 + +typedef enum +{ + CBF_CONE_FREE = 0, /* F : free domain */ + CBF_CONE_ZERO, /* L= : fixed at zero */ + CBF_CONE_LPOS, /* L+ : x >= 0 */ + CBF_CONE_LNEG, /* L- : x <= 0 */ + CBF_CONE_SOC, /* Q : standard SOC, x[0] >= ||x[1:]|| */ + CBF_CONE_RSOC, /* QR : rotated SOC, 2*x[0]*x[1] >= ||x[2:]||^2 */ + CBF_CONE_EXP, /* EXP: x[0] >= x[1] * exp(x[2]/x[1]), x[1] > 0 */ +} cbf_cone_t; + +typedef struct +{ + cbf_cone_t type; + int dim; + int start; /* start index in original CBF ordering */ +} cbf_block_t; + +typedef struct +{ + bool is_gz; + gzFile gz; + FILE *fp; + char *buf; + int line_no; +} cbf_reader_t; + +static cbf_reader_t *cbf_open(const char *filename) +{ + cbf_reader_t *r = (cbf_reader_t *)safe_calloc(1, sizeof(cbf_reader_t)); + r->buf = (char *)safe_malloc(LINE_BUF_SIZE); + size_t n = strlen(filename); + if (n > 3 && strcmp(filename + n - 3, ".gz") == 0) + { + r->is_gz = true; + r->gz = gzopen(filename, "rb"); + if (!r->gz) + { + free(r->buf); + free(r); + return NULL; + } + } + else + { + r->is_gz = false; + r->fp = fopen(filename, "r"); + if (!r->fp) + { + free(r->buf); + free(r); + return NULL; + } + } + return r; +} + +static void cbf_close(cbf_reader_t *r) +{ + if (!r) + return; + if (r->is_gz && r->gz) + gzclose(r->gz); + if (!r->is_gz && r->fp) + fclose(r->fp); + free(r->buf); + free(r); +} + +/* Reads one raw line into r->buf. Returns NULL on EOF. */ +static char *cbf_getline_raw(cbf_reader_t *r) +{ + char *s = r->is_gz ? gzgets(r->gz, r->buf, LINE_BUF_SIZE) : fgets(r->buf, LINE_BUF_SIZE, r->fp); + if (!s) + return NULL; + r->line_no++; + return s; +} + +/* Reads the next non-blank, non-comment line. Strips trailing whitespace. + Returns NULL on EOF. */ +static char *cbf_next_line(cbf_reader_t *r) +{ + while (1) + { + char *s = cbf_getline_raw(r); + if (!s) + return NULL; + char *p = s; + while (*p == ' ' || *p == '\t') + p++; + if (*p == '\0' || *p == '\n' || *p == '\r' || *p == '#') + continue; + /* Strip trailing whitespace / newline */ + size_t len = strlen(s); + while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r' || s[len - 1] == ' ' || s[len - 1] == '\t')) + s[--len] = '\0'; + return s; + } +} + +static bool cbf_parse_cone_name(const char *tok, cbf_cone_t *out) +{ + if (strcmp(tok, "F") == 0) + { + *out = CBF_CONE_FREE; + return true; + } + if (strcmp(tok, "L=") == 0) + { + *out = CBF_CONE_ZERO; + return true; + } + if (strcmp(tok, "L+") == 0) + { + *out = CBF_CONE_LPOS; + return true; + } + if (strcmp(tok, "L-") == 0) + { + *out = CBF_CONE_LNEG; + return true; + } + if (strcmp(tok, "Q") == 0) + { + *out = CBF_CONE_SOC; + return true; + } + if (strcmp(tok, "QR") == 0) + { + *out = CBF_CONE_RSOC; + return true; + } + if (strcmp(tok, "EXP") == 0) + { + *out = CBF_CONE_EXP; + return true; + } + return false; +} + +static bool cbf_is_lp_cone(cbf_cone_t ct) +{ + return ct == CBF_CONE_FREE || ct == CBF_CONE_ZERO || ct == CBF_CONE_LPOS || ct == CBF_CONE_LNEG; +} + +static bool cbf_is_nonlinear_cone(cbf_cone_t ct) +{ + return ct == CBF_CONE_SOC || ct == CBF_CONE_RSOC || ct == CBF_CONE_EXP; +} + +static int cbf_internal_cone_slots(cbf_cone_t ct, int dim) +{ + if (ct == CBF_CONE_SOC) + return dim; + if (ct == CBF_CONE_RSOC) + return dim; + if (ct == CBF_CONE_EXP) + return 3; + return dim; +} + +static int cbf_internal_cone_v_dim(cbf_cone_t ct, int dim) +{ + if (ct == CBF_CONE_SOC) + return dim - 2; + if (ct == CBF_CONE_RSOC) + return dim - 2; + return 1; /* EXP */ +} + +static cone_type_t cbf_internal_cone_type(cbf_cone_t ct) +{ + if (ct == CBF_CONE_SOC) + return CONE_STANDARD_SOC; + if (ct == CBF_CONE_RSOC) + return CONE_ROTATED_SOC; + return CONE_EXPONENTIAL; +} + +static int cbf_map_cone_component(cbf_cone_t ct, int dim, int base, int local_idx) +{ + if (ct == CBF_CONE_SOC) + { + /* CBF Q: (t, v...). Internal SOC: [v..., w, z]. */ + if (local_idx == 0) + return base + dim - 1; + if (local_idx == dim - 1) + return base + dim - 2; + return base + local_idx - 1; + } + if (ct == CBF_CONE_RSOC) + { + /* CBF QR: (s, t, v...). Internal RSOC: [v..., s, t]. */ + int vd = dim - 2; + if (local_idx == 0) + return base + vd; + if (local_idx == 1) + return base + vd + 1; + return base + local_idx - 2; + } + /* CBF EXP: (x0, x1, x2), internal: (r1=x2, r2=x1, r3=x0). */ + return base + (2 - local_idx); +} + +/* Read n blocks of "CONE_NAME dim" pairs. Rejects unsupported cones. */ +static cbf_block_t *cbf_read_blocks(cbf_reader_t *r, int nblk, int *total_out, const char *ctx) +{ + cbf_block_t *blk = (cbf_block_t *)safe_malloc(nblk * sizeof(cbf_block_t)); + int total = 0; + for (int i = 0; i < nblk; ++i) + { + char *ln = cbf_next_line(r); + if (!ln) + { + fprintf(stderr, "[cbf] %s: unexpected EOF in cone list at block %d\n", ctx, i); + free(blk); + return NULL; + } + char cone_name[16]; + int dim = 0; + if (sscanf(ln, "%15s %d", cone_name, &dim) != 2 || dim <= 0) + { + fprintf(stderr, "[cbf] %s: bad cone line '%s'\n", ctx, ln); + free(blk); + return NULL; + } + cbf_cone_t ct; + if (!cbf_parse_cone_name(cone_name, &ct)) + { + fprintf(stderr, "[cbf] %s: unsupported cone '%s' (need L=/L+/L-/F/Q/QR/EXP)\n", ctx, cone_name); + free(blk); + return NULL; + } + if (ct == CBF_CONE_SOC && dim < 2) + { + fprintf(stderr, "[cbf] %s: Q dim must be >= 2, got %d\n", ctx, dim); + free(blk); + return NULL; + } + if (ct == CBF_CONE_RSOC && dim < 3) + { + fprintf(stderr, "[cbf] %s: QR dim must be >= 3, got %d\n", ctx, dim); + free(blk); + return NULL; + } + if (ct == CBF_CONE_EXP && dim != 3) + { + fprintf(stderr, "[cbf] %s: EXP dim must be 3, got %d\n", ctx, dim); + free(blk); + return NULL; + } + blk[i].type = ct; + blk[i].dim = dim; + blk[i].start = total; + total += dim; + } + *total_out = total; + return blk; +} + +/* Skip an unsupported block of `nnz` coord lines. */ +static bool cbf_skip_lines(cbf_reader_t *r, int n) +{ + for (int i = 0; i < n; ++i) + { + if (!cbf_next_line(r)) + { + fprintf(stderr, "[cbf] EOF while skipping block (line %d of %d)\n", i, n); + return false; + } + } + return true; +} + +typedef struct +{ + /* Header */ + int ver; + int objsense_neg; /* 1 if MAX (negate objective) */ + + /* Variable cones (CBF ordering) */ + cbf_block_t *var_blocks; + int num_var_blocks; + int num_vars; + + /* Constraint cones (CBF ordering) */ + cbf_block_t *con_blocks; + int num_con_blocks; + int num_cons; + + /* Objective */ + double obj_constant; + double *obj_c; /* [num_vars] linear coefficients (CBF ordering) */ + + /* Constraint matrix in COO (row/col/val) — CBF ordering */ + int nnz_A; + int cap_A; + int *A_row; + int *A_col; + double *A_val; + + /* b vector for CBF Ax + b ∈ K_con (CBF ordering) */ + double *b; + + /* OBJQCOORD symmetric fill; objective is 0.5 * x^T Q x. CBF variable ordering. */ + int nnz_Q; + int cap_Q; + int *Q_row; + int *Q_col; + double *Q_val; +} cbf_state_t; + +static void cbf_state_free(cbf_state_t *s) +{ + free(s->var_blocks); + free(s->con_blocks); + free(s->obj_c); + free(s->A_row); + free(s->A_col); + free(s->A_val); + free(s->b); + free(s->Q_row); + free(s->Q_col); + free(s->Q_val); +} + +static bool cbf_read_ver(cbf_reader_t *r, cbf_state_t *s) +{ + char *ln = cbf_next_line(r); + if (!ln) + { + fprintf(stderr, "[cbf] EOF after VER header\n"); + return false; + } + if (sscanf(ln, "%d", &s->ver) != 1) + { + fprintf(stderr, "[cbf] bad VER line '%s'\n", ln); + return false; + } + return true; +} + +static bool cbf_read_objsense(cbf_reader_t *r, cbf_state_t *s) +{ + char *ln = cbf_next_line(r); + if (!ln) + { + fprintf(stderr, "[cbf] EOF after OBJSENSE header\n"); + return false; + } + char sense[16]; + if (sscanf(ln, "%15s", sense) != 1) + { + fprintf(stderr, "[cbf] bad OBJSENSE '%s'\n", ln); + return false; + } + if (strcmp(sense, "MIN") == 0) + s->objsense_neg = 0; + else if (strcmp(sense, "MAX") == 0) + s->objsense_neg = 1; + else + { + fprintf(stderr, "[cbf] OBJSENSE must be MIN or MAX, got '%s'\n", sense); + return false; + } + return true; +} + +static bool cbf_read_var(cbf_reader_t *r, cbf_state_t *s) +{ + if (s->var_blocks) + { + fprintf(stderr, "[cbf] duplicate VAR block\n"); + return false; + } + char *ln = cbf_next_line(r); + if (!ln) + return false; + int n = 0, k = 0; + if (sscanf(ln, "%d %d", &n, &k) != 2 || n < 0 || k < 0) + { + fprintf(stderr, "[cbf] bad VAR header '%s'\n", ln); + return false; + } + int total = 0; + s->var_blocks = cbf_read_blocks(r, k, &total, "VAR"); + if (!s->var_blocks) + return false; + if (total != n) + { + fprintf(stderr, "[cbf] VAR cone sum %d != n=%d\n", total, n); + return false; + } + s->num_var_blocks = k; + s->num_vars = n; + s->obj_c = (double *)safe_calloc(n, sizeof(double)); + return true; +} + +static bool cbf_read_con(cbf_reader_t *r, cbf_state_t *s) +{ + if (s->con_blocks) + { + fprintf(stderr, "[cbf] duplicate CON block\n"); + return false; + } + char *ln = cbf_next_line(r); + if (!ln) + return false; + int m = 0, k = 0; + if (sscanf(ln, "%d %d", &m, &k) != 2 || m < 0 || k < 0) + { + fprintf(stderr, "[cbf] bad CON header '%s'\n", ln); + return false; + } + int total = 0; + s->con_blocks = cbf_read_blocks(r, k, &total, "CON"); + if (!s->con_blocks) + return false; + if (total != m) + { + fprintf(stderr, "[cbf] CON cone sum %d != m=%d\n", total, m); + return false; + } + s->num_con_blocks = k; + s->num_cons = m; + s->b = (double *)safe_calloc(m, sizeof(double)); + return true; +} + +static bool cbf_read_objacoord(cbf_reader_t *r, cbf_state_t *s) +{ + char *ln = cbf_next_line(r); + if (!ln) + return false; + int nnz = 0; + if (sscanf(ln, "%d", &nnz) != 1 || nnz < 0) + { + fprintf(stderr, "[cbf] bad OBJACOORD header '%s'\n", ln); + return false; + } + if (!s->obj_c) + { + fprintf(stderr, "[cbf] OBJACOORD before VAR\n"); + return false; + } + for (int i = 0; i < nnz; ++i) + { + ln = cbf_next_line(r); + if (!ln) + { + fprintf(stderr, "[cbf] OBJACOORD truncated at %d/%d\n", i, nnz); + return false; + } + int col; + double val; + if (sscanf(ln, "%d %lf", &col, &val) != 2) + { + fprintf(stderr, "[cbf] bad OBJACOORD entry '%s'\n", ln); + return false; + } + if (col < 0 || col >= s->num_vars) + { + fprintf(stderr, "[cbf] OBJACOORD col %d out of range [0,%d)\n", col, s->num_vars); + return false; + } + s->obj_c[col] += val; + } + return true; +} + +static bool cbf_read_objbcoord(cbf_reader_t *r, cbf_state_t *s) +{ + char *ln = cbf_next_line(r); + if (!ln) + return false; + double val; + if (sscanf(ln, "%lf", &val) != 1) + { + fprintf(stderr, "[cbf] bad OBJBCOORD '%s'\n", ln); + return false; + } + s->obj_constant += val; + return true; +} + +static void cbf_reserve_Q(cbf_state_t *s, int need) +{ + if (s->cap_Q >= need) + return; + int new_cap = (s->cap_Q > 0) ? s->cap_Q : 16; + while (new_cap < need) + new_cap *= 2; + s->Q_row = (int *)safe_realloc(s->Q_row, (size_t)new_cap * sizeof(int)); + s->Q_col = (int *)safe_realloc(s->Q_col, (size_t)new_cap * sizeof(int)); + s->Q_val = (double *)safe_realloc(s->Q_val, (size_t)new_cap * sizeof(double)); + s->cap_Q = new_cap; +} + +/* OBJQCOORD: nnz lines of "i j val". Off-diagonal entries are symmetric-filled + into (i,j) and (j,i). Objective is 0.5 * x^T Q x + c^T x + f. */ +static bool cbf_read_objqcoord(cbf_reader_t *r, cbf_state_t *s) +{ + char *ln = cbf_next_line(r); + if (!ln) + return false; + int nnz = 0; + if (sscanf(ln, "%d", &nnz) != 1 || nnz < 0) + { + fprintf(stderr, "[cbf] bad OBJQCOORD header '%s'\n", ln); + return false; + } + if (!s->var_blocks) + { + fprintf(stderr, "[cbf] OBJQCOORD before VAR\n"); + return false; + } + /* Worst case: all off-diagonal, so up to 2*nnz internal entries. */ + cbf_reserve_Q(s, s->nnz_Q + 2 * nnz); + for (int k = 0; k < nnz; ++k) + { + ln = cbf_next_line(r); + if (!ln) + { + fprintf(stderr, "[cbf] OBJQCOORD truncated at %d/%d\n", k, nnz); + return false; + } + int i, j; + double val; + if (sscanf(ln, "%d %d %lf", &i, &j, &val) != 3) + { + fprintf(stderr, "[cbf] bad OBJQCOORD entry '%s'\n", ln); + return false; + } + if (i < 0 || i >= s->num_vars || j < 0 || j >= s->num_vars) + { + fprintf(stderr, "[cbf] OBJQCOORD entry (%d,%d) out of range\n", i, j); + return false; + } + if (i == j) + { + s->Q_row[s->nnz_Q] = i; + s->Q_col[s->nnz_Q] = j; + s->Q_val[s->nnz_Q] = val; + s->nnz_Q++; + } + else + { + s->Q_row[s->nnz_Q] = i; + s->Q_col[s->nnz_Q] = j; + s->Q_val[s->nnz_Q] = val; + s->nnz_Q++; + s->Q_row[s->nnz_Q] = j; + s->Q_col[s->nnz_Q] = i; + s->Q_val[s->nnz_Q] = val; + s->nnz_Q++; + } + } + return true; +} + +static void cbf_reserve_A(cbf_state_t *s, int need) +{ + if (s->cap_A >= need) + return; + int new_cap = (s->cap_A > 0) ? s->cap_A : 16; + while (new_cap < need) + new_cap *= 2; + s->A_row = (int *)safe_realloc(s->A_row, (size_t)new_cap * sizeof(int)); + s->A_col = (int *)safe_realloc(s->A_col, (size_t)new_cap * sizeof(int)); + s->A_val = (double *)safe_realloc(s->A_val, (size_t)new_cap * sizeof(double)); + s->cap_A = new_cap; +} + +static bool cbf_read_acoord(cbf_reader_t *r, cbf_state_t *s) +{ + char *ln = cbf_next_line(r); + if (!ln) + return false; + int nnz = 0; + if (sscanf(ln, "%d", &nnz) != 1 || nnz < 0) + { + fprintf(stderr, "[cbf] bad ACOORD header '%s'\n", ln); + return false; + } + if (!s->con_blocks || !s->var_blocks) + { + fprintf(stderr, "[cbf] ACOORD before VAR/CON\n"); + return false; + } + cbf_reserve_A(s, s->nnz_A + nnz); + for (int i = 0; i < nnz; ++i) + { + ln = cbf_next_line(r); + if (!ln) + { + fprintf(stderr, "[cbf] ACOORD truncated at %d/%d\n", i, nnz); + return false; + } + int row, col; + double val; + if (sscanf(ln, "%d %d %lf", &row, &col, &val) != 3) + { + fprintf(stderr, "[cbf] bad ACOORD entry '%s'\n", ln); + return false; + } + if (row < 0 || row >= s->num_cons || col < 0 || col >= s->num_vars) + { + fprintf(stderr, "[cbf] ACOORD entry (%d,%d) out of range\n", row, col); + return false; + } + s->A_row[s->nnz_A] = row; + s->A_col[s->nnz_A] = col; + s->A_val[s->nnz_A] = val; + s->nnz_A++; + } + return true; +} + +static bool cbf_read_bcoord(cbf_reader_t *r, cbf_state_t *s) +{ + char *ln = cbf_next_line(r); + if (!ln) + return false; + int nnz = 0; + if (sscanf(ln, "%d", &nnz) != 1 || nnz < 0) + { + fprintf(stderr, "[cbf] bad BCOORD header '%s'\n", ln); + return false; + } + if (!s->b) + { + fprintf(stderr, "[cbf] BCOORD before CON\n"); + return false; + } + for (int i = 0; i < nnz; ++i) + { + ln = cbf_next_line(r); + if (!ln) + { + fprintf(stderr, "[cbf] BCOORD truncated at %d/%d\n", i, nnz); + return false; + } + int row; + double val; + if (sscanf(ln, "%d %lf", &row, &val) != 2) + { + fprintf(stderr, "[cbf] bad BCOORD entry '%s'\n", ln); + return false; + } + if (row < 0 || row >= s->num_cons) + { + fprintf(stderr, "[cbf] BCOORD row %d out of range\n", row); + return false; + } + s->b[row] += val; + } + return true; +} + +static bool cbf_read_int_block(cbf_reader_t *r) +{ + char *ln = cbf_next_line(r); + if (!ln) + return false; + int n; + if (sscanf(ln, "%d", &n) != 1) + return false; + fprintf(stderr, "[cbf] INT block found (n=%d): integer variables not supported\n", n); + return false; +} + +/* Consume the whole CHANGE block (v3+): CHANGE header + arbitrary follow-up subblocks + until EOF. We treat any post-CHANGE data as ignored — base problem only. */ +static void cbf_consume_change(cbf_reader_t *r) +{ + while (cbf_next_line(r)) + { + /* discard */ + } +} + +/* Compute qp_problem column layout: + [ LP vars | VAR cone-block vars ] + Return arrays: + lp_offset[b] = column start of block b if LP, else -1 + cone_offset[b] = column start of VAR block b if cone, else -1 + total_vars = final variable count + cbf_to_qp[i] = mapping from CBF variable index i to internal qp column index */ +static void +cbf_build_layout(const cbf_state_t *s, int **lp_off_out, int **cone_off_out, int *total_vars_out, int **cbf_to_qp_out) +{ + int nb = s->num_var_blocks; + int *lp_off = (int *)safe_malloc(nb * sizeof(int)); + int *cone_off = (int *)safe_malloc(nb * sizeof(int)); + for (int i = 0; i < nb; ++i) + { + lp_off[i] = -1; + cone_off[i] = -1; + } + int col = 0; + /* LP-side first, preserving CBF order for F, L=, L+, L- blocks. */ + for (int i = 0; i < nb; ++i) + { + cbf_cone_t ct = s->var_blocks[i].type; + if (cbf_is_lp_cone(ct)) + { + lp_off[i] = col; + col += s->var_blocks[i].dim; + } + } + /* Then cone-slot blocks. Each takes its CBF cone dimension. */ + for (int i = 0; i < nb; ++i) + { + cbf_cone_t ct = s->var_blocks[i].type; + if (!cbf_is_nonlinear_cone(ct)) + continue; + cone_off[i] = col; + col += cbf_internal_cone_slots(ct, s->var_blocks[i].dim); + } + int *cbf_to_qp = (int *)safe_malloc(s->num_vars * sizeof(int)); + for (int i = 0; i < nb; ++i) + { + cbf_cone_t ct = s->var_blocks[i].type; + int start = s->var_blocks[i].start; + int dim = s->var_blocks[i].dim; + if (cbf_is_lp_cone(ct)) + { + int base = lp_off[i]; + for (int j = 0; j < dim; ++j) + cbf_to_qp[start + j] = base + j; + } + else + { + int base = cone_off[i]; + for (int j = 0; j < dim; ++j) + cbf_to_qp[start + j] = cbf_map_cone_component(ct, dim, base, j); + } + } + + *lp_off_out = lp_off; + *cone_off_out = cone_off; + *total_vars_out = col; + *cbf_to_qp_out = cbf_to_qp; +} + +/* Sort (row, col) coordinates and coalesce duplicates into a CSR matrix. + Returns malloc'd CsrComponent. */ +static CsrComponent * +cbf_coo_to_csr(int m, int n, int nnz, const int *rows, const int *cols, const double *vals, int *out_nnz) +{ + (void)n; + /* Simple bucket sort by row. */ + int *row_count = (int *)safe_calloc(m + 1, sizeof(int)); + for (int i = 0; i < nnz; ++i) + row_count[rows[i] + 1]++; + for (int i = 0; i < m; ++i) + row_count[i + 1] += row_count[i]; + + int *row_ptr = (int *)safe_malloc((m + 1) * sizeof(int)); + memcpy(row_ptr, row_count, (m + 1) * sizeof(int)); + + int alloc_nnz = nnz > 0 ? nnz : 1; + int *col_ind = (int *)safe_malloc((size_t)alloc_nnz * sizeof(int)); + double *val = (double *)safe_malloc((size_t)alloc_nnz * sizeof(double)); + int *cursor = row_count; + for (int i = 0; i < nnz; ++i) + { + int r = rows[i]; + int pos = cursor[r]++; + col_ind[pos] = cols[i]; + val[pos] = vals[i]; + } + free(row_count); + + /* Sort each row's entries by col and coalesce duplicates. */ + int write = 0; + for (int r = 0; r < m; ++r) + { + int s = row_ptr[r]; + int e = row_ptr[r + 1]; + /* Insertion sort — CBF rows are typically short. */ + for (int i = s + 1; i < e; ++i) + { + int c = col_ind[i]; + double v = val[i]; + int j = i - 1; + while (j >= s && col_ind[j] > c) + { + col_ind[j + 1] = col_ind[j]; + val[j + 1] = val[j]; + j--; + } + col_ind[j + 1] = c; + val[j + 1] = v; + } + int new_s = write; + int i = s; + while (i < e) + { + int c = col_ind[i]; + double acc = val[i]; + int j = i + 1; + while (j < e && col_ind[j] == c) + { + acc += val[j]; + j++; + } + if (acc != 0.0) + { + col_ind[write] = c; + val[write] = acc; + write++; + } + i = j; + } + row_ptr[r] = new_s; + } + row_ptr[m] = write; + *out_nnz = write; + + CsrComponent *csr = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + csr->row_ptr = row_ptr; + csr->col_ind = col_ind; + csr->val = val; + return csr; +} + +static bool cbf_same_fixed_value(double first, double second) +{ + double scale = 1.0 + fmax(fabs(first), fabs(second)); + return fabs(first - second) <= 1e-12 * scale; +} + +/* Mark singleton equalities fixing the radius coordinate of a standard SOC so + the cone projection can use its fixed-radius fast path. The original matrix + and all bounds remain unchanged. */ +static int cbf_mark_fixed_soc_radii(qp_problem_t *problem) +{ + int n = problem->num_variables; + int m = problem->num_constraints; + if (problem->cones.num_cones == 0 || n == 0 || m == 0 || !problem->constraint_matrix) + return 0; + + unsigned char *is_radius = (unsigned char *)safe_calloc((size_t)n, sizeof(unsigned char)); + unsigned char *candidate_state = (unsigned char *)safe_calloc((size_t)n, sizeof(unsigned char)); + double *candidate_value = (double *)safe_calloc((size_t)n, sizeof(double)); + CsrComponent *A = problem->constraint_matrix; + + for (int cone = 0; cone < problem->cones.num_cones; ++cone) + { + if (problem->cones.type[cone] != CONE_STANDARD_SOC) + continue; + int z = problem->cones.start_idx[cone] + problem->cones.v_dim[cone] + 1; + if (z >= 0 && z < n) + is_radius[z] = 1; + } + + for (int row = 0; row < m; ++row) + { + int begin = A->row_ptr[row]; + int end = A->row_ptr[row + 1]; + double lower = problem->constraint_lower_bound[row]; + double upper = problem->constraint_upper_bound[row]; + if (end - begin != 1 || !isfinite(lower) || lower != upper) + continue; + + int column = A->col_ind[begin]; + double coefficient = A->val[begin]; + if (column < 0 || column >= n || !is_radius[column] || !isfinite(coefficient) || coefficient == 0.0) + continue; + + double value = (lower - problem->affine_cone_offset[row]) / coefficient; + if (!isfinite(value) || value < 0.0) + continue; + + if (candidate_state[column] == 0) + { + candidate_state[column] = 1; + candidate_value[column] = value; + } + else if (candidate_state[column] == 1 && !cbf_same_fixed_value(candidate_value[column], value)) + { + candidate_state[column] = 2; + } + } + + int fixed = 0; + for (int column = 0; column < n; ++column) + fixed += candidate_state[column] == 1; + + if (fixed > 0) + { + if (!problem->cones.is_fixed) + { + problem->cones.fixed_mask_size = n; + problem->cones.is_fixed = (char *)safe_calloc((size_t)n, sizeof(char)); + } + if (!problem->primal_start) + problem->primal_start = (double *)safe_calloc((size_t)n, sizeof(double)); + + for (int column = 0; column < n; ++column) + { + if (candidate_state[column] != 1) + continue; + problem->cones.is_fixed[column] = 1; + problem->primal_start[column] = candidate_value[column]; + } + } + + free(is_radius); + free(candidate_state); + free(candidate_value); + return fixed; +} + +static qp_problem_t *cbf_finalize(cbf_state_t *s) +{ + int *lp_off, *cone_off, *cbf_to_qp; + int total_vars; + cbf_build_layout(s, &lp_off, &cone_off, &total_vars, &cbf_to_qp); + + int *cbf_row_to_qp = (int *)safe_malloc((size_t)(s->num_cons > 0 ? s->num_cons : 1) * sizeof(int)); + int *constraint_block_start = + (int *)safe_malloc((size_t)(s->num_con_blocks > 0 ? s->num_con_blocks : 1) * sizeof(int)); + int row_cursor = 0; + for (int b = 0; b < s->num_con_blocks; ++b) + { + cbf_cone_t ct = s->con_blocks[b].type; + int start = s->con_blocks[b].start; + int dim = s->con_blocks[b].dim; + constraint_block_start[b] = row_cursor; + if (cbf_is_lp_cone(ct)) + { + for (int j = 0; j < dim; ++j) + cbf_row_to_qp[start + j] = row_cursor + j; + } + else + { + for (int j = 0; j < dim; ++j) + cbf_row_to_qp[start + j] = cbf_map_cone_component(ct, dim, row_cursor, j); + } + row_cursor += cbf_internal_cone_slots(ct, dim); + } + + qp_problem_t *out = (qp_problem_t *)safe_calloc(1, sizeof(qp_problem_t)); + out->num_variables = total_vars; + out->num_constraints = s->num_cons; + out->affine_cone_offset = s->num_cons > 0 ? (double *)safe_calloc((size_t)s->num_cons, sizeof(double)) : NULL; + out->num_original_variables = total_vars; + out->objective_constant = s->obj_constant; + + out->objective_vector = (double *)safe_calloc(total_vars, sizeof(double)); + out->variable_lower_bound = (double *)safe_malloc(total_vars * sizeof(double)); + out->variable_upper_bound = (double *)safe_malloc(total_vars * sizeof(double)); + for (int i = 0; i < total_vars; ++i) + { + out->variable_lower_bound[i] = -INFINITY; + out->variable_upper_bound[i] = INFINITY; + } + double c_sign = s->objsense_neg ? -1.0 : 1.0; + for (int i = 0; i < s->num_vars; ++i) + out->objective_vector[cbf_to_qp[i]] = c_sign * s->obj_c[i]; + + /* Variable bounds from LP-cone membership. */ + for (int b = 0; b < s->num_var_blocks; ++b) + { + cbf_cone_t ct = s->var_blocks[b].type; + int base = lp_off[b]; + if (base < 0) + continue; + int dim = s->var_blocks[b].dim; + if (ct == CBF_CONE_LPOS) + { + for (int j = 0; j < dim; ++j) + out->variable_lower_bound[base + j] = 0.0; + } + else if (ct == CBF_CONE_ZERO) + { + for (int j = 0; j < dim; ++j) + { + out->variable_lower_bound[base + j] = 0.0; + out->variable_upper_bound[base + j] = 0.0; + } + } + else if (ct == CBF_CONE_LNEG) + { + for (int j = 0; j < dim; ++j) + out->variable_upper_bound[base + j] = 0.0; + } + /* CBF_CONE_FREE: leave -inf/inf. */ + } + + /* Variable-side cone-block descriptors. */ + int num_cones = 0; + for (int b = 0; b < s->num_var_blocks; ++b) + { + cbf_cone_t ct = s->var_blocks[b].type; + if (cbf_is_nonlinear_cone(ct)) + num_cones++; + } + out->cones.num_cones = num_cones; + if (num_cones > 0) + { + out->cones.start_idx = (int *)safe_malloc(num_cones * sizeof(int)); + out->cones.v_dim = (int *)safe_malloc(num_cones * sizeof(int)); + out->cones.type = (cone_type_t *)safe_malloc(num_cones * sizeof(cone_type_t)); + int k = 0; + for (int b = 0; b < s->num_var_blocks; ++b) + { + cbf_cone_t ct = s->var_blocks[b].type; + if (!cbf_is_nonlinear_cone(ct)) + continue; + out->cones.start_idx[k] = cone_off[b]; + out->cones.v_dim[k] = cbf_internal_cone_v_dim(ct, s->var_blocks[b].dim); + out->cones.type[k] = cbf_internal_cone_type(ct); + k++; + } + } + + int num_affine_cones = 0; + for (int b = 0; b < s->num_con_blocks; ++b) + num_affine_cones += cbf_is_nonlinear_cone(s->con_blocks[b].type); + out->affine_cones.num_cones = num_affine_cones; + if (num_affine_cones > 0) + { + out->affine_cones.start_idx = (int *)safe_malloc((size_t)num_affine_cones * sizeof(int)); + out->affine_cones.v_dim = (int *)safe_malloc((size_t)num_affine_cones * sizeof(int)); + out->affine_cones.type = (cone_type_t *)safe_malloc((size_t)num_affine_cones * sizeof(cone_type_t)); + int cone = 0; + for (int b = 0; b < s->num_con_blocks; ++b) + { + cbf_cone_t ct = s->con_blocks[b].type; + if (!cbf_is_nonlinear_cone(ct)) + continue; + out->affine_cones.start_idx[cone] = constraint_block_start[b]; + out->affine_cones.v_dim[cone] = cbf_internal_cone_v_dim(ct, s->con_blocks[b].dim); + out->affine_cones.type[cone] = cbf_internal_cone_type(ct); + cone++; + } + } + + /* Build A in internal row/column indexing. CBF states A x + b in K_con; + LP cone rows use canonical zero bounds and nonlinear cone rows are + identified by affine_cones. */ + int work_nnz = s->nnz_A; + int *rows = (int *)safe_malloc((size_t)(work_nnz > 0 ? work_nnz : 1) * sizeof(int)); + int *cols = (int *)safe_malloc((size_t)(work_nnz > 0 ? work_nnz : 1) * sizeof(int)); + double *vals = (double *)safe_malloc((size_t)(work_nnz > 0 ? work_nnz : 1) * sizeof(double)); + for (int i = 0; i < s->nnz_A; ++i) + { + rows[i] = cbf_row_to_qp[s->A_row[i]]; + cols[i] = cbf_to_qp[s->A_col[i]]; + vals[i] = s->A_val[i]; + } + int final_nnz = 0; + CsrComponent *A = cbf_coo_to_csr(s->num_cons, total_vars, work_nnz, rows, cols, vals, &final_nnz); + free(rows); + free(cols); + free(vals); + out->constraint_matrix = A; + out->constraint_matrix_num_nonzeros = final_nnz; + + out->constraint_lower_bound = (double *)safe_malloc((size_t)(s->num_cons > 0 ? s->num_cons : 1) * sizeof(double)); + out->constraint_upper_bound = (double *)safe_malloc((size_t)(s->num_cons > 0 ? s->num_cons : 1) * sizeof(double)); + for (int row = 0; row < s->num_cons; ++row) + { + out->constraint_lower_bound[row] = -INFINITY; + out->constraint_upper_bound[row] = INFINITY; + } + for (int b = 0; b < s->num_con_blocks; ++b) + { + cbf_cone_t ct = s->con_blocks[b].type; + int start = s->con_blocks[b].start; + int dim = s->con_blocks[b].dim; + for (int j = 0; j < dim; ++j) + { + int cbf_row = start + j; + int r = cbf_row_to_qp[cbf_row]; + if (cbf_is_nonlinear_cone(ct)) + { + out->affine_cone_offset[r] = s->b[cbf_row]; + continue; + } + double shifted_zero = -s->b[cbf_row]; + if (ct == CBF_CONE_FREE) + { + out->constraint_lower_bound[r] = -INFINITY; + out->constraint_upper_bound[r] = INFINITY; + } + else if (ct == CBF_CONE_ZERO) + { + out->constraint_lower_bound[r] = shifted_zero; + out->constraint_upper_bound[r] = shifted_zero; + } + else if (ct == CBF_CONE_LPOS) + { + out->constraint_lower_bound[r] = shifted_zero; + out->constraint_upper_bound[r] = INFINITY; + } + else + { /* CBF_CONE_LNEG */ + out->constraint_lower_bound[r] = -INFINITY; + out->constraint_upper_bound[r] = shifted_zero; + } + } + } + + (void)cbf_mark_fixed_soc_radii(out); + + out->num_quadratic_constraints = 0; + + /* MAX objsense negates Q too since max f = min -f. */ + if (s->nnz_Q > 0) + { + int *qrows = (int *)safe_malloc((size_t)s->nnz_Q * sizeof(int)); + int *qcols = (int *)safe_malloc((size_t)s->nnz_Q * sizeof(int)); + double *qvals = (double *)safe_malloc((size_t)s->nnz_Q * sizeof(double)); + for (int i = 0; i < s->nnz_Q; ++i) + { + qrows[i] = cbf_to_qp[s->Q_row[i]]; + qcols[i] = cbf_to_qp[s->Q_col[i]]; + qvals[i] = c_sign * s->Q_val[i]; + } + int q_final_nnz = 0; + CsrComponent *Q = cbf_coo_to_csr(total_vars, total_vars, s->nnz_Q, qrows, qcols, qvals, &q_final_nnz); + free(qrows); + free(qcols); + free(qvals); + out->objective_sparse_matrix = Q; + out->objective_sparse_matrix_num_nonzeros = q_final_nnz; + } + + free(lp_off); + free(cone_off); + free(cbf_to_qp); + free(cbf_row_to_qp); + free(constraint_block_start); + return out; +} + +qp_problem_t *read_cbf_file(const char *filename) +{ + cbf_reader_t *r = cbf_open(filename); + if (!r) + { + fprintf(stderr, "[cbf] cannot open '%s'\n", filename); + return NULL; + } + + cbf_state_t s = {0}; + s.ver = -1; + + bool ok = true; + while (ok) + { + char *ln = cbf_next_line(r); + if (!ln) + break; + char kw[32]; + if (sscanf(ln, "%31s", kw) != 1) + { + fprintf(stderr, "[cbf] bad keyword at line %d\n", r->line_no); + ok = false; + break; + } + if (strcmp(kw, "VER") == 0) + { + ok = cbf_read_ver(r, &s); + } + else if (strcmp(kw, "OBJSENSE") == 0) + { + ok = cbf_read_objsense(r, &s); + } + else if (strcmp(kw, "VAR") == 0) + { + ok = cbf_read_var(r, &s); + } + else if (strcmp(kw, "CON") == 0) + { + ok = cbf_read_con(r, &s); + } + else if (strcmp(kw, "OBJACOORD") == 0) + { + ok = cbf_read_objacoord(r, &s); + } + else if (strcmp(kw, "OBJBCOORD") == 0) + { + ok = cbf_read_objbcoord(r, &s); + } + else if (strcmp(kw, "OBJQCOORD") == 0) + { + ok = cbf_read_objqcoord(r, &s); + } + else if (strcmp(kw, "ACOORD") == 0) + { + ok = cbf_read_acoord(r, &s); + } + else if (strcmp(kw, "BCOORD") == 0) + { + ok = cbf_read_bcoord(r, &s); + } + else if (strcmp(kw, "INT") == 0) + { + ok = cbf_read_int_block(r); + } + else if (strcmp(kw, "CHANGE") == 0) + { + /* v3+ modification section: base problem is complete; ignore rest. */ + cbf_consume_change(r); + break; + } + else if (strcmp(kw, "PSDVAR") == 0 || strcmp(kw, "PSDCON") == 0 || strcmp(kw, "OBJFCOORD") == 0 || + strcmp(kw, "FCOORD") == 0 || strcmp(kw, "HCOORD") == 0 || strcmp(kw, "DCOORD") == 0) + { + fprintf(stderr, "[cbf] block '%s' not supported (PSD/free-matrix variables)\n", kw); + ok = false; + } + else + { + fprintf(stderr, "[cbf] unknown keyword '%s' at line %d\n", kw, r->line_no); + ok = false; + } + } + + cbf_close(r); + + if (!ok) + { + cbf_state_free(&s); + return NULL; + } + if (s.num_var_blocks == 0 || !s.var_blocks) + { + fprintf(stderr, "[cbf] missing VAR block\n"); + cbf_state_free(&s); + return NULL; + } + if (!s.con_blocks) + { + /* No constraints — synthesize empty CON. */ + s.num_con_blocks = 0; + s.num_cons = 0; + s.b = (double *)safe_calloc(1, sizeof(double)); /* placeholder */ + } + + qp_problem_t *out = cbf_finalize(&s); + cbf_state_free(&s); + return out; +} diff --git a/src/cli.c b/src/cli.c index e7ef240..85d2c13 100644 --- a/src/cli.c +++ b/src/cli.c @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +#include "cbf_parser.h" #include "mps_parser.h" #include "pdhcg.h" #include "presolve_wrapper.h" @@ -42,6 +43,18 @@ char *get_output_path(const char *output_dir, const char *instance_name, const c return full_path; } +/* Dispatch on file extension: .cbf(.gz) -> CBF parser, else MPS parser. */ +static qp_problem_t *read_problem_file(const char *filename) +{ + size_t n = strlen(filename); + const char *tail = filename; + if (n > 3 && strcmp(filename + n - 3, ".gz") == 0) + n -= 3; + if (n > 4 && strncmp(tail + n - 4, ".cbf", 4) == 0) + return read_cbf_file(filename); + return read_mps_file(filename); +} + char *extract_instance_name(const char *filename) { char *filename_copy = strdup(filename); @@ -112,6 +125,8 @@ void save_solver_summary(const pdhcg_result_t *result, const char *output_dir, c } fprintf(outfile, "Primal Objective Value: %e\n", result->primal_objective_value); fprintf(outfile, "Dual Objective Value: %e\n", result->dual_objective_value); + fprintf(outfile, "Absolute Primal Residual: %e\n", result->absolute_primal_residual); + fprintf(outfile, "Absolute Dual Residual: %e\n", result->absolute_dual_residual); fprintf(outfile, "Relative Primal Residual: %e\n", result->relative_primal_residual); fprintf(outfile, "Relative Dual Residual: %e\n", result->relative_dual_residual); fprintf(outfile, "Absolute Objective Gap: %e\n", result->objective_gap); @@ -140,10 +155,10 @@ void save_solver_summary(const pdhcg_result_t *result, const char *output_dir, c void print_usage(const char *prog_name) { - fprintf(stderr, "Usage: %s [OPTIONS] \n\n", prog_name); + fprintf(stderr, "Usage: %s [OPTIONS] \n\n", prog_name); fprintf(stderr, "Arguments:\n"); - fprintf(stderr, " Path to the input problem in MPS format (.mps .QPS or .mps.gz).\n"); + fprintf(stderr, " Input problem: .mps, .qps, .cbf, and gzip-compressed variants.\n"); fprintf(stderr, " Directory where output files will be saved. It will contain:\n"); fprintf(stderr, " - _summary.txt\n"); fprintf(stderr, " - _primal_solution.txt\n"); @@ -156,22 +171,28 @@ void print_usage(const char *prog_name) fprintf(stderr, " --iter_limit Iteration limit (default: %d).\n", INT32_MAX); fprintf(stderr, " --eps_opt Relative optimality tolerance (default: 1e-4).\n"); fprintf(stderr, " --eps_feas Relative feasibility tolerance (default: 1e-4).\n"); - fprintf(stderr, " --eps_infeas_detect Infeasibility detection tolerance (default: 1e-10).\n"); + fprintf(stderr, " --eps_infeas_detect Infeasibility detection tolerance (default: 1e-12).\n"); + fprintf(stderr, " --curtis_reid_iter Iterations for Curtis-Reid scaling (default: 0, disabled).\n"); fprintf(stderr, " --l_inf_ruiz_iter Iterations for L-inf Ruiz rescaling (default: 10).\n"); fprintf(stderr, " --no_pock_chambolle Disable Pock-Chambolle rescaling (default: enabled).\n"); fprintf(stderr, " --pock_chambolle_alpha Value for Pock-Chambolle alpha (default: 1.0).\n"); fprintf(stderr, " --no_bound_obj_rescaling Disable bound objective rescaling.\n"); + fprintf(stderr, " --no_cone_preserving_scaling Keep coordinate-wise cone scaling.\n"); fprintf(stderr, " --eval_freq Termination evaluation frequency (default: 200).\n"); + fprintf(stderr, " --artificial_restart_threshold Artificial restart threshold (default: 0.36).\n"); + fprintf(stderr, + " --sufficient_reduction_for_restart Sufficient reduction factor for restart (default: 0.2).\n"); + fprintf(stderr, " --necessary_reduction_for_restart Necessary reduction factor for restart (default: 0.8).\n"); fprintf(stderr, " --sv_max_iter Max iterations for singular value estimation (default: 5000).\n"); fprintf(stderr, " --sv_tol Tolerance for singular value estimation (default: 1e-4).\n"); fprintf(stderr, " --opt_norm Norm for optimality criteria: l2 or linf (default: linf).\n"); fprintf(stderr, " --inner_iter_limit Max iterations for the inner solver (default: 1000).\n"); fprintf(stderr, " --inner_init_tol Initial tolerance for the inner solver (default: 1e-3).\n"); fprintf(stderr, " --inner_min_tol Minimum tolerance for the inner solver (default: 1e-9).\n"); - fprintf(stderr, " --presolve Enable (1) or disable (0) presolve (default: 1).\n"); fprintf( stderr, " --no_diag_precond Disable Jacobi diagonal preconditioner for inner subproblem (default: enabled).\n"); + fprintf(stderr, " --soc_form
QCQP cone formulation: 'rotated' or 'standard' (default: rotated).\n"); #ifdef PDHCG_COMPILE_DISTRIBUTED fprintf(stderr, "\nDistributed Options (MPI & NCCL):\n"); @@ -210,6 +231,12 @@ int run_pdhcg(int argc, char *argv[]) {"inner_min_tol", required_argument, 0, 1017}, {"presolve", required_argument, 0, 1018}, {"no_diag_precond", no_argument, 0, 1019}, + {"soc_form", required_argument, 0, 1020}, + {"no_cone_preserving_scaling", no_argument, 0, 1021}, + {"artificial_restart_threshold", required_argument, 0, 1022}, + {"sufficient_reduction_for_restart", required_argument, 0, 1023}, + {"necessary_reduction_for_restart", required_argument, 0, 1024}, + {"curtis_reid_iter", required_argument, 0, 1025}, {0, 0, 0, 0}}; int opt; @@ -292,6 +319,32 @@ int run_pdhcg(int argc, char *argv[]) case 1019: params.diag_jacobi_precond = false; break; + case 1020: + if (strcmp(optarg, "rotated") == 0) + params.default_cone_type = CONE_ROTATED_SOC; + else if (strcmp(optarg, "standard") == 0) + params.default_cone_type = CONE_STANDARD_SOC; + else + { + fprintf(stderr, "Error: soc_form must be 'rotated' or 'standard'\n"); + return 1; + } + break; + case 1021: + params.use_cone_preserving_scaling = false; + break; + case 1022: + params.restart_params.artificial_restart_threshold = atof(optarg); + break; + case 1023: + params.restart_params.sufficient_reduction_for_restart = atof(optarg); + break; + case 1024: + params.restart_params.necessary_reduction_for_restart = atof(optarg); + break; + case 1025: + params.curtis_reid_iterations = atoi(optarg); + break; case '?': return 1; } @@ -311,7 +364,7 @@ int run_pdhcg(int argc, char *argv[]) if (instance_name == NULL) return 1; - qp_problem_t *problem = read_mps_file(filename); + qp_problem_t *problem = read_problem_file(filename); if (problem == NULL) { fprintf(stderr, "Failed to read or parse the file.\n"); @@ -384,6 +437,11 @@ int run_d_pdhcg(int argc, char *argv[]) {"inner_min_tol", required_argument, 0, 1017}, {"presolve", required_argument, 0, 1018}, {"no_diag_precond", no_argument, 0, 1019}, + {"no_cone_preserving_scaling", no_argument, 0, 1021}, + {"artificial_restart_threshold", required_argument, 0, 1022}, + {"sufficient_reduction_for_restart", required_argument, 0, 1023}, + {"necessary_reduction_for_restart", required_argument, 0, 1024}, + {"curtis_reid_iter", required_argument, 0, 1025}, {"grid_size", required_argument, 0, 2001}, {"partition_method", required_argument, 0, 2002}, {"permute_method", required_argument, 0, 2003}, @@ -473,6 +531,21 @@ int run_d_pdhcg(int argc, char *argv[]) case 1019: params.diag_jacobi_precond = false; break; + case 1021: + params.use_cone_preserving_scaling = false; + break; + case 1022: + params.restart_params.artificial_restart_threshold = atof(optarg); + break; + case 1023: + params.restart_params.sufficient_reduction_for_restart = atof(optarg); + break; + case 1024: + params.restart_params.necessary_reduction_for_restart = atof(optarg); + break; + case 1025: + params.curtis_reid_iterations = atoi(optarg); + break; case 2001: // --grid_size r,c { int r, c; @@ -575,8 +648,8 @@ int run_d_pdhcg(int argc, char *argv[]) if (rank_global == 0) { if (params.verbose) - printf("Rank 0: Loading MPS file '%s'...\n", filename); - problem = read_mps_file(filename); + printf("Rank 0: Loading problem file '%s'...\n", filename); + problem = read_problem_file(filename); if (problem == NULL) { diff --git a/src/cone_dispatch.cu b/src/cone_dispatch.cu new file mode 100644 index 0000000..5cdb8a3 --- /dev/null +++ b/src/cone_dispatch.cu @@ -0,0 +1,869 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "cone_dispatch.h" +#include "distributed_conic.h" +#include "pdhcg_kernels.cuh" +#include "utils.h" +#include + +typedef void (*cone_proj_launcher_t)(double *primal, + const double *var_rescale, + double *warm_start, + const int *start_idx, + const int *v_dim, + const double *power_alpha, + const char *is_fixed, + int count); + +typedef void (*cone_dual_res_launcher_t)(double *dual_residual, + double *complementarity_residual, + const double *objective_vector, + const double *dual_product, + const double *var_rescale, + const double *primal_solution, + double *warm_start, + const int *start_idx, + const int *v_dim, + const double *power_alpha, + const char *is_fixed, + int count); + +static void launch_rotated_thread_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + project_rotated_soc_kernel<<>>(p, vr, ws, si, vd, isf, n); +} +static void launch_rotated_warp_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n * 32 + t - 1) / t; + project_rotated_soc_warp_kernel<<>>(p, vr, ws, si, vd, isf, n); +} +static void launch_rotated_block_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)pa; + project_rotated_soc_block_kernel<<>>(p, vr, NULL, 0.0, ws, si, vd, isf, n); +} +static void launch_rotated_grid_weighted_impl(double *p, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const char *isf, + int n) +{ + int threads = THREADS_PER_BLOCK; + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + int blocks = n * blocks_per_cone; + CUDA_CHECK(cudaMemsetAsync(ws + n, 0, (size_t)5 * n * sizeof(double))); + initialize_rotated_soc_grid_weighted_kernel<<>>( + p, vr, qd, tau, ws, si, vd, isf, n, blocks_per_cone); + finalize_rotated_soc_grid_weighted_initialization_kernel<<<(n + threads - 1) / threads, threads>>>( + p, vr, qd, tau, ws, si, vd, isf, n); + for (int iteration = 0; iteration < PDHCG_CONE_GRID_ROOT_ITERATIONS; ++iteration) + { + CUDA_CHECK(cudaMemsetAsync(ws + n, 0, (size_t)2 * n * sizeof(double))); + reduce_rotated_soc_grid_weighted_root_kernel<<>>( + p, vr, qd, tau, ws, si, vd, isf, n, blocks_per_cone); + finalize_rotated_soc_grid_weighted_root_kernel<<<(n + threads - 1) / threads, threads>>>( + p, vr, qd, tau, ws, si, vd, isf, n); + } + CUDA_CHECK(cudaMemsetAsync(ws + n, 0, (size_t)2 * n * sizeof(double))); + reduce_rotated_soc_grid_axis_objective_kernel<<>>( + p, vr, qd, tau, ws, si, vd, isf, n, blocks_per_cone); + finalize_rotated_soc_grid_axis_objective_kernel<<<(n + threads - 1) / threads, threads>>>( + p, vr, qd, tau, ws, si, vd, n); + apply_rotated_soc_grid_weighted_kernel<<>>(p, vr, qd, tau, ws, si, vd, isf, n, blocks_per_cone); +} +static void launch_rotated_grid_weighted_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)pa; + launch_rotated_grid_weighted_impl(p, vr, NULL, 0.0, ws, si, vd, isf, n); +} +static void launch_rotated_grid_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)vr; + (void)pa; + (void)isf; + int t = THREADS_PER_BLOCK; + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + int b = n * blocks_per_cone; + CUDA_CHECK(cudaMemsetAsync(ws, 0, (size_t)n * sizeof(double))); + project_rotated_soc_grid_reduce_kernel<<>>(p, ws, si, vd, n, blocks_per_cone); + project_rotated_soc_grid_finalize_kernel<<<(n + t - 1) / t, t>>>(p, ws, si, vd, n); + project_rotated_soc_grid_apply_kernel<<>>(p, ws, si, vd, n, blocks_per_cone); +} +static void launch_standard_thread_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + project_standard_soc_kernel<<>>(p, vr, ws, si, vd, isf, n); +} +static void launch_standard_warp_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n * 32 + t - 1) / t; + project_standard_soc_warp_kernel<<>>(p, vr, ws, si, vd, isf, n); +} +static void launch_standard_block_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)pa; + project_standard_soc_block_kernel<<>>(p, vr, NULL, 0.0, ws, si, vd, isf, n); +} +static void launch_standard_grid_weighted_impl(double *p, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const char *isf, + int n) +{ + int threads = THREADS_PER_BLOCK; + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + int blocks = n * blocks_per_cone; + CUDA_CHECK(cudaMemsetAsync(ws + n, 0, (size_t)5 * n * sizeof(double))); + initialize_standard_soc_grid_weighted_kernel<<>>( + p, vr, qd, tau, ws, si, vd, isf, n, blocks_per_cone); + finalize_standard_soc_grid_weighted_initialization_kernel<<<(n + threads - 1) / threads, threads>>>( + p, vr, qd, tau, ws, si, vd, isf, n); + for (int iteration = 0; iteration < PDHCG_CONE_GRID_ROOT_ITERATIONS; ++iteration) + { + CUDA_CHECK(cudaMemsetAsync(ws + n, 0, (size_t)2 * n * sizeof(double))); + reduce_standard_soc_grid_weighted_root_kernel<<>>( + p, vr, qd, tau, ws, si, vd, isf, n, blocks_per_cone); + finalize_standard_soc_grid_weighted_root_kernel<<<(n + threads - 1) / threads, threads>>>( + p, vr, qd, tau, ws, si, vd, n); + } + apply_standard_soc_grid_weighted_kernel<<>>(p, vr, qd, tau, ws, si, vd, isf, n, blocks_per_cone); +} +static void launch_standard_grid_weighted_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)pa; + launch_standard_grid_weighted_impl(p, vr, NULL, 0.0, ws, si, vd, isf, n); +} +static void launch_standard_grid_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)vr; + (void)pa; + (void)isf; + int t = THREADS_PER_BLOCK; + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + int b = n * blocks_per_cone; + CUDA_CHECK(cudaMemsetAsync(ws, 0, (size_t)n * sizeof(double))); + project_standard_soc_grid_reduce_kernel<<>>(p, ws, si, vd, n, blocks_per_cone); + project_standard_soc_grid_finalize_kernel<<<(n + t - 1) / t, t>>>(p, ws, si, vd, n); + project_standard_soc_grid_apply_kernel<<>>(p, ws, si, vd, n, blocks_per_cone); +} +static void launch_exp_thread_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + project_exp_cone_kernel<<>>(p, vr, ws, si, vd, isf, n); +} +static void launch_power_thread_proj( + double *p, const double *vr, double *ws, const int *si, const int *vd, const double *pa, const char *isf, int n) +{ + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + project_power_cone_kernel<<>>(p, vr, ws, si, vd, pa, isf, n); +} + +static const cone_proj_launcher_t proj_launch_table[NUM_CONE_TYPES][NUM_PROJ_METHODS] = { + [CONE_ROTATED_SOC] = + { + [PROJ_METHOD_THREAD] = launch_rotated_thread_proj, + [PROJ_METHOD_WARP] = launch_rotated_warp_proj, + [PROJ_METHOD_BLOCK] = launch_rotated_block_proj, + [PROJ_METHOD_GRID] = launch_rotated_grid_proj, + [PROJ_METHOD_GRID_WEIGHTED] = launch_rotated_grid_weighted_proj, + }, + [CONE_STANDARD_SOC] = + { + [PROJ_METHOD_THREAD] = launch_standard_thread_proj, + [PROJ_METHOD_WARP] = launch_standard_warp_proj, + [PROJ_METHOD_BLOCK] = launch_standard_block_proj, + [PROJ_METHOD_GRID] = launch_standard_grid_proj, + [PROJ_METHOD_GRID_WEIGHTED] = launch_standard_grid_weighted_proj, + }, + [CONE_EXPONENTIAL] = + { + [PROJ_METHOD_THREAD] = launch_exp_thread_proj, + [PROJ_METHOD_WARP] = NULL, + [PROJ_METHOD_BLOCK] = NULL, + [PROJ_METHOD_GRID] = NULL, + [PROJ_METHOD_GRID_WEIGHTED] = NULL, + }, + [CONE_POWER] = + { + [PROJ_METHOD_THREAD] = launch_power_thread_proj, + [PROJ_METHOD_WARP] = NULL, + [PROJ_METHOD_BLOCK] = NULL, + [PROJ_METHOD_GRID] = NULL, + [PROJ_METHOD_GRID_WEIGHTED] = NULL, + }, +}; + +static void launch_rotated_thread_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + compute_cone_dual_residual_kernel<<>>(dr, cr, obj, dp, vr, ps, ws, si, vd, isf, n); +} +static void launch_rotated_warp_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n * 32 + t - 1) / t; + compute_cone_dual_residual_warp_kernel<<>>(dr, cr, obj, dp, vr, ps, ws, si, vd, isf, n); +} +static void launch_rotated_grid_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)cr; + (void)ps; + (void)pa; + (void)isf; + int t = THREADS_PER_BLOCK; + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + int b = n * blocks_per_cone; + CUDA_CHECK(cudaMemsetAsync(ws, 0, (size_t)n * sizeof(double))); + compute_cone_dual_residual_grid_reduce_kernel<<>>(obj, dp, ws, si, vd, n, blocks_per_cone); + compute_cone_dual_residual_grid_finalize_kernel<<<(n + t - 1) / t, t>>>(dr, obj, dp, vr, ws, si, vd, n); + compute_cone_dual_residual_grid_apply_kernel<<>>(dr, obj, dp, vr, ws, si, vd, n, blocks_per_cone); +} +static void launch_standard_thread_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + compute_cone_dual_residual_standard_kernel<<>>(dr, cr, obj, dp, vr, ps, ws, si, vd, isf, n); +} +static void launch_standard_warp_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n * 32 + t - 1) / t; + compute_cone_dual_residual_standard_warp_kernel<<>>(dr, cr, obj, dp, vr, ps, ws, si, vd, isf, n); +} +static void launch_standard_grid_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)cr; + (void)ps; + (void)pa; + (void)isf; + int t = THREADS_PER_BLOCK; + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + int b = n * blocks_per_cone; + CUDA_CHECK(cudaMemsetAsync(ws, 0, (size_t)n * sizeof(double))); + compute_cone_dual_residual_standard_grid_reduce_kernel<<>>(obj, dp, ws, si, vd, n, blocks_per_cone); + compute_cone_dual_residual_standard_grid_finalize_kernel<<<(n + t - 1) / t, t>>>(dr, obj, dp, vr, ws, si, vd, n); + compute_cone_dual_residual_standard_grid_apply_kernel<<>>(dr, obj, dp, vr, ws, si, vd, n, blocks_per_cone); +} +static void launch_exp_thread_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + compute_cone_dual_residual_exp_kernel<<>>(dr, cr, obj, dp, vr, ps, ws, si, vd, isf, n); +} +static void launch_power_thread_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + compute_cone_dual_residual_power_kernel<<>>(dr, cr, obj, dp, vr, ps, ws, si, vd, pa, isf, n); +} + +static void launch_projected_mapping_only_dual_impl( + double *dual_residual, const int *start_idx, const int *v_dim, int count, int blocks_per_cone) +{ + clear_cone_residual_grid_kernel<<>>( + dual_residual, start_idx, v_dim, count, blocks_per_cone); +} + +static void launch_block_projected_mapping_only_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)cr; + (void)obj; + (void)dp; + (void)vr; + (void)ps; + (void)ws; + (void)pa; + (void)isf; + launch_projected_mapping_only_dual_impl(dr, si, vd, n, 1); +} + +static void launch_grid_projected_mapping_only_dual(double *dr, + double *cr, + const double *obj, + const double *dp, + const double *vr, + const double *ps, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)cr; + (void)obj; + (void)dp; + (void)vr; + (void)ps; + (void)ws; + (void)pa; + (void)isf; + launch_projected_mapping_only_dual_impl(dr, si, vd, n, PDHCG_LARGE_CONE_BLOCKS_PER_CONE); +} + +typedef void (*cone_proj_diag_q_launcher_t)(double *pdhg_primal, + double *reflected_primal, + const double *current_primal, + const double *var_rescale, + const double *Q_diag, + double tau, + double *warm_start, + const int *start_idx, + const int *v_dim, + const double *power_alpha, + const char *is_fixed, + int count); + +static void launch_rotated_thread_proj_diag_q(double *pp, + double *rp, + const double *cp, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + project_rotated_soc_diag_q_kernel<<>>(pp, rp, cp, vr, qd, tau, ws, si, vd, isf, n); +} +static void launch_rotated_block_proj_diag_q(double *pp, + double *rp, + const double *cp, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + project_rotated_soc_block_kernel<<>>(pp, vr, qd, tau, ws, si, vd, isf, n); + recompute_reflected_at_cone_block_kernel<<>>(rp, pp, cp, si, vd, n); +} +static void launch_rotated_grid_weighted_proj_diag_q(double *pp, + double *rp, + const double *cp, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + launch_rotated_grid_weighted_impl(pp, vr, qd, tau, ws, si, vd, isf, n); + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + recompute_reflected_at_cone_grid_kernel<<>>( + rp, pp, cp, si, vd, n, blocks_per_cone); +} +static void launch_standard_thread_proj_diag_q(double *pp, + double *rp, + const double *cp, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + project_standard_soc_diag_q_kernel<<>>(pp, rp, cp, vr, qd, tau, ws, si, vd, isf, n); +} +static void launch_standard_block_proj_diag_q(double *pp, + double *rp, + const double *cp, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + project_standard_soc_block_kernel<<>>(pp, vr, qd, tau, ws, si, vd, isf, n); + recompute_reflected_at_cone_block_kernel<<>>(rp, pp, cp, si, vd, n); +} +static void launch_standard_grid_weighted_proj_diag_q(double *pp, + double *rp, + const double *cp, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + launch_standard_grid_weighted_impl(pp, vr, qd, tau, ws, si, vd, isf, n); + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + recompute_reflected_at_cone_grid_kernel<<>>( + rp, pp, cp, si, vd, n, blocks_per_cone); +} +static void launch_exp_thread_proj_diag_q(double *pp, + double *rp, + const double *cp, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + (void)pa; + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + project_exp_cone_diag_q_kernel<<>>(pp, rp, cp, vr, qd, tau, ws, si, vd, isf, n); +} +static void launch_power_thread_proj_diag_q(double *pp, + double *rp, + const double *cp, + const double *vr, + const double *qd, + double tau, + double *ws, + const int *si, + const int *vd, + const double *pa, + const char *isf, + int n) +{ + int t = THREADS_PER_BLOCK; + int b = (n + t - 1) / t; + project_power_cone_diag_q_kernel<<>>(pp, rp, cp, vr, qd, tau, ws, si, vd, pa, isf, n); +} + +static const cone_proj_diag_q_launcher_t proj_diag_q_launch_table[NUM_CONE_TYPES][NUM_PROJ_METHODS] = { + [CONE_ROTATED_SOC] = + { + [PROJ_METHOD_THREAD] = launch_rotated_thread_proj_diag_q, + [PROJ_METHOD_WARP] = NULL, + [PROJ_METHOD_BLOCK] = launch_rotated_block_proj_diag_q, + [PROJ_METHOD_GRID] = NULL, + [PROJ_METHOD_GRID_WEIGHTED] = launch_rotated_grid_weighted_proj_diag_q, + }, + [CONE_STANDARD_SOC] = + { + [PROJ_METHOD_THREAD] = launch_standard_thread_proj_diag_q, + [PROJ_METHOD_WARP] = NULL, + [PROJ_METHOD_BLOCK] = launch_standard_block_proj_diag_q, + [PROJ_METHOD_GRID] = NULL, + [PROJ_METHOD_GRID_WEIGHTED] = launch_standard_grid_weighted_proj_diag_q, + }, + [CONE_EXPONENTIAL] = + { + [PROJ_METHOD_THREAD] = launch_exp_thread_proj_diag_q, + [PROJ_METHOD_WARP] = NULL, + [PROJ_METHOD_BLOCK] = NULL, + [PROJ_METHOD_GRID] = NULL, + [PROJ_METHOD_GRID_WEIGHTED] = NULL, + }, + [CONE_POWER] = + { + [PROJ_METHOD_THREAD] = launch_power_thread_proj_diag_q, + [PROJ_METHOD_WARP] = NULL, + [PROJ_METHOD_BLOCK] = NULL, + [PROJ_METHOD_GRID] = NULL, + [PROJ_METHOD_GRID_WEIGHTED] = NULL, + }, +}; + +static const cone_dual_res_launcher_t dual_res_launch_table[NUM_CONE_TYPES][NUM_PROJ_METHODS] = { + [CONE_ROTATED_SOC] = + { + [PROJ_METHOD_THREAD] = launch_rotated_thread_dual, + [PROJ_METHOD_WARP] = launch_rotated_warp_dual, + [PROJ_METHOD_BLOCK] = launch_block_projected_mapping_only_dual, + [PROJ_METHOD_GRID] = launch_rotated_grid_dual, + [PROJ_METHOD_GRID_WEIGHTED] = launch_grid_projected_mapping_only_dual, + }, + [CONE_STANDARD_SOC] = + { + [PROJ_METHOD_THREAD] = launch_standard_thread_dual, + [PROJ_METHOD_WARP] = launch_standard_warp_dual, + [PROJ_METHOD_BLOCK] = launch_block_projected_mapping_only_dual, + [PROJ_METHOD_GRID] = launch_standard_grid_dual, + [PROJ_METHOD_GRID_WEIGHTED] = launch_grid_projected_mapping_only_dual, + }, + [CONE_EXPONENTIAL] = + { + [PROJ_METHOD_THREAD] = launch_exp_thread_dual, + [PROJ_METHOD_WARP] = NULL, + [PROJ_METHOD_BLOCK] = NULL, + [PROJ_METHOD_GRID] = NULL, + [PROJ_METHOD_GRID_WEIGHTED] = NULL, + }, + [CONE_POWER] = + { + [PROJ_METHOD_THREAD] = launch_power_thread_dual, + [PROJ_METHOD_WARP] = NULL, + [PROJ_METHOD_BLOCK] = NULL, + [PROJ_METHOD_GRID] = NULL, + [PROJ_METHOD_GRID_WEIGHTED] = NULL, + }, +}; + +void project_cone_runtime(pdhg_solver_state_t *state, cone_runtime_t *runtime, double *vector, double *warm_start) +{ + const double *coordinate_rescaling = + runtime->axis == CONE_AXIS_VARIABLE ? state->variable_rescaling : runtime->coordinate_rescaling; + for (int b = 0; b < runtime->num_buckets; ++b) + { + const cone_bucket_t *bk = &runtime->buckets[b]; + const double *pa = runtime->power_alpha ? runtime->power_alpha + bk->offset : NULL; + proj_launch_table[bk->type][bk->method](vector, + coordinate_rescaling, + warm_start + PDHCG_CONE_WORKSPACE_STRIDE * bk->offset, + runtime->start_idx + bk->offset, + runtime->v_dim + bk->offset, + pa, + runtime->is_fixed, + bk->count); + } + project_split_cones(state, runtime, vector); +} + +void project_cone_runtime_diag_q(pdhg_solver_state_t *state, cone_runtime_t *runtime, double primal_step_size) +{ + const double *Q_diag = state->quadratic_objective_term->diagonal_objective_matrix; + double *pdhg_primal = state->pdhg_primal_solution; + double *reflected_primal = state->reflected_primal_solution; + const double *current_primal = state->current_primal_solution; + + for (int b = 0; b < runtime->num_buckets; ++b) + { + const cone_bucket_t *bk = &runtime->buckets[b]; + const double *pa = runtime->power_alpha ? runtime->power_alpha + bk->offset : NULL; + cone_proj_method_t method = PROJ_METHOD_THREAD; + if (bk->type == CONE_STANDARD_SOC && bk->method != PROJ_METHOD_THREAD) + method = bk->method == PROJ_METHOD_GRID || bk->method == PROJ_METHOD_GRID_WEIGHTED + ? PROJ_METHOD_GRID_WEIGHTED + : PROJ_METHOD_BLOCK; + else if (bk->type == CONE_ROTATED_SOC && bk->method != PROJ_METHOD_THREAD) + method = bk->method == PROJ_METHOD_GRID || bk->method == PROJ_METHOD_GRID_WEIGHTED + ? PROJ_METHOD_GRID_WEIGHTED + : PROJ_METHOD_BLOCK; + proj_diag_q_launch_table[bk->type][method](pdhg_primal, + reflected_primal, + current_primal, + state->variable_rescaling, + Q_diag, + primal_step_size, + runtime->projection_warm_start + + PDHCG_CONE_WORKSPACE_STRIDE * bk->offset, + runtime->start_idx + bk->offset, + runtime->v_dim + bk->offset, + pa, + runtime->is_fixed, + bk->count); + } + project_split_cones(state, runtime, pdhg_primal); + recompute_split_cone_reflected(state, reflected_primal, pdhg_primal, current_primal); +} + +void compute_cone_dual_residual(pdhg_solver_state_t *state, const double *effective_obj) +{ + if (state->cones.num_blocks > 0) + { + CUDA_CHECK(cudaMemsetAsync( + state->cones.complementarity_residual, 0, (size_t)state->cones.num_blocks * sizeof(double))); + } + for (int b = 0; b < state->cones.num_buckets; ++b) + { + const cone_bucket_t *bk = &state->cones.buckets[b]; + const double *pa = state->cones.power_alpha ? state->cones.power_alpha + bk->offset : NULL; + dual_res_launch_table[bk->type][bk->method](state->dual_residual, + state->cones.complementarity_residual + bk->offset, + effective_obj, + state->dual_product, + state->variable_rescaling, + state->pdhg_primal_solution, + state->cones.residual_warm_start + + PDHCG_CONE_WORKSPACE_STRIDE * bk->offset, + state->cones.start_idx + bk->offset, + state->cones.v_dim + bk->offset, + pa, + state->cones.is_fixed, + bk->count); + } + compute_split_cone_dual_residual(state, effective_obj); +} + +void recompute_cone_reflection(pdhg_solver_state_t *state) +{ + int threads = THREADS_PER_BLOCK; + for (int b = 0; b < state->cones.num_buckets; ++b) + { + const cone_bucket_t *bk = &state->cones.buckets[b]; + if (bk->method == PROJ_METHOD_GRID || bk->method == PROJ_METHOD_GRID_WEIGHTED) + { + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + recompute_reflected_at_cone_grid_kernel<<count * blocks_per_cone, threads>>>( + state->reflected_primal_solution, + state->pdhg_primal_solution, + state->current_primal_solution, + state->cones.start_idx + bk->offset, + state->cones.v_dim + bk->offset, + bk->count, + blocks_per_cone); + } + else if (bk->method == PROJ_METHOD_BLOCK) + { + recompute_reflected_at_cone_block_kernel<<count, threads>>>(state->reflected_primal_solution, + state->pdhg_primal_solution, + state->current_primal_solution, + state->cones.start_idx + bk->offset, + state->cones.v_dim + bk->offset, + bk->count); + } + else if (bk->method == PROJ_METHOD_WARP) + { + int blocks = (bk->count * 32 + threads - 1) / threads; + recompute_reflected_at_cone_warp_kernel<<>>(state->reflected_primal_solution, + state->pdhg_primal_solution, + state->current_primal_solution, + state->cones.start_idx + bk->offset, + state->cones.v_dim + bk->offset, + bk->count); + } + else + { + int blocks = (bk->count + threads - 1) / threads; + recompute_reflected_at_cone_kernel<<>>(state->reflected_primal_solution, + state->pdhg_primal_solution, + state->current_primal_solution, + state->cones.start_idx + bk->offset, + state->cones.v_dim + bk->offset, + bk->count); + } + } + recompute_split_cone_reflected( + state, state->reflected_primal_solution, state->pdhg_primal_solution, state->current_primal_solution); +} + +void set_cone_dual_slack(pdhg_solver_state_t *state, const double *effective_obj) +{ + int threads = THREADS_PER_BLOCK; + for (int b = 0; b < state->cones.num_buckets; ++b) + { + const cone_bucket_t *bk = &state->cones.buckets[b]; + if (bk->method == PROJ_METHOD_GRID || bk->method == PROJ_METHOD_GRID_WEIGHTED) + { + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + set_cone_dual_slack_grid_kernel<<count * blocks_per_cone, threads>>>(state->dual_slack, + effective_obj, + state->dual_product, + state->cones.start_idx + + bk->offset, + state->cones.v_dim + bk->offset, + bk->count, + blocks_per_cone); + } + else if (bk->method == PROJ_METHOD_BLOCK) + { + set_cone_dual_slack_grid_kernel<<count, threads>>>(state->dual_slack, + effective_obj, + state->dual_product, + state->cones.start_idx + bk->offset, + state->cones.v_dim + bk->offset, + bk->count, + 1); + } + else if (bk->method == PROJ_METHOD_WARP) + { + int blocks = (bk->count * 32 + threads - 1) / threads; + set_cone_dual_slack_warp_kernel<<>>(state->dual_slack, + effective_obj, + state->dual_product, + state->cones.start_idx + bk->offset, + state->cones.v_dim + bk->offset, + bk->count); + } + else + { + int blocks = (bk->count + threads - 1) / threads; + set_cone_dual_slack_kernel<<>>(state->dual_slack, + effective_obj, + state->dual_product, + state->cones.start_idx + bk->offset, + state->cones.v_dim + bk->offset, + bk->count); + } + } + set_split_cone_dual_slack(state, state->dual_slack, effective_obj, state->dual_product); +} diff --git a/src/cone_utils.c b/src/cone_utils.c new file mode 100644 index 0000000..8212414 --- /dev/null +++ b/src/cone_utils.c @@ -0,0 +1,194 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "cone_utils.h" +#include "utils.h" + +#include +#include +#include +#include +#include + +int cone_length(cone_type_t type, int v_dim) +{ + if (type == CONE_EXPONENTIAL || type == CONE_POWER) + return 3; + if (type == CONE_STANDARD_SOC || type == CONE_ROTATED_SOC) + return v_dim >= 0 && v_dim <= INT_MAX - 2 ? v_dim + 2 : -1; + return -1; +} + +int cone_block_length(const cone_blocks_t *blocks, int block) +{ + if (!blocks || block < 0 || block >= blocks->num_cones || !blocks->type || !blocks->v_dim) + return -1; + return cone_length(blocks->type[block], blocks->v_dim[block]); +} + +int cone_blocks_init_from_specs(cone_blocks_t *blocks, + int num_cones, + const cone_spec_t *specs, + int ambient_dimension, + bool allow_fixed, + const char *label) +{ + if (!blocks) + return -1; + if (num_cones < 0 || ambient_dimension < 0 || (num_cones > 0 && !specs)) + { + fprintf(stderr, + "[create_qp_problem] Invalid %s cone metadata " + "(num_cones=%d, ambient_dimension=%d, specs=%p).\n", + label ? label : "", + num_cones, + ambient_dimension, + (const void *)specs); + return -1; + } + + cone_blocks_free(blocks); + if (num_cones == 0) + return 0; + + const char *kind = label ? label : ""; + char *owner = ambient_dimension > 0 ? (char *)safe_calloc((size_t)ambient_dimension, sizeof(char)) : NULL; + int any_fixed = 0; + int any_power = 0; + + for (int cone = 0; cone < num_cones; ++cone) + { + int length = cone_length(specs[cone].type, specs[cone].v_dim); + int start = specs[cone].start_idx; + if (length <= 0 || start < 0 || (long long)start + length > ambient_dimension) + { + fprintf(stderr, + "[create_qp_problem] %s cone %d has invalid type, size, or range " + "(start=%d, length=%d, ambient_dimension=%d).\n", + kind, + cone, + start, + length, + ambient_dimension); + free(owner); + return -1; + } + if (!allow_fixed && specs[cone].is_fixed) + { + fprintf(stderr, "[create_qp_problem] %s cone %d does not support fixed slots.\n", kind, cone); + free(owner); + return -1; + } + if (specs[cone].type == CONE_POWER && + !(isfinite(specs[cone].power_alpha) && specs[cone].power_alpha > 0.0 && specs[cone].power_alpha < 1.0)) + { + fprintf(stderr, + "[create_qp_problem] %s power cone %d requires alpha in (0,1); got %.6g.\n", + kind, + cone, + specs[cone].power_alpha); + free(owner); + return -1; + } + for (int index = start; index < start + length; ++index) + { + if (owner[index]) + { + fprintf(stderr, "[create_qp_problem] %s cone %d overlaps at index %d.\n", kind, cone, index); + free(owner); + return -1; + } + owner[index] = 1; + } + any_fixed |= specs[cone].is_fixed != NULL; + any_power |= specs[cone].type == CONE_POWER; + } + free(owner); + + size_t count = (size_t)num_cones; + blocks->num_cones = num_cones; + blocks->start_idx = (int *)safe_malloc(count * sizeof(int)); + blocks->v_dim = (int *)safe_malloc(count * sizeof(int)); + blocks->type = (cone_type_t *)safe_malloc(count * sizeof(cone_type_t)); + blocks->power_alpha = any_power ? (double *)safe_calloc(count, sizeof(double)) : NULL; + if (any_fixed) + { + blocks->fixed_mask_size = ambient_dimension; + blocks->is_fixed = (char *)safe_calloc((size_t)ambient_dimension, sizeof(char)); + } + + for (int cone = 0; cone < num_cones; ++cone) + { + int length = cone_length(specs[cone].type, specs[cone].v_dim); + blocks->start_idx[cone] = specs[cone].start_idx; + blocks->v_dim[cone] = + (specs[cone].type == CONE_EXPONENTIAL || specs[cone].type == CONE_POWER) ? 1 : specs[cone].v_dim; + blocks->type[cone] = specs[cone].type; + if (blocks->power_alpha && specs[cone].type == CONE_POWER) + blocks->power_alpha[cone] = specs[cone].power_alpha; + if (blocks->is_fixed && specs[cone].is_fixed) + { + int start = specs[cone].start_idx; + for (int slot = 0; slot < length; ++slot) + blocks->is_fixed[start + slot] = specs[cone].is_fixed[slot] ? 1 : 0; + } + } + return 0; +} + +void cone_blocks_clone(cone_blocks_t *dst, const cone_blocks_t *src) +{ + if (!dst || !src) + return; + if (dst == src) + return; + cone_blocks_free(dst); + dst->num_cones = src->num_cones; + if (src->num_cones <= 0) + return; + + size_t count = (size_t)src->num_cones; + dst->start_idx = (int *)safe_malloc(count * sizeof(int)); + dst->v_dim = (int *)safe_malloc(count * sizeof(int)); + dst->type = (cone_type_t *)safe_malloc(count * sizeof(cone_type_t)); + memcpy(dst->start_idx, src->start_idx, count * sizeof(int)); + memcpy(dst->v_dim, src->v_dim, count * sizeof(int)); + memcpy(dst->type, src->type, count * sizeof(cone_type_t)); + + if (src->power_alpha) + { + dst->power_alpha = (double *)safe_malloc(count * sizeof(double)); + memcpy(dst->power_alpha, src->power_alpha, count * sizeof(double)); + } + if (src->is_fixed && src->fixed_mask_size > 0) + { + dst->fixed_mask_size = src->fixed_mask_size; + dst->is_fixed = (char *)safe_malloc((size_t)src->fixed_mask_size * sizeof(char)); + memcpy(dst->is_fixed, src->is_fixed, (size_t)src->fixed_mask_size * sizeof(char)); + } +} + +void cone_blocks_free(cone_blocks_t *blocks) +{ + if (!blocks) + return; + free(blocks->start_idx); + free(blocks->v_dim); + free(blocks->type); + free(blocks->power_alpha); + free(blocks->is_fixed); + memset(blocks, 0, sizeof(*blocks)); +} diff --git a/src/kernels/pdhcg_conic_block_kernels.cu b/src/kernels/pdhcg_conic_block_kernels.cu new file mode 100644 index 0000000..13ec9c8 --- /dev/null +++ b/src/kernels/pdhcg_conic_block_kernels.cu @@ -0,0 +1,2053 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "cone_section_projection.cuh" +#include "pdhcg_kernels.cuh" + +#include +#include +#include + +__device__ static inline void cone_block_sum3(double *first, double *second, double *third, double scratch[96]) +{ + int lane = threadIdx.x & 31; + int warp = threadIdx.x >> 5; + unsigned mask = __activemask(); + double a = *first; + double b = *second; + double c = *third; + for (int offset = 16; offset > 0; offset >>= 1) + { + a += __shfl_down_sync(mask, a, offset); + b += __shfl_down_sync(mask, b, offset); + c += __shfl_down_sync(mask, c, offset); + } + if (lane == 0) + { + scratch[3 * warp + 0] = a; + scratch[3 * warp + 1] = b; + scratch[3 * warp + 2] = c; + } + __syncthreads(); + + int num_warps = (blockDim.x + 31) >> 5; + if (warp == 0) + { + a = lane < num_warps ? scratch[3 * lane + 0] : 0.0; + b = lane < num_warps ? scratch[3 * lane + 1] : 0.0; + c = lane < num_warps ? scratch[3 * lane + 2] : 0.0; + for (int offset = 16; offset > 0; offset >>= 1) + { + a += __shfl_down_sync(0xffffffffu, a, offset); + b += __shfl_down_sync(0xffffffffu, b, offset); + c += __shfl_down_sync(0xffffffffu, c, offset); + } + if (lane == 0) + { + scratch[0] = a; + scratch[1] = b; + scratch[2] = c; + } + } + __syncthreads(); + *first = scratch[0]; + *second = scratch[1]; + *third = scratch[2]; + __syncthreads(); +} + +__device__ static inline double cone_block_max(double value, double scratch[96]) +{ + int lane = threadIdx.x & 31; + int warp = threadIdx.x >> 5; + unsigned mask = __activemask(); + for (int offset = 16; offset > 0; offset >>= 1) + value = fmax(value, __shfl_down_sync(mask, value, offset)); + if (lane == 0) + scratch[warp] = value; + __syncthreads(); + + int num_warps = (blockDim.x + 31) >> 5; + if (warp == 0) + { + value = lane < num_warps ? scratch[lane] : 0.0; + for (int offset = 16; offset > 0; offset >>= 1) + value = fmax(value, __shfl_down_sync(0xffffffffu, value, offset)); + if (lane == 0) + scratch[0] = value; + } + __syncthreads(); + value = scratch[0]; + __syncthreads(); + return value; +} + +__device__ static inline void cone_atomic_max_positive(double *address, double value) +{ + atomicMax(reinterpret_cast(address), + static_cast(__double_as_longlong(value))); +} + +enum standard_soc_block_mode +{ + SOC_BLOCK_IDENTITY = 0, + SOC_BLOCK_ZERO_FREE = 1, + SOC_BLOCK_APEX = 2, + SOC_BLOCK_SCALAR_Z = 3, + SOC_BLOCK_FIXED_Z_ROOT = 4, + SOC_BLOCK_FREE_Z_ROOT = 5, + SOC_BLOCK_ZERO_Z_ROOT = 6 +}; + +__global__ void project_standard_soc_block_kernel(double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + + __shared__ double scratch[96]; + __shared__ double fixed_norm2; + __shared__ double radius2; + __shared__ double lambda; + __shared__ double lo; + __shared__ double hi; + __shared__ double z_input; + __shared__ double omega_z; + __shared__ int mode; + __shared__ int lower_branch; + __shared__ int done; + + int start = start_idx[cone]; + int k = v_dim[cone]; + int u_length = k + 1; + int z_index = start + u_length; + bool fixed_z = is_fixed && is_fixed[z_index]; + + double local_fixed_norm2 = 0.0; + double local_free_norm2 = 0.0; + double local_polar_norm2 = 0.0; + double local_max_omega = 0.0; + for (int slot = threadIdx.x; slot < u_length; slot += blockDim.x) + { + int index = start + slot; + double value = point[index] / rescaling[index]; + if (is_fixed && is_fixed[index]) + local_fixed_norm2 += value * value; + else + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + local_free_norm2 += value * value; + local_polar_norm2 += (omega * value) * (omega * value); + local_max_omega = fmax(local_max_omega, omega); + } + } + cone_block_sum3(&local_fixed_norm2, &local_free_norm2, &local_polar_norm2, scratch); + local_max_omega = cone_block_max(local_max_omega, scratch); + + if (threadIdx.x == 0) + { + fixed_norm2 = local_fixed_norm2; + z_input = point[z_index] / rescaling[z_index]; + omega_z = cone_section_weight(rescaling, q_diag, tau, z_index); + int free_count = 0; + for (int slot = 0; slot < u_length; ++slot) + free_count += !(is_fixed && is_fixed[start + slot]); + + if (fixed_z) + { + radius2 = fmax(0.0, z_input * z_input - fixed_norm2); + if (free_count == 0 || local_free_norm2 <= radius2) + mode = SOC_BLOCK_IDENTITY; + else if (!(radius2 > 0.0)) + mode = SOC_BLOCK_ZERO_FREE; + else + { + mode = SOC_BLOCK_FIXED_Z_ROOT; + hi = sqrt(local_polar_norm2) / sqrt(radius2) * (1.0 + 64.0 * DBL_EPSILON); + } + } + else if (z_input >= 0.0 && fixed_norm2 + local_free_norm2 <= z_input * z_input) + { + mode = SOC_BLOCK_IDENTITY; + } + else if (free_count == 0) + { + mode = SOC_BLOCK_SCALAR_Z; + } + else if (fixed_norm2 == 0.0 && -omega_z * z_input >= sqrt(local_polar_norm2)) + { + mode = SOC_BLOCK_APEX; + } + else if (z_input == 0.0) + { + mode = SOC_BLOCK_ZERO_Z_ROOT; + lambda = omega_z; + } + else + { + mode = SOC_BLOCK_FREE_Z_ROOT; + lower_branch = z_input > 0.0; + lo = lower_branch ? 0.0 : omega_z * (1.0 + 1e-14); + hi = lower_branch ? omega_z * (1.0 - 1e-14) + : cone_section_negative_soc_upper( + omega_z, -omega_z * z_input, fixed_norm2, local_polar_norm2, local_max_omega); + } + } + __syncthreads(); + + if (mode == SOC_BLOCK_IDENTITY) + return; + if (mode == SOC_BLOCK_ZERO_FREE || mode == SOC_BLOCK_APEX) + { + for (int slot = threadIdx.x; slot < u_length; slot += blockDim.x) + if (!(is_fixed && is_fixed[start + slot])) + point[start + slot] = 0.0; + if (mode == SOC_BLOCK_APEX && threadIdx.x == 0) + point[z_index] = 0.0; + return; + } + if (mode == SOC_BLOCK_SCALAR_Z) + { + if (threadIdx.x == 0) + point[z_index] = fmax(z_input, sqrt(fixed_norm2)) * rescaling[z_index]; + return; + } + + if (mode == SOC_BLOCK_FIXED_Z_ROOT) + { + if (threadIdx.x == 0) + { + lo = 0.0; + done = hi > 0.0 && isfinite(hi); + if (!done) + hi = warm_start && warm_start[cone] > 0.0 && isfinite(warm_start[cone]) ? warm_start[cone] : 1.0; + } + __syncthreads(); + for (int expansion = 0; expansion < 80; ++expansion) + { + if (done) + break; + double norm2 = 0.0; + double unused = 0.0; + double unused2 = 0.0; + for (int slot = threadIdx.x; slot < u_length; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + hi); + norm2 += value * value; + } + cone_block_sum3(&norm2, &unused, &unused2, scratch); + if (threadIdx.x == 0) + { + done = norm2 <= radius2; + if (!done) + hi *= 2.0; + } + __syncthreads(); + if (done) + break; + } + __syncthreads(); + + if (threadIdx.x == 0) + { + double warm = warm_start ? warm_start[cone] : 0.0; + lambda = warm > lo && warm < hi && isfinite(warm) ? warm : 0.5 * (lo + hi); + done = 0; + } + __syncthreads(); + for (int iteration = 0; iteration < 30; ++iteration) + { + double norm2 = 0.0; + double derivative = 0.0; + double unused = 0.0; + for (int slot = threadIdx.x; slot < u_length; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + lambda); + norm2 += value * value; + derivative -= 2.0 * value * value / (omega + lambda); + } + cone_block_sum3(&norm2, &derivative, &unused, scratch); + if (threadIdx.x == 0) + { + double f = norm2 - radius2; + if (f > 0.0) + lo = lambda; + else + hi = lambda; + double next = lambda - f / derivative; + if (!isfinite(next) || !(next > lo && next < hi)) + next = 0.5 * (lo + hi); + done = fabs(f) <= 1e-13 * (1.0 + radius2) || hi - lo <= 1e-13 * (1.0 + hi + lo); + if (!done) + lambda = next; + } + __syncthreads(); + if (done) + break; + } + } + else if (mode == SOC_BLOCK_ZERO_Z_ROOT) + { + double norm2 = 0.0; + double unused = 0.0; + double unused2 = 0.0; + for (int slot = threadIdx.x; slot < u_length; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + lambda); + norm2 += value * value; + } + cone_block_sum3(&norm2, &unused, &unused2, scratch); + if (threadIdx.x == 0) + point[z_index] = sqrt(fixed_norm2 + norm2) * rescaling[z_index]; + } + else + { + if (!lower_branch) + { + if (threadIdx.x == 0) + { + done = hi > lo && isfinite(hi); + if (!done) + hi = 2.0 * omega_z; + } + __syncthreads(); + for (int expansion = 0; expansion < 80; ++expansion) + { + if (done) + break; + double norm2 = 0.0; + double unused = 0.0; + double unused2 = 0.0; + for (int slot = threadIdx.x; slot < u_length; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + hi); + norm2 += value * value; + } + cone_block_sum3(&norm2, &unused, &unused2, scratch); + if (threadIdx.x == 0) + { + double z = omega_z * z_input / (omega_z - hi); + done = fixed_norm2 + norm2 >= z * z; + if (!done) + hi *= 2.0; + } + __syncthreads(); + if (done) + break; + } + } + __syncthreads(); + if (threadIdx.x == 0) + { + double warm = warm_start ? warm_start[cone] : 0.0; + lambda = warm > lo && warm < hi && isfinite(warm) ? warm : 0.5 * (lo + hi); + done = 0; + } + __syncthreads(); + for (int iteration = 0; iteration < 35; ++iteration) + { + double norm2 = 0.0; + double derivative = 0.0; + double unused = 0.0; + for (int slot = threadIdx.x; slot < u_length; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + lambda); + norm2 += value * value; + derivative -= 2.0 * value * value / (omega + lambda); + } + cone_block_sum3(&norm2, &derivative, &unused, scratch); + if (threadIdx.x == 0) + { + double z = omega_z * z_input / (omega_z - lambda); + double f = fixed_norm2 + norm2 - z * z; + derivative -= 2.0 * z * z / (omega_z - lambda); + if ((lower_branch && f > 0.0) || (!lower_branch && f < 0.0)) + lo = lambda; + else + hi = lambda; + double next = lambda - f / derivative; + if (!isfinite(next) || !(next > lo && next < hi)) + next = 0.5 * (lo + hi); + done = fabs(f) <= 1e-13 * (1.0 + fixed_norm2 + norm2 + z * z) || hi - lo <= 1e-13 * (1.0 + hi + lo); + if (!done) + lambda = next; + } + __syncthreads(); + if (done) + break; + } + if (threadIdx.x == 0) + point[z_index] *= omega_z / (omega_z - lambda); + } + + if (warm_start && threadIdx.x == 0) + warm_start[cone] = lambda; + for (int slot = threadIdx.x; slot < u_length; slot += blockDim.x) + { + int index = start + slot; + if (!(is_fixed && is_fixed[index])) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + point[index] *= omega / (omega + lambda); + } + } +} + +enum standard_soc_grid_weighted_mode +{ + SOC_GRID_IDENTITY = 0, + SOC_GRID_ZERO_FREE = 1, + SOC_GRID_APEX = 2, + SOC_GRID_SCALAR_Z = 3, + SOC_GRID_FIXED_EXPAND = 4, + SOC_GRID_FIXED_ROOT = 5, + SOC_GRID_FREE_EXPAND = 6, + SOC_GRID_FREE_ROOT = 7, + SOC_GRID_ZERO_Z_EVAL = 8, + SOC_GRID_FIXED_APPLY = 9, + SOC_GRID_FREE_APPLY = 10, + SOC_GRID_ZERO_Z_APPLY = 11 +}; + +__global__ void initialize_standard_soc_grid_weighted_kernel(const double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int u_length = v_dim[cone] + 1; + double fixed_norm2 = 0.0; + double free_norm2 = 0.0; + double polar_norm2 = 0.0; + double free_count = 0.0; + double max_omega = 0.0; + for (int slot = part * blockDim.x + threadIdx.x; slot < u_length; slot += blocks_per_cone * blockDim.x) + { + int index = start + slot; + double value = point[index] / rescaling[index]; + if (is_fixed && is_fixed[index]) + fixed_norm2 += value * value; + else + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + free_norm2 += value * value; + polar_norm2 += (omega * value) * (omega * value); + free_count += 1.0; + max_omega = fmax(max_omega, omega); + } + } + __shared__ double scratch[96]; + cone_block_sum3(&fixed_norm2, &free_norm2, &polar_norm2, scratch); + double unused = 0.0; + double unused2 = 0.0; + cone_block_sum3(&free_count, &unused, &unused2, scratch); + max_omega = cone_block_max(max_omega, scratch); + if (threadIdx.x == 0) + { + atomicAdd(workspace + num_cones + cone, fixed_norm2); + atomicAdd(workspace + 2 * num_cones + cone, free_norm2); + atomicAdd(workspace + 3 * num_cones + cone, polar_norm2); + atomicAdd(workspace + 4 * num_cones + cone, free_count); + cone_atomic_max_positive(workspace + 5 * num_cones + cone, max_omega); + } +} + +__global__ void finalize_standard_soc_grid_weighted_initialization_kernel(const double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone >= num_cones) + return; + int start = start_idx[cone]; + int z_index = start + v_dim[cone] + 1; + double warm = workspace[cone]; + double fixed_norm2 = workspace[num_cones + cone]; + double free_norm2 = workspace[2 * num_cones + cone]; + double polar_norm2 = workspace[3 * num_cones + cone]; + int free_count = (int)workspace[4 * num_cones + cone]; + double max_omega = workspace[5 * num_cones + cone]; + double z = point[z_index] / rescaling[z_index]; + double omega_z_value = cone_section_weight(rescaling, q_diag, tau, z_index); + bool fixed_z = is_fixed && is_fixed[z_index]; + int selected_mode; + double constant = fixed_norm2; + double lower = 0.0; + double upper = 0.0; + double trial = warm; + + if (fixed_z) + { + constant = fmax(0.0, z * z - fixed_norm2); + if (free_count == 0 || free_norm2 <= constant) + selected_mode = SOC_GRID_IDENTITY; + else if (!(constant > 0.0)) + selected_mode = SOC_GRID_ZERO_FREE; + else + { + lower = 0.0; + upper = sqrt(polar_norm2) / sqrt(constant) * (1.0 + 64.0 * DBL_EPSILON); + if (upper > 0.0 && isfinite(upper)) + { + selected_mode = SOC_GRID_FIXED_ROOT; + trial = warm > lower && warm < upper && isfinite(warm) ? warm : 0.5 * upper; + } + else + { + selected_mode = SOC_GRID_FIXED_EXPAND; + trial = warm > 0.0 && isfinite(warm) ? warm : 1.0; + upper = trial; + } + } + } + else if (z >= 0.0 && fixed_norm2 + free_norm2 <= z * z) + { + selected_mode = SOC_GRID_IDENTITY; + } + else if (free_count == 0) + { + selected_mode = SOC_GRID_SCALAR_Z; + } + else if (fixed_norm2 == 0.0 && -omega_z_value * z >= sqrt(polar_norm2)) + { + selected_mode = SOC_GRID_APEX; + } + else if (z == 0.0) + { + selected_mode = SOC_GRID_ZERO_Z_EVAL; + trial = omega_z_value; + } + else if (z > 0.0) + { + selected_mode = SOC_GRID_FREE_ROOT; + lower = 0.0; + upper = omega_z_value * (1.0 - 1e-14); + trial = warm > lower && warm < upper && isfinite(warm) ? warm : 0.5 * (lower + upper); + } + else + { + lower = omega_z_value * (1.0 + 1e-14); + double endpoint_polar = -omega_z_value * z; + upper = cone_section_negative_soc_upper(omega_z_value, endpoint_polar, fixed_norm2, polar_norm2, max_omega); + if (upper > lower && isfinite(upper)) + { + selected_mode = SOC_GRID_FREE_ROOT; + trial = warm > lower && warm < upper && isfinite(warm) ? warm : 0.5 * (lower + upper); + } + else + { + selected_mode = SOC_GRID_FREE_EXPAND; + trial = warm > lower && isfinite(warm) ? warm : 2.0 * omega_z_value; + upper = trial; + } + } + + workspace[cone] = trial; + workspace[4 * num_cones + cone] = (double)selected_mode; + workspace[5 * num_cones + cone] = constant; + workspace[6 * num_cones + cone] = lower; + workspace[7 * num_cones + cone] = upper; +} + +__global__ void reduce_standard_soc_grid_weighted_root_kernel(const double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + int selected_mode = (int)workspace[4 * num_cones + cone]; + if (selected_mode < SOC_GRID_FIXED_EXPAND || selected_mode > SOC_GRID_ZERO_Z_EVAL) + return; + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int u_length = v_dim[cone] + 1; + double lambda_value = workspace[cone]; + double norm2 = 0.0; + double derivative = 0.0; + for (int slot = part * blockDim.x + threadIdx.x; slot < u_length; slot += blocks_per_cone * blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + lambda_value); + norm2 += value * value; + derivative -= 2.0 * value * value / (omega + lambda_value); + } + __shared__ double scratch[96]; + double unused = 0.0; + cone_block_sum3(&norm2, &derivative, &unused, scratch); + if (threadIdx.x == 0) + { + atomicAdd(workspace + num_cones + cone, norm2); + atomicAdd(workspace + 2 * num_cones + cone, derivative); + } +} + +__global__ void finalize_standard_soc_grid_weighted_root_kernel(double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone >= num_cones) + return; + int selected_mode = (int)workspace[4 * num_cones + cone]; + if (selected_mode < SOC_GRID_FIXED_EXPAND || selected_mode > SOC_GRID_ZERO_Z_EVAL) + return; + int start = start_idx[cone]; + int z_index = start + v_dim[cone] + 1; + double lambda_value = workspace[cone]; + double sum = workspace[num_cones + cone]; + double derivative = workspace[2 * num_cones + cone]; + double constant = workspace[5 * num_cones + cone]; + double lower = workspace[6 * num_cones + cone]; + double upper = workspace[7 * num_cones + cone]; + + if (selected_mode == SOC_GRID_ZERO_Z_EVAL) + { + point[z_index] = sqrt(constant + sum) * rescaling[z_index]; + workspace[4 * num_cones + cone] = (double)SOC_GRID_ZERO_Z_APPLY; + return; + } + + double f; + if (selected_mode == SOC_GRID_FIXED_EXPAND || selected_mode == SOC_GRID_FIXED_ROOT) + { + f = sum - constant; + if (selected_mode == SOC_GRID_FIXED_EXPAND) + { + if (f > 0.0) + { + lower = lambda_value; + lambda_value *= 2.0; + } + else + { + upper = lambda_value; + selected_mode = SOC_GRID_FIXED_ROOT; + lambda_value = 0.5 * (lower + upper); + } + } + else + { + if (f > 0.0) + lower = lambda_value; + else + upper = lambda_value; + bool converged = fabs(f) <= 1e-13 * (1.0 + constant) || upper - lower <= 1e-13 * (1.0 + upper + lower); + if (converged) + selected_mode = SOC_GRID_FIXED_APPLY; + else + { + double next = lambda_value - f / derivative; + lambda_value = isfinite(next) && next > lower && next < upper ? next : 0.5 * (lower + upper); + } + } + } + else + { + double z_input = point[z_index] / rescaling[z_index]; + double omega_z_value = cone_section_weight(rescaling, q_diag, tau, z_index); + double z = omega_z_value * z_input / (omega_z_value - lambda_value); + f = constant + sum - z * z; + derivative -= 2.0 * z * z / (omega_z_value - lambda_value); + if (selected_mode == SOC_GRID_FREE_EXPAND) + { + if (f < 0.0) + { + lower = lambda_value; + lambda_value *= 2.0; + } + else + { + upper = lambda_value; + selected_mode = SOC_GRID_FREE_ROOT; + lambda_value = 0.5 * (lower + upper); + } + } + else + { + bool lower_branch_value = z_input > 0.0; + if ((lower_branch_value && f > 0.0) || (!lower_branch_value && f < 0.0)) + lower = lambda_value; + else + upper = lambda_value; + bool converged = + fabs(f) <= 1e-13 * (1.0 + constant + sum + z * z) || upper - lower <= 1e-13 * (1.0 + upper + lower); + if (converged) + selected_mode = SOC_GRID_FREE_APPLY; + else + { + double next = lambda_value - f / derivative; + lambda_value = isfinite(next) && next > lower && next < upper ? next : 0.5 * (lower + upper); + } + } + } + workspace[cone] = lambda_value; + workspace[4 * num_cones + cone] = (double)selected_mode; + workspace[6 * num_cones + cone] = lower; + workspace[7 * num_cones + cone] = upper; +} + +__global__ void apply_standard_soc_grid_weighted_kernel(double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + int selected_mode = (int)workspace[4 * num_cones + cone]; + if (selected_mode == SOC_GRID_IDENTITY || selected_mode == SOC_GRID_SCALAR_Z) + { + if (selected_mode == SOC_GRID_SCALAR_Z && blockIdx.x % blocks_per_cone == 0 && threadIdx.x == 0) + { + int z_index = start_idx[cone] + v_dim[cone] + 1; + double z = point[z_index] / rescaling[z_index]; + point[z_index] = fmax(z, sqrt(workspace[5 * num_cones + cone])) * rescaling[z_index]; + } + return; + } + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int u_length = v_dim[cone] + 1; + if (selected_mode == SOC_GRID_ZERO_FREE || selected_mode == SOC_GRID_APEX) + { + for (int slot = part * blockDim.x + threadIdx.x; slot < u_length; slot += blocks_per_cone * blockDim.x) + if (!(is_fixed && is_fixed[start + slot])) + point[start + slot] = 0.0; + if (selected_mode == SOC_GRID_APEX && part == 0 && threadIdx.x == 0) + point[start + u_length] = 0.0; + return; + } + + double lambda_value = workspace[cone]; + for (int slot = part * blockDim.x + threadIdx.x; slot < u_length; slot += blocks_per_cone * blockDim.x) + { + int index = start + slot; + if (!(is_fixed && is_fixed[index])) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + point[index] *= omega / (omega + lambda_value); + } + } + bool free_z_mode = selected_mode == SOC_GRID_FREE_EXPAND || selected_mode == SOC_GRID_FREE_ROOT || + selected_mode == SOC_GRID_FREE_APPLY; + if (free_z_mode && part == 0 && threadIdx.x == 0) + { + int z_index = start + u_length; + double omega_z_value = cone_section_weight(rescaling, q_diag, tau, z_index); + point[z_index] *= omega_z_value / (omega_z_value - lambda_value); + } +} + +enum rotated_soc_block_mode +{ + RSOC_BLOCK_IDENTITY = 0, + RSOC_BLOCK_ZERO_FREE = 1, + RSOC_BLOCK_FIXED_ENDPOINTS_ROOT = 2, + RSOC_BLOCK_ONE_ENDPOINT_ZERO = 3, + RSOC_BLOCK_ONE_ENDPOINT_SCALAR = 4, + RSOC_BLOCK_ONE_ENDPOINT_ROOT = 5, + RSOC_BLOCK_APEX = 6, + RSOC_BLOCK_BALANCED = 7, + RSOC_BLOCK_FREE_ROOT = 8, + RSOC_BLOCK_AXIS = 9 +}; + +__global__ void project_rotated_soc_block_kernel(double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + + __shared__ double scratch[96]; + __shared__ double fixed_norm2; + __shared__ double radius2; + __shared__ double lambda; + __shared__ double lo; + __shared__ double hi; + __shared__ double s_input; + __shared__ double t_input; + __shared__ double omega_s; + __shared__ double omega_t; + __shared__ double projected_s; + __shared__ double projected_t; + __shared__ double free_objective; + __shared__ int mode; + __shared__ int lower_branch; + __shared__ int done; + + int start = start_idx[cone]; + int k = v_dim[cone]; + int s_index = start + k; + int t_index = s_index + 1; + bool fixed_s = is_fixed && is_fixed[s_index]; + bool fixed_t = is_fixed && is_fixed[t_index]; + + double local_fixed_norm2 = 0.0; + double local_free_norm2 = 0.0; + double local_polar_norm2 = 0.0; + double local_free_objective = 0.0; + double local_max_omega = 0.0; + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + { + int index = start + slot; + double value = point[index] / rescaling[index]; + if (is_fixed && is_fixed[index]) + local_fixed_norm2 += value * value; + else + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + local_free_norm2 += value * value; + local_polar_norm2 += (omega * value) * (omega * value); + local_free_objective += omega * value * value; + local_max_omega = fmax(local_max_omega, omega); + } + } + cone_block_sum3(&local_fixed_norm2, &local_free_norm2, &local_polar_norm2, scratch); + double unused = 0.0; + double unused2 = 0.0; + cone_block_sum3(&local_free_objective, &unused, &unused2, scratch); + local_max_omega = cone_block_max(local_max_omega, scratch); + + if (threadIdx.x == 0) + { + fixed_norm2 = local_fixed_norm2; + free_objective = local_free_objective; + s_input = point[s_index] / rescaling[s_index]; + t_input = point[t_index] / rescaling[t_index]; + omega_s = cone_section_weight(rescaling, q_diag, tau, s_index); + omega_t = cone_section_weight(rescaling, q_diag, tau, t_index); + int free_count = 0; + for (int slot = 0; slot < k; ++slot) + free_count += !(is_fixed && is_fixed[start + slot]); + + if (fixed_s && fixed_t) + { + radius2 = fmax(0.0, 2.0 * s_input * t_input - fixed_norm2); + if (free_count == 0 || local_free_norm2 <= radius2) + mode = RSOC_BLOCK_IDENTITY; + else if (!(radius2 > 0.0)) + mode = RSOC_BLOCK_ZERO_FREE; + else + { + mode = RSOC_BLOCK_FIXED_ENDPOINTS_ROOT; + hi = sqrt(local_polar_norm2) / sqrt(radius2) * (1.0 + 64.0 * DBL_EPSILON); + } + } + else if (fixed_s || fixed_t) + { + double fixed_endpoint = fixed_s ? s_input : t_input; + double free_endpoint = fixed_s ? t_input : s_input; + if (!(fixed_endpoint > 0.0)) + mode = RSOC_BLOCK_ONE_ENDPOINT_ZERO; + else if (free_endpoint >= 0.0 && fixed_norm2 + local_free_norm2 <= 2.0 * fixed_endpoint * free_endpoint) + mode = RSOC_BLOCK_IDENTITY; + else if (free_count == 0) + mode = RSOC_BLOCK_ONE_ENDPOINT_SCALAR; + else + { + mode = RSOC_BLOCK_ONE_ENDPOINT_ROOT; + double metric = fixed_s ? omega_t : omega_s; + double violation = fixed_norm2 + local_free_norm2 - 2.0 * fixed_endpoint * free_endpoint; + hi = metric * violation / (2.0 * fixed_endpoint * fixed_endpoint); + hi *= 1.0 + 64.0 * DBL_EPSILON; + } + } + else if (s_input >= 0.0 && t_input >= 0.0 && fixed_norm2 + local_free_norm2 <= 2.0 * s_input * t_input) + { + mode = RSOC_BLOCK_IDENTITY; + } + else + { + double bs = omega_s * s_input; + double bt = omega_t * t_input; + if (fixed_norm2 == 0.0 && bs <= 0.0 && bt <= 0.0 && local_polar_norm2 <= 2.0 * bs * bt) + { + mode = RSOC_BLOCK_APEX; + } + else + { + double root_metric = sqrt(omega_s) * sqrt(omega_t); + double balance = sqrt(omega_s) * s_input + sqrt(omega_t) * t_input; + double balance_scale = 1.0 + fabs(sqrt(omega_s) * s_input) + fabs(sqrt(omega_t) * t_input); + lambda = root_metric; + if (fabs(balance) <= 64.0 * DBL_EPSILON * balance_scale) + mode = RSOC_BLOCK_BALANCED; + else + { + mode = RSOC_BLOCK_FREE_ROOT; + lower_branch = balance > 0.0; + lo = lower_branch ? 0.0 : root_metric * (1.0 + 1e-14); + if (lower_branch) + { + hi = root_metric * (1.0 - 1e-14); + } + else + { + hi = cone_section_negative_rsoc_upper( + omega_s, omega_t, s_input, t_input, fixed_norm2, local_polar_norm2, local_max_omega); + } + } + } + } + } + __syncthreads(); + + if (mode == RSOC_BLOCK_IDENTITY) + return; + if (mode == RSOC_BLOCK_ZERO_FREE || mode == RSOC_BLOCK_APEX) + { + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + if (!(is_fixed && is_fixed[start + slot])) + point[start + slot] = 0.0; + if (mode == RSOC_BLOCK_APEX && threadIdx.x == 0) + { + point[s_index] = 0.0; + point[t_index] = 0.0; + } + return; + } + if (mode == RSOC_BLOCK_ONE_ENDPOINT_ZERO) + { + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + if (!(is_fixed && is_fixed[start + slot])) + point[start + slot] = 0.0; + if (threadIdx.x == 0) + { + if (fixed_s) + point[t_index] = fmax(t_input, 0.0) * rescaling[t_index]; + else + point[s_index] = fmax(s_input, 0.0) * rescaling[s_index]; + } + return; + } + if (mode == RSOC_BLOCK_ONE_ENDPOINT_SCALAR) + { + if (threadIdx.x == 0) + { + double fixed_endpoint = fixed_s ? s_input : t_input; + double free_endpoint = fixed_s ? t_input : s_input; + double projected = fmax(free_endpoint, fixed_norm2 / (2.0 * fixed_endpoint)); + if (fixed_s) + point[t_index] = projected * rescaling[t_index]; + else + point[s_index] = projected * rescaling[s_index]; + } + return; + } + + if (mode == RSOC_BLOCK_FIXED_ENDPOINTS_ROOT || mode == RSOC_BLOCK_ONE_ENDPOINT_ROOT) + { + if (threadIdx.x == 0) + { + lo = 0.0; + double metric = mode == RSOC_BLOCK_ONE_ENDPOINT_ROOT ? (fixed_s ? omega_t : omega_s) : 1.0; + done = hi > 0.0 && isfinite(hi); + if (!done) + hi = warm_start && warm_start[cone] > 0.0 && isfinite(warm_start[cone]) ? warm_start[cone] : metric; + } + __syncthreads(); + for (int expansion = 0; expansion < 80; ++expansion) + { + if (done) + break; + double norm2 = 0.0; + double dummy = 0.0; + double dummy2 = 0.0; + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + hi); + norm2 += value * value; + } + cone_block_sum3(&norm2, &dummy, &dummy2, scratch); + if (threadIdx.x == 0) + { + if (mode == RSOC_BLOCK_FIXED_ENDPOINTS_ROOT) + done = norm2 <= radius2; + else + { + double fixed_endpoint = fixed_s ? s_input : t_input; + double free_endpoint = fixed_s ? t_input : s_input; + double omega_endpoint = fixed_s ? omega_t : omega_s; + double endpoint = free_endpoint + hi * fixed_endpoint / omega_endpoint; + done = fixed_norm2 + norm2 <= 2.0 * fixed_endpoint * endpoint; + } + if (!done) + hi *= 2.0; + } + __syncthreads(); + if (done) + break; + } + __syncthreads(); + + if (threadIdx.x == 0) + { + double warm = warm_start ? warm_start[cone] : 0.0; + lambda = warm > lo && warm < hi && isfinite(warm) ? warm : 0.5 * (lo + hi); + done = 0; + } + __syncthreads(); + for (int iteration = 0; iteration < 30; ++iteration) + { + double norm2 = 0.0; + double derivative = 0.0; + double dummy = 0.0; + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + lambda); + norm2 += value * value; + derivative -= 2.0 * value * value / (omega + lambda); + } + cone_block_sum3(&norm2, &derivative, &dummy, scratch); + if (threadIdx.x == 0) + { + double target; + double f; + if (mode == RSOC_BLOCK_FIXED_ENDPOINTS_ROOT) + { + target = radius2; + f = norm2 - target; + } + else + { + double fixed_endpoint = fixed_s ? s_input : t_input; + double free_endpoint = fixed_s ? t_input : s_input; + double omega_endpoint = fixed_s ? omega_t : omega_s; + double endpoint = free_endpoint + lambda * fixed_endpoint / omega_endpoint; + target = 2.0 * fixed_endpoint * endpoint; + f = fixed_norm2 + norm2 - target; + derivative -= 2.0 * fixed_endpoint * fixed_endpoint / omega_endpoint; + } + if (f > 0.0) + lo = lambda; + else + hi = lambda; + double next = lambda - f / derivative; + if (!isfinite(next) || !(next > lo && next < hi)) + next = 0.5 * (lo + hi); + done = fabs(f) <= 1e-13 * (1.0 + target) || hi - lo <= 1e-13 * (1.0 + hi + lo); + if (!done) + lambda = next; + } + __syncthreads(); + if (done) + break; + } + + if (threadIdx.x == 0 && mode == RSOC_BLOCK_ONE_ENDPOINT_ROOT) + { + double fixed_endpoint = fixed_s ? s_input : t_input; + double free_endpoint = fixed_s ? t_input : s_input; + double omega_endpoint = fixed_s ? omega_t : omega_s; + double projected = free_endpoint + lambda * fixed_endpoint / omega_endpoint; + if (fixed_s) + point[t_index] = projected * rescaling[t_index]; + else + point[s_index] = projected * rescaling[s_index]; + } + } + else if (mode == RSOC_BLOCK_BALANCED) + { + double norm2 = 0.0; + double dummy = 0.0; + double dummy2 = 0.0; + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + lambda); + norm2 += value * value; + } + cone_block_sum3(&norm2, &dummy, &dummy2, scratch); + if (threadIdx.x == 0) + { + double root_metric = sqrt(omega_s) * sqrt(omega_t); + double product = 0.5 * root_metric * (fixed_norm2 + norm2); + double delta = sqrt(omega_s) * s_input; + double scaled_t = 0.5 * (-delta + sqrt(fmax(0.0, delta * delta + 4.0 * product))); + double scaled_s = scaled_t + delta; + projected_s = scaled_s / sqrt(omega_s); + projected_t = scaled_t / sqrt(omega_t); + } + __syncthreads(); + } + else + { + if (!lower_branch) + { + if (threadIdx.x == 0) + { + done = hi > lo && isfinite(hi); + if (!done) + hi = 2.0 * sqrt(omega_s) * sqrt(omega_t); + } + __syncthreads(); + for (int expansion = 0; expansion < 80; ++expansion) + { + if (done) + break; + double norm2 = 0.0; + double dummy = 0.0; + double dummy2 = 0.0; + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + hi); + norm2 += value * value; + } + cone_block_sum3(&norm2, &dummy, &dummy2, scratch); + if (threadIdx.x == 0) + { + double determinant = omega_s * omega_t - hi * hi; + double s = omega_t * (omega_s * s_input + hi * t_input) / determinant; + double t = omega_s * (omega_t * t_input + hi * s_input) / determinant; + double f = (s >= 0.0 && t >= 0.0) ? fixed_norm2 + norm2 - 2.0 * s * t : INFINITY; + done = f >= 0.0; + if (!done) + hi *= 2.0; + } + __syncthreads(); + if (done) + break; + } + } + __syncthreads(); + + if (threadIdx.x == 0) + { + double warm = warm_start ? warm_start[cone] : 0.0; + lambda = warm > lo && warm < hi && isfinite(warm) ? warm : 0.5 * (lo + hi); + done = 0; + } + __syncthreads(); + for (int iteration = 0; iteration < 40; ++iteration) + { + double norm2 = 0.0; + double derivative = 0.0; + double dummy = 0.0; + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + lambda); + norm2 += value * value; + derivative -= 2.0 * value * value / (omega + lambda); + } + cone_block_sum3(&norm2, &derivative, &dummy, scratch); + if (threadIdx.x == 0) + { + double determinant = omega_s * omega_t - lambda * lambda; + double s = omega_t * (omega_s * s_input + lambda * t_input) / determinant; + double t = omega_s * (omega_t * t_input + lambda * s_input) / determinant; + double f = INFINITY; + if (s >= 0.0 && t >= 0.0) + { + f = fixed_norm2 + norm2 - 2.0 * s * t; + double ds = (omega_t * t + lambda * s) / determinant; + double dt = (lambda * t + omega_s * s) / determinant; + derivative -= 2.0 * (ds * t + s * dt); + } + if ((lower_branch && f > 0.0) || (!lower_branch && f < 0.0)) + lo = lambda; + else + hi = lambda; + double next = lambda - f / derivative; + if (!isfinite(next) || !(next > lo && next < hi)) + next = 0.5 * (lo + hi); + done = isfinite(f) && + (fabs(f) <= 1e-13 * (1.0 + fixed_norm2 + norm2 + 2.0 * s * t) || + hi - lo <= 1e-13 * (1.0 + hi + lo)); + if (!done) + lambda = next; + } + __syncthreads(); + if (done) + break; + } + if (threadIdx.x == 0) + { + double determinant = omega_s * omega_t - lambda * lambda; + projected_s = omega_t * (omega_s * s_input + lambda * t_input) / determinant; + projected_t = omega_s * (omega_t * t_input + lambda * s_input) / determinant; + } + __syncthreads(); + } + + if (mode == RSOC_BLOCK_BALANCED || mode == RSOC_BLOCK_FREE_ROOT) + { + double smooth_vector_objective = 0.0; + double dummy = 0.0; + double dummy2 = 0.0; + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double input = point[index] / rescaling[index]; + double value = input * omega / (omega + lambda); + double delta = value - input; + smooth_vector_objective += omega * delta * delta; + } + cone_block_sum3(&smooth_vector_objective, &dummy, &dummy2, scratch); + if (threadIdx.x == 0 && fixed_norm2 == 0.0) + { + double smooth_objective = smooth_vector_objective + + omega_s * (projected_s - s_input) * (projected_s - s_input) + + omega_t * (projected_t - t_input) * (projected_t - t_input); + double s_axis = fmax(s_input, 0.0); + double s_axis_objective = + free_objective + omega_s * (s_axis - s_input) * (s_axis - s_input) + omega_t * t_input * t_input; + double t_axis = fmax(t_input, 0.0); + double t_axis_objective = + free_objective + omega_s * s_input * s_input + omega_t * (t_axis - t_input) * (t_axis - t_input); + if (s_axis_objective < smooth_objective && s_axis_objective <= t_axis_objective) + { + projected_s = s_axis; + projected_t = 0.0; + mode = RSOC_BLOCK_AXIS; + } + else if (t_axis_objective < smooth_objective) + { + projected_s = 0.0; + projected_t = t_axis; + mode = RSOC_BLOCK_AXIS; + } + } + __syncthreads(); + } + + if (mode == RSOC_BLOCK_AXIS) + { + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + if (!(is_fixed && is_fixed[start + slot])) + point[start + slot] = 0.0; + } + else + { + for (int slot = threadIdx.x; slot < k; slot += blockDim.x) + { + int index = start + slot; + if (!(is_fixed && is_fixed[index])) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + point[index] *= omega / (omega + lambda); + } + } + if (warm_start && threadIdx.x == 0) + warm_start[cone] = lambda; + } + if (threadIdx.x == 0 && (mode == RSOC_BLOCK_BALANCED || mode == RSOC_BLOCK_FREE_ROOT || mode == RSOC_BLOCK_AXIS)) + { + point[s_index] = projected_s * rescaling[s_index]; + point[t_index] = projected_t * rescaling[t_index]; + } +} + +enum rotated_soc_grid_weighted_mode +{ + RSOC_GRID_IDENTITY = 0, + RSOC_GRID_ZERO_FREE = 1, + RSOC_GRID_ONE_ENDPOINT_ZERO = 2, + RSOC_GRID_ONE_ENDPOINT_SCALAR = 3, + RSOC_GRID_APEX = 4, + RSOC_GRID_FIXED_EXPAND = 5, + RSOC_GRID_FIXED_ROOT = 6, + RSOC_GRID_FIXED_APPLY = 7, + RSOC_GRID_ONE_EXPAND = 8, + RSOC_GRID_ONE_ROOT = 9, + RSOC_GRID_ONE_APPLY = 10, + RSOC_GRID_BALANCED_EVAL = 11, + RSOC_GRID_FREE_EXPAND = 12, + RSOC_GRID_FREE_ROOT = 13, + RSOC_GRID_FREE_APPLY = 14, + RSOC_GRID_BALANCED_APPLY = 15, + RSOC_GRID_AXIS = 16 +}; + +__device__ static inline void rotated_soc_grid_free_endpoints(const double *point, + const double *rescaling, + const double *q_diag, + double tau, + int s_index, + int t_index, + double lambda, + double *projected_s, + double *projected_t) +{ + double s = point[s_index] / rescaling[s_index]; + double t = point[t_index] / rescaling[t_index]; + double omega_s = cone_section_weight(rescaling, q_diag, tau, s_index); + double omega_t = cone_section_weight(rescaling, q_diag, tau, t_index); + double determinant = omega_s * omega_t - lambda * lambda; + *projected_s = omega_t * (omega_s * s + lambda * t) / determinant; + *projected_t = omega_s * (omega_t * t + lambda * s) / determinant; +} + +__global__ void initialize_rotated_soc_grid_weighted_kernel(const double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int k = v_dim[cone]; + double fixed_norm2 = 0.0; + double free_norm2 = 0.0; + double polar_norm2 = 0.0; + double free_count = 0.0; + double max_omega = 0.0; + for (int slot = part * blockDim.x + threadIdx.x; slot < k; slot += blocks_per_cone * blockDim.x) + { + int index = start + slot; + double value = point[index] / rescaling[index]; + if (is_fixed && is_fixed[index]) + fixed_norm2 += value * value; + else + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + free_norm2 += value * value; + polar_norm2 += (omega * value) * (omega * value); + free_count += 1.0; + max_omega = fmax(max_omega, omega); + } + } + __shared__ double scratch[96]; + cone_block_sum3(&fixed_norm2, &free_norm2, &polar_norm2, scratch); + double unused = 0.0; + double unused2 = 0.0; + cone_block_sum3(&free_count, &unused, &unused2, scratch); + max_omega = cone_block_max(max_omega, scratch); + if (threadIdx.x == 0) + { + atomicAdd(workspace + num_cones + cone, fixed_norm2); + atomicAdd(workspace + 2 * num_cones + cone, free_norm2); + atomicAdd(workspace + 3 * num_cones + cone, polar_norm2); + atomicAdd(workspace + 4 * num_cones + cone, free_count); + cone_atomic_max_positive(workspace + 5 * num_cones + cone, max_omega); + } +} + +__global__ void finalize_rotated_soc_grid_weighted_initialization_kernel(const double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone >= num_cones) + return; + int start = start_idx[cone]; + int k = v_dim[cone]; + int s_index = start + k; + int t_index = s_index + 1; + bool fixed_s = is_fixed && is_fixed[s_index]; + bool fixed_t = is_fixed && is_fixed[t_index]; + double warm = workspace[cone]; + double fixed_norm2 = workspace[num_cones + cone]; + double free_norm2 = workspace[2 * num_cones + cone]; + double polar_norm2 = workspace[3 * num_cones + cone]; + int free_count = (int)workspace[4 * num_cones + cone]; + double max_omega = workspace[5 * num_cones + cone]; + double s = point[s_index] / rescaling[s_index]; + double t = point[t_index] / rescaling[t_index]; + double omega_s_value = cone_section_weight(rescaling, q_diag, tau, s_index); + double omega_t_value = cone_section_weight(rescaling, q_diag, tau, t_index); + int selected_mode; + double constant = fixed_norm2; + double lower = 0.0; + double upper = 0.0; + double trial = warm; + + if (fixed_s && fixed_t) + { + constant = fmax(0.0, 2.0 * s * t - fixed_norm2); + if (free_count == 0 || free_norm2 <= constant) + selected_mode = RSOC_GRID_IDENTITY; + else if (!(constant > 0.0)) + selected_mode = RSOC_GRID_ZERO_FREE; + else + { + upper = sqrt(polar_norm2) / sqrt(constant) * (1.0 + 64.0 * DBL_EPSILON); + if (upper > 0.0 && isfinite(upper)) + { + selected_mode = RSOC_GRID_FIXED_ROOT; + trial = warm > 0.0 && warm < upper && isfinite(warm) ? warm : 0.5 * upper; + } + else + { + selected_mode = RSOC_GRID_FIXED_EXPAND; + trial = warm > 0.0 && isfinite(warm) ? warm : 1.0; + upper = trial; + } + } + } + else if (fixed_s || fixed_t) + { + double fixed_endpoint = fixed_s ? s : t; + double free_endpoint = fixed_s ? t : s; + if (!(fixed_endpoint > 0.0)) + selected_mode = RSOC_GRID_ONE_ENDPOINT_ZERO; + else if (free_endpoint >= 0.0 && fixed_norm2 + free_norm2 <= 2.0 * fixed_endpoint * free_endpoint) + selected_mode = RSOC_GRID_IDENTITY; + else if (free_count == 0) + selected_mode = RSOC_GRID_ONE_ENDPOINT_SCALAR; + else + { + double metric = fixed_s ? omega_t_value : omega_s_value; + double violation = fixed_norm2 + free_norm2 - 2.0 * fixed_endpoint * free_endpoint; + upper = metric * violation / (2.0 * fixed_endpoint * fixed_endpoint); + upper *= 1.0 + 64.0 * DBL_EPSILON; + if (upper > 0.0 && isfinite(upper)) + { + selected_mode = RSOC_GRID_ONE_ROOT; + trial = warm > 0.0 && warm < upper && isfinite(warm) ? warm : 0.5 * upper; + } + else + { + selected_mode = RSOC_GRID_ONE_EXPAND; + trial = warm > 0.0 && isfinite(warm) ? warm : metric; + upper = trial; + } + } + } + else if (s >= 0.0 && t >= 0.0 && fixed_norm2 + free_norm2 <= 2.0 * s * t) + { + selected_mode = RSOC_GRID_IDENTITY; + } + else + { + double bs = omega_s_value * s; + double bt = omega_t_value * t; + if (fixed_norm2 == 0.0 && bs <= 0.0 && bt <= 0.0 && polar_norm2 <= 2.0 * bs * bt) + { + selected_mode = RSOC_GRID_APEX; + } + else + { + double sqrt_omega_s = sqrt(omega_s_value); + double sqrt_omega_t = sqrt(omega_t_value); + double root_metric = sqrt_omega_s * sqrt_omega_t; + double scaled_s = sqrt_omega_s * s; + double scaled_t = sqrt_omega_t * t; + double balance = scaled_s + scaled_t; + double balance_scale = 1.0 + fabs(scaled_s) + fabs(scaled_t); + if (fabs(balance) <= 64.0 * DBL_EPSILON * balance_scale) + { + selected_mode = RSOC_GRID_BALANCED_EVAL; + trial = root_metric; + } + else if (balance > 0.0) + { + selected_mode = RSOC_GRID_FREE_ROOT; + lower = 0.0; + upper = root_metric * (1.0 - 1e-14); + trial = warm > lower && warm < upper && isfinite(warm) ? warm : 0.5 * (lower + upper); + } + else + { + lower = root_metric * (1.0 + 1e-14); + upper = cone_section_negative_rsoc_upper( + omega_s_value, omega_t_value, s, t, fixed_norm2, polar_norm2, max_omega); + if (upper > lower && isfinite(upper)) + { + selected_mode = RSOC_GRID_FREE_ROOT; + trial = warm > lower && warm < upper && isfinite(warm) ? warm : 0.5 * (lower + upper); + } + else + { + selected_mode = RSOC_GRID_FREE_EXPAND; + trial = warm > lower && isfinite(warm) ? warm : 2.0 * root_metric; + upper = trial; + } + } + } + } + + workspace[cone] = trial; + workspace[4 * num_cones + cone] = (double)selected_mode; + workspace[5 * num_cones + cone] = constant; + workspace[6 * num_cones + cone] = lower; + workspace[7 * num_cones + cone] = upper; +} + +__global__ void reduce_rotated_soc_grid_weighted_root_kernel(const double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + int selected_mode = (int)workspace[4 * num_cones + cone]; + bool active = selected_mode == RSOC_GRID_FIXED_EXPAND || selected_mode == RSOC_GRID_FIXED_ROOT || + selected_mode == RSOC_GRID_ONE_EXPAND || selected_mode == RSOC_GRID_ONE_ROOT || + selected_mode == RSOC_GRID_BALANCED_EVAL || selected_mode == RSOC_GRID_FREE_EXPAND || + selected_mode == RSOC_GRID_FREE_ROOT; + if (!active) + return; + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int k = v_dim[cone]; + double lambda_value = workspace[cone]; + double norm2 = 0.0; + double derivative = 0.0; + for (int slot = part * blockDim.x + threadIdx.x; slot < k; slot += blocks_per_cone * blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double value = (point[index] / rescaling[index]) * omega / (omega + lambda_value); + norm2 += value * value; + derivative -= 2.0 * value * value / (omega + lambda_value); + } + __shared__ double scratch[96]; + double unused = 0.0; + cone_block_sum3(&norm2, &derivative, &unused, scratch); + if (threadIdx.x == 0) + { + atomicAdd(workspace + num_cones + cone, norm2); + atomicAdd(workspace + 2 * num_cones + cone, derivative); + } +} + +__global__ void finalize_rotated_soc_grid_weighted_root_kernel(const double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone >= num_cones) + return; + int selected_mode = (int)workspace[4 * num_cones + cone]; + bool active = selected_mode == RSOC_GRID_FIXED_EXPAND || selected_mode == RSOC_GRID_FIXED_ROOT || + selected_mode == RSOC_GRID_ONE_EXPAND || selected_mode == RSOC_GRID_ONE_ROOT || + selected_mode == RSOC_GRID_BALANCED_EVAL || selected_mode == RSOC_GRID_FREE_EXPAND || + selected_mode == RSOC_GRID_FREE_ROOT; + if (!active) + return; + int start = start_idx[cone]; + int k = v_dim[cone]; + int s_index = start + k; + int t_index = s_index + 1; + bool fixed_s = is_fixed && is_fixed[s_index]; + double s_input_value = point[s_index] / rescaling[s_index]; + double t_input_value = point[t_index] / rescaling[t_index]; + double omega_s_value = cone_section_weight(rescaling, q_diag, tau, s_index); + double omega_t_value = cone_section_weight(rescaling, q_diag, tau, t_index); + double lambda_value = workspace[cone]; + double sum = workspace[num_cones + cone]; + double derivative = workspace[2 * num_cones + cone]; + double constant = workspace[5 * num_cones + cone]; + double lower = workspace[6 * num_cones + cone]; + double upper = workspace[7 * num_cones + cone]; + double f = 0.0; + + if (selected_mode == RSOC_GRID_FIXED_EXPAND || selected_mode == RSOC_GRID_FIXED_ROOT) + { + f = sum - constant; + if (selected_mode == RSOC_GRID_FIXED_EXPAND) + { + if (f > 0.0) + { + lower = lambda_value; + lambda_value *= 2.0; + } + else + { + upper = lambda_value; + selected_mode = RSOC_GRID_FIXED_ROOT; + lambda_value = 0.5 * (lower + upper); + } + } + else + { + if (f > 0.0) + lower = lambda_value; + else + upper = lambda_value; + bool converged = fabs(f) <= 1e-13 * (1.0 + constant) || upper - lower <= 1e-13 * (1.0 + upper + lower); + if (converged) + selected_mode = RSOC_GRID_FIXED_APPLY; + else + { + double next = lambda_value - f / derivative; + lambda_value = isfinite(next) && next > lower && next < upper ? next : 0.5 * (lower + upper); + } + } + } + else if (selected_mode == RSOC_GRID_ONE_EXPAND || selected_mode == RSOC_GRID_ONE_ROOT) + { + double fixed_endpoint = fixed_s ? s_input_value : t_input_value; + double free_endpoint = fixed_s ? t_input_value : s_input_value; + double omega_endpoint = fixed_s ? omega_t_value : omega_s_value; + double projected_endpoint = free_endpoint + lambda_value * fixed_endpoint / omega_endpoint; + f = constant + sum - 2.0 * fixed_endpoint * projected_endpoint; + derivative -= 2.0 * fixed_endpoint * fixed_endpoint / omega_endpoint; + if (selected_mode == RSOC_GRID_ONE_EXPAND) + { + if (f > 0.0) + { + lower = lambda_value; + lambda_value *= 2.0; + } + else + { + upper = lambda_value; + selected_mode = RSOC_GRID_ONE_ROOT; + lambda_value = 0.5 * (lower + upper); + } + } + else + { + if (f > 0.0) + lower = lambda_value; + else + upper = lambda_value; + bool converged = + fabs(f) <= 1e-13 * (1.0 + constant + sum) || upper - lower <= 1e-13 * (1.0 + upper + lower); + if (converged) + selected_mode = RSOC_GRID_ONE_APPLY; + else + { + double next = lambda_value - f / derivative; + lambda_value = isfinite(next) && next > lower && next < upper ? next : 0.5 * (lower + upper); + } + } + } + else if (selected_mode == RSOC_GRID_BALANCED_EVAL) + { + double root_metric = sqrt(omega_s_value) * sqrt(omega_t_value); + double product = 0.5 * root_metric * (constant + sum); + double delta = sqrt(omega_s_value) * s_input_value; + double scaled_t = 0.5 * (-delta + sqrt(fmax(0.0, delta * delta + 4.0 * product))); + workspace[6 * num_cones + cone] = (scaled_t + delta) / sqrt(omega_s_value); + workspace[7 * num_cones + cone] = scaled_t / sqrt(omega_t_value); + selected_mode = RSOC_GRID_BALANCED_APPLY; + } + else + { + double determinant = omega_s_value * omega_t_value - lambda_value * lambda_value; + double projected_s_value; + double projected_t_value; + rotated_soc_grid_free_endpoints( + point, rescaling, q_diag, tau, s_index, t_index, lambda_value, &projected_s_value, &projected_t_value); + f = projected_s_value >= 0.0 && projected_t_value >= 0.0 + ? constant + sum - 2.0 * projected_s_value * projected_t_value + : INFINITY; + if (isfinite(f)) + { + double ds = (omega_t_value * projected_t_value + lambda_value * projected_s_value) / determinant; + double dt = (lambda_value * projected_t_value + omega_s_value * projected_s_value) / determinant; + derivative -= 2.0 * (ds * projected_t_value + projected_s_value * dt); + } + if (selected_mode == RSOC_GRID_FREE_EXPAND) + { + if (f < 0.0) + { + lower = lambda_value; + lambda_value *= 2.0; + } + else + { + upper = lambda_value; + selected_mode = RSOC_GRID_FREE_ROOT; + lambda_value = 0.5 * (lower + upper); + } + } + else + { + bool lower_branch_value = sqrt(omega_s_value) * s_input_value + sqrt(omega_t_value) * t_input_value > 0.0; + if ((lower_branch_value && f > 0.0) || (!lower_branch_value && f < 0.0)) + lower = lambda_value; + else + upper = lambda_value; + bool converged = isfinite(f) && + (fabs(f) <= 1e-13 * (1.0 + constant + sum + 2.0 * projected_s_value * projected_t_value) || + upper - lower <= 1e-13 * (1.0 + upper + lower)); + if (converged) + selected_mode = RSOC_GRID_FREE_APPLY; + else + { + double next = lambda_value - f / derivative; + lambda_value = isfinite(next) && next > lower && next < upper ? next : 0.5 * (lower + upper); + } + } + } + + workspace[cone] = lambda_value; + workspace[4 * num_cones + cone] = (double)selected_mode; + if (selected_mode != RSOC_GRID_BALANCED_APPLY) + { + workspace[6 * num_cones + cone] = lower; + workspace[7 * num_cones + cone] = upper; + } +} + +__global__ void reduce_rotated_soc_grid_axis_objective_kernel(const double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones || workspace[5 * num_cones + cone] != 0.0) + return; + int selected_mode = (int)workspace[4 * num_cones + cone]; + if (selected_mode != RSOC_GRID_FREE_EXPAND && selected_mode != RSOC_GRID_FREE_ROOT && + selected_mode != RSOC_GRID_FREE_APPLY && selected_mode != RSOC_GRID_BALANCED_APPLY) + return; + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int k = v_dim[cone]; + double lambda_value = workspace[cone]; + double smooth_objective = 0.0; + double axis_objective = 0.0; + for (int slot = part * blockDim.x + threadIdx.x; slot < k; slot += blocks_per_cone * blockDim.x) + { + int index = start + slot; + if (is_fixed && is_fixed[index]) + continue; + double omega = cone_section_weight(rescaling, q_diag, tau, index); + double input = point[index] / rescaling[index]; + double projected = input * omega / (omega + lambda_value); + double delta = projected - input; + smooth_objective += omega * delta * delta; + axis_objective += omega * input * input; + } + __shared__ double scratch[96]; + double unused = 0.0; + cone_block_sum3(&smooth_objective, &axis_objective, &unused, scratch); + if (threadIdx.x == 0) + { + atomicAdd(workspace + num_cones + cone, smooth_objective); + atomicAdd(workspace + 2 * num_cones + cone, axis_objective); + } +} + +__global__ void finalize_rotated_soc_grid_axis_objective_kernel(const double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone >= num_cones || workspace[5 * num_cones + cone] != 0.0) + return; + int selected_mode = (int)workspace[4 * num_cones + cone]; + if (selected_mode != RSOC_GRID_FREE_EXPAND && selected_mode != RSOC_GRID_FREE_ROOT && + selected_mode != RSOC_GRID_FREE_APPLY && selected_mode != RSOC_GRID_BALANCED_APPLY) + return; + int start = start_idx[cone]; + int k = v_dim[cone]; + int s_index = start + k; + int t_index = s_index + 1; + double s_input_value = point[s_index] / rescaling[s_index]; + double t_input_value = point[t_index] / rescaling[t_index]; + double omega_s_value = cone_section_weight(rescaling, q_diag, tau, s_index); + double omega_t_value = cone_section_weight(rescaling, q_diag, tau, t_index); + double projected_s_value; + double projected_t_value; + if (selected_mode == RSOC_GRID_BALANCED_APPLY) + { + projected_s_value = workspace[6 * num_cones + cone]; + projected_t_value = workspace[7 * num_cones + cone]; + } + else + { + rotated_soc_grid_free_endpoints( + point, rescaling, q_diag, tau, s_index, t_index, workspace[cone], &projected_s_value, &projected_t_value); + } + double smooth_objective = workspace[num_cones + cone] + + omega_s_value * (projected_s_value - s_input_value) * (projected_s_value - s_input_value) + + omega_t_value * (projected_t_value - t_input_value) * (projected_t_value - t_input_value); + double vector_axis_objective = workspace[2 * num_cones + cone]; + double s_axis = fmax(s_input_value, 0.0); + double s_axis_objective = vector_axis_objective + + omega_s_value * (s_axis - s_input_value) * (s_axis - s_input_value) + + omega_t_value * t_input_value * t_input_value; + double t_axis = fmax(t_input_value, 0.0); + double t_axis_objective = vector_axis_objective + omega_s_value * s_input_value * s_input_value + + omega_t_value * (t_axis - t_input_value) * (t_axis - t_input_value); + if (s_axis_objective < smooth_objective && s_axis_objective <= t_axis_objective) + { + workspace[6 * num_cones + cone] = s_axis; + workspace[7 * num_cones + cone] = 0.0; + workspace[4 * num_cones + cone] = (double)RSOC_GRID_AXIS; + } + else if (t_axis_objective < smooth_objective) + { + workspace[6 * num_cones + cone] = 0.0; + workspace[7 * num_cones + cone] = t_axis; + workspace[4 * num_cones + cone] = (double)RSOC_GRID_AXIS; + } +} + +__global__ void apply_rotated_soc_grid_weighted_kernel(double *__restrict__ point, + const double *__restrict__ rescaling, + const double *__restrict__ q_diag, + double tau, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + int selected_mode = (int)workspace[4 * num_cones + cone]; + if (selected_mode == RSOC_GRID_IDENTITY) + return; + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int k = v_dim[cone]; + int s_index = start + k; + int t_index = s_index + 1; + bool fixed_s = is_fixed && is_fixed[s_index]; + bool fixed_t = is_fixed && is_fixed[t_index]; + + bool zero_vector = selected_mode == RSOC_GRID_ZERO_FREE || selected_mode == RSOC_GRID_ONE_ENDPOINT_ZERO || + selected_mode == RSOC_GRID_APEX || selected_mode == RSOC_GRID_AXIS; + if (zero_vector) + { + for (int slot = part * blockDim.x + threadIdx.x; slot < k; slot += blocks_per_cone * blockDim.x) + if (!(is_fixed && is_fixed[start + slot])) + point[start + slot] = 0.0; + } + else if (selected_mode != RSOC_GRID_ONE_ENDPOINT_SCALAR) + { + double lambda_value = workspace[cone]; + for (int slot = part * blockDim.x + threadIdx.x; slot < k; slot += blocks_per_cone * blockDim.x) + { + int index = start + slot; + if (!(is_fixed && is_fixed[index])) + { + double omega = cone_section_weight(rescaling, q_diag, tau, index); + point[index] *= omega / (omega + lambda_value); + } + } + } + + if (part == 0 && threadIdx.x == 0) + { + if (selected_mode == RSOC_GRID_ONE_ENDPOINT_ZERO) + { + if (fixed_s) + point[t_index] = fmax(point[t_index] / rescaling[t_index], 0.0) * rescaling[t_index]; + else + point[s_index] = fmax(point[s_index] / rescaling[s_index], 0.0) * rescaling[s_index]; + } + else if (selected_mode == RSOC_GRID_ONE_ENDPOINT_SCALAR) + { + double fixed_endpoint = fixed_s ? point[s_index] / rescaling[s_index] : point[t_index] / rescaling[t_index]; + int free_index = fixed_s ? t_index : s_index; + double input = point[free_index] / rescaling[free_index]; + point[free_index] = + fmax(input, workspace[5 * num_cones + cone] / (2.0 * fixed_endpoint)) * rescaling[free_index]; + } + else if (selected_mode == RSOC_GRID_APEX) + { + point[s_index] = 0.0; + point[t_index] = 0.0; + } + else if (selected_mode == RSOC_GRID_ONE_EXPAND || selected_mode == RSOC_GRID_ONE_ROOT || + selected_mode == RSOC_GRID_ONE_APPLY) + { + double lambda_value = workspace[cone]; + double fixed_endpoint = fixed_s ? point[s_index] / rescaling[s_index] : point[t_index] / rescaling[t_index]; + int free_index = fixed_s ? t_index : s_index; + double input = point[free_index] / rescaling[free_index]; + double omega = cone_section_weight(rescaling, q_diag, tau, free_index); + point[free_index] = (input + lambda_value * fixed_endpoint / omega) * rescaling[free_index]; + } + else if (selected_mode == RSOC_GRID_BALANCED_APPLY || selected_mode == RSOC_GRID_AXIS) + { + if (!fixed_s) + point[s_index] = workspace[6 * num_cones + cone] * rescaling[s_index]; + if (!fixed_t) + point[t_index] = workspace[7 * num_cones + cone] * rescaling[t_index]; + } + else if (selected_mode == RSOC_GRID_FREE_EXPAND || selected_mode == RSOC_GRID_FREE_ROOT || + selected_mode == RSOC_GRID_FREE_APPLY) + { + double projected_s; + double projected_t; + rotated_soc_grid_free_endpoints( + point, rescaling, q_diag, tau, s_index, t_index, workspace[cone], &projected_s, &projected_t); + point[s_index] = projected_s * rescaling[s_index]; + point[t_index] = projected_t * rescaling[t_index]; + } + } +} + +__global__ void recompute_reflected_at_cone_block_kernel(double *__restrict__ reflected_primal, + const double *__restrict__ pdhg_primal, + const double *__restrict__ current_primal, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + int start = start_idx[cone]; + int length = v_dim[cone] + 2; + for (int slot = threadIdx.x; slot < length; slot += blockDim.x) + { + int index = start + slot; + reflected_primal[index] = 2.0 * pdhg_primal[index] - current_primal[index]; + } +} + +__global__ void clear_cone_residual_grid_kernel(double *__restrict__ dual_residual, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int length = v_dim[cone] + 2; + for (int slot = part * blockDim.x + threadIdx.x; slot < length; slot += blocks_per_cone * blockDim.x) + dual_residual[start + slot] = 0.0; +} diff --git a/src/kernels/pdhcg_conic_kernels.cu b/src/kernels/pdhcg_conic_kernels.cu new file mode 100644 index 0000000..e31dea0 --- /dev/null +++ b/src/kernels/pdhcg_conic_kernels.cu @@ -0,0 +1,4627 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "cone_section_projection.cuh" +#include "pdhcg_kernels.cuh" +#include +#include +#include + +__global__ void project_rotated_soc_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + const double INV_SQRT2 = 0.7071067811865475; + + int start = start_idx[blk]; + int k = v_dim[blk]; + if (cone_section_has_fixed(is_fixed, start, k + 2)) + { + project_rotated_soc_section_serial( + primal_solution, variable_rescaling, NULL, 0.0, warm_start + blk, start, k, is_fixed); + return; + } + double *v = primal_solution + start; + double *sptr = primal_solution + start + k; + double *tptr = primal_solution + start + k + 1; + + double s = *sptr; + double t = *tptr; + + double w = (s - t) * INV_SQRT2; + double z = (s + t) * INV_SQRT2; + + double d_s = variable_rescaling[start + k]; + double d_t = variable_rescaling[start + k + 1]; + double d_st = sqrt(d_s * d_t); + + bool diag_uniform = true; + for (int m = 0; m < k && diag_uniform; ++m) + { + if (variable_rescaling[start + m] != d_st) + diag_uniform = false; + } + if (diag_uniform) + { + double sumsq = w * w; + for (int m = 0; m < k; ++m) + sumsq += v[m] * v[m]; + double r = sqrt(sumsq); + if (r <= z) + return; + if (r <= -z) + { + for (int m = 0; m < k; ++m) + v[m] = 0.0; + *sptr = 0.0; + *tptr = 0.0; + return; + } + double scale = (z + r) / (2.0 * r); + for (int m = 0; m < k; ++m) + v[m] *= scale; + double w_new = scale * w; + double z_new = scale * r; + *sptr = (z_new + w_new) * INV_SQRT2; + *tptr = (z_new - w_new) * INV_SQRT2; + return; + } + + double r_inv_sq = w * w; + double r_pos_sq = w * w; + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_st; + double v_m = v[m]; + r_inv_sq += (v_m / dh) * (v_m / dh); + r_pos_sq += (v_m * dh) * (v_m * dh); + } + double r_inv = sqrt(r_inv_sq); + if (r_inv <= z) + return; + double r_pos = sqrt(r_pos_sq); + if (r_pos <= -z) + { + for (int m = 0; m < k; ++m) + v[m] = 0.0; + *sptr = 0.0; + *tptr = 0.0; + return; + } + + double lo, hi; + bool z_pos = (z > 0.0); + if (z_pos) + { + lo = 0.0; + hi = 0.5 - 1e-14; + } + else + { + lo = 0.5 + 1e-14; + hi = 1.0; + for (int doubling = 0; doubling < 60; ++doubling) + { + double sum_hi = 0.0; + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + double tt = v[m] * dh / (dh2 + 2.0 * hi); + sum_hi += tt * tt; + } + double tw_hi = w / (1.0 + 2.0 * hi); + sum_hi += tw_hi * tw_hi; + double zt_hi = z / (1.0 - 2.0 * hi); + double f_hi = sum_hi - zt_hi * zt_hi; + if (f_hi > 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_lam = warm_start[blk]; + if (warm_lam > lo && warm_lam < hi) + { + double sum_w = 0.0; + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + double tt = v[m] * dh / (dh2 + 2.0 * warm_lam); + sum_w += tt * tt; + } + double tw = w / (1.0 + 2.0 * warm_lam); + sum_w += tw * tw; + double zt = z / (1.0 - 2.0 * warm_lam); + double f = sum_w - zt * zt; + if (fabs(f) < 1e-12) + { + double w_new = w / (1.0 + 2.0 * warm_lam); + double z_new = z / (1.0 - 2.0 * warm_lam); + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + v[m] = v[m] * dh2 / (dh2 + 2.0 * warm_lam); + } + *sptr = (z_new + w_new) * INV_SQRT2; + *tptr = (z_new - w_new) * INV_SQRT2; + return; + } + if (z_pos) + { + if (f > 0.0) + lo = warm_lam; + else + hi = warm_lam; + } + else + { + if (f > 0.0) + hi = warm_lam; + else + lo = warm_lam; + } + } + + for (int it = 0; it < 60; ++it) + { + double lam = 0.5 * (lo + hi); + double sum = 0.0; + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + double tt = v[m] * dh / (dh2 + 2.0 * lam); + sum += tt * tt; + } + double tw = w / (1.0 + 2.0 * lam); + sum += tw * tw; + double zt = z / (1.0 - 2.0 * lam); + double f = sum - zt * zt; + if (z_pos) + { + if (f > 0.0) + lo = lam; + else + hi = lam; + } + else + { + if (f > 0.0) + hi = lam; + else + lo = lam; + } + if ((hi - lo) / (1.0 + hi + lo) < 1e-13) + break; + } + double lam = 0.5 * (lo + hi); + warm_start[blk] = lam; + + double w_new = w / (1.0 + 2.0 * lam); + double z_new = z / (1.0 - 2.0 * lam); + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + v[m] = v[m] * dh2 / (dh2 + 2.0 * lam); + } + *sptr = (z_new + w_new) * INV_SQRT2; + *tptr = (z_new - w_new) * INV_SQRT2; +} + +__global__ void compute_cone_dual_residual_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + const double INV_SQRT2 = 0.7071067811865475; + int start = start_idx[blk]; + int k = v_dim[blk]; + + if (cone_section_has_fixed(is_fixed, start, k + 2)) + { + for (int slot = 0; slot < k + 2; ++slot) + { + int index = start + slot; + double residual = objective_vector[index] - dual_product[index]; + dual_residual[index] = is_fixed[index] ? primal_solution[index] : primal_solution[index] - residual; + } + project_rotated_soc_section_serial( + dual_residual, variable_rescaling, NULL, 0.0, warm_start + blk, start, k, is_fixed); + for (int slot = 0; slot < k + 2; ++slot) + { + int index = start + slot; + dual_residual[index] = + is_fixed[index] ? 0.0 : (primal_solution[index] - dual_residual[index]) * variable_rescaling[index]; + } + complementarity_residual[blk] = 0.0; + return; + } + + double r_s = objective_vector[start + k] - dual_product[start + k]; + double r_t = objective_vector[start + k + 1] - dual_product[start + k + 1]; + double r_w = (r_s - r_t) * INV_SQRT2; + double r_z = (r_s + r_t) * INV_SQRT2; + + double d_s = variable_rescaling[start + k]; + double d_t = variable_rescaling[start + k + 1]; + double d_st = sqrt(d_s * d_t); + + bool diag_uniform = true; + for (int m = 0; m < k && diag_uniform; ++m) + { + if (variable_rescaling[start + m] != d_st) + diag_uniform = false; + } + + if (diag_uniform) + { + double sumsq = r_w * r_w; + for (int m = 0; m < k; ++m) + { + double v_m = objective_vector[start + m] - dual_product[start + m]; + sumsq += v_m * v_m; + } + double r_norm = sqrt(sumsq); + + double v_factor, p_s, p_t; + if (r_norm <= r_z) + { + v_factor = 0.0; + p_s = r_s; + p_t = r_t; + } + else if (r_norm <= -r_z) + { + v_factor = 1.0; + p_s = 0.0; + p_t = 0.0; + } + else + { + double scale = (r_z + r_norm) / (2.0 * r_norm); + v_factor = 1.0 - scale; + double w_new = scale * r_w; + double z_new = scale * r_norm; + p_s = (z_new + w_new) * INV_SQRT2; + p_t = (z_new - w_new) * INV_SQRT2; + } + for (int m = 0; m < k; ++m) + { + double v_m = objective_vector[start + m] - dual_product[start + m]; + dual_residual[start + m] = v_m * v_factor * variable_rescaling[start + m]; + } + dual_residual[start + k] = (r_s - p_s) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_t - p_t) * variable_rescaling[start + k + 1]; + return; + } + + double r_inv_sq = r_w * r_w; + double r_pos_sq = r_w * r_w; + for (int m = 0; m < k; ++m) + { + double e_m = d_st / variable_rescaling[start + m]; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + r_inv_sq += (rc_m / e_m) * (rc_m / e_m); + r_pos_sq += (rc_m * e_m) * (rc_m * e_m); + } + double r_inv = sqrt(r_inv_sq); + double r_pos = sqrt(r_pos_sq); + + if (r_inv <= r_z) + { + for (int m = 0; m < k; ++m) + dual_residual[start + m] = 0.0; + dual_residual[start + k] = 0.0; + dual_residual[start + k + 1] = 0.0; + return; + } + if (r_pos <= -r_z) + { + for (int m = 0; m < k; ++m) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + dual_residual[start + m] = rc_m * variable_rescaling[start + m]; + } + dual_residual[start + k] = r_s * variable_rescaling[start + k]; + dual_residual[start + k + 1] = r_t * variable_rescaling[start + k + 1]; + return; + } + + double lo, hi; + bool z_pos = (r_z > 0.0); + if (z_pos) + { + lo = 0.0; + hi = 0.5 - 1e-14; + } + else + { + lo = 0.5 + 1e-14; + hi = 1.0; + for (int doubling = 0; doubling < 60; ++doubling) + { + double sum_hi = 0.0; + for (int m = 0; m < k; ++m) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double tt = rc_m * e_m / (e_m2 + 2.0 * hi); + sum_hi += tt * tt; + } + double tw_hi = r_w / (1.0 + 2.0 * hi); + sum_hi += tw_hi * tw_hi; + double zt_hi = r_z / (1.0 - 2.0 * hi); + double f_hi = sum_hi - zt_hi * zt_hi; + if (f_hi > 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_lam = warm_start[blk]; + if (warm_lam > lo && warm_lam < hi) + { + double sum_w = 0.0; + for (int m = 0; m < k; ++m) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double tt = rc_m * e_m / (e_m2 + 2.0 * warm_lam); + sum_w += tt * tt; + } + double tw = r_w / (1.0 + 2.0 * warm_lam); + sum_w += tw * tw; + double zt = r_z / (1.0 - 2.0 * warm_lam); + double f = sum_w - zt * zt; + if (fabs(f) < 1e-12) + { + double p_w_w = r_w / (1.0 + 2.0 * warm_lam); + double p_z_w = r_z / (1.0 - 2.0 * warm_lam); + double p_s_w = (p_z_w + p_w_w) * INV_SQRT2; + double p_t_w = (p_z_w - p_w_w) * INV_SQRT2; + for (int m = 0; m < k; ++m) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double p_m = rc_m * e_m2 / (e_m2 + 2.0 * warm_lam); + dual_residual[start + m] = (rc_m - p_m) * variable_rescaling[start + m]; + } + dual_residual[start + k] = (r_s - p_s_w) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_t - p_t_w) * variable_rescaling[start + k + 1]; + return; + } + if (z_pos) + { + if (f > 0.0) + lo = warm_lam; + else + hi = warm_lam; + } + else + { + if (f > 0.0) + hi = warm_lam; + else + lo = warm_lam; + } + } + + for (int it = 0; it < 60; ++it) + { + double lam = 0.5 * (lo + hi); + double sum = 0.0; + for (int m = 0; m < k; ++m) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double tt = rc_m * e_m / (e_m2 + 2.0 * lam); + sum += tt * tt; + } + double tw = r_w / (1.0 + 2.0 * lam); + sum += tw * tw; + double zt = r_z / (1.0 - 2.0 * lam); + double f = sum - zt * zt; + if (z_pos) + { + if (f > 0.0) + lo = lam; + else + hi = lam; + } + else + { + if (f > 0.0) + hi = lam; + else + lo = lam; + } + if ((hi - lo) / (1.0 + hi + lo) < 1e-13) + break; + } + double lam = 0.5 * (lo + hi); + warm_start[blk] = lam; + + double p_w = r_w / (1.0 + 2.0 * lam); + double p_z = r_z / (1.0 - 2.0 * lam); + double p_s = (p_z + p_w) * INV_SQRT2; + double p_t = (p_z - p_w) * INV_SQRT2; + + for (int m = 0; m < k; ++m) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double p_m = rc_m * e_m2 / (e_m2 + 2.0 * lam); + dual_residual[start + m] = (rc_m - p_m) * variable_rescaling[start + m]; + } + dual_residual[start + k] = (r_s - p_s) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_t - p_t) * variable_rescaling[start + k + 1]; +} + +__global__ void project_standard_soc_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int start = start_idx[blk]; + int k = v_dim[blk]; + if (cone_section_has_fixed(is_fixed, start, k + 2)) + { + project_standard_soc_section_serial( + primal_solution, variable_rescaling, NULL, 0.0, warm_start + blk, start, k, is_fixed); + return; + } + double *v = primal_solution + start; + double *wptr = primal_solution + start + k; + double *zptr = primal_solution + start + k + 1; + + double w = *wptr; + double z = *zptr; + + double d_z = variable_rescaling[start + k + 1]; + double dhat_w = variable_rescaling[start + k] / d_z; + double dhat_w2 = dhat_w * dhat_w; + + bool diag_uniform = (dhat_w == 1.0); + for (int m = 0; m < k && diag_uniform; ++m) + { + if (variable_rescaling[start + m] != d_z) + diag_uniform = false; + } + + if (diag_uniform) + { + double sumsq = w * w; + for (int m = 0; m < k; ++m) + sumsq += v[m] * v[m]; + double r = sqrt(sumsq); + if (r <= z) + return; + if (r <= -z) + { + for (int m = 0; m < k; ++m) + v[m] = 0.0; + *wptr = 0.0; + *zptr = 0.0; + return; + } + double scale = (z + r) / (2.0 * r); + for (int m = 0; m < k; ++m) + v[m] *= scale; + *wptr = scale * w; + *zptr = scale * r; + return; + } + + double r_inv_sq = (w / dhat_w) * (w / dhat_w); + double r_pos_sq = (w * dhat_w) * (w * dhat_w); + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_z; + double v_m = v[m]; + r_inv_sq += (v_m / dh) * (v_m / dh); + r_pos_sq += (v_m * dh) * (v_m * dh); + } + double r_inv = sqrt(r_inv_sq); + if (r_inv <= z) + return; + double r_pos = sqrt(r_pos_sq); + if (r_pos <= -z) + { + for (int m = 0; m < k; ++m) + v[m] = 0.0; + *wptr = 0.0; + *zptr = 0.0; + return; + } + + double lo, hi; + bool z_pos = (z > 0.0); + if (z_pos) + { + lo = 0.0; + hi = 0.5 - 1e-14; + } + else + { + lo = 0.5 + 1e-14; + hi = 1.0; + for (int doubling = 0; doubling < 60; ++doubling) + { + double sum_hi = 0.0; + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + double t = v[m] * dh / (dh2 + 2.0 * hi); + sum_hi += t * t; + } + double tw_hi = w * dhat_w / (dhat_w2 + 2.0 * hi); + sum_hi += tw_hi * tw_hi; + double zt_hi = z / (1.0 - 2.0 * hi); + double f_hi = sum_hi - zt_hi * zt_hi; + if (f_hi > 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_lam = warm_start[blk]; + if (warm_lam > lo && warm_lam < hi) + { + double sum_w = 0.0; + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + double t = v[m] * dh / (dh2 + 2.0 * warm_lam); + sum_w += t * t; + } + double tw = w * dhat_w / (dhat_w2 + 2.0 * warm_lam); + sum_w += tw * tw; + double zt = z / (1.0 - 2.0 * warm_lam); + double f = sum_w - zt * zt; + if (fabs(f) < 1e-12) + { + *zptr = z / (1.0 - 2.0 * warm_lam); + *wptr = w * dhat_w2 / (dhat_w2 + 2.0 * warm_lam); + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + v[m] = v[m] * dh2 / (dh2 + 2.0 * warm_lam); + } + return; + } + if (z_pos) + { + if (f > 0.0) + lo = warm_lam; + else + hi = warm_lam; + } + else + { + if (f > 0.0) + hi = warm_lam; + else + lo = warm_lam; + } + } + + for (int it = 0; it < 60; ++it) + { + double lam = 0.5 * (lo + hi); + double sum = 0.0; + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + double t = v[m] * dh / (dh2 + 2.0 * lam); + sum += t * t; + } + double tw = w * dhat_w / (dhat_w2 + 2.0 * lam); + sum += tw * tw; + double zt = z / (1.0 - 2.0 * lam); + double f = sum - zt * zt; + if (z_pos) + { + if (f > 0.0) + lo = lam; + else + hi = lam; + } + else + { + if (f > 0.0) + hi = lam; + else + lo = lam; + } + if ((hi - lo) / (1.0 + hi + lo) < 1e-13) + break; + } + double lam = 0.5 * (lo + hi); + warm_start[blk] = lam; + + *zptr = z / (1.0 - 2.0 * lam); + *wptr = w * dhat_w2 / (dhat_w2 + 2.0 * lam); + for (int m = 0; m < k; ++m) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + v[m] = v[m] * dh2 / (dh2 + 2.0 * lam); + } +} + +__global__ void compute_cone_dual_residual_standard_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int start = start_idx[blk]; + int k = v_dim[blk]; + + if (cone_section_has_fixed(is_fixed, start, k + 2)) + { + for (int slot = 0; slot < k + 2; ++slot) + { + int index = start + slot; + double residual = objective_vector[index] - dual_product[index]; + dual_residual[index] = is_fixed[index] ? primal_solution[index] : primal_solution[index] - residual; + } + project_standard_soc_section_serial( + dual_residual, variable_rescaling, NULL, 0.0, warm_start + blk, start, k, is_fixed); + for (int slot = 0; slot < k + 2; ++slot) + { + int index = start + slot; + dual_residual[index] = + is_fixed[index] ? 0.0 : (primal_solution[index] - dual_residual[index]) * variable_rescaling[index]; + } + complementarity_residual[blk] = 0.0; + return; + } + + double r_w = objective_vector[start + k] - dual_product[start + k]; + double r_z = objective_vector[start + k + 1] - dual_product[start + k + 1]; + + double d_z = variable_rescaling[start + k + 1]; + double e_w = d_z / variable_rescaling[start + k]; + double e_w2 = e_w * e_w; + + bool diag_uniform = (e_w == 1.0); + for (int m = 0; m < k && diag_uniform; ++m) + { + if (variable_rescaling[start + m] != d_z) + diag_uniform = false; + } + + if (diag_uniform) + { + double sumsq = r_w * r_w; + for (int m = 0; m < k; ++m) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + sumsq += rc_m * rc_m; + } + double r = sqrt(sumsq); + double v_factor, p_w, p_z; + if (r <= r_z) + { + v_factor = 0.0; + p_w = r_w; + p_z = r_z; + } + else if (r <= -r_z) + { + v_factor = 1.0; + p_w = 0.0; + p_z = 0.0; + } + else + { + double scale = (r_z + r) / (2.0 * r); + v_factor = 1.0 - scale; + p_w = scale * r_w; + p_z = scale * r; + } + for (int m = 0; m < k; ++m) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + dual_residual[start + m] = rc_m * v_factor * variable_rescaling[start + m]; + } + dual_residual[start + k] = (r_w - p_w) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_z - p_z) * variable_rescaling[start + k + 1]; + return; + } + + double r_inv_sq = (r_w / e_w) * (r_w / e_w); + double r_pos_sq = (r_w * e_w) * (r_w * e_w); + for (int m = 0; m < k; ++m) + { + double e_m = d_z / variable_rescaling[start + m]; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + r_inv_sq += (rc_m / e_m) * (rc_m / e_m); + r_pos_sq += (rc_m * e_m) * (rc_m * e_m); + } + double r_inv = sqrt(r_inv_sq); + double r_pos = sqrt(r_pos_sq); + + if (r_inv <= r_z) + { + for (int m = 0; m < k; ++m) + dual_residual[start + m] = 0.0; + dual_residual[start + k] = 0.0; + dual_residual[start + k + 1] = 0.0; + return; + } + if (r_pos <= -r_z) + { + for (int m = 0; m < k; ++m) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + dual_residual[start + m] = rc_m * variable_rescaling[start + m]; + } + dual_residual[start + k] = r_w * variable_rescaling[start + k]; + dual_residual[start + k + 1] = r_z * variable_rescaling[start + k + 1]; + return; + } + + double lo, hi; + bool z_pos = (r_z > 0.0); + if (z_pos) + { + lo = 0.0; + hi = 0.5 - 1e-14; + } + else + { + lo = 0.5 + 1e-14; + hi = 1.0; + for (int doubling = 0; doubling < 60; ++doubling) + { + double sum_hi = 0.0; + for (int m = 0; m < k; ++m) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double t = rc_m * e_m / (e_m2 + 2.0 * hi); + sum_hi += t * t; + } + double tw_hi = r_w * e_w / (e_w2 + 2.0 * hi); + sum_hi += tw_hi * tw_hi; + double zt_hi = r_z / (1.0 - 2.0 * hi); + double f_hi = sum_hi - zt_hi * zt_hi; + if (f_hi > 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_lam = warm_start[blk]; + if (warm_lam > lo && warm_lam < hi) + { + double sum_w = 0.0; + for (int m = 0; m < k; ++m) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double t = rc_m * e_m / (e_m2 + 2.0 * warm_lam); + sum_w += t * t; + } + double tw = r_w * e_w / (e_w2 + 2.0 * warm_lam); + sum_w += tw * tw; + double zt = r_z / (1.0 - 2.0 * warm_lam); + double f = sum_w - zt * zt; + if (fabs(f) < 1e-12) + { + double p_z_w = r_z / (1.0 - 2.0 * warm_lam); + double p_w_w = r_w * e_w2 / (e_w2 + 2.0 * warm_lam); + for (int m = 0; m < k; ++m) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double p_m = rc_m * e_m2 / (e_m2 + 2.0 * warm_lam); + dual_residual[start + m] = (rc_m - p_m) * variable_rescaling[start + m]; + } + dual_residual[start + k] = (r_w - p_w_w) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_z - p_z_w) * variable_rescaling[start + k + 1]; + return; + } + if (z_pos) + { + if (f > 0.0) + lo = warm_lam; + else + hi = warm_lam; + } + else + { + if (f > 0.0) + hi = warm_lam; + else + lo = warm_lam; + } + } + + for (int it = 0; it < 60; ++it) + { + double lam = 0.5 * (lo + hi); + double sum = 0.0; + for (int m = 0; m < k; ++m) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double t = rc_m * e_m / (e_m2 + 2.0 * lam); + sum += t * t; + } + double tw = r_w * e_w / (e_w2 + 2.0 * lam); + sum += tw * tw; + double zt = r_z / (1.0 - 2.0 * lam); + double f = sum - zt * zt; + if (z_pos) + { + if (f > 0.0) + lo = lam; + else + hi = lam; + } + else + { + if (f > 0.0) + hi = lam; + else + lo = lam; + } + if ((hi - lo) / (1.0 + hi + lo) < 1e-13) + break; + } + double lam = 0.5 * (lo + hi); + warm_start[blk] = lam; + + double p_z = r_z / (1.0 - 2.0 * lam); + double p_w = r_w * e_w2 / (e_w2 + 2.0 * lam); + + for (int m = 0; m < k; ++m) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double p_m = rc_m * e_m2 / (e_m2 + 2.0 * lam); + dual_residual[start + m] = (rc_m - p_m) * variable_rescaling[start + m]; + } + dual_residual[start + k] = (r_w - p_w) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_z - p_z) * variable_rescaling[start + k + 1]; +} + +static __device__ __forceinline__ double large_cone_block_sum(double value) +{ + __shared__ double warp_sums[32]; + const unsigned mask = 0xffffffffu; + const int lane = threadIdx.x & 31; + const int warp = threadIdx.x >> 5; + const int num_warps = (blockDim.x + 31) >> 5; + + for (int offset = 16; offset > 0; offset >>= 1) + value += __shfl_down_sync(mask, value, offset); + if (lane == 0) + warp_sums[warp] = value; + __syncthreads(); + + value = (warp == 0 && lane < num_warps) ? warp_sums[lane] : 0.0; + if (warp == 0) + { + for (int offset = 16; offset > 0; offset >>= 1) + value += __shfl_down_sync(mask, value, offset); + } + return value; +} + +__global__ void project_rotated_soc_grid_reduce_kernel(double *__restrict__ primal_solution, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + int part = blockIdx.x - cone * blocks_per_cone; + if (cone >= num_cones) + return; + + int start = start_idx[cone]; + int k = v_dim[cone]; + double sum = 0.0; + for (int m = part * blockDim.x + threadIdx.x; m < k; m += blocks_per_cone * blockDim.x) + { + double value = primal_solution[start + m]; + sum += value * value; + } + sum = large_cone_block_sum(sum); + if (threadIdx.x == 0) + atomicAdd(workspace + cone, sum); +} + +__global__ void project_rotated_soc_grid_finalize_kernel(double *__restrict__ primal_solution, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone >= num_cones) + return; + + const double INV_SQRT2 = 0.7071067811865475; + int start = start_idx[cone]; + int k = v_dim[cone]; + double s = primal_solution[start + k]; + double t = primal_solution[start + k + 1]; + double w = (s - t) * INV_SQRT2; + double z = (s + t) * INV_SQRT2; + double radius = sqrt(fmax(0.0, workspace[cone] + w * w)); + + if (radius <= z) + { + workspace[cone] = 1.0; + return; + } + if (radius <= -z) + { + workspace[cone] = 0.0; + primal_solution[start + k] = 0.0; + primal_solution[start + k + 1] = 0.0; + return; + } + + double scale = (z + radius) / (2.0 * radius); + double w_new = scale * w; + double z_new = scale * radius; + workspace[cone] = scale; + primal_solution[start + k] = (z_new + w_new) * INV_SQRT2; + primal_solution[start + k + 1] = (z_new - w_new) * INV_SQRT2; +} + +__global__ void project_rotated_soc_grid_apply_kernel(double *__restrict__ primal_solution, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + int part = blockIdx.x - cone * blocks_per_cone; + if (cone >= num_cones) + return; + + double scale = workspace[cone]; + if (scale == 1.0) + return; + + int start = start_idx[cone]; + int k = v_dim[cone]; + for (int m = part * blockDim.x + threadIdx.x; m < k; m += blocks_per_cone * blockDim.x) + { + primal_solution[start + m] *= scale; + } +} + +__global__ void compute_cone_dual_residual_grid_reduce_kernel(const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + int part = blockIdx.x - cone * blocks_per_cone; + if (cone >= num_cones) + return; + + int start = start_idx[cone]; + int k = v_dim[cone]; + double sum = 0.0; + for (int m = part * blockDim.x + threadIdx.x; m < k; m += blocks_per_cone * blockDim.x) + { + double residual = objective_vector[start + m] - dual_product[start + m]; + sum += residual * residual; + } + sum = large_cone_block_sum(sum); + if (threadIdx.x == 0) + atomicAdd(workspace + cone, sum); +} + +__global__ void compute_cone_dual_residual_grid_finalize_kernel(double *__restrict__ dual_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone >= num_cones) + return; + + const double INV_SQRT2 = 0.7071067811865475; + int start = start_idx[cone]; + int k = v_dim[cone]; + double r_s = objective_vector[start + k] - dual_product[start + k]; + double r_t = objective_vector[start + k + 1] - dual_product[start + k + 1]; + double r_w = (r_s - r_t) * INV_SQRT2; + double r_z = (r_s + r_t) * INV_SQRT2; + double norm = sqrt(fmax(0.0, workspace[cone] + r_w * r_w)); + double factor; + double p_s; + double p_t; + + if (norm <= r_z) + { + factor = 0.0; + p_s = r_s; + p_t = r_t; + } + else if (norm <= -r_z) + { + factor = 1.0; + p_s = 0.0; + p_t = 0.0; + } + else + { + double scale = (r_z + norm) / (2.0 * norm); + double w_new = scale * r_w; + double z_new = scale * norm; + factor = 1.0 - scale; + p_s = (z_new + w_new) * INV_SQRT2; + p_t = (z_new - w_new) * INV_SQRT2; + } + + workspace[cone] = factor; + dual_residual[start + k] = (r_s - p_s) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_t - p_t) * variable_rescaling[start + k + 1]; +} + +__global__ void compute_cone_dual_residual_grid_apply_kernel(double *__restrict__ dual_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + int part = blockIdx.x - cone * blocks_per_cone; + if (cone >= num_cones) + return; + + double factor = workspace[cone]; + int start = start_idx[cone]; + int k = v_dim[cone]; + for (int m = part * blockDim.x + threadIdx.x; m < k; m += blocks_per_cone * blockDim.x) + { + int idx = start + m; + double residual = objective_vector[idx] - dual_product[idx]; + dual_residual[idx] = residual * factor * variable_rescaling[idx]; + } +} + +__global__ void project_standard_soc_grid_reduce_kernel(double *__restrict__ primal_solution, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + int part = blockIdx.x - cone * blocks_per_cone; + if (cone >= num_cones) + return; + + int start = start_idx[cone]; + int k = v_dim[cone]; + double sum = 0.0; + for (int m = part * blockDim.x + threadIdx.x; m < k; m += blocks_per_cone * blockDim.x) + { + double value = primal_solution[start + m]; + sum += value * value; + } + sum = large_cone_block_sum(sum); + if (threadIdx.x == 0) + atomicAdd(workspace + cone, sum); +} + +__global__ void project_standard_soc_grid_finalize_kernel(double *__restrict__ primal_solution, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone >= num_cones) + return; + + int start = start_idx[cone]; + int k = v_dim[cone]; + double w = primal_solution[start + k]; + double z = primal_solution[start + k + 1]; + double radius = sqrt(fmax(0.0, workspace[cone] + w * w)); + + if (radius <= z) + { + workspace[cone] = 1.0; + return; + } + if (radius <= -z) + { + workspace[cone] = 0.0; + primal_solution[start + k] = 0.0; + primal_solution[start + k + 1] = 0.0; + return; + } + + double scale = (z + radius) / (2.0 * radius); + workspace[cone] = scale; + primal_solution[start + k] = scale * w; + primal_solution[start + k + 1] = scale * radius; +} + +__global__ void project_standard_soc_grid_apply_kernel(double *__restrict__ primal_solution, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + int part = blockIdx.x - cone * blocks_per_cone; + if (cone >= num_cones) + return; + + double scale = workspace[cone]; + if (scale == 1.0) + return; + + int start = start_idx[cone]; + int k = v_dim[cone]; + for (int m = part * blockDim.x + threadIdx.x; m < k; m += blocks_per_cone * blockDim.x) + { + primal_solution[start + m] *= scale; + } +} + +__global__ void compute_cone_dual_residual_standard_grid_reduce_kernel(const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + int part = blockIdx.x - cone * blocks_per_cone; + if (cone >= num_cones) + return; + + int start = start_idx[cone]; + int k = v_dim[cone]; + double sum = 0.0; + for (int m = part * blockDim.x + threadIdx.x; m < k; m += blocks_per_cone * blockDim.x) + { + double residual = objective_vector[start + m] - dual_product[start + m]; + sum += residual * residual; + } + sum = large_cone_block_sum(sum); + if (threadIdx.x == 0) + atomicAdd(workspace + cone, sum); +} + +__global__ void compute_cone_dual_residual_standard_grid_finalize_kernel(double *__restrict__ dual_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone >= num_cones) + return; + + int start = start_idx[cone]; + int k = v_dim[cone]; + double r_w = objective_vector[start + k] - dual_product[start + k]; + double r_z = objective_vector[start + k + 1] - dual_product[start + k + 1]; + double radius = sqrt(fmax(0.0, workspace[cone] + r_w * r_w)); + double factor; + double p_w; + double p_z; + + if (radius <= r_z) + { + factor = 0.0; + p_w = r_w; + p_z = r_z; + } + else if (radius <= -r_z) + { + factor = 1.0; + p_w = 0.0; + p_z = 0.0; + } + else + { + double scale = (r_z + radius) / (2.0 * radius); + factor = 1.0 - scale; + p_w = scale * r_w; + p_z = scale * radius; + } + + workspace[cone] = factor; + dual_residual[start + k] = (r_w - p_w) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_z - p_z) * variable_rescaling[start + k + 1]; +} + +__global__ void compute_cone_dual_residual_standard_grid_apply_kernel(double *__restrict__ dual_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ workspace, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + int part = blockIdx.x - cone * blocks_per_cone; + if (cone >= num_cones) + return; + + double factor = workspace[cone]; + int start = start_idx[cone]; + int k = v_dim[cone]; + for (int m = part * blockDim.x + threadIdx.x; m < k; m += blocks_per_cone * blockDim.x) + { + int idx = start + m; + double residual = objective_vector[idx] - dual_product[idx]; + dual_residual[idx] = residual * factor * variable_rescaling[idx]; + } +} + +__global__ void project_rotated_soc_warp_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int tid = blockIdx.x * blockDim.x + threadIdx.x; + int blk = tid >> 5; + int lane = tid & 31; + if (blk >= num_blocks) + return; + + const double INV_SQRT2 = 0.7071067811865475; + const unsigned MASK = 0xffffffffu; + + int start = start_idx[blk]; + int k = v_dim[blk]; + + int has_fixed = lane == 0 ? cone_section_has_fixed(is_fixed, start, k + 2) : 0; + has_fixed = __shfl_sync(MASK, has_fixed, 0); + if (has_fixed) + { + if (lane == 0) + project_rotated_soc_section_serial( + primal_solution, variable_rescaling, NULL, 0.0, warm_start + blk, start, k, is_fixed); + return; + } + + double s_val = primal_solution[start + k]; + double t_val = primal_solution[start + k + 1]; + + double w = (s_val - t_val) * INV_SQRT2; + double z = (s_val + t_val) * INV_SQRT2; + + double d_s = variable_rescaling[start + k]; + double d_t = variable_rescaling[start + k + 1]; + double d_st = sqrt(d_s * d_t); + + int my_diff = 0; + for (int m = lane; m < k; m += 32) + { + if (variable_rescaling[start + m] != d_st) + my_diff = 1; + } + for (int o = 16; o > 0; o >>= 1) + my_diff |= __shfl_xor_sync(MASK, my_diff, o); + + if (my_diff == 0) + { + double my_sumsq = (lane == 0) ? w * w : 0.0; + for (int m = lane; m < k; m += 32) + { + double v_m = primal_solution[start + m]; + my_sumsq += v_m * v_m; + } + for (int o = 16; o > 0; o >>= 1) + my_sumsq += __shfl_xor_sync(MASK, my_sumsq, o); + double r = sqrt(my_sumsq); + if (r <= z) + return; + if (r <= -z) + { + for (int m = lane; m < k; m += 32) + primal_solution[start + m] = 0.0; + if (lane == 0) + { + primal_solution[start + k] = 0.0; + primal_solution[start + k + 1] = 0.0; + } + return; + } + double scale = (z + r) / (2.0 * r); + for (int m = lane; m < k; m += 32) + primal_solution[start + m] *= scale; + double w_new = scale * w; + double z_new = scale * r; + if (lane == 0) + { + primal_solution[start + k] = (z_new + w_new) * INV_SQRT2; + primal_solution[start + k + 1] = (z_new - w_new) * INV_SQRT2; + } + return; + } + + double my_inv = (lane == 0) ? w * w : 0.0; + double my_pos = (lane == 0) ? w * w : 0.0; + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_st; + double v_m = primal_solution[start + m]; + my_inv += (v_m / dh) * (v_m / dh); + my_pos += (v_m * dh) * (v_m * dh); + } + for (int o = 16; o > 0; o >>= 1) + { + my_inv += __shfl_xor_sync(MASK, my_inv, o); + my_pos += __shfl_xor_sync(MASK, my_pos, o); + } + double r_inv = sqrt(my_inv); + if (r_inv <= z) + return; + double r_pos = sqrt(my_pos); + if (r_pos <= -z) + { + for (int m = lane; m < k; m += 32) + primal_solution[start + m] = 0.0; + if (lane == 0) + { + primal_solution[start + k] = 0.0; + primal_solution[start + k + 1] = 0.0; + } + return; + } + + double lo, hi; + bool z_pos = (z > 0.0); + if (z_pos) + { + lo = 0.0; + hi = 0.5 - 1e-14; + } + else + { + lo = 0.5 + 1e-14; + hi = 1.0; + for (int doubling = 0; doubling < 60; ++doubling) + { + double my_sum = (lane == 0) ? (w / (1.0 + 2.0 * hi)) * (w / (1.0 + 2.0 * hi)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + double tt = primal_solution[start + m] * dh / (dh2 + 2.0 * hi); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt_hi = z / (1.0 - 2.0 * hi); + double f_hi = my_sum - zt_hi * zt_hi; + if (f_hi > 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_lam = warm_start[blk]; + if (warm_lam > lo && warm_lam < hi) + { + double my_sum = (lane == 0) ? (w / (1.0 + 2.0 * warm_lam)) * (w / (1.0 + 2.0 * warm_lam)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + double tt = primal_solution[start + m] * dh / (dh2 + 2.0 * warm_lam); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt = z / (1.0 - 2.0 * warm_lam); + double f = my_sum - zt * zt; + if (fabs(f) < 1e-12) + { + double w_new = w / (1.0 + 2.0 * warm_lam); + double z_new = z / (1.0 - 2.0 * warm_lam); + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + primal_solution[start + m] = primal_solution[start + m] * dh2 / (dh2 + 2.0 * warm_lam); + } + if (lane == 0) + { + primal_solution[start + k] = (z_new + w_new) * INV_SQRT2; + primal_solution[start + k + 1] = (z_new - w_new) * INV_SQRT2; + } + return; + } + if (z_pos) + { + if (f > 0.0) + lo = warm_lam; + else + hi = warm_lam; + } + else + { + if (f > 0.0) + hi = warm_lam; + else + lo = warm_lam; + } + } + + for (int it = 0; it < 60; ++it) + { + double lam = 0.5 * (lo + hi); + double my_sum = (lane == 0) ? (w / (1.0 + 2.0 * lam)) * (w / (1.0 + 2.0 * lam)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + double tt = primal_solution[start + m] * dh / (dh2 + 2.0 * lam); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt = z / (1.0 - 2.0 * lam); + double f = my_sum - zt * zt; + if (z_pos) + { + if (f > 0.0) + lo = lam; + else + hi = lam; + } + else + { + if (f > 0.0) + hi = lam; + else + lo = lam; + } + if ((hi - lo) / (1.0 + hi + lo) < 1e-13) + break; + } + double lam = 0.5 * (lo + hi); + if (lane == 0) + warm_start[blk] = lam; + + double w_new = w / (1.0 + 2.0 * lam); + double z_new = z / (1.0 - 2.0 * lam); + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_st; + double dh2 = dh * dh; + primal_solution[start + m] = primal_solution[start + m] * dh2 / (dh2 + 2.0 * lam); + } + if (lane == 0) + { + primal_solution[start + k] = (z_new + w_new) * INV_SQRT2; + primal_solution[start + k + 1] = (z_new - w_new) * INV_SQRT2; + } +} + +__global__ void compute_cone_dual_residual_warp_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int tid = blockIdx.x * blockDim.x + threadIdx.x; + int blk = tid >> 5; + int lane = tid & 31; + if (blk >= num_blocks) + return; + + const double INV_SQRT2 = 0.7071067811865475; + const unsigned MASK = 0xffffffffu; + + int start = start_idx[blk]; + int k = v_dim[blk]; + + int has_fixed = lane == 0 ? cone_section_has_fixed(is_fixed, start, k + 2) : 0; + has_fixed = __shfl_sync(MASK, has_fixed, 0); + if (has_fixed) + { + if (lane == 0) + { + for (int slot = 0; slot < k + 2; ++slot) + { + int index = start + slot; + double residual = objective_vector[index] - dual_product[index]; + dual_residual[index] = is_fixed[index] ? primal_solution[index] : primal_solution[index] - residual; + } + project_rotated_soc_section_serial( + dual_residual, variable_rescaling, NULL, 0.0, warm_start + blk, start, k, is_fixed); + for (int slot = 0; slot < k + 2; ++slot) + { + int index = start + slot; + dual_residual[index] = + is_fixed[index] ? 0.0 : (primal_solution[index] - dual_residual[index]) * variable_rescaling[index]; + } + complementarity_residual[blk] = 0.0; + } + return; + } + + double r_s = objective_vector[start + k] - dual_product[start + k]; + double r_t = objective_vector[start + k + 1] - dual_product[start + k + 1]; + double r_w = (r_s - r_t) * INV_SQRT2; + double r_z = (r_s + r_t) * INV_SQRT2; + + double d_s = variable_rescaling[start + k]; + double d_t = variable_rescaling[start + k + 1]; + double d_st = sqrt(d_s * d_t); + + int my_diff = 0; + for (int m = lane; m < k; m += 32) + { + if (variable_rescaling[start + m] != d_st) + my_diff = 1; + } + for (int o = 16; o > 0; o >>= 1) + my_diff |= __shfl_xor_sync(MASK, my_diff, o); + + if (my_diff == 0) + { + double my_sumsq = (lane == 0) ? r_w * r_w : 0.0; + for (int m = lane; m < k; m += 32) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + my_sumsq += rc_m * rc_m; + } + for (int o = 16; o > 0; o >>= 1) + my_sumsq += __shfl_xor_sync(MASK, my_sumsq, o); + double r_norm = sqrt(my_sumsq); + + double v_factor, p_s, p_t; + if (r_norm <= r_z) + { + v_factor = 0.0; + p_s = r_s; + p_t = r_t; + } + else if (r_norm <= -r_z) + { + v_factor = 1.0; + p_s = 0.0; + p_t = 0.0; + } + else + { + double scale = (r_z + r_norm) / (2.0 * r_norm); + v_factor = 1.0 - scale; + double w_new = scale * r_w; + double z_new = scale * r_norm; + p_s = (z_new + w_new) * INV_SQRT2; + p_t = (z_new - w_new) * INV_SQRT2; + } + + for (int m = lane; m < k; m += 32) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + dual_residual[start + m] = rc_m * v_factor * variable_rescaling[start + m]; + } + if (lane == 0) + { + dual_residual[start + k] = (r_s - p_s) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_t - p_t) * variable_rescaling[start + k + 1]; + } + return; + } + + double my_inv = (lane == 0) ? r_w * r_w : 0.0; + double my_pos = (lane == 0) ? r_w * r_w : 0.0; + for (int m = lane; m < k; m += 32) + { + double e_m = d_st / variable_rescaling[start + m]; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + my_inv += (rc_m / e_m) * (rc_m / e_m); + my_pos += (rc_m * e_m) * (rc_m * e_m); + } + for (int o = 16; o > 0; o >>= 1) + { + my_inv += __shfl_xor_sync(MASK, my_inv, o); + my_pos += __shfl_xor_sync(MASK, my_pos, o); + } + double r_inv = sqrt(my_inv); + double r_pos = sqrt(my_pos); + + if (r_inv <= r_z) + { + for (int m = lane; m < k; m += 32) + dual_residual[start + m] = 0.0; + if (lane == 0) + { + dual_residual[start + k] = 0.0; + dual_residual[start + k + 1] = 0.0; + } + return; + } + if (r_pos <= -r_z) + { + for (int m = lane; m < k; m += 32) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + dual_residual[start + m] = rc_m * variable_rescaling[start + m]; + } + if (lane == 0) + { + dual_residual[start + k] = r_s * variable_rescaling[start + k]; + dual_residual[start + k + 1] = r_t * variable_rescaling[start + k + 1]; + } + return; + } + + double lo, hi; + bool z_pos = (r_z > 0.0); + if (z_pos) + { + lo = 0.0; + hi = 0.5 - 1e-14; + } + else + { + lo = 0.5 + 1e-14; + hi = 1.0; + for (int doubling = 0; doubling < 60; ++doubling) + { + double my_sum = (lane == 0) ? (r_w / (1.0 + 2.0 * hi)) * (r_w / (1.0 + 2.0 * hi)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double tt = rc_m * e_m / (e_m2 + 2.0 * hi); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt_hi = r_z / (1.0 - 2.0 * hi); + double f_hi = my_sum - zt_hi * zt_hi; + if (f_hi > 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_lam = warm_start[blk]; + if (warm_lam > lo && warm_lam < hi) + { + double my_sum = (lane == 0) ? (r_w / (1.0 + 2.0 * warm_lam)) * (r_w / (1.0 + 2.0 * warm_lam)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double tt = rc_m * e_m / (e_m2 + 2.0 * warm_lam); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt = r_z / (1.0 - 2.0 * warm_lam); + double f = my_sum - zt * zt; + if (fabs(f) < 1e-12) + { + double p_w_w = r_w / (1.0 + 2.0 * warm_lam); + double p_z_w = r_z / (1.0 - 2.0 * warm_lam); + double p_s_w = (p_z_w + p_w_w) * INV_SQRT2; + double p_t_w = (p_z_w - p_w_w) * INV_SQRT2; + for (int m = lane; m < k; m += 32) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double p_m = rc_m * e_m2 / (e_m2 + 2.0 * warm_lam); + dual_residual[start + m] = (rc_m - p_m) * variable_rescaling[start + m]; + } + if (lane == 0) + { + dual_residual[start + k] = (r_s - p_s_w) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_t - p_t_w) * variable_rescaling[start + k + 1]; + } + return; + } + if (z_pos) + { + if (f > 0.0) + lo = warm_lam; + else + hi = warm_lam; + } + else + { + if (f > 0.0) + hi = warm_lam; + else + lo = warm_lam; + } + } + + for (int it = 0; it < 60; ++it) + { + double lam = 0.5 * (lo + hi); + double my_sum = (lane == 0) ? (r_w / (1.0 + 2.0 * lam)) * (r_w / (1.0 + 2.0 * lam)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double tt = rc_m * e_m / (e_m2 + 2.0 * lam); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt = r_z / (1.0 - 2.0 * lam); + double f = my_sum - zt * zt; + if (z_pos) + { + if (f > 0.0) + lo = lam; + else + hi = lam; + } + else + { + if (f > 0.0) + hi = lam; + else + lo = lam; + } + if ((hi - lo) / (1.0 + hi + lo) < 1e-13) + break; + } + double lam = 0.5 * (lo + hi); + if (lane == 0) + warm_start[blk] = lam; + + double p_w = r_w / (1.0 + 2.0 * lam); + double p_z = r_z / (1.0 - 2.0 * lam); + double p_s = (p_z + p_w) * INV_SQRT2; + double p_t = (p_z - p_w) * INV_SQRT2; + + for (int m = lane; m < k; m += 32) + { + double e_m = d_st / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double p_m = rc_m * e_m2 / (e_m2 + 2.0 * lam); + dual_residual[start + m] = (rc_m - p_m) * variable_rescaling[start + m]; + } + if (lane == 0) + { + dual_residual[start + k] = (r_s - p_s) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_t - p_t) * variable_rescaling[start + k + 1]; + } +} + +__global__ void project_standard_soc_warp_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int tid = blockIdx.x * blockDim.x + threadIdx.x; + int blk = tid >> 5; + int lane = tid & 31; + if (blk >= num_blocks) + return; + + const unsigned MASK = 0xffffffffu; + + int start = start_idx[blk]; + int k = v_dim[blk]; + + int has_fixed = lane == 0 ? cone_section_has_fixed(is_fixed, start, k + 2) : 0; + has_fixed = __shfl_sync(MASK, has_fixed, 0); + if (has_fixed) + { + if (lane == 0) + project_standard_soc_section_serial( + primal_solution, variable_rescaling, NULL, 0.0, warm_start + blk, start, k, is_fixed); + return; + } + + double w = primal_solution[start + k]; + double z = primal_solution[start + k + 1]; + + double d_z = variable_rescaling[start + k + 1]; + double dhat_w = variable_rescaling[start + k] / d_z; + double dhat_w2 = dhat_w * dhat_w; + + int my_diff = (lane == 0 && dhat_w != 1.0) ? 1 : 0; + for (int m = lane; m < k; m += 32) + { + if (variable_rescaling[start + m] != d_z) + my_diff = 1; + } + for (int o = 16; o > 0; o >>= 1) + my_diff |= __shfl_xor_sync(MASK, my_diff, o); + + if (my_diff == 0) + { + double my_sumsq = (lane == 0) ? w * w : 0.0; + for (int m = lane; m < k; m += 32) + { + double v_m = primal_solution[start + m]; + my_sumsq += v_m * v_m; + } + for (int o = 16; o > 0; o >>= 1) + my_sumsq += __shfl_xor_sync(MASK, my_sumsq, o); + double r = sqrt(my_sumsq); + if (r <= z) + return; + if (r <= -z) + { + for (int m = lane; m < k; m += 32) + primal_solution[start + m] = 0.0; + if (lane == 0) + { + primal_solution[start + k] = 0.0; + primal_solution[start + k + 1] = 0.0; + } + return; + } + double scale = (z + r) / (2.0 * r); + for (int m = lane; m < k; m += 32) + primal_solution[start + m] *= scale; + if (lane == 0) + { + primal_solution[start + k] = scale * w; + primal_solution[start + k + 1] = scale * r; + } + return; + } + + double my_inv = (lane == 0) ? (w / dhat_w) * (w / dhat_w) : 0.0; + double my_pos = (lane == 0) ? (w * dhat_w) * (w * dhat_w) : 0.0; + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_z; + double v_m = primal_solution[start + m]; + my_inv += (v_m / dh) * (v_m / dh); + my_pos += (v_m * dh) * (v_m * dh); + } + for (int o = 16; o > 0; o >>= 1) + { + my_inv += __shfl_xor_sync(MASK, my_inv, o); + my_pos += __shfl_xor_sync(MASK, my_pos, o); + } + double r_inv = sqrt(my_inv); + if (r_inv <= z) + return; + double r_pos = sqrt(my_pos); + if (r_pos <= -z) + { + for (int m = lane; m < k; m += 32) + primal_solution[start + m] = 0.0; + if (lane == 0) + { + primal_solution[start + k] = 0.0; + primal_solution[start + k + 1] = 0.0; + } + return; + } + + double lo, hi; + bool z_pos = (z > 0.0); + if (z_pos) + { + lo = 0.0; + hi = 0.5 - 1e-14; + } + else + { + lo = 0.5 + 1e-14; + hi = 1.0; + for (int doubling = 0; doubling < 60; ++doubling) + { + double my_sum = + (lane == 0) ? (w * dhat_w / (dhat_w2 + 2.0 * hi)) * (w * dhat_w / (dhat_w2 + 2.0 * hi)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + double tt = primal_solution[start + m] * dh / (dh2 + 2.0 * hi); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt_hi = z / (1.0 - 2.0 * hi); + double f_hi = my_sum - zt_hi * zt_hi; + if (f_hi > 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_lam = warm_start[blk]; + if (warm_lam > lo && warm_lam < hi) + { + double my_sum = + (lane == 0) ? (w * dhat_w / (dhat_w2 + 2.0 * warm_lam)) * (w * dhat_w / (dhat_w2 + 2.0 * warm_lam)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + double tt = primal_solution[start + m] * dh / (dh2 + 2.0 * warm_lam); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt = z / (1.0 - 2.0 * warm_lam); + double f = my_sum - zt * zt; + if (fabs(f) < 1e-12) + { + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + primal_solution[start + m] = primal_solution[start + m] * dh2 / (dh2 + 2.0 * warm_lam); + } + if (lane == 0) + { + primal_solution[start + k + 1] = z / (1.0 - 2.0 * warm_lam); + primal_solution[start + k] = w * dhat_w2 / (dhat_w2 + 2.0 * warm_lam); + } + return; + } + if (z_pos) + { + if (f > 0.0) + lo = warm_lam; + else + hi = warm_lam; + } + else + { + if (f > 0.0) + hi = warm_lam; + else + lo = warm_lam; + } + } + + for (int it = 0; it < 60; ++it) + { + double lam = 0.5 * (lo + hi); + double my_sum = (lane == 0) ? (w * dhat_w / (dhat_w2 + 2.0 * lam)) * (w * dhat_w / (dhat_w2 + 2.0 * lam)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + double tt = primal_solution[start + m] * dh / (dh2 + 2.0 * lam); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt = z / (1.0 - 2.0 * lam); + double f = my_sum - zt * zt; + if (z_pos) + { + if (f > 0.0) + lo = lam; + else + hi = lam; + } + else + { + if (f > 0.0) + hi = lam; + else + lo = lam; + } + if ((hi - lo) / (1.0 + hi + lo) < 1e-13) + break; + } + double lam = 0.5 * (lo + hi); + if (lane == 0) + warm_start[blk] = lam; + + for (int m = lane; m < k; m += 32) + { + double dh = variable_rescaling[start + m] / d_z; + double dh2 = dh * dh; + primal_solution[start + m] = primal_solution[start + m] * dh2 / (dh2 + 2.0 * lam); + } + if (lane == 0) + { + primal_solution[start + k + 1] = z / (1.0 - 2.0 * lam); + primal_solution[start + k] = w * dhat_w2 / (dhat_w2 + 2.0 * lam); + } +} + +__global__ void compute_cone_dual_residual_standard_warp_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int tid = blockIdx.x * blockDim.x + threadIdx.x; + int blk = tid >> 5; + int lane = tid & 31; + if (blk >= num_blocks) + return; + + const unsigned MASK = 0xffffffffu; + + int start = start_idx[blk]; + int k = v_dim[blk]; + + int has_fixed = lane == 0 ? cone_section_has_fixed(is_fixed, start, k + 2) : 0; + has_fixed = __shfl_sync(MASK, has_fixed, 0); + if (has_fixed) + { + if (lane == 0) + { + for (int slot = 0; slot < k + 2; ++slot) + { + int index = start + slot; + double residual = objective_vector[index] - dual_product[index]; + dual_residual[index] = is_fixed[index] ? primal_solution[index] : primal_solution[index] - residual; + } + project_standard_soc_section_serial( + dual_residual, variable_rescaling, NULL, 0.0, warm_start + blk, start, k, is_fixed); + for (int slot = 0; slot < k + 2; ++slot) + { + int index = start + slot; + dual_residual[index] = + is_fixed[index] ? 0.0 : (primal_solution[index] - dual_residual[index]) * variable_rescaling[index]; + } + complementarity_residual[blk] = 0.0; + } + return; + } + + double r_w = objective_vector[start + k] - dual_product[start + k]; + double r_z = objective_vector[start + k + 1] - dual_product[start + k + 1]; + + double d_z = variable_rescaling[start + k + 1]; + double e_w = d_z / variable_rescaling[start + k]; + double e_w2 = e_w * e_w; + + int my_diff = (lane == 0 && e_w != 1.0) ? 1 : 0; + for (int m = lane; m < k; m += 32) + { + if (variable_rescaling[start + m] != d_z) + my_diff = 1; + } + for (int o = 16; o > 0; o >>= 1) + my_diff |= __shfl_xor_sync(MASK, my_diff, o); + + if (my_diff == 0) + { + double my_sumsq = (lane == 0) ? r_w * r_w : 0.0; + for (int m = lane; m < k; m += 32) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + my_sumsq += rc_m * rc_m; + } + for (int o = 16; o > 0; o >>= 1) + my_sumsq += __shfl_xor_sync(MASK, my_sumsq, o); + double r = sqrt(my_sumsq); + double v_factor, p_w, p_z; + if (r <= r_z) + { + v_factor = 0.0; + p_w = r_w; + p_z = r_z; + } + else if (r <= -r_z) + { + v_factor = 1.0; + p_w = 0.0; + p_z = 0.0; + } + else + { + double scale = (r_z + r) / (2.0 * r); + v_factor = 1.0 - scale; + p_w = scale * r_w; + p_z = scale * r; + } + for (int m = lane; m < k; m += 32) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + dual_residual[start + m] = rc_m * v_factor * variable_rescaling[start + m]; + } + if (lane == 0) + { + dual_residual[start + k] = (r_w - p_w) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_z - p_z) * variable_rescaling[start + k + 1]; + } + return; + } + + double my_inv = (lane == 0) ? (r_w / e_w) * (r_w / e_w) : 0.0; + double my_pos = (lane == 0) ? (r_w * e_w) * (r_w * e_w) : 0.0; + for (int m = lane; m < k; m += 32) + { + double e_m = d_z / variable_rescaling[start + m]; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + my_inv += (rc_m / e_m) * (rc_m / e_m); + my_pos += (rc_m * e_m) * (rc_m * e_m); + } + for (int o = 16; o > 0; o >>= 1) + { + my_inv += __shfl_xor_sync(MASK, my_inv, o); + my_pos += __shfl_xor_sync(MASK, my_pos, o); + } + double r_inv = sqrt(my_inv); + double r_pos = sqrt(my_pos); + + if (r_inv <= r_z) + { + for (int m = lane; m < k; m += 32) + dual_residual[start + m] = 0.0; + if (lane == 0) + { + dual_residual[start + k] = 0.0; + dual_residual[start + k + 1] = 0.0; + } + return; + } + if (r_pos <= -r_z) + { + for (int m = lane; m < k; m += 32) + { + double rc_m = objective_vector[start + m] - dual_product[start + m]; + dual_residual[start + m] = rc_m * variable_rescaling[start + m]; + } + if (lane == 0) + { + dual_residual[start + k] = r_w * variable_rescaling[start + k]; + dual_residual[start + k + 1] = r_z * variable_rescaling[start + k + 1]; + } + return; + } + + double lo, hi; + bool z_pos = (r_z > 0.0); + if (z_pos) + { + lo = 0.0; + hi = 0.5 - 1e-14; + } + else + { + lo = 0.5 + 1e-14; + hi = 1.0; + for (int doubling = 0; doubling < 60; ++doubling) + { + double my_sum = (lane == 0) ? (r_w * e_w / (e_w2 + 2.0 * hi)) * (r_w * e_w / (e_w2 + 2.0 * hi)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double tt = rc_m * e_m / (e_m2 + 2.0 * hi); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt_hi = r_z / (1.0 - 2.0 * hi); + double f_hi = my_sum - zt_hi * zt_hi; + if (f_hi > 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_lam = warm_start[blk]; + if (warm_lam > lo && warm_lam < hi) + { + double my_sum = + (lane == 0) ? (r_w * e_w / (e_w2 + 2.0 * warm_lam)) * (r_w * e_w / (e_w2 + 2.0 * warm_lam)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double tt = rc_m * e_m / (e_m2 + 2.0 * warm_lam); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt = r_z / (1.0 - 2.0 * warm_lam); + double f = my_sum - zt * zt; + if (fabs(f) < 1e-12) + { + double p_z_w = r_z / (1.0 - 2.0 * warm_lam); + double p_w_w = r_w * e_w2 / (e_w2 + 2.0 * warm_lam); + for (int m = lane; m < k; m += 32) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double p_m = rc_m * e_m2 / (e_m2 + 2.0 * warm_lam); + dual_residual[start + m] = (rc_m - p_m) * variable_rescaling[start + m]; + } + if (lane == 0) + { + dual_residual[start + k] = (r_w - p_w_w) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_z - p_z_w) * variable_rescaling[start + k + 1]; + } + return; + } + if (z_pos) + { + if (f > 0.0) + lo = warm_lam; + else + hi = warm_lam; + } + else + { + if (f > 0.0) + hi = warm_lam; + else + lo = warm_lam; + } + } + + for (int it = 0; it < 60; ++it) + { + double lam = 0.5 * (lo + hi); + double my_sum = (lane == 0) ? (r_w * e_w / (e_w2 + 2.0 * lam)) * (r_w * e_w / (e_w2 + 2.0 * lam)) : 0.0; + for (int m = lane; m < k; m += 32) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double tt = rc_m * e_m / (e_m2 + 2.0 * lam); + my_sum += tt * tt; + } + for (int o = 16; o > 0; o >>= 1) + my_sum += __shfl_xor_sync(MASK, my_sum, o); + double zt = r_z / (1.0 - 2.0 * lam); + double f = my_sum - zt * zt; + if (z_pos) + { + if (f > 0.0) + lo = lam; + else + hi = lam; + } + else + { + if (f > 0.0) + hi = lam; + else + lo = lam; + } + if ((hi - lo) / (1.0 + hi + lo) < 1e-13) + break; + } + double lam = 0.5 * (lo + hi); + if (lane == 0) + warm_start[blk] = lam; + + double p_z = r_z / (1.0 - 2.0 * lam); + double p_w = r_w * e_w2 / (e_w2 + 2.0 * lam); + + for (int m = lane; m < k; m += 32) + { + double e_m = d_z / variable_rescaling[start + m]; + double e_m2 = e_m * e_m; + double rc_m = objective_vector[start + m] - dual_product[start + m]; + double p_m = rc_m * e_m2 / (e_m2 + 2.0 * lam); + dual_residual[start + m] = (rc_m - p_m) * variable_rescaling[start + m]; + } + if (lane == 0) + { + dual_residual[start + k] = (r_w - p_w) * variable_rescaling[start + k]; + dual_residual[start + k + 1] = (r_z - p_z) * variable_rescaling[start + k + 1]; + } +} + +/* Project onto D K_exp via Parikh-Boyd Newton on rho = u_1/u_2 (u = D^{-1} x). */ +__device__ static inline void project_exp_cone_point( + double r1, double r2, double r3, double d1, double d2, double d3, double *xo, double *yo, double *zo) +{ + const double E_CONST = 2.718281828459045; + double rr1 = r1 / d1, rr2 = r2 / d2, rr3 = r3 / d3; + + if (rr2 > 0.0) + { + double ratio = rr1 / rr2; + if (ratio < 700.0 && rr2 * exp(ratio) <= rr3) + { + *xo = r1; + *yo = r2; + *zo = r3; + return; + } + } + else if (rr2 == 0.0 && rr1 <= 0.0 && rr3 >= 0.0) + { + *xo = r1; + *yo = r2; + *zo = r3; + return; + } + + if (r1 > 0.0) + { + double ratio = (d2 * r2) / (d1 * r1); + if (ratio < 700.0 && d1 * r1 * exp(ratio) + E_CONST * d3 * r3 <= 0.0) + { + *xo = 0.0; + *yo = 0.0; + *zo = 0.0; + return; + } + } + else if (r1 == 0.0 && r2 <= 0.0 && r3 <= 0.0) + { + *xo = 0.0; + *yo = 0.0; + *zo = 0.0; + return; + } + + if (rr1 <= 0.0 && rr2 <= 0.0) + { + *xo = r1; + *yo = 0.0; + *zo = (rr3 < 0.0) ? 0.0 : r3; + return; + } + + double alpha = (d3 / d1) * (d3 / d1); + double beta = (d3 / d2) * (d3 / d2); + double rho = 0.0; + bool diverged = false; + for (int it = 0; it < 100; ++it) + { + if (rho >= 299.0 || rho <= -299.0) + { + diverged = true; + break; + } + double e_rho = exp(rho); + double e_2rho = e_rho * e_rho; + double one_m_rho = 1.0 - rho; + + double a_term = alpha - beta * rho * one_m_rho; + double b_term = beta * rr1 * one_m_rho - alpha * rr2; + double f = rr1 - rr2 * rho + rr3 * e_rho * a_term + e_2rho * b_term; + + double da_drho = -beta * (1.0 - 2.0 * rho); + double db_drho = -beta * rr1; + double df = -rr2 + rr3 * e_rho * (a_term + da_drho) + e_2rho * (2.0 * b_term + db_drho); + + if (fabs(df) < 1e-300) + break; + double step = f / df; + if (step > 10.0) + step = 10.0; + if (step < -10.0) + step = -10.0; + rho -= step; + if (fabs(step) < 1e-13 * (1.0 + fabs(rho))) + break; + } + + double e_rho = exp(rho); + double denom = rho + alpha * e_rho * e_rho; + double u2 = (rr1 + alpha * rr3 * e_rho) / denom; + + if (diverged || !isfinite(u2) || u2 <= 0.0) + { + *xo = (r1 < 0.0) ? r1 : 0.0; + *yo = 0.0; + *zo = (r3 > 0.0) ? r3 : 0.0; + return; + } + + double u1 = rho * u2; + double u3 = u2 * e_rho; + + *xo = d1 * u1; + *yo = d2 * u2; + *zo = d3 * u3; +} + +/* y-fixed cross-section of D K_exp: weighted 1D Newton-bisection on u = exp((rz/d_r)/y_eff). */ +__device__ static inline void project_2d_exp_persp( + double rz0, double ry, double rt0, double d_r, double d_y, double d_t, double *warm_start, double *rzo, double *rto) +{ + if (d_r <= 0.0 || d_y <= 0.0 || d_t <= 0.0) + { + *rzo = rz0; + *rto = rt0; + return; + } + double y_eff = ry / d_y; + if (y_eff <= 0.0) + { + *rzo = (rz0 < 0.0) ? rz0 : 0.0; + *rto = (rt0 > 0.0) ? rt0 : 0.0; + return; + } + + double arg = (rz0 / d_r) / y_eff; + if (arg < 700.0) + { + double rhs = y_eff * d_t * exp(arg); + if (rhs <= rt0) + { + *rzo = rz0; + *rto = rt0; + return; + } + } + + double a = d_t * d_t * y_eff; + double b = d_t * rt0; + double c = d_r * d_r * y_eff; + double e = d_r * rz0; + + double u = *warm_start; + double u_lo = 1e-30; + double u_hi = 1.0; + for (int g = 0; g < 200; ++g) + { + double lu = log(u_hi); + double f_hi = a * u_hi * u_hi - b * u_hi + c * lu - e; + if (isfinite(f_hi) && f_hi > 0.0) + break; + u_hi *= 4.0; + if (u_hi > 1e150) + break; + } + for (int g = 0; g < 200; ++g) + { + double lu = log(u_lo); + double f_lo = a * u_lo * u_lo - b * u_lo + c * lu - e; + if (isfinite(f_lo) && f_lo < 0.0) + break; + u_lo *= 0.25; + if (u_lo < 1e-300) + break; + } + if (u_lo >= u_hi) + { + *rzo = rz0; + *rto = rt0; + return; + } + + if (!(u > u_lo && u < u_hi) || !isfinite(u)) + u = exp(0.5 * (log(u_lo) + log(u_hi))); + + for (int it = 0; it < 80; ++it) + { + double lu = log(u); + double f = a * u * u - b * u + c * lu - e; + double df = 2.0 * a * u - b + c / u; + if (f > 0.0) + u_hi = u; + else + u_lo = u; + double u_new; + if (df > 1e-300 && isfinite(df) && isfinite(f)) + { + u_new = u - f / df; + if (!isfinite(u_new) || u_new <= u_lo || u_new >= u_hi) + u_new = exp(0.5 * (log(u_lo) + log(u_hi))); + } + else + { + u_new = exp(0.5 * (log(u_lo) + log(u_hi))); + } + if (fabs(u_new - u) < 1e-14 * (1.0 + fabs(u_new))) + { + u = u_new; + break; + } + u = u_new; + } + *warm_start = u; + *rzo = d_r * y_eff * log(u); + *rto = d_t * y_eff * u; +} + +__device__ static inline double exp_cone_boundary(double x, double y) +{ + if (!(y > 0.0)) + return x <= 0.0 ? 0.0 : INFINITY; + double exponent = x / y; + double log_value = log(y) + exponent; + if (log_value >= log(DBL_MAX)) + return INFINITY; + if (log_value <= log(DBL_MIN)) + return 0.0; + return exp(log_value); +} + +__device__ static inline bool exp_cone_contains_point(double x, double y, double z) +{ + if (y > 0.0 && z > 0.0) + { + double lhs = log(y) + x / y; + double rhs = log(z); + double tolerance = 64.0 * DBL_EPSILON * (1.0 + fabs(lhs) + fabs(rhs)); + return lhs <= rhs + tolerance; + } + return y == 0.0 && x <= 0.0 && z >= 0.0; +} + +__device__ static inline double +exp_fixed_x_objective(double y, double x, double input_y, double input_z, double weight_y, double weight_z) +{ + double z = exp_cone_boundary(x, y); + if (!isfinite(z)) + return INFINITY; + double dy = y - input_y; + double dz = z - input_z; + return weight_y * dy * dy + weight_z * dz * dz; +} + +__device__ static inline double +exp_fixed_z_objective(double y, double z, double input_x, double input_y, double weight_x, double weight_y) +{ + double x = y > 0.0 ? y * (log(z) - log(y)) : 0.0; + double dx = x - input_x; + double dy = y - input_y; + return weight_x * dx * dx + weight_y * dy * dy; +} + +__device__ static inline double exp_xz_log_violation(double y, double x, double z) +{ + if (!(y > 0.0) || !(z > 0.0)) + return x <= 0.0 ? -INFINITY : INFINITY; + return log(y) + x / y - log(z); +} + +__device__ static inline void project_exp_cone_section(double *point, + const double *rescaling, + const double *q_diag, + double tau, + double *warm_start, + int start, + const char *is_fixed) +{ + bool fixed_x = is_fixed[start + 0] != 0; + bool fixed_y = is_fixed[start + 1] != 0; + bool fixed_z = is_fixed[start + 2] != 0; + double input_x = point[start + 0] / rescaling[start + 0]; + double input_y = point[start + 1] / rescaling[start + 1]; + double input_z = point[start + 2] / rescaling[start + 2]; + + if (exp_cone_contains_point(input_x, input_y, input_z) || (fixed_x && fixed_y && fixed_z)) + return; + + double weight_x = cone_section_weight(rescaling, q_diag, tau, start + 0); + double weight_y = cone_section_weight(rescaling, q_diag, tau, start + 1); + double weight_z = cone_section_weight(rescaling, q_diag, tau, start + 2); + double output_x = input_x; + double output_y = input_y; + double output_z = input_z; + + if (fixed_x && fixed_y) + { + output_z = fmax(input_z, exp_cone_boundary(input_x, input_y)); + } + else if (fixed_y && fixed_z) + { + if (input_y == 0.0) + output_x = fmin(input_x, 0.0); + else + output_x = fmin(input_x, input_y * (log(input_z) - log(input_y))); + } + else if (fixed_x && fixed_z) + { + if (input_z == 0.0) + { + output_y = 0.0; + } + else if (input_x > 0.0) + { + double center = input_x; + double left = fmax(DBL_MIN, input_x / 1024.0); + while (exp_xz_log_violation(left, input_x, input_z) <= 0.0 && left > DBL_MIN) + left *= 0.5; + double lo = left; + double hi = center; + for (int iteration = 0; iteration < 100; ++iteration) + { + double mid = 0.5 * (lo + hi); + if (exp_xz_log_violation(mid, input_x, input_z) > 0.0) + lo = mid; + else + hi = mid; + } + double lower = 0.5 * (lo + hi); + + lo = center; + hi = fmax(2.0 * center, input_z); + while (exp_xz_log_violation(hi, input_x, input_z) < 0.0 && hi < DBL_MAX / 4.0) + hi *= 2.0; + for (int iteration = 0; iteration < 100; ++iteration) + { + double mid = 0.5 * (lo + hi); + if (exp_xz_log_violation(mid, input_x, input_z) <= 0.0) + lo = mid; + else + hi = mid; + } + double upper = 0.5 * (lo + hi); + output_y = fmin(fmax(input_y, lower), upper); + } + else + { + double lo = 0.0; + double hi = fmax(1.0, fmax(input_z, fabs(input_x))); + while (exp_xz_log_violation(hi, input_x, input_z) < 0.0 && hi < DBL_MAX / 4.0) + hi *= 2.0; + for (int iteration = 0; iteration < 100; ++iteration) + { + double mid = 0.5 * (lo + hi); + if (exp_xz_log_violation(mid, input_x, input_z) <= 0.0) + lo = mid; + else + hi = mid; + } + output_y = fmin(fmax(input_y, 0.0), 0.5 * (lo + hi)); + } + } + else if (fixed_y) + { + if (!(input_y > 0.0)) + { + output_x = fmin(input_x, 0.0); + output_z = fmax(input_z, 0.0); + } + else + { + double effective_x = sqrt(weight_x); + double effective_y = sqrt(weight_y); + double effective_z = sqrt(weight_z); + double scaled_x; + double scaled_z; + project_2d_exp_persp(effective_x * input_x, + effective_y * input_y, + effective_z * input_z, + effective_x, + effective_y, + effective_z, + warm_start, + &scaled_x, + &scaled_z); + output_x = scaled_x / effective_x; + output_z = scaled_z / effective_z; + } + } + else if (fixed_x) + { + double scale = 1.0 + fabs(input_x) + fabs(input_y) + fabs(input_z); + double lo = input_x > 0.0 ? fmax(DBL_MIN, input_x / 700.0) : 0.0; + double hi = scale; + double previous = exp_fixed_x_objective(0.5 * hi, input_x, input_y, input_z, weight_y, weight_z); + double current = exp_fixed_x_objective(hi, input_x, input_y, input_z, weight_y, weight_z); + for (int expansion = 0; expansion < 80 && current < previous && hi < DBL_MAX / 4.0; ++expansion) + { + previous = current; + hi *= 2.0; + current = exp_fixed_x_objective(hi, input_x, input_y, input_z, weight_y, weight_z); + } + const double ratio = 0.6180339887498948482; + double a = lo; + double b = hi; + double c = b - ratio * (b - a); + double d = a + ratio * (b - a); + double fc = exp_fixed_x_objective(c, input_x, input_y, input_z, weight_y, weight_z); + double fd = exp_fixed_x_objective(d, input_x, input_y, input_z, weight_y, weight_z); + for (int iteration = 0; iteration < 100; ++iteration) + { + if (fc <= fd) + { + b = d; + d = c; + fd = fc; + c = b - ratio * (b - a); + fc = exp_fixed_x_objective(c, input_x, input_y, input_z, weight_y, weight_z); + } + else + { + a = c; + c = d; + fc = fd; + d = a + ratio * (b - a); + fd = exp_fixed_x_objective(d, input_x, input_y, input_z, weight_y, weight_z); + } + } + output_y = 0.5 * (a + b); + output_z = exp_cone_boundary(input_x, output_y); + if (input_x <= 0.0) + { + double closure_z = fmax(input_z, 0.0); + double closure_objective = + weight_y * input_y * input_y + weight_z * (closure_z - input_z) * (closure_z - input_z); + double smooth_objective = exp_fixed_x_objective(output_y, input_x, input_y, input_z, weight_y, weight_z); + if (closure_objective <= smooth_objective) + { + output_y = 0.0; + output_z = closure_z; + } + } + } + else if (fixed_z) + { + if (input_z == 0.0) + { + output_x = fmin(input_x, 0.0); + output_y = 0.0; + } + else + { + double scale = 1.0 + fabs(input_x) + fabs(input_y) + input_z; + double lo = 0.0; + double hi = scale; + double previous = exp_fixed_z_objective(0.5 * hi, input_z, input_x, input_y, weight_x, weight_y); + double current = exp_fixed_z_objective(hi, input_z, input_x, input_y, weight_x, weight_y); + for (int expansion = 0; expansion < 80 && current < previous && hi < DBL_MAX / 4.0; ++expansion) + { + previous = current; + hi *= 2.0; + current = exp_fixed_z_objective(hi, input_z, input_x, input_y, weight_x, weight_y); + } + const double ratio = 0.6180339887498948482; + double a = lo; + double b = hi; + double c = b - ratio * (b - a); + double d = a + ratio * (b - a); + double fc = exp_fixed_z_objective(c, input_z, input_x, input_y, weight_x, weight_y); + double fd = exp_fixed_z_objective(d, input_z, input_x, input_y, weight_x, weight_y); + for (int iteration = 0; iteration < 100; ++iteration) + { + if (fc <= fd) + { + b = d; + d = c; + fd = fc; + c = b - ratio * (b - a); + fc = exp_fixed_z_objective(c, input_z, input_x, input_y, weight_x, weight_y); + } + else + { + a = c; + c = d; + fc = fd; + d = a + ratio * (b - a); + fd = exp_fixed_z_objective(d, input_z, input_x, input_y, weight_x, weight_y); + } + } + output_y = 0.5 * (a + b); + output_x = output_y > 0.0 ? output_y * (log(input_z) - log(output_y)) : 0.0; + double closure_x = fmin(input_x, 0.0); + double closure_objective = + weight_x * (closure_x - input_x) * (closure_x - input_x) + weight_y * input_y * input_y; + double smooth_objective = exp_fixed_z_objective(output_y, input_z, input_x, input_y, weight_x, weight_y); + if (closure_objective <= smooth_objective) + { + output_x = closure_x; + output_y = 0.0; + } + } + } + + if (!fixed_x) + point[start + 0] = output_x * rescaling[start + 0]; + if (!fixed_y) + point[start + 1] = output_y * rescaling[start + 1]; + if (!fixed_z) + point[start + 2] = output_z * rescaling[start + 2]; +} + +__global__ void project_exp_cone_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + (void)v_dim; + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int s_idx = start_idx[blk]; + double r1 = primal_solution[s_idx + 0]; + double r2 = primal_solution[s_idx + 1]; + double r3 = primal_solution[s_idx + 2]; + + double d1 = variable_rescaling[s_idx + 0]; + double d2 = variable_rescaling[s_idx + 1]; + double d3 = variable_rescaling[s_idx + 2]; + + if (cone_section_has_fixed(is_fixed, s_idx, 3)) + { + project_exp_cone_section(primal_solution, variable_rescaling, NULL, 0.0, warm_start + blk, s_idx, is_fixed); + return; + } + + double xo, yo, zo; + project_exp_cone_point(r1, r2, r3, d1, d2, d3, &xo, &yo, &zo); + + primal_solution[s_idx + 0] = xo; + primal_solution[s_idx + 1] = yo; + primal_solution[s_idx + 2] = zo; +} + +__global__ void compute_cone_dual_residual_exp_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + (void)v_dim; + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int s_idx = start_idx[blk]; + double r1 = objective_vector[s_idx + 0] - dual_product[s_idx + 0]; + double r2 = objective_vector[s_idx + 1] - dual_product[s_idx + 1]; + double r3 = objective_vector[s_idx + 2] - dual_product[s_idx + 2]; + + if (cone_section_has_fixed(is_fixed, s_idx, 3)) + { + const double residual[3] = {r1, r2, r3}; + for (int slot = 0; slot < 3; ++slot) + { + int index = s_idx + slot; + dual_residual[index] = is_fixed[index] ? primal_solution[index] : primal_solution[index] - residual[slot]; + } + project_exp_cone_section(dual_residual, variable_rescaling, NULL, 0.0, warm_start + blk, s_idx, is_fixed); + for (int slot = 0; slot < 3; ++slot) + { + int index = s_idx + slot; + dual_residual[index] = + is_fixed[index] ? 0.0 : (primal_solution[index] - dual_residual[index]) * variable_rescaling[index]; + } + complementarity_residual[blk] = 0.0; + return; + } + + double d1 = 1.0 / variable_rescaling[s_idx + 0]; + double d2 = 1.0 / variable_rescaling[s_idx + 1]; + double d3 = 1.0 / variable_rescaling[s_idx + 2]; + + /* Moreau: dist(r, K_exp^*) = ||-proj_{K_exp}(-r)|| with inverse-scaled d. */ + double xo, yo, zo; + project_exp_cone_point(-r1, -r2, -r3, d1, d2, d3, &xo, &yo, &zo); + + dual_residual[s_idx + 0] = -xo * variable_rescaling[s_idx + 0]; + dual_residual[s_idx + 1] = -yo * variable_rescaling[s_idx + 1]; + dual_residual[s_idx + 2] = -zo * variable_rescaling[s_idx + 2]; +} + +/* 3-dim alpha-power cone K_a = {(x,y,z) : x >= 0, y >= 0, x^a * y^(1-a) >= |z|}. + Weighted projection: solves + min_{(x,y,z) in K_a} 0.5 * ( wx*(x-rx)^2 + wy*(y-ry)^2 + wz*(z-rz)^2 ) + with wi > 0. In-cone test is metric-independent; opposite-cone test is not. + Bisection on rho = |z_proj| in [0, |r_z|] using KKT-derived formulas + x(rho) = 0.5 (rx + sqrt(rx^2 + 4 a (wz/wx) rho (|rz|-rho))) + y(rho) = 0.5 (ry + sqrt(ry^2 + 4 (1-a) (wz/wy) rho (|rz|-rho))) + G(rho) = x^a y^(1-a) - rho. */ +__device__ static inline double positive_quadratic_root(double r, double q) +{ + if (!(q > 0.0)) + return fmax(r, 0.0); + double disc = hypot(r, 2.0 * sqrt(q)); + if (r >= 0.0) + return 0.5 * (r + disc); + return (2.0 * q) / (disc - r); +} + +__device__ static inline void project_power_cone_point_normalized( + double rx, double ry, double rz, double wx, double wy, double wz, double alpha, double *xo, double *yo, double *zo) +{ + double abs_rz = fabs(rz); + double sgn_rz = (rz >= 0.0) ? 1.0 : -1.0; + double om = 1.0 - alpha; + + if (abs_rz == 0.0) + { + *xo = fmax(rx, 0.0); + *yo = fmax(ry, 0.0); + *zo = 0.0; + return; + } + + if (rx > 0.0 && ry > 0.0) + { + if (alpha * log(rx) + om * log(ry) >= log(abs_rz)) + { + *xo = rx; + *yo = ry; + *zo = rz; + return; + } + } + + /* Opposite cone under weighted inner product: + proj^w(r) = 0 iff (wx*rx, wy*ry, wz*rz) in -K_a^*, i.e., + (-wx*rx)/a)^a * ((-wy*ry)/(1-a))^(1-a) >= wz*|rz|, rx <= 0, ry <= 0. */ + if (rx <= 0.0 && ry <= 0.0) + { + double u = (rx < 0.0) ? (-wx * rx) / alpha : 0.0; + double v = (ry < 0.0) ? (-wy * ry) / om : 0.0; + if (u > 0.0 && v > 0.0 && alpha * log(u) + om * log(v) >= log(wz) + log(abs_rz)) + { + *xo = 0.0; + *yo = 0.0; + *zo = 0.0; + return; + } + } + + double c_x = 4.0 * alpha * (wz / wx); + double c_y = 4.0 * om * (wz / wy); + + /* + * Bisect in log(rho). When one input axis is negative and alpha is close + * to an endpoint, the positive root can be many orders of magnitude below + * |r_z|. A linear relative floor would then converge to an infeasible + * point instead of the nonzero root. + */ + double lo = log(DBL_MIN); + double hi = log(abs_rz); + if (!(hi > lo)) + { + *xo = fmax(rx, 0.0); + *yo = fmax(ry, 0.0); + *zo = 0.0; + return; + } + + for (int it = 0; it < 80; ++it) + { + double log_rho = lo + 0.5 * (hi - lo); + double rho = exp(log_rho); + double x = positive_quadratic_root(rx, 0.25 * c_x * rho * (abs_rz - rho)); + double y = positive_quadratic_root(ry, 0.25 * c_y * rho * (abs_rz - rho)); + bool above_boundary = x > 0.0 && y > 0.0 && alpha * log(x) + om * log(y) > log_rho; + if (above_boundary) + lo = log_rho; + else + hi = log_rho; + } + double rho = exp(lo + 0.5 * (hi - lo)); + *xo = positive_quadratic_root(rx, 0.25 * c_x * rho * (abs_rz - rho)); + *yo = positive_quadratic_root(ry, 0.25 * c_y * rho * (abs_rz - rho)); + double log_bound = alpha * log(*xo) + om * log(*yo); + *zo = sgn_rz * fmin(rho, exp(log_bound)); +} + +__device__ static inline void project_power_cone_point( + double rx, double ry, double rz, double wx, double wy, double wz, double alpha, double *xo, double *yo, double *zo) +{ + /* The cone and weighted projection are positively homogeneous. Normalize + the point so products such as rho * (|r_z| - rho) cannot overflow. */ + double scale = fmax(fabs(rx), fmax(fabs(ry), fabs(rz))); + if (!(scale > 0.0) || !isfinite(scale)) + { + project_power_cone_point_normalized(rx, ry, rz, wx, wy, wz, alpha, xo, yo, zo); + return; + } + + double xn, yn, zn; + project_power_cone_point_normalized(rx / scale, ry / scale, rz / scale, wx, wy, wz, alpha, &xn, &yn, &zn); + *xo = xn * scale; + *yo = yn * scale; + *zo = zn * scale; +} + +/* Project x,y while z is fixed. The active boundary is x^a y^(1-a) = |z|. */ +__device__ static inline double +power_xy_log_boundary(double lambda, double rx, double ry, double wx, double wy, double alpha) +{ + double om = 1.0 - alpha; + double x = positive_quadratic_root(rx, (lambda / wx) * alpha); + double y = positive_quadratic_root(ry, (lambda / wy) * om); + if (!(x > 0.0) || !(y > 0.0)) + return -INFINITY; + return alpha * log(x) + om * log(y); +} + +__device__ static inline void project_power_xy_fixed_z_normalized( + double rx, double ry, double fixed_z, double wx, double wy, double alpha, double *xo, double *yo) +{ + double c = fabs(fixed_z); + double om = 1.0 - alpha; + if (c == 0.0) + { + *xo = fmax(rx, 0.0); + *yo = fmax(ry, 0.0); + return; + } + + if (rx > 0.0 && ry > 0.0 && alpha * log(rx) + om * log(ry) >= log(c)) + { + *xo = rx; + *yo = ry; + return; + } + + double target = log(c); + double hi = fmin(wx, wy); + if (!(hi > 0.0) || !isfinite(hi)) + hi = 1.0; + for (int it = 0; it < 2048; ++it) + { + double log_boundary = power_xy_log_boundary(hi, rx, ry, wx, wy, alpha); + if (log_boundary >= target || isnan(log_boundary)) + break; + if (hi >= 0.5 * DBL_MAX) + { + hi = DBL_MAX; + break; + } + hi *= 2.0; + } + + double lambda; + double floor_log_boundary = power_xy_log_boundary(DBL_MIN, rx, ry, wx, wy, alpha); + if (floor_log_boundary >= target) + { + double lo = 0.0; + double floor_hi = DBL_MIN; + for (int it = 0; it < 80; ++it) + { + double candidate = lo + 0.5 * (floor_hi - lo); + if (power_xy_log_boundary(candidate, rx, ry, wx, wy, alpha) < target) + lo = candidate; + else + floor_hi = candidate; + } + lambda = lo + 0.5 * (floor_hi - lo); + } + else + { + double log_lo = log(DBL_MIN); + double log_hi = log(hi); + for (int it = 0; it < 96; ++it) + { + double log_lambda = log_lo + 0.5 * (log_hi - log_lo); + double candidate = exp(log_lambda); + if (power_xy_log_boundary(candidate, rx, ry, wx, wy, alpha) < target) + log_lo = log_lambda; + else + log_hi = log_lambda; + } + lambda = exp(log_lo + 0.5 * (log_hi - log_lo)); + } + *xo = positive_quadratic_root(rx, (lambda / wx) * alpha); + *yo = positive_quadratic_root(ry, (lambda / wy) * om); +} + +__device__ static inline void project_power_xy_fixed_z( + double rx, double ry, double fixed_z, double wx, double wy, double alpha, double *xo, double *yo) +{ + double scale = fmax(fabs(rx), fmax(fabs(ry), fabs(fixed_z))); + if (!(scale > 0.0) || !isfinite(scale)) + { + project_power_xy_fixed_z_normalized(rx, ry, fixed_z, wx, wy, alpha, xo, yo); + return; + } + + double xn, yn; + project_power_xy_fixed_z_normalized(rx / scale, ry / scale, fixed_z / scale, wx, wy, alpha, &xn, &yn); + *xo = xn * scale; + *yo = yn * scale; +} + +/* With one nonnegative axis fixed, project the other axis and z onto + |z| <= fixed_axis^fixed_exp * other^other_exp. On the active boundary, + direct bisection in other is stable even when the KKT multiplier is tiny. */ +__device__ static inline double power_exp_from_log(double log_value) +{ + if (log_value >= log(DBL_MAX)) + return INFINITY; + if (log_value <= log(DBL_MIN)) + return 0.0; + return exp(log_value); +} + +__device__ static inline double power_section_derivative( + double other, double r_other, double abs_rz, double w_other, double wz, double log_coefficient, double other_exp) +{ + if (!(other > 0.0)) + return -INFINITY; + + double log_other = log(other); + double bound = power_exp_from_log(log_coefficient + other_exp * log_other); + double slope = power_exp_from_log(log_coefficient + log(other_exp) + (other_exp - 1.0) * log_other); + double linear_term = w_other * (other - r_other); + double gap = bound - abs_rz; + if (gap == 0.0 || slope == 0.0) + return linear_term; + if (!isfinite(slope)) + return copysign(INFINITY, gap); + return linear_term + wz * gap * slope; +} + +__device__ static inline void project_power_section_fixed_axis_normalized(double fixed_axis, + double r_other, + double rz, + double w_other, + double wz, + double fixed_exp, + double other_exp, + double *other_out, + double *z_out) +{ + double abs_rz = fabs(rz); + if (!(fixed_axis > 0.0) || abs_rz == 0.0) + { + *other_out = fmax(r_other, 0.0); + *z_out = 0.0; + return; + } + double log_coefficient = fixed_exp * log(fixed_axis); + + if (r_other > 0.0 && log_coefficient + other_exp * log(r_other) >= log(abs_rz)) + { + *other_out = r_other; + *z_out = rz; + return; + } + + double log_feasible_other = (log(abs_rz) - log_coefficient) / other_exp; + double feasible_other = power_exp_from_log(log_feasible_other); + if (feasible_other == 0.0) + { + *other_out = 0.0; + *z_out = 0.0; + return; + } + + double lo = 0.0; + double hi = fmax(1.0, fmax(r_other, 0.0)); + if (isfinite(feasible_other)) + hi = fmin(hi, feasible_other); + for (int it = 0; it < 1024; ++it) + { + double derivative = power_section_derivative(hi, r_other, abs_rz, w_other, wz, log_coefficient, other_exp); + if (!(derivative < 0.0)) + break; + if (isfinite(feasible_other) && hi >= feasible_other) + break; + double next_hi = hi * 2.0; + if (!isfinite(next_hi)) + { + hi = isfinite(feasible_other) ? feasible_other : DBL_MAX; + break; + } + hi = isfinite(feasible_other) ? fmin(next_hi, feasible_other) : next_hi; + } + + for (int it = 0; it < 80; ++it) + { + double other = lo + 0.5 * (hi - lo); + if (other == 0.0) + break; + double derivative = power_section_derivative(other, r_other, abs_rz, w_other, wz, log_coefficient, other_exp); + if (derivative < 0.0) + lo = other; + else + hi = other; + } + double other = lo + 0.5 * (hi - lo); + double projected_abs_z = other > 0.0 ? power_exp_from_log(log_coefficient + other_exp * log(other)) : 0.0; + *other_out = other; + *z_out = copysign(fmin(projected_abs_z, abs_rz), rz); +} + +__device__ static inline void project_power_section_fixed_axis(double fixed_axis, + double r_other, + double rz, + double w_other, + double wz, + double fixed_exp, + double other_exp, + double *other_out, + double *z_out) +{ + double scale = fmax(fixed_axis, fmax(fabs(r_other), fabs(rz))); + if (!(scale > 0.0) || !isfinite(scale)) + { + project_power_section_fixed_axis_normalized( + fixed_axis, r_other, rz, w_other, wz, fixed_exp, other_exp, other_out, z_out); + return; + } + + double normalized_other, normalized_z; + project_power_section_fixed_axis_normalized(fixed_axis / scale, + r_other / scale, + rz / scale, + w_other, + wz, + fixed_exp, + other_exp, + &normalized_other, + &normalized_z); + *other_out = normalized_other * scale; + *z_out = normalized_z * scale; +} + +__device__ static inline void project_power_cone_point_with_fixed(double rx, + double ry, + double rz, + double wx, + double wy, + double wz, + double alpha, + bool fixed_x, + bool fixed_y, + bool fixed_z, + double *xo, + double *yo, + double *zo) +{ + double om = 1.0 - alpha; + *xo = rx; + *yo = ry; + *zo = rz; + + if (!fixed_x && !fixed_y && !fixed_z) + { + project_power_cone_point(rx, ry, rz, wx, wy, wz, alpha, xo, yo, zo); + return; + } + + if (fixed_z) + { + if (fixed_x && fixed_y) + return; + if (fixed_x) + { + double lower = fabs(rz) == 0.0 ? 0.0 : exp((log(fabs(rz)) - alpha * log(rx)) / om); + *yo = fmax(ry, lower); + return; + } + if (fixed_y) + { + double lower = fabs(rz) == 0.0 ? 0.0 : exp((log(fabs(rz)) - om * log(ry)) / alpha); + *xo = fmax(rx, lower); + return; + } + project_power_xy_fixed_z(rx, ry, rz, wx, wy, alpha, xo, yo); + return; + } + + if (fixed_x && fixed_y) + { + double bound = 0.0; + if (rx > 0.0 && ry > 0.0) + { + double log_bound = alpha * log(rx) + om * log(ry); + bound = log_bound < log(DBL_MAX) ? exp(log_bound) : INFINITY; + } + *zo = fmax(-bound, fmin(rz, bound)); + return; + } + if (fixed_x) + { + project_power_section_fixed_axis(rx, ry, rz, wy, wz, alpha, om, yo, zo); + return; + } + if (fixed_y) + { + project_power_section_fixed_axis(ry, rx, rz, wx, wz, om, alpha, xo, zo); + return; + } +} + +__global__ void project_power_cone_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const double *__restrict__ power_alpha, + const char *__restrict__ is_fixed, + int num_blocks) +{ + (void)v_dim; + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int s_idx = start_idx[blk]; + double r1 = primal_solution[s_idx + 0]; + double r2 = primal_solution[s_idx + 1]; + double r3 = primal_solution[s_idx + 2]; + + double d1 = variable_rescaling[s_idx + 0]; + double d2 = variable_rescaling[s_idx + 1]; + double d3 = variable_rescaling[s_idx + 2]; + double alpha = power_alpha[blk]; + + /* Prox in scaled space with metric I equals prox in actual space with metric diag(d^2). */ + double rx = r1 / d1; + double ry = r2 / d2; + double rz = r3 / d3; + double wx = d1 * d1; + double wy = d2 * d2; + double wz = d3 * d3; + double xo, yo, zo; + project_power_cone_point_with_fixed(rx, + ry, + rz, + wx, + wy, + wz, + alpha, + is_fixed && is_fixed[s_idx + 0], + is_fixed && is_fixed[s_idx + 1], + is_fixed && is_fixed[s_idx + 2], + &xo, + &yo, + &zo); + primal_solution[s_idx + 0] = xo * d1; + primal_solution[s_idx + 1] = yo * d2; + primal_solution[s_idx + 2] = zo * d3; +} + +__global__ void compute_cone_dual_residual_power_kernel(double *__restrict__ dual_residual, + double *__restrict__ complementarity_residual, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const double *__restrict__ variable_rescaling, + const double *__restrict__ primal_solution, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const double *__restrict__ power_alpha, + const char *__restrict__ is_fixed, + int num_blocks) +{ + (void)warm_start; + (void)v_dim; + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int s_idx = start_idx[blk]; + double r1 = objective_vector[s_idx + 0] - dual_product[s_idx + 0]; + double r2 = objective_vector[s_idx + 1] - dual_product[s_idx + 1]; + double r3 = objective_vector[s_idx + 2] - dual_product[s_idx + 2]; + double alpha = power_alpha[blk]; + + bool fixed_x = is_fixed && is_fixed[s_idx + 0]; + bool fixed_y = is_fixed && is_fixed[s_idx + 1]; + bool fixed_z = is_fixed && is_fixed[s_idx + 2]; + if (fixed_x || fixed_y || fixed_z) + { + double d1 = variable_rescaling[s_idx + 0]; + double d2 = variable_rescaling[s_idx + 1]; + double d3 = variable_rescaling[s_idx + 2]; + double x = primal_solution[s_idx + 0] / d1; + double y = primal_solution[s_idx + 1] / d2; + double z = primal_solution[s_idx + 2] / d3; + double q1 = r1 * d1; + double q2 = r2 * d2; + double q3 = r3 * d3; + + dual_residual[s_idx + 0] = fixed_x ? 0.0 : q1; + dual_residual[s_idx + 1] = fixed_y ? 0.0 : q2; + dual_residual[s_idx + 2] = fixed_z ? 0.0 : q3; + if (fixed_x && fixed_y && fixed_z) + return; + + double abs_z = fabs(z); + double bound = 0.0; + bool regular = x > 0.0 && y > 0.0 && isfinite(x) && isfinite(y) && isfinite(z); + if (regular) + { + double log_bound = alpha * log(x) + (1.0 - alpha) * log(y); + bound = exp(log_bound); + regular = isfinite(bound) && bound > 0.0; + } + + if (regular && abs_z > 0.0) + { + double normal[3] = { + -alpha * bound / x, + -(1.0 - alpha) * bound / y, + copysign(1.0, z), + }; + double q[3] = {q1, q2, q3}; + bool fixed[3] = {fixed_x, fixed_y, fixed_z}; + double normal_scale = 0.0; + for (int i = 0; i < 3; ++i) + { + if (!fixed[i]) + normal_scale = fmax(normal_scale, fabs(normal[i])); + } + if (!(normal_scale > 0.0) || !isfinite(normal_scale)) + { + regular = false; + } + + double dot = 0.0; + double normal2 = 0.0; + for (int i = 0; i < 3 && regular; ++i) + { + if (!fixed[i]) + { + double scaled_normal = normal[i] / normal_scale; + dot += q[i] * scaled_normal; + normal2 += scaled_normal * scaled_normal; + } + } + if (regular) + { + double scaled_lambda = (dot < 0.0 && normal2 > 0.0) ? -dot / normal2 : 0.0; + double lambda = scaled_lambda / normal_scale; + for (int i = 0; i < 3; ++i) + { + if (!fixed[i]) + dual_residual[s_idx + i] = q[i] + scaled_lambda * (normal[i] / normal_scale); + } + double slack_scale = fmax(1.0, fmax(bound, abs_z)); + double complementarity = lambda * (fmax(bound - abs_z, 0.0) / slack_scale); + complementarity_residual[blk] = complementarity; + return; + } + } + + if (regular) + return; + + /* Degenerate axes are nonsmooth. A unit metric projection supplies a + scale-independent KKT guard without changing the adaptive mapping. */ + double rx = x - (fixed_x ? 0.0 : r1 / d1); + double ry = y - (fixed_y ? 0.0 : r2 / d2); + double rz = z - (fixed_z ? 0.0 : r3 / d3); + double xo, yo, zo; + project_power_cone_point_with_fixed( + rx, ry, rz, d1 * d1, d2 * d2, d3 * d3, alpha, fixed_x, fixed_y, fixed_z, &xo, &yo, &zo); + if (!fixed_x) + dual_residual[s_idx + 0] = (x - xo) * d1 * d1; + if (!fixed_y) + dual_residual[s_idx + 1] = (y - yo) * d2 * d2; + if (!fixed_z) + dual_residual[s_idx + 2] = (z - zo) * d3 * d3; + return; + } + + double vr1 = variable_rescaling[s_idx + 0]; + double vr2 = variable_rescaling[s_idx + 1]; + double vr3 = variable_rescaling[s_idx + 2]; + + /* Moreau via primal projection: dual_res = -Proj_K(-r * vr). */ + double xo, yo, zo; + project_power_cone_point(-r1 * vr1, -r2 * vr2, -r3 * vr3, 1.0, 1.0, 1.0, alpha, &xo, &yo, &zo); + + dual_residual[s_idx + 0] = -xo; + dual_residual[s_idx + 1] = -yo; + dual_residual[s_idx + 2] = -zo; +} + +__global__ void compute_power_cone_primal_violation_kernel(double *__restrict__ absolute_violation, + double *__restrict__ relative_violation, + const double *__restrict__ primal_solution, + const double *__restrict__ variable_rescaling, + const int *__restrict__ start_idx, + const double *__restrict__ power_alpha, + double homogeneous_scale, + int num_blocks) +{ + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int start = start_idx[blk]; + double x = primal_solution[start + 0] / variable_rescaling[start + 0]; + double y = primal_solution[start + 1] / variable_rescaling[start + 1]; + double z = primal_solution[start + 2] / variable_rescaling[start + 2]; + if (!isfinite(x) || !isfinite(y) || !isfinite(z)) + { + absolute_violation[blk] = INFINITY; + relative_violation[blk] = INFINITY; + return; + } + double violation = fmax(-x, -y); + double abs_z = fabs(z); + if (abs_z > 0.0) + { + double bound = 0.0; + if (x > 0.0 && y > 0.0) + { + double alpha = power_alpha[blk]; + double log_bound = alpha * log(x) + (1.0 - alpha) * log(y); + double log_abs_z = log(abs_z); + double roundoff_tolerance = 64.0 * DBL_EPSILON * (1.0 + fabs(log_bound) + fabs(log_abs_z)); + if (log_bound + roundoff_tolerance >= log_abs_z) + { + violation = fmax(violation, 0.0); + absolute_violation[blk] = violation; + relative_violation[blk] = violation / (homogeneous_scale + fmax(fabs(x), fmax(fabs(y), abs_z))); + return; + } + bound = exp(log_bound); + } + violation = fmax(violation, abs_z - bound); + } + violation = fmax(violation, 0.0); + absolute_violation[blk] = violation; + relative_violation[blk] = violation / (homogeneous_scale + fmax(fabs(x), fmax(fabs(y), abs_z))); +} + +__global__ void project_power_cone_diag_q_kernel(double *__restrict__ pdhg_primal, + double *__restrict__ reflected_primal, + const double *__restrict__ current_primal, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const double *__restrict__ power_alpha, + const char *__restrict__ is_fixed, + int num_blocks) +{ + (void)warm_start; + (void)v_dim; + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int s_idx = start_idx[blk]; + double r1 = pdhg_primal[s_idx + 0]; + double r2 = pdhg_primal[s_idx + 1]; + double r3 = pdhg_primal[s_idx + 2]; + + double d1 = variable_rescaling[s_idx + 0]; + double d2 = variable_rescaling[s_idx + 1]; + double d3 = variable_rescaling[s_idx + 2]; + double alpha = power_alpha[blk]; + + /* Effective weight in actual space: omega_i = (1 + tau*Q_ii) * d_i^2. */ + double w1 = 1.0 + tau * Q_diag[s_idx + 0]; + double w2 = 1.0 + tau * Q_diag[s_idx + 1]; + double w3 = 1.0 + tau * Q_diag[s_idx + 2]; + double om_x = w1 * d1 * d1; + double om_y = w2 * d2 * d2; + double om_z = w3 * d3 * d3; + double rx = r1 / d1; + double ry = r2 / d2; + double rz = r3 / d3; + double xo, yo, zo; + project_power_cone_point_with_fixed(rx, + ry, + rz, + om_x, + om_y, + om_z, + alpha, + is_fixed && is_fixed[s_idx + 0], + is_fixed && is_fixed[s_idx + 1], + is_fixed && is_fixed[s_idx + 2], + &xo, + &yo, + &zo); + pdhg_primal[s_idx + 0] = xo * d1; + pdhg_primal[s_idx + 1] = yo * d2; + pdhg_primal[s_idx + 2] = zo * d3; + for (int m = 0; m < 3; ++m) + { + int idx = s_idx + m; + reflected_primal[idx] = 2.0 * pdhg_primal[idx] - current_primal[idx]; + } +} + +__global__ void set_cone_dual_slack_kernel(double *__restrict__ dual_slack, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_blocks) +{ + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + int start = start_idx[blk]; + int k = v_dim[blk]; + for (int m = 0; m < k + 2; ++m) + { + int idx = start + m; + dual_slack[idx] = objective_vector[idx] - dual_product[idx]; + } +} + +__global__ void set_cone_dual_slack_grid_kernel(double *__restrict__ dual_slack, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int n = v_dim[cone] + 2; + for (int m = part * blockDim.x + threadIdx.x; m < n; m += blocks_per_cone * blockDim.x) + { + int idx = start + m; + dual_slack[idx] = objective_vector[idx] - dual_product[idx]; + } +} + +__global__ void set_cone_dual_slack_warp_kernel(double *__restrict__ dual_slack, + const double *__restrict__ objective_vector, + const double *__restrict__ dual_product, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones) +{ + int global_thread = blockIdx.x * blockDim.x + threadIdx.x; + int cone = global_thread >> 5; + if (cone >= num_cones) + return; + + int lane = global_thread & 31; + int start = start_idx[cone]; + int n = v_dim[cone] + 2; + for (int m = lane; m < n; m += 32) + { + int idx = start + m; + dual_slack[idx] = objective_vector[idx] - dual_product[idx]; + } +} + +__global__ void recompute_reflected_at_cone_kernel(double *__restrict__ reflected_primal, + const double *__restrict__ pdhg_primal, + const double *__restrict__ current_primal, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_blocks) +{ + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + int start = start_idx[blk]; + int k = v_dim[blk]; + for (int m = 0; m < k + 2; ++m) + { + int idx = start + m; + reflected_primal[idx] = 2.0 * pdhg_primal[idx] - current_primal[idx]; + } +} + +__global__ void recompute_reflected_at_cone_warp_kernel(double *__restrict__ reflected_primal, + const double *__restrict__ pdhg_primal, + const double *__restrict__ current_primal, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones) +{ + int global_thread = blockIdx.x * blockDim.x + threadIdx.x; + int cone = global_thread >> 5; + if (cone >= num_cones) + return; + + int lane = global_thread & 31; + int start = start_idx[cone]; + int n = v_dim[cone] + 2; + for (int m = lane; m < n; m += 32) + { + int idx = start + m; + reflected_primal[idx] = 2.0 * pdhg_primal[idx] - current_primal[idx]; + } +} + +__global__ void recompute_reflected_at_cone_grid_kernel(double *__restrict__ reflected_primal, + const double *__restrict__ pdhg_primal, + const double *__restrict__ current_primal, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int n = v_dim[cone] + 2; + for (int m = part * blockDim.x + threadIdx.x; m < n; m += blocks_per_cone * blockDim.x) + { + int idx = start + m; + reflected_primal[idx] = 2.0 * pdhg_primal[idx] - current_primal[idx]; + } +} + +/* Weighted prox onto D K_soc; effective rescaling e_i = sqrt(w_i) d_i, w_i = 1 + tau Q_i. */ +__global__ void project_standard_soc_diag_q_kernel(double *__restrict__ pdhg_primal, + double *__restrict__ reflected_primal, + const double *__restrict__ current_primal, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int start = start_idx[blk]; + int k = v_dim[blk]; + int w_off = start + k; + int z_off = start + k + 1; + + if (cone_section_has_fixed(is_fixed, start, k + 2)) + { + project_standard_soc_section_serial( + pdhg_primal, variable_rescaling, Q_diag, tau, warm_start + blk, start, k, is_fixed); + for (int slot = 0; slot < k + 2; ++slot) + { + int index = start + slot; + reflected_primal[index] = 2.0 * pdhg_primal[index] - current_primal[index]; + } + return; + } + + double r_w = pdhg_primal[w_off]; + double r_z = pdhg_primal[z_off]; + + double d_z = variable_rescaling[z_off]; + double w_w = 1.0 + tau * Q_diag[w_off]; + double w_z = 1.0 + tau * Q_diag[z_off]; + double sqrt_w_w = sqrt(w_w); + double sqrt_w_z = sqrt(w_z); + double e_z = sqrt_w_z * d_z; + double e_w = sqrt_w_w * variable_rescaling[w_off]; + double eh_w = e_w / e_z; + double eh_w2 = eh_w * eh_w; + + double r_inv_sq = w_w * (r_w / eh_w) * (r_w / eh_w); + double r_pos_sq = w_w * (r_w * eh_w) * (r_w * eh_w); + for (int m = 0; m < k; ++m) + { + double w_m = 1.0 + tau * Q_diag[start + m]; + double e_m = sqrt(w_m) * variable_rescaling[start + m]; + double eh_m = e_m / e_z; + double r_m = pdhg_primal[start + m]; + r_inv_sq += w_m * (r_m / eh_m) * (r_m / eh_m); + r_pos_sq += w_m * (r_m * eh_m) * (r_m * eh_m); + } + double w_z_r_z_sq = w_z * r_z * r_z; + + if (r_inv_sq <= w_z_r_z_sq && r_z >= 0.0) + { + for (int m = 0; m < k; ++m) + { + int idx = start + m; + reflected_primal[idx] = 2.0 * pdhg_primal[idx] - current_primal[idx]; + } + reflected_primal[w_off] = 2.0 * r_w - current_primal[w_off]; + reflected_primal[z_off] = 2.0 * r_z - current_primal[z_off]; + return; + } + + if (r_pos_sq <= w_z_r_z_sq && r_z <= 0.0) + { + for (int m = 0; m < k; ++m) + { + int idx = start + m; + pdhg_primal[idx] = 0.0; + reflected_primal[idx] = -current_primal[idx]; + } + pdhg_primal[w_off] = 0.0; + pdhg_primal[z_off] = 0.0; + reflected_primal[w_off] = -current_primal[w_off]; + reflected_primal[z_off] = -current_primal[z_off]; + return; + } + + /* Fast path: no Q on cone slots and uniform d_v = d_z (LP-style symmetric case). */ + if (Q_diag[w_off] == 0.0 && Q_diag[z_off] == 0.0) + { + bool no_cone_Q = true; + bool d_uniform = (variable_rescaling[w_off] == d_z); + for (int m = 0; m < k; ++m) + { + if (Q_diag[start + m] != 0.0) + { + no_cone_Q = false; + break; + } + if (variable_rescaling[start + m] != d_z) + { + d_uniform = false; + break; + } + } + if (no_cone_Q && d_uniform) + { + double sumsq = r_w * r_w; + for (int m = 0; m < k; ++m) + { + double vm = pdhg_primal[start + m]; + sumsq += vm * vm; + } + double rnorm = sqrt(sumsq); + /* in-cone (rnorm <= r_z, r_z >= 0) and at-origin (rnorm <= -r_z, r_z <= 0) handled above */ + double scale = (r_z + rnorm) / (2.0 * rnorm); + for (int m = 0; m < k; ++m) + { + double v_new = scale * pdhg_primal[start + m]; + pdhg_primal[start + m] = v_new; + int idx = start + m; + reflected_primal[idx] = 2.0 * v_new - current_primal[idx]; + } + double w_new = scale * r_w; + double z_new = scale * rnorm; + pdhg_primal[w_off] = w_new; + pdhg_primal[z_off] = z_new; + reflected_primal[w_off] = 2.0 * w_new - current_primal[w_off]; + reflected_primal[z_off] = 2.0 * z_new - current_primal[z_off]; + return; + } + } + + double lo, hi; + bool z_pos = (r_z > 0.0); + if (z_pos) + { + lo = 0.0; + hi = 0.5 - 1e-14; + } + else + { + lo = 0.5 + 1e-14; + hi = 1.0; + for (int doubling = 0; doubling < 60; ++doubling) + { + double sum_hi = 0.0; + for (int m = 0; m < k; ++m) + { + double w_m = 1.0 + tau * Q_diag[start + m]; + double e_m = sqrt(w_m) * variable_rescaling[start + m]; + double eh_m = e_m / e_z; + double eh_m2 = eh_m * eh_m; + double r_m = pdhg_primal[start + m]; + double t = sqrt(w_m) * r_m * eh_m / (eh_m2 + 2.0 * hi); + sum_hi += t * t; + } + double tw_hi = sqrt_w_w * r_w * eh_w / (eh_w2 + 2.0 * hi); + sum_hi += tw_hi * tw_hi; + double tz_hi = sqrt_w_z * r_z / (1.0 - 2.0 * hi); + double f_hi = sum_hi - tz_hi * tz_hi; + if (f_hi > 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_lam = warm_start[blk]; + if (warm_lam > lo && warm_lam < hi) + { + double sum_w = 0.0; + for (int m = 0; m < k; ++m) + { + double w_m = 1.0 + tau * Q_diag[start + m]; + double e_m = sqrt(w_m) * variable_rescaling[start + m]; + double eh_m = e_m / e_z; + double eh_m2 = eh_m * eh_m; + double r_m = pdhg_primal[start + m]; + double t = sqrt(w_m) * r_m * eh_m / (eh_m2 + 2.0 * warm_lam); + sum_w += t * t; + } + double tw = sqrt_w_w * r_w * eh_w / (eh_w2 + 2.0 * warm_lam); + sum_w += tw * tw; + double tz = sqrt_w_z * r_z / (1.0 - 2.0 * warm_lam); + double f = sum_w - tz * tz; + if (fabs(f) < 1e-12) + { + double new_z = r_z / (1.0 - 2.0 * warm_lam); + double new_w = r_w * eh_w2 / (eh_w2 + 2.0 * warm_lam); + pdhg_primal[z_off] = new_z; + pdhg_primal[w_off] = new_w; + reflected_primal[z_off] = 2.0 * new_z - current_primal[z_off]; + reflected_primal[w_off] = 2.0 * new_w - current_primal[w_off]; + for (int m = 0; m < k; ++m) + { + int idx = start + m; + double w_m = 1.0 + tau * Q_diag[idx]; + double e_m = sqrt(w_m) * variable_rescaling[idx]; + double eh_m = e_m / e_z; + double eh_m2 = eh_m * eh_m; + double r_m = pdhg_primal[idx]; + double new_m = r_m * eh_m2 / (eh_m2 + 2.0 * warm_lam); + pdhg_primal[idx] = new_m; + reflected_primal[idx] = 2.0 * new_m - current_primal[idx]; + } + return; + } + if (z_pos) + { + if (f > 0.0) + lo = warm_lam; + else + hi = warm_lam; + } + else + { + if (f > 0.0) + hi = warm_lam; + else + lo = warm_lam; + } + } + + for (int it = 0; it < 60; ++it) + { + double lam = 0.5 * (lo + hi); + double sum = 0.0; + for (int m = 0; m < k; ++m) + { + double w_m = 1.0 + tau * Q_diag[start + m]; + double e_m = sqrt(w_m) * variable_rescaling[start + m]; + double eh_m = e_m / e_z; + double eh_m2 = eh_m * eh_m; + double r_m = pdhg_primal[start + m]; + double t = sqrt(w_m) * r_m * eh_m / (eh_m2 + 2.0 * lam); + sum += t * t; + } + double tw = sqrt_w_w * r_w * eh_w / (eh_w2 + 2.0 * lam); + sum += tw * tw; + double tz = sqrt_w_z * r_z / (1.0 - 2.0 * lam); + double f = sum - tz * tz; + if (z_pos) + { + if (f > 0.0) + lo = lam; + else + hi = lam; + } + else + { + if (f > 0.0) + hi = lam; + else + lo = lam; + } + if ((hi - lo) / (1.0 + hi + lo) < 1e-13) + break; + } + double lam = 0.5 * (lo + hi); + warm_start[blk] = lam; + + double new_z = r_z / (1.0 - 2.0 * lam); + double new_w = r_w * eh_w2 / (eh_w2 + 2.0 * lam); + pdhg_primal[z_off] = new_z; + pdhg_primal[w_off] = new_w; + reflected_primal[z_off] = 2.0 * new_z - current_primal[z_off]; + reflected_primal[w_off] = 2.0 * new_w - current_primal[w_off]; + for (int m = 0; m < k; ++m) + { + int idx = start + m; + double w_m = 1.0 + tau * Q_diag[idx]; + double e_m = sqrt(w_m) * variable_rescaling[idx]; + double eh_m = e_m / e_z; + double eh_m2 = eh_m * eh_m; + double r_m = pdhg_primal[idx]; + double new_m = r_m * eh_m2 / (eh_m2 + 2.0 * lam); + pdhg_primal[idx] = new_m; + reflected_primal[idx] = 2.0 * new_m - current_primal[idx]; + } +} + +/* Weighted prox onto D K_exp; coordinate change y_i = sqrt(w_i) x_i gives e_i = sqrt(w_i) d_i. */ +__global__ void project_exp_cone_diag_q_kernel(double *__restrict__ pdhg_primal, + double *__restrict__ reflected_primal, + const double *__restrict__ current_primal, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + (void)v_dim; + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + int s_idx = start_idx[blk]; + + if (cone_section_has_fixed(is_fixed, s_idx, 3)) + { + project_exp_cone_section(pdhg_primal, variable_rescaling, Q_diag, tau, warm_start + blk, s_idx, is_fixed); + for (int slot = 0; slot < 3; ++slot) + { + int index = s_idx + slot; + reflected_primal[index] = 2.0 * pdhg_primal[index] - current_primal[index]; + } + return; + } + + double r1 = pdhg_primal[s_idx + 0]; + double r2 = pdhg_primal[s_idx + 1]; + double r3 = pdhg_primal[s_idx + 2]; + + double d1 = variable_rescaling[s_idx + 0]; + double d2 = variable_rescaling[s_idx + 1]; + double d3 = variable_rescaling[s_idx + 2]; + + double w1 = 1.0 + tau * Q_diag[s_idx + 0]; + double w2 = 1.0 + tau * Q_diag[s_idx + 1]; + double w3 = 1.0 + tau * Q_diag[s_idx + 2]; + + /* Clamp guards against negative drift in Q_diag invalidating sqrt(w_i). */ + if (!(w1 > 0.0)) + w1 = 1.0; + if (!(w2 > 0.0)) + w2 = 1.0; + if (!(w3 > 0.0)) + w3 = 1.0; + + double sw1 = sqrt(w1); + double sw2 = sqrt(w2); + double sw3 = sqrt(w3); + + double e1 = sw1 * d1; + double e2 = sw2 * d2; + double e3 = sw3 * d3; + + double u1 = sw1 * r1; + double u2 = sw2 * r2; + double u3 = sw3 * r3; + double y1_out, y2_out, y3_out; + project_exp_cone_point(u1, u2, u3, e1, e2, e3, &y1_out, &y2_out, &y3_out); + double x1 = y1_out / sw1; + double x2 = y2_out / sw2; + double x3 = y3_out / sw3; + + pdhg_primal[s_idx + 0] = x1; + pdhg_primal[s_idx + 1] = x2; + pdhg_primal[s_idx + 2] = x3; + + reflected_primal[s_idx + 0] = 2.0 * x1 - current_primal[s_idx + 0]; + reflected_primal[s_idx + 1] = 2.0 * x2 - current_primal[s_idx + 1]; + reflected_primal[s_idx + 2] = 2.0 * x3 - current_primal[s_idx + 2]; +} + +/* Direct (s,t) bisection in zeta = xi/sqrt(w_s w_t); alpha = sqrt(w_t/w_s) carries asymmetry. */ +__global__ void project_rotated_soc_diag_q_kernel(double *__restrict__ pdhg_primal, + double *__restrict__ reflected_primal, + const double *__restrict__ current_primal, + const double *__restrict__ variable_rescaling, + const double *__restrict__ Q_diag, + double tau, + double *__restrict__ warm_start, + const int *__restrict__ start_idx, + const int *__restrict__ v_dim, + const char *__restrict__ is_fixed, + int num_blocks) +{ + int blk = blockIdx.x * blockDim.x + threadIdx.x; + if (blk >= num_blocks) + return; + + const double W_FLOOR = 1e-300; + + int start = start_idx[blk]; + int k = v_dim[blk]; + int len = k + 2; + + if (cone_section_has_fixed(is_fixed, start, len)) + { + project_rotated_soc_section_serial( + pdhg_primal, variable_rescaling, Q_diag, tau, warm_start + blk, start, k, is_fixed); + for (int slot = 0; slot < len; ++slot) + { + int index = start + slot; + reflected_primal[index] = 2.0 * pdhg_primal[index] - current_primal[index]; + } + return; + } + + double r_s = pdhg_primal[start + k]; + double r_t = pdhg_primal[start + k + 1]; + + double q_s = Q_diag[start + k]; + double q_t = Q_diag[start + k + 1]; + double w_s = 1.0 + tau * q_s; + double w_t = 1.0 + tau * q_t; + if (!(w_s > W_FLOOR)) + w_s = W_FLOOR; + if (!(w_t > W_FLOOR)) + w_t = W_FLOOR; + double sigma = sqrt(w_s * w_t); + double alpha = sqrt(w_t / w_s); + double inv_alpha = 1.0 / alpha; + + double d_s = variable_rescaling[start + k]; + double d_t = variable_rescaling[start + k + 1]; + double d_st = sqrt(d_s * d_t); + + const double INV_SQRT2 = 0.7071067811865475; + /* Fast path: no Q on cone slots (w_s = w_t = 1 and all w_v_i = 1) and uniform d_v = d_st. + This is the COMMON case for QCQP transform aux vars. Reduces to LP-style RSOC closed form. */ + if (q_s == 0.0 && q_t == 0.0) + { + bool no_cone_Q = true; + bool d_uniform = true; + for (int m = 0; m < k; ++m) + { + if (Q_diag[start + m] != 0.0) + { + no_cone_Q = false; + break; + } + if (variable_rescaling[start + m] != d_st) + { + d_uniform = false; + break; + } + } + if (no_cone_Q && d_uniform) + { + double w_val = (r_s - r_t) * INV_SQRT2; + double z_val = (r_s + r_t) * INV_SQRT2; + double sumsq = w_val * w_val; + for (int m = 0; m < k; ++m) + { + double vm = pdhg_primal[start + m]; + sumsq += vm * vm; + } + double rnorm = sqrt(sumsq); + if (rnorm <= z_val) + { + for (int m = 0; m < len; ++m) + { + int idx = start + m; + reflected_primal[idx] = 2.0 * pdhg_primal[idx] - current_primal[idx]; + } + return; + } + if (rnorm <= -z_val) + { + for (int m = 0; m < k; ++m) + { + pdhg_primal[start + m] = 0.0; + int idx = start + m; + reflected_primal[idx] = -current_primal[idx]; + } + pdhg_primal[start + k] = 0.0; + pdhg_primal[start + k + 1] = 0.0; + reflected_primal[start + k] = -current_primal[start + k]; + reflected_primal[start + k + 1] = -current_primal[start + k + 1]; + return; + } + double scale = (z_val + rnorm) / (2.0 * rnorm); + double w_new = scale * w_val; + double z_new = scale * rnorm; + for (int m = 0; m < k; ++m) + { + double v_new = scale * pdhg_primal[start + m]; + pdhg_primal[start + m] = v_new; + int idx = start + m; + reflected_primal[idx] = 2.0 * v_new - current_primal[idx]; + } + double s_new = (z_new + w_new) * INV_SQRT2; + double t_new = (z_new - w_new) * INV_SQRT2; + pdhg_primal[start + k] = s_new; + pdhg_primal[start + k + 1] = t_new; + reflected_primal[start + k] = 2.0 * s_new - current_primal[start + k]; + reflected_primal[start + k + 1] = 2.0 * t_new - current_primal[start + k + 1]; + return; + } + } + + { + double lhs = 0.0; + for (int m = 0; m < k; ++m) + { + double d_m = variable_rescaling[start + m]; + double Ds = d_st / d_m; + double rv = pdhg_primal[start + m]; + double term = Ds * rv; + lhs += term * term; + } + if (r_s >= 0.0 && r_t >= 0.0 && lhs <= 2.0 * r_s * r_t) + { + for (int m = 0; m < len; ++m) + { + int idx = start + m; + double pv = pdhg_primal[idx]; + reflected_primal[idx] = 2.0 * pv - current_primal[idx]; + } + return; + } + } + + if (r_s <= 0.0 && r_t <= 0.0) + { + double rhs = 2.0 * sigma * sigma * r_s * r_t; + double lhs = 0.0; + for (int m = 0; m < k; ++m) + { + double d_m = variable_rescaling[start + m]; + double q_m = Q_diag[start + m]; + double w_m = 1.0 + tau * q_m; + if (!(w_m > W_FLOOR)) + w_m = W_FLOOR; + double rv = pdhg_primal[start + m]; + double term = d_m * w_m * rv / d_st; + lhs += term * term; + } + if (lhs <= rhs) + { + for (int m = 0; m < k; ++m) + pdhg_primal[start + m] = 0.0; + pdhg_primal[start + k] = 0.0; + pdhg_primal[start + k + 1] = 0.0; + for (int m = 0; m < len; ++m) + { + int idx = start + m; + reflected_primal[idx] = -current_primal[idx]; + } + return; + } + } + + double lo, hi; + int bracket_kind; /* 0: f increasing on bracket; 1: f decreasing. */ + bool need_doubling = false; + double sum_alpha = r_s + alpha * r_t; + + if (r_s > 0.0 && r_t > 0.0) + { + lo = 0.0; + hi = 1.0 - 1e-14; + bracket_kind = 1; + } + else if (r_s < 0.0 && r_t < 0.0) + { + lo = 1.0 + 1e-14; + hi = 2.0; + bracket_kind = 0; + need_doubling = true; + } + else if (r_s <= 0.0 && r_t >= 0.0) + { + if (sum_alpha <= 0.0) + { + lo = 1.0 + 1e-14; + if (r_t == 0.0) + { + hi = 2.0; + need_doubling = true; + } + else + { + hi = -r_s / (alpha * r_t); + if (!(hi > lo)) + hi = lo + 1.0; + } + bracket_kind = 0; + } + else + { + lo = (r_t > 0.0) ? (-r_s / (alpha * r_t)) : 0.0; + if (!(lo >= 0.0)) + lo = 0.0; + hi = 1.0 - 1e-14; + if (!(lo < hi)) + lo = hi - 1e-7; + bracket_kind = 1; + } + } + else + { + if (sum_alpha <= 0.0) + { + lo = 1.0 + 1e-14; + if (r_s == 0.0) + { + hi = 2.0; + need_doubling = true; + } + else + { + hi = -alpha * r_t / r_s; + if (!(hi > lo)) + hi = lo + 1.0; + } + bracket_kind = 0; + } + else + { + lo = (r_s > 0.0) ? (-alpha * r_t / r_s) : 0.0; + if (!(lo >= 0.0)) + lo = 0.0; + hi = 1.0 - 1e-14; + if (!(lo < hi)) + lo = hi - 1e-7; + bracket_kind = 1; + } + } + +#define ORACLE_EVAL(ZETA, F_OUT) \ + do \ + { \ + double _zeta = (ZETA); \ + double _denom = 1.0 - _zeta * _zeta; \ + double _s = (r_s + _zeta * alpha * r_t) / _denom; \ + double _t = (r_t + _zeta * inv_alpha * r_s) / _denom; \ + double _sv = 0.0; \ + for (int _m = 0; _m < k; ++_m) \ + { \ + double _dm = variable_rescaling[start + _m]; \ + double _Ds = d_st / _dm; \ + double _qm = Q_diag[start + _m]; \ + double _wm = 1.0 + tau * _qm; \ + if (!(_wm > W_FLOOR)) \ + _wm = W_FLOOR; \ + double _Dh2 = _Ds * _Ds * sigma / _wm; \ + double _rv = pdhg_primal[start + _m]; \ + double _vz = _rv / (1.0 + _zeta * _Dh2); \ + double _tm = _Ds * _vz; \ + _sv += _tm * _tm; \ + } \ + (F_OUT) = _sv - 2.0 * _s * _t; \ + } while (0) + + if (need_doubling) + { + double f_hi; + for (int dbl = 0; dbl < 60; ++dbl) + { + ORACLE_EVAL(hi, f_hi); + if (f_hi >= 0.0) + break; + lo = hi; + hi *= 2.0; + } + } + + double warm_zeta = warm_start[blk]; + if (warm_zeta > lo && warm_zeta < hi) + { + double f_w; + ORACLE_EVAL(warm_zeta, f_w); + if (fabs(f_w) < 1e-12) + { + double zeta = warm_zeta; + double denom = 1.0 - zeta * zeta; + double s_new = (r_s + zeta * alpha * r_t) / denom; + double t_new = (r_t + zeta * inv_alpha * r_s) / denom; + for (int m = 0; m < k; ++m) + { + double d_m = variable_rescaling[start + m]; + double Ds = d_st / d_m; + double q_m = Q_diag[start + m]; + double w_m = 1.0 + tau * q_m; + if (!(w_m > W_FLOOR)) + w_m = W_FLOOR; + double Dh2 = Ds * Ds * sigma / w_m; + double rv = pdhg_primal[start + m]; + pdhg_primal[start + m] = rv / (1.0 + zeta * Dh2); + } + pdhg_primal[start + k] = s_new; + pdhg_primal[start + k + 1] = t_new; + for (int m = 0; m < len; ++m) + { + int idx = start + m; + double pv = pdhg_primal[idx]; + reflected_primal[idx] = 2.0 * pv - current_primal[idx]; + } + return; + } + if (bracket_kind == 0) + { + if (f_w < 0.0) + lo = warm_zeta; + else + hi = warm_zeta; + } + else + { + if (f_w > 0.0) + lo = warm_zeta; + else + hi = warm_zeta; + } + } + + for (int it = 0; it < 80; ++it) + { + double mid = 0.5 * (lo + hi); + double f_m; + ORACLE_EVAL(mid, f_m); + if (bracket_kind == 0) + { + if (f_m < 0.0) + lo = mid; + else + hi = mid; + } + else + { + if (f_m > 0.0) + lo = mid; + else + hi = mid; + } + if ((hi - lo) / (1.0 + fabs(hi) + fabs(lo)) < 1e-13) + break; + } + double zeta = 0.5 * (lo + hi); + warm_start[blk] = zeta; + + double denom = 1.0 - zeta * zeta; + double s_new = (r_s + zeta * alpha * r_t) / denom; + double t_new = (r_t + zeta * inv_alpha * r_s) / denom; + for (int m = 0; m < k; ++m) + { + double d_m = variable_rescaling[start + m]; + double Ds = d_st / d_m; + double q_m = Q_diag[start + m]; + double w_m = 1.0 + tau * q_m; + if (!(w_m > W_FLOOR)) + w_m = W_FLOOR; + double Dh2 = Ds * Ds * sigma / w_m; + double rv = pdhg_primal[start + m]; + pdhg_primal[start + m] = rv / (1.0 + zeta * Dh2); + } + pdhg_primal[start + k] = s_new; + pdhg_primal[start + k + 1] = t_new; + + for (int m = 0; m < len; ++m) + { + int idx = start + m; + double pv = pdhg_primal[idx]; + reflected_primal[idx] = 2.0 * pv - current_primal[idx]; + } +#undef ORACLE_EVAL +} diff --git a/src/pdhcg_kernels.cu b/src/kernels/pdhcg_kernels.cu similarity index 72% rename from src/pdhcg_kernels.cu rename to src/kernels/pdhcg_kernels.cu index a555ed5..ef1ff81 100644 --- a/src/pdhcg_kernels.cu +++ b/src/kernels/pdhcg_kernels.cu @@ -41,6 +41,73 @@ element_wise_mul_kernel(const double *__restrict__ A, const double *__restrict__ C[idx] = A[idx] * B[idx]; } } + +__global__ void +vector_sub_kernel(double *__restrict__ direction, const double *__restrict__ a, const double *__restrict__ b, int n) +{ + for (int i = blockDim.x * blockIdx.x + threadIdx.x; i < n; i += blockDim.x * gridDim.x) + { + direction[i] = a[i] - b[i]; + } +} + +__global__ void +vector_add_kernel(const double *__restrict__ a, const double *__restrict__ b, double *__restrict__ out, int n) +{ + for (int i = blockDim.x * blockIdx.x + threadIdx.x; i < n; i += blockDim.x * gridDim.x) + { + out[i] = a[i] + b[i]; + } +} + +__global__ void project_primal_onto_bounds_kernel(double *__restrict__ primal_solution, + const double *__restrict__ variable_lower_bound, + const double *__restrict__ variable_upper_bound, + int num_variables) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < num_variables) + { + primal_solution[i] = fmax(variable_lower_bound[i], fmin(primal_solution[i], variable_upper_bound[i])); + } +} + +__global__ void prepare_projected_gradient_point_kernel(double *__restrict__ projected_point, + const double *__restrict__ primal_solution, + const double *__restrict__ effective_objective, + const double *__restrict__ dual_product, + const double *__restrict__ variable_lower_bound, + const double *__restrict__ variable_upper_bound, + double step_size, + int num_variables) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < num_variables) + { + double gradient = effective_objective[i] - dual_product[i]; + double point = primal_solution[i] - step_size * gradient; + projected_point[i] = fmax(variable_lower_bound[i], fmin(point, variable_upper_bound[i])); + } +} + +__global__ void augment_projected_gradient_residual_kernel(double *__restrict__ dual_residual, + const double *__restrict__ primal_solution, + const double *__restrict__ projected_point, + const double *__restrict__ variable_rescaling, + double step_size, + int num_variables) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < num_variables) + { + double residual = (primal_solution[i] - projected_point[i]) / step_size * variable_rescaling[i]; + if (!isfinite(residual)) + residual = copysign(INFINITY, residual); + if (fabs(residual) > fabs(dual_residual[i])) + dual_residual[i] = residual; + } +} + __global__ void compute_lp_next_pdhg_primal_solution_kernel(const double *current_primal, double *reflected_primal, const double *dual_product, @@ -127,20 +194,30 @@ __global__ void compute_diagonal_q_next_pdhg_primal_solution_kernel(const double } } +__device__ static inline double +next_constraint_dual(double current_dual, double primal_value, double lower_bound, double upper_bound, double step_size) +{ + double projected_value = fmax(lower_bound, fmin(primal_value - current_dual / step_size, upper_bound)); + return current_dual - step_size * primal_value + step_size * projected_value; +} + __global__ void compute_next_pdhg_dual_solution_kernel(const double *current_dual, double *reflected_dual, const double *primal_product, - const double *const_lb, - const double *const_ub, + const double *affine_cone_offset, + const double *constraint_lower_bound, + const double *constraint_upper_bound, int n, double step_size) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) { - double temp = current_dual[i] / step_size - primal_product[i]; - double temp_proj = fmax(-const_ub[i], fmin(temp, -const_lb[i])); - reflected_dual[i] = 2.0 * (temp - temp_proj) * step_size - current_dual[i]; + double current = current_dual[i]; + double value = primal_product[i] + affine_cone_offset[i]; + double next = + next_constraint_dual(current, value, constraint_lower_bound[i], constraint_upper_bound[i], step_size); + reflected_dual[i] = 2.0 * next - current; } } @@ -148,18 +225,58 @@ __global__ void compute_next_pdhg_dual_solution_major_kernel(const double *curre double *pdhg_dual, double *reflected_dual, const double *primal_product, - const double *const_lb, - const double *const_ub, + const double *affine_cone_offset, + const double *constraint_lower_bound, + const double *constraint_upper_bound, int n, double step_size) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) { - double temp = current_dual[i] / step_size - primal_product[i]; - double temp_proj = fmax(-const_ub[i], fmin(temp, -const_lb[i])); - pdhg_dual[i] = (temp - temp_proj) * step_size; - reflected_dual[i] = 2.0 * pdhg_dual[i] - current_dual[i]; + double current = current_dual[i]; + double value = primal_product[i] + affine_cone_offset[i]; + double next = + next_constraint_dual(current, value, constraint_lower_bound[i], constraint_upper_bound[i], step_size); + pdhg_dual[i] = next; + reflected_dual[i] = 2.0 * next - current; + } +} + +__global__ void prepare_constraint_dual_update_kernel(const double *current_dual, + const double *primal_product, + const double *affine_cone_offset, + const double *constraint_lower_bound, + const double *constraint_upper_bound, + double *projected_constraint_value, + int n, + double step_size) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < n) + { + double value = primal_product[i] + affine_cone_offset[i] - current_dual[i] / step_size; + projected_constraint_value[i] = fmax(constraint_lower_bound[i], fmin(value, constraint_upper_bound[i])); + } +} + +__global__ void finish_constraint_dual_update_kernel(const double *current_dual, + const double *primal_product, + const double *affine_cone_offset, + const double *projected_constraint_value, + double *pdhg_dual, + double *reflected_dual, + int n, + double step_size) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < n) + { + double value = primal_product[i] + affine_cone_offset[i]; + double next_dual = current_dual[i] - step_size * value + step_size * projected_constraint_value[i]; + if (pdhg_dual) + pdhg_dual[i] = next_dual; + reflected_dual[i] = 2.0 * next_dual - current_dual[i]; } } @@ -520,6 +637,7 @@ __global__ void primal_bb_update_direction_kernel_precond(double *pdhg_primal_so __global__ void compute_lp_residual_kernel(double *primal_residual, const double *primal_product, + const double *affine_cone_offset, const double *constraint_lower_bound, const double *constraint_upper_bound, const double *dual_solution, @@ -529,9 +647,11 @@ __global__ void compute_lp_residual_kernel(double *primal_residual, const double *objective_vector, const double *constraint_rescaling, const double *variable_rescaling, + double *affine_dual_membership, double *dual_obj_contribution, const double *const_lb_finite, const double *const_ub_finite, + bool defer_constraint_projection, int num_constraints, int num_variables) { @@ -539,11 +659,20 @@ __global__ void compute_lp_residual_kernel(double *primal_residual, if (i < num_constraints) { - double clamped_val = fmax(constraint_lower_bound[i], fmin(primal_product[i], constraint_upper_bound[i])); - primal_residual[i] = (primal_product[i] - clamped_val) * constraint_rescaling[i]; + double value = primal_product[i] + affine_cone_offset[i]; + double projected_value = fmax(constraint_lower_bound[i], fmin(value, constraint_upper_bound[i])); + if (defer_constraint_projection) + { + primal_residual[i] = projected_value; + affine_dual_membership[i] = 0.0; + } + else + { + primal_residual[i] = (value - projected_value) * constraint_rescaling[i]; + } - dual_obj_contribution[i] = - fmax(dual_solution[i], 0.0) * const_lb_finite[i] + fmin(dual_solution[i], 0.0) * const_ub_finite[i]; + dual_obj_contribution[i] = fmax(dual_solution[i], 0.0) * const_lb_finite[i] + + fmin(dual_solution[i], 0.0) * const_ub_finite[i] - affine_cone_offset[i] * dual_solution[i]; } else if (i < num_constraints + num_variables) { @@ -554,6 +683,7 @@ __global__ void compute_lp_residual_kernel(double *primal_residual, __global__ void compute_qp_residual_kernel(double *primal_residual, const double *primal_product, + const double *affine_cone_offset, const double *primal_obj_product, const double *primal_solution, const double *constraint_lower_bound, @@ -567,10 +697,12 @@ __global__ void compute_qp_residual_kernel(double *primal_residual, const double *objective_vector, const double *constraint_rescaling, const double *variable_rescaling, + double *affine_dual_membership, double *dual_obj_contribution, const double *const_lb_finite, const double *const_ub_finite, const double step_size, + bool defer_constraint_projection, int num_constraints, int num_variables) { @@ -578,11 +710,20 @@ __global__ void compute_qp_residual_kernel(double *primal_residual, if (i < num_constraints) { - double clamped_val = fmax(constraint_lower_bound[i], fmin(primal_product[i], constraint_upper_bound[i])); - primal_residual[i] = (primal_product[i] - clamped_val) * constraint_rescaling[i]; + double value = primal_product[i] + affine_cone_offset[i]; + double projected_value = fmax(constraint_lower_bound[i], fmin(value, constraint_upper_bound[i])); + if (defer_constraint_projection) + { + primal_residual[i] = projected_value; + affine_dual_membership[i] = 0.0; + } + else + { + primal_residual[i] = (value - projected_value) * constraint_rescaling[i]; + } - dual_obj_contribution[i] = - fmax(dual_solution[i], 0.0) * const_lb_finite[i] + fmin(dual_solution[i], 0.0) * const_ub_finite[i]; + dual_obj_contribution[i] = fmax(dual_solution[i], 0.0) * const_lb_finite[i] + + fmin(dual_solution[i], 0.0) * const_ub_finite[i] - affine_cone_offset[i] * dual_solution[i]; } else if (i < num_constraints + num_variables) { @@ -596,6 +737,107 @@ __global__ void compute_qp_residual_kernel(double *primal_residual, } } +__global__ void finish_affine_cone_residuals_kernel(double *primal_residual, + const double *primal_product, + const double *affine_cone_offset, + const double *constraint_rescaling, + double *dual_membership, + const double *dual_membership_rescaling, + int n) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < n) + { + double value = primal_product[i] + affine_cone_offset[i]; + primal_residual[i] = (value - primal_residual[i]) * constraint_rescaling[i]; + dual_membership[i] *= dual_membership_rescaling[i]; + } +} + +__global__ void prepare_affine_cone_residuals_kernel(double *projection_point, + double *complementarity_residual, + const double *primal_product, + const double *affine_cone_offset, + const double *dual_solution, + const int *start_idx, + const int *v_dim, + double constraint_bound_rescaling, + int num_cones) +{ + int cone = blockIdx.x; + if (cone >= num_cones) + return; + int start = start_idx[cone]; + int length = v_dim[cone] + 2; + double dot = 0.0; + for (int slot = threadIdx.x; slot < length; slot += blockDim.x) + { + int i = start + slot; + double dual = dual_solution[i]; + projection_point[i] = -dual; + dot += dual * (primal_product[i] + affine_cone_offset[i]); + } + + extern __shared__ double partial_sum[]; + partial_sum[threadIdx.x] = dot; + __syncthreads(); + for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) + { + if (threadIdx.x < stride) + partial_sum[threadIdx.x] += partial_sum[threadIdx.x + stride]; + __syncthreads(); + } + if (threadIdx.x == 0) + complementarity_residual[cone] = fabs(partial_sum[0]) / constraint_bound_rescaling; +} + +__global__ void prepare_affine_cone_residuals_grid_kernel(double *projection_point, + double *complementarity_accumulator, + const double *primal_product, + const double *affine_cone_offset, + const double *dual_solution, + const int *start_idx, + const int *v_dim, + int num_cones, + int blocks_per_cone) +{ + int cone = blockIdx.x / blocks_per_cone; + if (cone >= num_cones) + return; + int part = blockIdx.x - cone * blocks_per_cone; + int start = start_idx[cone]; + int length = v_dim[cone] + 2; + double dot = 0.0; + for (int slot = part * blockDim.x + threadIdx.x; slot < length; slot += blocks_per_cone * blockDim.x) + { + int index = start + slot; + double dual = dual_solution[index]; + projection_point[index] = -dual; + dot += dual * (primal_product[index] + affine_cone_offset[index]); + } + + extern __shared__ double partial_sum[]; + partial_sum[threadIdx.x] = dot; + __syncthreads(); + for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) + { + if (threadIdx.x < stride) + partial_sum[threadIdx.x] += partial_sum[threadIdx.x + stride]; + __syncthreads(); + } + if (threadIdx.x == 0) + atomicAdd(complementarity_accumulator + cone, partial_sum[0]); +} + +__global__ void finish_affine_cone_complementarity_kernel(double *complementarity_residual, + double constraint_bound_rescaling, + int num_cones) +{ + int cone = blockIdx.x * blockDim.x + threadIdx.x; + if (cone < num_cones) + complementarity_residual[cone] = fabs(complementarity_residual[cone]) / constraint_bound_rescaling; +} + __global__ void recover_primal_obj_dual_product(double *dual_product, double *primal_obj_product, const double *variable_rescaling, @@ -684,6 +926,7 @@ __global__ void compute_dual_infeasibility_kernel(const double *dual_product, __global__ void dual_solution_dual_objective_contribution_kernel(const double *constraint_lower_bound_finite_val, const double *constraint_upper_bound_finite_val, + const double *affine_cone_offset, const double *dual_solution, int num_constraints, double *dual_objective_dual_solution_contribution_array) @@ -694,7 +937,8 @@ dual_solution_dual_objective_contribution_kernel(const double *constraint_lower_ { dual_objective_dual_solution_contribution_array[i] = fmax(dual_solution[i], 0.0) * constraint_lower_bound_finite_val[i] + - fmin(dual_solution[i], 0.0) * constraint_upper_bound_finite_val[i]; + fmin(dual_solution[i], 0.0) * constraint_upper_bound_finite_val[i] - + affine_cone_offset[i] * dual_solution[i]; } } diff --git a/src/mps_parser.c b/src/mps_parser.c index 4390916..4d77550 100644 --- a/src/mps_parser.c +++ b/src/mps_parser.c @@ -28,879 +28,1147 @@ limitations under the License. #define READER_BUFFER_SIZE (4 * 1024 * 1024) -typedef struct NameNode { - char *name; - int index; - struct NameNode *next; +typedef struct NameNode +{ + char *name; + int index; + struct NameNode *next; } NameNode; -typedef struct { - NameNode **buckets; - size_t num_buckets; - size_t size; +typedef struct +{ + NameNode **buckets; + size_t num_buckets; + size_t size; } NameMap; -static unsigned long hash_string(const char *str) { - unsigned long hash = 5381; - int c; - while ((c = *str++)) { - hash = ((hash << 5) + hash) + c; - } - return hash; +static unsigned long hash_string(const char *str) +{ + unsigned long hash = 5381; + int c; + while ((c = *str++)) + { + hash = ((hash << 5) + hash) + c; + } + return hash; } -static void namemap_init(NameMap *map, size_t num_buckets) { - map->num_buckets = num_buckets; - map->size = 0; - map->buckets = safe_calloc(num_buckets, sizeof(NameNode *)); +static void namemap_init(NameMap *map, size_t num_buckets) +{ + map->num_buckets = num_buckets; + map->size = 0; + map->buckets = safe_calloc(num_buckets, sizeof(NameNode *)); } -static void namemap_resize(NameMap *map) { - int old_num_buckets = map->num_buckets; - NameNode **old_buckets = map->buckets; +static void namemap_resize(NameMap *map) +{ + int old_num_buckets = map->num_buckets; + NameNode **old_buckets = map->buckets; - int new_num_buckets = old_num_buckets * 2; - map->num_buckets = new_num_buckets; - map->buckets = safe_calloc(new_num_buckets, sizeof(NameNode *)); + int new_num_buckets = old_num_buckets * 2; + map->num_buckets = new_num_buckets; + map->buckets = safe_calloc(new_num_buckets, sizeof(NameNode *)); - for (int i = 0; i < old_num_buckets; ++i) { - NameNode *current = old_buckets[i]; - while (current) { - NameNode *next = current->next; + for (int i = 0; i < old_num_buckets; ++i) + { + NameNode *current = old_buckets[i]; + while (current) + { + NameNode *next = current->next; - unsigned long h = - hash_string(current->name) % (unsigned long)new_num_buckets; + unsigned long h = hash_string(current->name) % (unsigned long)new_num_buckets; - current->next = map->buckets[h]; - map->buckets[h] = current; + current->next = map->buckets[h]; + map->buckets[h] = current; - current = next; + current = next; + } } - } - free(old_buckets); + free(old_buckets); } -static void namemap_free(NameMap *map) { - if (!map || !map->buckets) - return; - for (size_t i = 0; i < map->num_buckets; ++i) { - NameNode *current = map->buckets[i]; - while (current) { - NameNode *to_free = current; - current = current->next; - free(to_free->name); - free(to_free); - } - } - free(map->buckets); - memset(map, 0, sizeof(NameMap)); +static void namemap_free(NameMap *map) +{ + if (!map || !map->buckets) + return; + for (size_t i = 0; i < map->num_buckets; ++i) + { + NameNode *current = map->buckets[i]; + while (current) + { + NameNode *to_free = current; + current = current->next; + free(to_free->name); + free(to_free); + } + } + free(map->buckets); + memset(map, 0, sizeof(NameMap)); } -static int namemap_get(const NameMap *map, const char *name) { - unsigned long h = hash_string(name) % (unsigned long)map->num_buckets; - for (NameNode *p = map->buckets[h]; p; p = p->next) { - if (strcmp(p->name, name) == 0) { - return p->index; +static int namemap_get(const NameMap *map, const char *name) +{ + unsigned long h = hash_string(name) % (unsigned long)map->num_buckets; + for (NameNode *p = map->buckets[h]; p; p = p->next) + { + if (strcmp(p->name, name) == 0) + { + return p->index; + } } - } - return -1; + return -1; } -static int namemap_put(NameMap *map, const char *name) { +static int namemap_put(NameMap *map, const char *name) +{ - if (map->size >= map->num_buckets * 0.75) { - namemap_resize(map); - } + if (map->size >= map->num_buckets * 0.75) + { + namemap_resize(map); + } - unsigned long h = hash_string(name) % (unsigned long)map->num_buckets; + unsigned long h = hash_string(name) % (unsigned long)map->num_buckets; - for (NameNode *p = map->buckets[h]; p; p = p->next) { - if (strcmp(p->name, name) == 0) { - return p->index; + for (NameNode *p = map->buckets[h]; p; p = p->next) + { + if (strcmp(p->name, name) == 0) + { + return p->index; + } } - } - NameNode *new_node = safe_malloc(sizeof(NameNode)); + NameNode *new_node = safe_malloc(sizeof(NameNode)); - new_node->name = strdup(name); - if (!new_node->name) { - free(new_node); - return -1; - } + new_node->name = strdup(name); + if (!new_node->name) + { + free(new_node); + return -1; + } - new_node->index = map->size++; - new_node->next = map->buckets[h]; - map->buckets[h] = new_node; + new_node->index = map->size++; + new_node->next = map->buckets[h]; + map->buckets[h] = new_node; - return new_node->index; + return new_node->index; } -typedef struct { - bool is_gz; - union FileHandle { - gzFile gz_f; - FILE *f; - } handle; - - char *buffer; - char *current_pos; - char *end_pos; +typedef struct +{ + bool is_gz; + union FileHandle + { + gzFile gz_f; + FILE *f; + } handle; + + char *buffer; + char *current_pos; + char *end_pos; } FastLineReader; -static FastLineReader *fast_reader_open(const char *filename) { - FastLineReader *reader = safe_calloc(1, sizeof(FastLineReader)); - - reader->buffer = safe_malloc(READER_BUFFER_SIZE); - - if (strlen(filename) > 3 && - strcmp(filename + strlen(filename) - 3, ".gz") == 0) { - reader->is_gz = true; - reader->handle.gz_f = gzopen(filename, "rb"); - if (!reader->handle.gz_f) { - free(reader->buffer); - free(reader); - return NULL; +static FastLineReader *fast_reader_open(const char *filename) +{ + FastLineReader *reader = safe_calloc(1, sizeof(FastLineReader)); + + reader->buffer = safe_malloc(READER_BUFFER_SIZE); + + if (strlen(filename) > 3 && strcmp(filename + strlen(filename) - 3, ".gz") == 0) + { + reader->is_gz = true; + reader->handle.gz_f = gzopen(filename, "rb"); + if (!reader->handle.gz_f) + { + free(reader->buffer); + free(reader); + return NULL; + } } - } else { - reader->is_gz = false; - reader->handle.f = fopen(filename, "r"); - if (!reader->handle.f) { - free(reader->buffer); - free(reader); - return NULL; + else + { + reader->is_gz = false; + reader->handle.f = fopen(filename, "r"); + if (!reader->handle.f) + { + free(reader->buffer); + free(reader); + return NULL; + } } - } - reader->current_pos = reader->buffer; - reader->end_pos = reader->buffer; + reader->current_pos = reader->buffer; + reader->end_pos = reader->buffer; - return reader; + return reader; } -static void fast_reader_close(FastLineReader *reader) { - if (!reader) - return; - if (reader->is_gz) { - if (reader->handle.gz_f) - gzclose(reader->handle.gz_f); - } else { - if (reader->handle.f) - fclose(reader->handle.f); - } - free(reader->buffer); - free(reader); +static void fast_reader_close(FastLineReader *reader) +{ + if (!reader) + return; + if (reader->is_gz) + { + if (reader->handle.gz_f) + gzclose(reader->handle.gz_f); + } + else + { + if (reader->handle.f) + fclose(reader->handle.f); + } + free(reader->buffer); + free(reader); } -static char *fast_reader_gets(FastLineReader *reader, char *line_buf, - int line_buf_size) { - int len = 0; +static char *fast_reader_gets(FastLineReader *reader, char *line_buf, int line_buf_size) +{ + int len = 0; + + while (1) + { + + if (reader->current_pos >= reader->end_pos) + { + if (reader->is_gz) + { + int bytes_read = gzread(reader->handle.gz_f, reader->buffer, READER_BUFFER_SIZE); + if (bytes_read <= 0) + { + + return (len > 0) ? line_buf : NULL; + } + reader->end_pos = reader->buffer + bytes_read; + } + else + { + size_t bytes_read = fread(reader->buffer, 1, READER_BUFFER_SIZE, reader->handle.f); + if (bytes_read <= 0) + { + return (len > 0) ? line_buf : NULL; + } + reader->end_pos = reader->buffer + bytes_read; + } + reader->current_pos = reader->buffer; + } - while (1) { + char *newline_pos = (char *)memchr(reader->current_pos, '\n', reader->end_pos - reader->current_pos); - if (reader->current_pos >= reader->end_pos) { - if (reader->is_gz) { - int bytes_read = - gzread(reader->handle.gz_f, reader->buffer, READER_BUFFER_SIZE); - if (bytes_read <= 0) { + int bytes_to_copy; + bool line_complete = (newline_pos != NULL); - return (len > 0) ? line_buf : NULL; + if (line_complete) + { + bytes_to_copy = newline_pos - reader->current_pos + 1; } - reader->end_pos = reader->buffer + bytes_read; - } else { - size_t bytes_read = - fread(reader->buffer, 1, READER_BUFFER_SIZE, reader->handle.f); - if (bytes_read <= 0) { - return (len > 0) ? line_buf : NULL; + else + { + bytes_to_copy = reader->end_pos - reader->current_pos; } - reader->end_pos = reader->buffer + bytes_read; - } - reader->current_pos = reader->buffer; - } - char *newline_pos = (char *)memchr(reader->current_pos, '\n', - reader->end_pos - reader->current_pos); - - int bytes_to_copy; - bool line_complete = (newline_pos != NULL); - - if (line_complete) { - bytes_to_copy = newline_pos - reader->current_pos + 1; - } else { - bytes_to_copy = reader->end_pos - reader->current_pos; - } - - if (len + bytes_to_copy >= line_buf_size) { - fprintf(stderr, "Error: Line too long to fit in buffer.\n"); - return NULL; - } + if (len + bytes_to_copy >= line_buf_size) + { + fprintf(stderr, "Error: Line too long to fit in buffer.\n"); + return NULL; + } - memcpy(line_buf + len, reader->current_pos, bytes_to_copy); - len += bytes_to_copy; - reader->current_pos += bytes_to_copy; - line_buf[len] = '\0'; + memcpy(line_buf + len, reader->current_pos, bytes_to_copy); + len += bytes_to_copy; + reader->current_pos += bytes_to_copy; + line_buf[len] = '\0'; - if (line_complete) { - return line_buf; + if (line_complete) + { + return line_buf; + } } - } } -typedef struct { - int *row_indices; - int *col_indices; - double *values; - size_t nnz; - size_t capacity; +typedef struct +{ + int *row_indices; + int *col_indices; + double *values; + size_t nnz; + size_t capacity; } CooMatrix; -typedef struct { - char *name; - char type; +typedef struct +{ + char *name; + char type; } BufferedRow; -typedef struct { - - gzFile gz_file; - FILE *file; - bool is_gzipped; - - NameMap row_map; - NameMap col_map; - - CooMatrix coo_matrix; - CooMatrix coo_matrix_q; - BufferedRow *buffered_rows; - size_t num_buffered_rows; - size_t buffered_rows_capacity; - - char *constraint_types; - double *objective_coeffs; - double *var_lower_bounds; - double *var_upper_bounds; - double *constraint_lower_bounds; - double *constraint_upper_bounds; - - size_t col_capacity; - size_t constraint_capacity; - - char *objective_row_name; - char *current_col_name; - double objective_constant; - bool is_maximize; - int error_flag; +typedef struct +{ + int row_idx; + CooMatrix coo; +} QcMatrixAccum; + +typedef struct +{ + + gzFile gz_file; + FILE *file; + bool is_gzipped; + + NameMap row_map; + NameMap col_map; + + CooMatrix coo_matrix; + CooMatrix coo_matrix_q; + BufferedRow *buffered_rows; + size_t num_buffered_rows; + size_t buffered_rows_capacity; + + char *constraint_types; + double *objective_coeffs; + double *var_lower_bounds; + double *var_upper_bounds; + double *constraint_lower_bounds; + double *constraint_upper_bounds; + + size_t col_capacity; + size_t constraint_capacity; + + char *objective_row_name; + char *current_col_name; + double objective_constant; + bool is_maximize; + int error_flag; + + QcMatrixAccum *qc_accums; + size_t num_qc_accums; + size_t qc_accums_capacity; + int qc_current_accum; + bool qc_current_is_obj; } MpsParserState; -static int add_coo_entry(CooMatrix *coo, int row, int col, double value) { - if (coo->nnz >= coo->capacity) { - size_t new_capacity = (coo->capacity == 0) ? 1024 : coo->capacity * 2; - coo->row_indices = - (int *)safe_realloc(coo->row_indices, new_capacity * sizeof(int)); - coo->col_indices = - (int *)safe_realloc(coo->col_indices, new_capacity * sizeof(int)); - coo->values = - (double *)safe_realloc(coo->values, new_capacity * sizeof(double)); - coo->capacity = new_capacity; - } - coo->row_indices[coo->nnz] = row; - coo->col_indices[coo->nnz] = col; - coo->values[coo->nnz] = value; - coo->nnz++; - return 0; +static int add_coo_entry(CooMatrix *coo, int row, int col, double value) +{ + if (coo->nnz >= coo->capacity) + { + size_t new_capacity = (coo->capacity == 0) ? 1024 : coo->capacity * 2; + coo->row_indices = (int *)safe_realloc(coo->row_indices, new_capacity * sizeof(int)); + coo->col_indices = (int *)safe_realloc(coo->col_indices, new_capacity * sizeof(int)); + coo->values = (double *)safe_realloc(coo->values, new_capacity * sizeof(double)); + coo->capacity = new_capacity; + } + coo->row_indices[coo->nnz] = row; + coo->col_indices[coo->nnz] = col; + coo->values[coo->nnz] = value; + coo->nnz++; + return 0; } -static bool ensure_column_capacity(MpsParserState *state) { - if (state->col_map.size < state->col_capacity) { - return true; - } +static bool ensure_column_capacity(MpsParserState *state) +{ + if (state->col_map.size < state->col_capacity) + { + return true; + } - size_t new_cap = (state->col_capacity == 0) ? 256 : state->col_capacity * 2; + size_t new_cap = (state->col_capacity == 0) ? 256 : state->col_capacity * 2; - if (new_cap < state->col_capacity) { - return false; - } + if (new_cap < state->col_capacity) + { + return false; + } - state->objective_coeffs = - (double *)safe_realloc(state->objective_coeffs, new_cap * sizeof(double)); - state->var_lower_bounds = - (double *)safe_realloc(state->var_lower_bounds, new_cap * sizeof(double)); - state->var_upper_bounds = - (double *)safe_realloc(state->var_upper_bounds, new_cap * sizeof(double)); + state->objective_coeffs = (double *)safe_realloc(state->objective_coeffs, new_cap * sizeof(double)); + state->var_lower_bounds = (double *)safe_realloc(state->var_lower_bounds, new_cap * sizeof(double)); + state->var_upper_bounds = (double *)safe_realloc(state->var_upper_bounds, new_cap * sizeof(double)); - for (size_t i = state->col_capacity; i < new_cap; ++i) { - state->objective_coeffs[i] = 0.0; - state->var_lower_bounds[i] = 0.0; - state->var_upper_bounds[i] = INFINITY; - } + for (size_t i = state->col_capacity; i < new_cap; ++i) + { + state->objective_coeffs[i] = 0.0; + state->var_lower_bounds[i] = 0.0; + state->var_upper_bounds[i] = INFINITY; + } - state->col_capacity = new_cap; - return true; + state->col_capacity = new_cap; + return true; } static void free_parser_state(MpsParserState *state); static int finalize_rows(MpsParserState *state); -static int parse_rows_section(MpsParserState *state, char **tokens, - int n_tokens); -static int parse_columns_section(MpsParserState *state, char **tokens, - int n_tokens); -static int parse_rhs_section(MpsParserState *state, char **tokens, - int n_tokens); -static int parse_ranges_section(MpsParserState *state, char **tokens, - int n_tokens); -static int parse_bounds_section(MpsParserState *state, char **tokens, - int n_tokens); -static int parse_quadobj_section(MpsParserState *state, char **tokens, - int n_tokens, bool fill_sym); -static int coo_to_csr_component(CsrComponent *csr, CooMatrix *coo, - size_t num_constraints); -typedef enum { - SEC_NONE, - SEC_ROWS, - SEC_COLUMNS, - SEC_RHS, - SEC_RANGES, - SEC_BOUNDS, - SEC_OBJSENSE, - SEC_ENDATA, - SEC_QUADOBJ, - SEC_QMATRIX +static int parse_rows_section(MpsParserState *state, char **tokens, int n_tokens); +static int parse_columns_section(MpsParserState *state, char **tokens, int n_tokens); +static int parse_rhs_section(MpsParserState *state, char **tokens, int n_tokens); +static int parse_ranges_section(MpsParserState *state, char **tokens, int n_tokens); +static int parse_bounds_section(MpsParserState *state, char **tokens, int n_tokens); +static int parse_quadobj_section(MpsParserState *state, char **tokens, int n_tokens, bool fill_sym); +static int open_qcmatrix_row(MpsParserState *state, const char *row_name); +static int parse_qcmatrix_section(MpsParserState *state, char **tokens, int n_tokens); +static int coo_to_csr_component(CsrComponent *csr, CooMatrix *coo, size_t num_constraints); +typedef enum +{ + SEC_NONE, + SEC_ROWS, + SEC_COLUMNS, + SEC_RHS, + SEC_RANGES, + SEC_BOUNDS, + SEC_OBJSENSE, + SEC_ENDATA, + SEC_QUADOBJ, + SEC_QMATRIX, + SEC_QCMATRIX } MpsSection; -qp_problem_t *read_mps_file(const char *filename) { - MpsParserState state = {0}; - MpsSection current_section = SEC_NONE; - bool rows_finalized = false; +qp_problem_t *read_mps_file(const char *filename) +{ + MpsParserState state = {0}; + state.qc_current_accum = -1; + MpsSection current_section = SEC_NONE; + bool rows_finalized = false; + + FastLineReader *reader = fast_reader_open(filename); + if (!reader) + { + fprintf(stderr, "ERROR: Could not open file %s\n", filename); + } + + namemap_init(&state.row_map, 1024); + namemap_init(&state.col_map, 1024); + + char line[4096]; + while (fast_reader_gets(reader, line, sizeof(line))) + { + if (state.error_flag) + break; + + if (line[0] == '*' || line[0] == '\n' || line[0] == '\r') + continue; + + char *tokens[6] = {NULL}; + int n_tokens = 0; + char *saveptr; + char *token = strtok_r(line, " \t\n\r", &saveptr); + while (token != NULL && n_tokens < 6) + { + tokens[n_tokens++] = token; + token = strtok_r(NULL, " \t\n\r", &saveptr); + } + if (n_tokens == 0) + continue; + + if (n_tokens == 1 && isalpha(tokens[0][0])) + { + MpsSection next_section = SEC_NONE; + if (strcmp(tokens[0], "ROWS") == 0) + next_section = SEC_ROWS; + else if (strcmp(tokens[0], "COLUMNS") == 0) + next_section = SEC_COLUMNS; + else if (strcmp(tokens[0], "RHS") == 0) + next_section = SEC_RHS; + else if (strcmp(tokens[0], "RANGES") == 0) + next_section = SEC_RANGES; + else if (strcmp(tokens[0], "BOUNDS") == 0) + next_section = SEC_BOUNDS; + else if (strcmp(tokens[0], "OBJSENSE") == 0) + next_section = SEC_OBJSENSE; + else if (strcmp(tokens[0], "QUADOBJ") == 0) + next_section = SEC_QUADOBJ; + else if (strcmp(tokens[0], "QMATRIX") == 0) + next_section = SEC_QMATRIX; + else if (strcmp(tokens[0], "ENDATA") == 0) + { + next_section = SEC_ENDATA; + } + + if (current_section == SEC_ROWS && next_section != SEC_ROWS && !rows_finalized) + { + if (finalize_rows(&state) != 0) + state.error_flag = 1; + rows_finalized = true; + } + + current_section = next_section; + if (current_section == SEC_ENDATA) + break; + continue; + } + + if (n_tokens == 2 && strcmp(tokens[0], "QCMATRIX") == 0) + { + if (current_section == SEC_ROWS && !rows_finalized) + { + if (finalize_rows(&state) != 0) + state.error_flag = 1; + rows_finalized = true; + } + current_section = SEC_QCMATRIX; + if (open_qcmatrix_row(&state, tokens[1]) != 0) + state.error_flag = 1; + continue; + } - FastLineReader *reader = fast_reader_open(filename); - if (!reader) { - fprintf(stderr, "ERROR: Could not open file %s\n", filename); - } + switch (current_section) + { + case SEC_OBJSENSE: + if (n_tokens > 0 && (strcmp(tokens[0], "MAX") == 0 || strcmp(tokens[0], "MAXIMIZE") == 0)) + { + state.is_maximize = true; + } + break; + case SEC_ROWS: + if (parse_rows_section(&state, tokens, n_tokens) != 0) + state.error_flag = 1; + break; + case SEC_COLUMNS: + if (parse_columns_section(&state, tokens, n_tokens) != 0) + state.error_flag = 1; + break; + case SEC_RHS: + if (parse_rhs_section(&state, tokens, n_tokens) != 0) + state.error_flag = 1; + break; + case SEC_RANGES: + if (parse_ranges_section(&state, tokens, n_tokens) != 0) + state.error_flag = 1; + break; + case SEC_BOUNDS: + if (parse_bounds_section(&state, tokens, n_tokens) != 0) + state.error_flag = 1; + break; + case SEC_QUADOBJ: + if (parse_quadobj_section(&state, tokens, n_tokens, true) != 0) + state.error_flag = 1; + break; + case SEC_QMATRIX: + if (parse_quadobj_section(&state, tokens, n_tokens, false) != 0) + state.error_flag = 1; + break; + case SEC_QCMATRIX: + if (parse_qcmatrix_section(&state, tokens, n_tokens) != 0) + state.error_flag = 1; + break; + default: + + break; + } + } - namemap_init(&state.row_map, 1024); - namemap_init(&state.col_map, 1024); + fast_reader_close(reader); - char line[4096]; - while (fast_reader_gets(reader, line, sizeof(line))) { if (state.error_flag) - break; - - if (line[0] == '*' || line[0] == '\n' || line[0] == '\r') - continue; - - char *tokens[6] = {NULL}; - int n_tokens = 0; - char *saveptr; - char *token = strtok_r(line, " \t\n\r", &saveptr); - while (token != NULL && n_tokens < 6) { - tokens[n_tokens++] = token; - token = strtok_r(NULL, " \t\n\r", &saveptr); - } - if (n_tokens == 0) - continue; - - if (n_tokens == 1 && isalpha(tokens[0][0])) { - MpsSection next_section = SEC_NONE; - if (strcmp(tokens[0], "ROWS") == 0) - next_section = SEC_ROWS; - else if (strcmp(tokens[0], "COLUMNS") == 0) - next_section = SEC_COLUMNS; - else if (strcmp(tokens[0], "RHS") == 0) - next_section = SEC_RHS; - else if (strcmp(tokens[0], "RANGES") == 0) - next_section = SEC_RANGES; - else if (strcmp(tokens[0], "BOUNDS") == 0) - next_section = SEC_BOUNDS; - else if (strcmp(tokens[0], "OBJSENSE") == 0) - next_section = SEC_OBJSENSE; - else if (strcmp(tokens[0], "QUADOBJ") == 0) - next_section = SEC_QUADOBJ; - else if (strcmp(tokens[0], "QMATRIX") == 0) - next_section = SEC_QMATRIX; - else if (strcmp(tokens[0], "ENDATA") == 0) { - next_section = SEC_ENDATA; - } - - if (current_section == SEC_ROWS && next_section != SEC_ROWS && - !rows_finalized) { - if (finalize_rows(&state) != 0) - state.error_flag = 1; - rows_finalized = true; - } - - current_section = next_section; - if (current_section == SEC_ENDATA) - break; - continue; - } - - switch (current_section) { - case SEC_OBJSENSE: - if (n_tokens > 0 && (strcmp(tokens[0], "MAX") == 0 || - strcmp(tokens[0], "MAXIMIZE") == 0)) { - state.is_maximize = true; - } - break; - case SEC_ROWS: - if (parse_rows_section(&state, tokens, n_tokens) != 0) - state.error_flag = 1; - break; - case SEC_COLUMNS: - if (parse_columns_section(&state, tokens, n_tokens) != 0) - state.error_flag = 1; - break; - case SEC_RHS: - if (parse_rhs_section(&state, tokens, n_tokens) != 0) - state.error_flag = 1; - break; - case SEC_RANGES: - if (parse_ranges_section(&state, tokens, n_tokens) != 0) - state.error_flag = 1; - break; - case SEC_BOUNDS: - if (parse_bounds_section(&state, tokens, n_tokens) != 0) - state.error_flag = 1; - break; - case SEC_QUADOBJ: - if (parse_quadobj_section(&state, tokens, n_tokens, true) != 0) - state.error_flag = 1; - break; - case SEC_QMATRIX: - if (parse_quadobj_section(&state, tokens, n_tokens, false) != 0) - state.error_flag = 1; - break; - default: - - break; - } - } - - fast_reader_close(reader); - - if (state.error_flag) { - fprintf(stderr, "ERROR: Failed to parse MPS file.\n"); - free_parser_state(&state); - return NULL; - } - - qp_problem_t *prob = safe_calloc(1, sizeof(qp_problem_t)); - - prob->num_variables = state.col_map.size; - prob->num_constraints = state.row_map.size; - prob->constraint_matrix_num_nonzeros = state.coo_matrix.nnz; - prob->objective_sparse_matrix_num_nonzeros = state.coo_matrix_q.nnz; - prob->num_rank_lowrank_obj = 0; - prob->objective_lowrank_matrix_num_nonzeros = 0; - prob->objective_lowrank_matrix = NULL; - prob->objective_constant = - state.is_maximize ? -state.objective_constant : state.objective_constant; - - prob->objective_vector = state.objective_coeffs; - prob->variable_lower_bound = state.var_lower_bounds; - prob->variable_upper_bound = state.var_upper_bounds; - prob->constraint_lower_bound = state.constraint_lower_bounds; - prob->constraint_upper_bound = state.constraint_upper_bounds; - - prob->primal_start = NULL; - prob->dual_start = NULL; - - state.objective_coeffs = NULL; - state.var_lower_bounds = NULL; - state.var_upper_bounds = NULL; - state.constraint_lower_bounds = NULL; - state.constraint_upper_bounds = NULL; - - if (state.is_maximize) { - for (int i = 0; i < prob->num_variables; ++i) { - prob->objective_vector[i] *= -1.0; - } - for (int i = 0; i < prob->objective_sparse_matrix_num_nonzeros; ++i) { - state.coo_matrix_q.values[i] *= -1.0; - } - } - - prob->constraint_matrix = (CsrComponent *)safe_malloc(sizeof(CsrComponent)); - - prob->constraint_matrix->row_ptr = NULL; - prob->constraint_matrix->col_ind = NULL; - prob->constraint_matrix->val = NULL; - - if (coo_to_csr_component(prob->constraint_matrix, &state.coo_matrix, - prob->num_constraints) != 0) { - fprintf(stderr, - "ERROR: Failed to convert COO Constraint matrix to CSR format.\n"); - qp_problem_free(prob); - free_parser_state(&state); - return NULL; - } + { + fprintf(stderr, "ERROR: Failed to parse MPS file.\n"); + free_parser_state(&state); + return NULL; + } - prob->objective_sparse_matrix = - (CsrComponent *)safe_malloc(sizeof(CsrComponent)); + qp_problem_t *prob = safe_calloc(1, sizeof(qp_problem_t)); + + prob->num_variables = state.col_map.size; + prob->num_constraints = state.row_map.size; + prob->affine_cone_offset = + prob->num_constraints > 0 ? (double *)safe_calloc((size_t)prob->num_constraints, sizeof(double)) : NULL; + prob->constraint_matrix_num_nonzeros = state.coo_matrix.nnz; + prob->objective_sparse_matrix_num_nonzeros = state.coo_matrix_q.nnz; + prob->num_rank_lowrank_obj = 0; + prob->objective_lowrank_matrix_num_nonzeros = 0; + prob->objective_lowrank_matrix = NULL; + prob->objective_constant = state.is_maximize ? -state.objective_constant : state.objective_constant; + + prob->objective_vector = state.objective_coeffs; + prob->variable_lower_bound = state.var_lower_bounds; + prob->variable_upper_bound = state.var_upper_bounds; + prob->constraint_lower_bound = state.constraint_lower_bounds; + prob->constraint_upper_bound = state.constraint_upper_bounds; + + prob->primal_start = NULL; + prob->dual_start = NULL; + + state.objective_coeffs = NULL; + state.var_lower_bounds = NULL; + state.var_upper_bounds = NULL; + state.constraint_lower_bounds = NULL; + state.constraint_upper_bounds = NULL; + + if (state.is_maximize) + { + for (int i = 0; i < prob->num_variables; ++i) + { + prob->objective_vector[i] *= -1.0; + } + for (int i = 0; i < prob->objective_sparse_matrix_num_nonzeros; ++i) + { + state.coo_matrix_q.values[i] *= -1.0; + } + } - prob->objective_sparse_matrix->row_ptr = NULL; - prob->objective_sparse_matrix->col_ind = NULL; - prob->objective_sparse_matrix->val = NULL; + prob->constraint_matrix = (CsrComponent *)safe_malloc(sizeof(CsrComponent)); - if (coo_to_csr_component(prob->objective_sparse_matrix, &state.coo_matrix_q, - prob->num_variables) != 0) { - fprintf(stderr, - "ERROR: Failed to convert COO Objective matrix to CSR format.\n"); - qp_problem_free(prob); - free_parser_state(&state); - return NULL; - } - prob->objective_lowrank_matrix = - (CsrComponent *)safe_malloc(sizeof(CsrComponent)); - - prob->objective_lowrank_matrix->row_ptr = (int *)safe_calloc(1, sizeof(int)); - ; - prob->objective_lowrank_matrix->col_ind = NULL; - prob->objective_lowrank_matrix->val = NULL; - - free_parser_state(&state); - return prob; -} + prob->constraint_matrix->row_ptr = NULL; + prob->constraint_matrix->col_ind = NULL; + prob->constraint_matrix->val = NULL; -static int parse_rows_section(MpsParserState *state, char **tokens, - int n_tokens) { - if (n_tokens < 2) - return 0; + if (coo_to_csr_component(prob->constraint_matrix, &state.coo_matrix, prob->num_constraints) != 0) + { + fprintf(stderr, "ERROR: Failed to convert COO Constraint matrix to CSR format.\n"); + qp_problem_free(prob); + free_parser_state(&state); + return NULL; + } - if (state->num_buffered_rows >= state->buffered_rows_capacity) { - state->buffered_rows_capacity = (state->buffered_rows_capacity == 0) - ? 64 - : state->buffered_rows_capacity * 2; - state->buffered_rows = (BufferedRow *)safe_realloc( - state->buffered_rows, - state->buffered_rows_capacity * sizeof(BufferedRow)); - } - - BufferedRow *new_row = &state->buffered_rows[state->num_buffered_rows]; - new_row->type = tokens[0][0]; - new_row->name = strdup(tokens[1]); - if (!new_row->name) - return -1; + prob->objective_sparse_matrix = (CsrComponent *)safe_malloc(sizeof(CsrComponent)); - state->num_buffered_rows++; - return 0; -} + prob->objective_sparse_matrix->row_ptr = NULL; + prob->objective_sparse_matrix->col_ind = NULL; + prob->objective_sparse_matrix->val = NULL; + + if (coo_to_csr_component(prob->objective_sparse_matrix, &state.coo_matrix_q, prob->num_variables) != 0) + { + fprintf(stderr, "ERROR: Failed to convert COO Objective matrix to CSR format.\n"); + qp_problem_free(prob); + free_parser_state(&state); + return NULL; + } + prob->objective_lowrank_matrix = (CsrComponent *)safe_malloc(sizeof(CsrComponent)); + + prob->objective_lowrank_matrix->row_ptr = (int *)safe_calloc(1, sizeof(int)); + ; + prob->objective_lowrank_matrix->col_ind = NULL; + prob->objective_lowrank_matrix->val = NULL; + + prob->num_quadratic_constraints = 0; + prob->quadratic_constraint_row_indices = NULL; + prob->quadratic_constraint_matrices = NULL; + prob->quadratic_constraint_matrix_num_nonzeros = NULL; + size_t qc_kept = 0; + for (size_t i = 0; i < state.num_qc_accums; ++i) + { + if (state.qc_accums[i].coo.nnz > 0) + qc_kept++; + } + if (qc_kept > 0) + { + prob->num_quadratic_constraints = (int)qc_kept; + prob->quadratic_constraint_row_indices = (int *)safe_calloc(qc_kept, sizeof(int)); + prob->quadratic_constraint_matrices = (CsrComponent **)safe_calloc(qc_kept, sizeof(CsrComponent *)); + prob->quadratic_constraint_matrix_num_nonzeros = (int *)safe_calloc(qc_kept, sizeof(int)); + size_t k = 0; + for (size_t i = 0; i < state.num_qc_accums; ++i) + { + QcMatrixAccum *acc = &state.qc_accums[i]; + if (acc->coo.nnz == 0) + continue; + if (state.is_maximize) + { + for (size_t j = 0; j < acc->coo.nnz; ++j) + acc->coo.values[j] *= -1.0; + } + prob->quadratic_constraint_row_indices[k] = acc->row_idx; + prob->quadratic_constraint_matrix_num_nonzeros[k] = (int)acc->coo.nnz; + CsrComponent *csr = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + if (coo_to_csr_component(csr, &acc->coo, prob->num_variables) != 0) + { + fprintf(stderr, "ERROR: Failed to convert QCMATRIX row %d to CSR.\n", acc->row_idx); + free(csr); + qp_problem_free(prob); + free_parser_state(&state); + return NULL; + } + prob->quadratic_constraint_matrices[k] = csr; + k++; + } + } -static int finalize_rows(MpsParserState *state) { - int obj_idx = -1; - - for (size_t i = 0; i < state->num_buffered_rows; ++i) { - if (state->buffered_rows[i].type == 'N') { - obj_idx = (int)i; - break; - } - } - - if (obj_idx == -1 && state->num_buffered_rows > 0) { - obj_idx = 0; - } - - if (obj_idx != -1) { - state->objective_row_name = strdup(state->buffered_rows[obj_idx].name); - if (!state->objective_row_name) - return -1; - } - - for (size_t i = 0; i < state->num_buffered_rows; ++i) { - if ((int)i == obj_idx) - continue; - - char type = state->buffered_rows[i].type; - if (type == 'E' || type == 'L' || type == 'G') { - size_t current_size = state->row_map.size; - if (current_size >= state->constraint_capacity) { - state->constraint_capacity = (state->constraint_capacity == 0) - ? 64 - : state->constraint_capacity * 2; - state->constraint_types = (char *)safe_realloc( - state->constraint_types, state->constraint_capacity * sizeof(char)); - } - namemap_put(&state->row_map, state->buffered_rows[i].name); - state->constraint_types[current_size] = type; - } - } - size_t num_constraints = state->row_map.size; - if (num_constraints > 0) { - state->constraint_lower_bounds = - safe_malloc(num_constraints * sizeof(double)); - state->constraint_upper_bounds = - safe_malloc(num_constraints * sizeof(double)); - - for (size_t i = 0; i < num_constraints; ++i) { - char type = state->constraint_types[i]; - if (type == 'L') { - state->constraint_lower_bounds[i] = -INFINITY; - state->constraint_upper_bounds[i] = 0.0; - } else if (type == 'G') { - state->constraint_lower_bounds[i] = 0.0; - state->constraint_upper_bounds[i] = INFINITY; - } else // 'E' - { - state->constraint_lower_bounds[i] = 0.0; - state->constraint_upper_bounds[i] = 0.0; - } - } - } - return 0; + free_parser_state(&state); + return prob; } -static int parse_columns_section(MpsParserState *state, char **tokens, - int n_tokens) { - if (n_tokens < 2) - return 0; +static int parse_rows_section(MpsParserState *state, char **tokens, int n_tokens) +{ + if (n_tokens < 2) + return 0; - if (n_tokens >= 2 && strcmp(tokens[1], "'MARKER'") == 0) { + if (state->num_buffered_rows >= state->buffered_rows_capacity) + { + state->buffered_rows_capacity = (state->buffered_rows_capacity == 0) ? 64 : state->buffered_rows_capacity * 2; + state->buffered_rows = + (BufferedRow *)safe_realloc(state->buffered_rows, state->buffered_rows_capacity * sizeof(BufferedRow)); + } + + BufferedRow *new_row = &state->buffered_rows[state->num_buffered_rows]; + new_row->type = tokens[0][0]; + new_row->name = strdup(tokens[1]); + if (!new_row->name) + return -1; + + state->num_buffered_rows++; return 0; - } +} - const char *col_name = NULL; - int pair_start_index; +static int finalize_rows(MpsParserState *state) +{ + int obj_idx = -1; - if (n_tokens % 2 != 0) { - free(state->current_col_name); - state->current_col_name = strdup(tokens[0]); - if (!state->current_col_name) - return -1; - - col_name = state->current_col_name; - pair_start_index = 1; - } else { - if (!state->current_col_name) { - fprintf(stderr, - "ERROR: Column data found before any column name was defined.\n"); - return -1; - } - col_name = state->current_col_name; - pair_start_index = 0; - } - - if (!ensure_column_capacity(state)) - return -1; + for (size_t i = 0; i < state->num_buffered_rows; ++i) + { + if (state->buffered_rows[i].type == 'N') + { + obj_idx = (int)i; + break; + } + } - int col_idx = namemap_put(&state->col_map, col_name); - if (col_idx == -1) - return -1; + if (obj_idx == -1 && state->num_buffered_rows > 0) + { + obj_idx = 0; + } - for (int i = pair_start_index; i + 1 < n_tokens; i += 2) { - const char *row_name = tokens[i]; - double value = atof(tokens[i + 1]); + if (obj_idx != -1) + { + state->objective_row_name = strdup(state->buffered_rows[obj_idx].name); + if (!state->objective_row_name) + return -1; + } - if (state->objective_row_name && - strcmp(row_name, state->objective_row_name) == 0) { - state->objective_coeffs[col_idx] += value; - } else { - int row_idx = namemap_get(&state->row_map, row_name); - if (row_idx != -1) { - if (add_coo_entry(&state->coo_matrix, row_idx, col_idx, value) != 0) { - return -1; + for (size_t i = 0; i < state->num_buffered_rows; ++i) + { + if ((int)i == obj_idx) + continue; + + char type = state->buffered_rows[i].type; + if (type == 'E' || type == 'L' || type == 'G') + { + size_t current_size = state->row_map.size; + if (current_size >= state->constraint_capacity) + { + state->constraint_capacity = (state->constraint_capacity == 0) ? 64 : state->constraint_capacity * 2; + state->constraint_types = + (char *)safe_realloc(state->constraint_types, state->constraint_capacity * sizeof(char)); + } + namemap_put(&state->row_map, state->buffered_rows[i].name); + state->constraint_types[current_size] = type; + } + } + size_t num_constraints = state->row_map.size; + if (num_constraints > 0) + { + state->constraint_lower_bounds = safe_malloc(num_constraints * sizeof(double)); + state->constraint_upper_bounds = safe_malloc(num_constraints * sizeof(double)); + + for (size_t i = 0; i < num_constraints; ++i) + { + char type = state->constraint_types[i]; + if (type == 'L') + { + state->constraint_lower_bounds[i] = -INFINITY; + state->constraint_upper_bounds[i] = 0.0; + } + else if (type == 'G') + { + state->constraint_lower_bounds[i] = 0.0; + state->constraint_upper_bounds[i] = INFINITY; + } + else // 'E' + { + state->constraint_lower_bounds[i] = 0.0; + state->constraint_upper_bounds[i] = 0.0; + } } - } } - } - return 0; -} -static int parse_quadobj_section(MpsParserState *state, char **tokens, - int n_tokens, bool fill_sym) { - if (n_tokens < 3) return 0; +} - const char *row_name1 = tokens[0]; - const char *row_name2 = tokens[1]; - double value = atof(tokens[2]); +static int parse_columns_section(MpsParserState *state, char **tokens, int n_tokens) +{ + if (n_tokens < 2) + return 0; - if (value == 0.0) - return 0; + if (n_tokens >= 2 && strcmp(tokens[1], "'MARKER'") == 0) + { + return 0; + } - int col_idx1 = namemap_get(&state->col_map, row_name1); - int col_idx2 = namemap_get(&state->col_map, row_name2); - if (col_idx1 == -1 || col_idx2 == -1) { - fprintf(stderr, - "Warning: Variable '%s' or '%s' not found in COLUMNS. Skipping " - "Q-entry.\n", - row_name1, row_name2); - return 0; - } + const char *col_name = NULL; + int pair_start_index; + + if (n_tokens % 2 != 0) + { + free(state->current_col_name); + state->current_col_name = strdup(tokens[0]); + if (!state->current_col_name) + return -1; - if (col_idx1 == col_idx2) { - if (add_coo_entry(&state->coo_matrix_q, col_idx1, col_idx1, value) != 0) { - return -1; + col_name = state->current_col_name; + pair_start_index = 1; } - } else { - if (add_coo_entry(&state->coo_matrix_q, col_idx1, col_idx2, value) != 0) { - return -1; + else + { + if (!state->current_col_name) + { + fprintf(stderr, "ERROR: Column data found before any column name was defined.\n"); + return -1; + } + col_name = state->current_col_name; + pair_start_index = 0; } - if (fill_sym) { - if (add_coo_entry(&state->coo_matrix_q, col_idx2, col_idx1, value) != 0) { + + if (!ensure_column_capacity(state)) return -1; - } + + int col_idx = namemap_put(&state->col_map, col_name); + if (col_idx == -1) + return -1; + + for (int i = pair_start_index; i + 1 < n_tokens; i += 2) + { + const char *row_name = tokens[i]; + double value = atof(tokens[i + 1]); + + if (state->objective_row_name && strcmp(row_name, state->objective_row_name) == 0) + { + state->objective_coeffs[col_idx] += value; + } + else + { + int row_idx = namemap_get(&state->row_map, row_name); + if (row_idx != -1) + { + if (add_coo_entry(&state->coo_matrix, row_idx, col_idx, value) != 0) + { + return -1; + } + } + } } - } - return 0; + return 0; } -static int parse_rhs_section(MpsParserState *state, char **tokens, - int n_tokens) { - - for (int i = 1; i + 1 < n_tokens; i += 2) { - const char *row_name = tokens[i]; - double value = atof(tokens[i + 1]); - - if (state->objective_row_name && - strcmp(row_name, state->objective_row_name) == 0) { - state->objective_constant = -value; - } else { - int row_idx = namemap_get(&state->row_map, row_name); - if (row_idx != -1) { - char type = state->constraint_types[row_idx]; - if (type == 'L') - state->constraint_upper_bounds[row_idx] = value; - else if (type == 'G') - state->constraint_lower_bounds[row_idx] = value; - else { - state->constraint_lower_bounds[row_idx] = value; - state->constraint_upper_bounds[row_idx] = value; - } - } - } - } - return 0; +static int parse_quadobj_section(MpsParserState *state, char **tokens, int n_tokens, bool fill_sym) +{ + if (n_tokens < 3) + return 0; + + const char *row_name1 = tokens[0]; + const char *row_name2 = tokens[1]; + double value = atof(tokens[2]); + + if (value == 0.0) + return 0; + + int col_idx1 = namemap_get(&state->col_map, row_name1); + int col_idx2 = namemap_get(&state->col_map, row_name2); + if (col_idx1 == -1 || col_idx2 == -1) + { + fprintf(stderr, + "Warning: Variable '%s' or '%s' not found in COLUMNS. Skipping " + "Q-entry.\n", + row_name1, + row_name2); + return 0; + } + + if (col_idx1 == col_idx2) + { + if (add_coo_entry(&state->coo_matrix_q, col_idx1, col_idx1, value) != 0) + { + return -1; + } + } + else + { + if (add_coo_entry(&state->coo_matrix_q, col_idx1, col_idx2, value) != 0) + { + return -1; + } + if (fill_sym) + { + if (add_coo_entry(&state->coo_matrix_q, col_idx2, col_idx1, value) != 0) + { + return -1; + } + } + } + return 0; } -static int parse_ranges_section(MpsParserState *state, char **tokens, - int n_tokens) { +static int open_qcmatrix_row(MpsParserState *state, const char *row_name) +{ + state->qc_current_accum = -1; + state->qc_current_is_obj = false; + + if (state->objective_row_name && strcmp(row_name, state->objective_row_name) == 0) + { + state->qc_current_is_obj = true; + return 0; + } - for (int i = 1; i + 1 < n_tokens; i += 2) { - const char *row_name = tokens[i]; - double range_val = atof(tokens[i + 1]); int row_idx = namemap_get(&state->row_map, row_name); + if (row_idx == -1) + { + fprintf(stderr, "Warning: QCMATRIX references unknown row '%s'. Skipping.\n", row_name); + return 0; + } - if (row_idx != -1) { - char type = state->constraint_types[row_idx]; - double rhs = (type == 'L') ? state->constraint_upper_bounds[row_idx] - : state->constraint_lower_bounds[row_idx]; - - if (type == 'G') { - state->constraint_upper_bounds[row_idx] = rhs + fabs(range_val); - } else if (type == 'L') { - state->constraint_lower_bounds[row_idx] = rhs - fabs(range_val); - } else if (type == 'E') { - if (range_val >= 0) { - state->constraint_upper_bounds[row_idx] = rhs + range_val; - } else { - state->constraint_lower_bounds[row_idx] = rhs + range_val; - } - } - } - } - return 0; -} + for (size_t i = 0; i < state->num_qc_accums; ++i) + { + if (state->qc_accums[i].row_idx == row_idx) + { + state->qc_current_accum = (int)i; + return 0; + } + } -static int parse_bounds_section(MpsParserState *state, char **tokens, - int n_tokens) { - if (n_tokens < 3) + if (state->num_qc_accums >= state->qc_accums_capacity) + { + size_t new_cap = state->qc_accums_capacity == 0 ? 16 : state->qc_accums_capacity * 2; + state->qc_accums = (QcMatrixAccum *)safe_realloc(state->qc_accums, new_cap * sizeof(QcMatrixAccum)); + memset(state->qc_accums + state->qc_accums_capacity, + 0, + (new_cap - state->qc_accums_capacity) * sizeof(QcMatrixAccum)); + state->qc_accums_capacity = new_cap; + } + state->qc_accums[state->num_qc_accums].row_idx = row_idx; + state->qc_current_accum = (int)state->num_qc_accums; + state->num_qc_accums++; return 0; +} - const char *bound_type = tokens[0]; +static int parse_qcmatrix_section(MpsParserState *state, char **tokens, int n_tokens) +{ + if (n_tokens < 3) + return 0; + if (state->qc_current_accum < 0 && !state->qc_current_is_obj) + { + return 0; + } + + const char *col_name1 = tokens[0]; + const char *col_name2 = tokens[1]; + double value = atof(tokens[2]); + if (value == 0.0) + return 0; + + int col_idx1 = namemap_get(&state->col_map, col_name1); + int col_idx2 = namemap_get(&state->col_map, col_name2); + if (col_idx1 == -1 || col_idx2 == -1) + { + fprintf(stderr, + "Warning: QCMATRIX references unknown variable '%s' or '%s'. " + "Skipping entry.\n", + col_name1, + col_name2); + return 0; + } - const char *col_name = tokens[2]; - double value = (n_tokens > 3) ? atof(tokens[3]) : 0.0; + CooMatrix *coo = state->qc_current_is_obj ? &state->coo_matrix_q : &state->qc_accums[state->qc_current_accum].coo; + return add_coo_entry(coo, col_idx1, col_idx2, value); +} - int col_idx = namemap_get(&state->col_map, col_name); - if (col_idx == -1) +static int parse_rhs_section(MpsParserState *state, char **tokens, int n_tokens) +{ + + for (int i = 1; i + 1 < n_tokens; i += 2) + { + const char *row_name = tokens[i]; + double value = atof(tokens[i + 1]); + + if (state->objective_row_name && strcmp(row_name, state->objective_row_name) == 0) + { + state->objective_constant = -value; + } + else + { + int row_idx = namemap_get(&state->row_map, row_name); + if (row_idx != -1) + { + char type = state->constraint_types[row_idx]; + if (type == 'L') + state->constraint_upper_bounds[row_idx] = value; + else if (type == 'G') + state->constraint_lower_bounds[row_idx] = value; + else + { + state->constraint_lower_bounds[row_idx] = value; + state->constraint_upper_bounds[row_idx] = value; + } + } + } + } return 0; +} - if (strcmp(bound_type, "LO") == 0) { - state->var_lower_bounds[col_idx] = value; - } else if (strcmp(bound_type, "UP") == 0) { - state->var_upper_bounds[col_idx] = value; - } else if (strcmp(bound_type, "FX") == 0) { - state->var_lower_bounds[col_idx] = value; - state->var_upper_bounds[col_idx] = value; - } else if (strcmp(bound_type, "FR") == 0) { - state->var_lower_bounds[col_idx] = -INFINITY; - state->var_upper_bounds[col_idx] = INFINITY; - } else if (strcmp(bound_type, "MI") == 0) { - state->var_lower_bounds[col_idx] = -INFINITY; - } else if (strcmp(bound_type, "PL") == 0) { - state->var_upper_bounds[col_idx] = INFINITY; - } else if (strcmp(bound_type, "BV") == 0) { - state->var_lower_bounds[col_idx] = 0.0; - state->var_upper_bounds[col_idx] = 1.0; - } - return 0; +static int parse_ranges_section(MpsParserState *state, char **tokens, int n_tokens) +{ + + for (int i = 1; i + 1 < n_tokens; i += 2) + { + const char *row_name = tokens[i]; + double range_val = atof(tokens[i + 1]); + int row_idx = namemap_get(&state->row_map, row_name); + + if (row_idx != -1) + { + char type = state->constraint_types[row_idx]; + double rhs = + (type == 'L') ? state->constraint_upper_bounds[row_idx] : state->constraint_lower_bounds[row_idx]; + + if (type == 'G') + { + state->constraint_upper_bounds[row_idx] = rhs + fabs(range_val); + } + else if (type == 'L') + { + state->constraint_lower_bounds[row_idx] = rhs - fabs(range_val); + } + else if (type == 'E') + { + if (range_val >= 0) + { + state->constraint_upper_bounds[row_idx] = rhs + range_val; + } + else + { + state->constraint_lower_bounds[row_idx] = rhs + range_val; + } + } + } + } + return 0; } -static int coo_to_csr_component(CsrComponent *csr, CooMatrix *coo, - size_t num_rows) { - csr->row_ptr = (int *)safe_calloc(num_rows + 1, sizeof(int)); - if (!csr->row_ptr) - return -1; +static int parse_bounds_section(MpsParserState *state, char **tokens, int n_tokens) +{ + if (n_tokens < 3) + return 0; - if (coo->nnz > 0) { - csr->col_ind = (int *)safe_malloc(coo->nnz * sizeof(int)); - csr->val = (double *)safe_malloc(coo->nnz * sizeof(double)); + const char *bound_type = tokens[0]; - if (!csr->col_ind || !csr->val) - return -1; - for (size_t i = 0; i < coo->nnz; ++i) { - csr->row_ptr[coo->row_indices[i] + 1]++; + const char *col_name = tokens[2]; + double value = (n_tokens > 3) ? atof(tokens[3]) : 0.0; + + int col_idx = namemap_get(&state->col_map, col_name); + if (col_idx == -1) + return 0; + + if (strcmp(bound_type, "LO") == 0) + { + state->var_lower_bounds[col_idx] = value; + } + else if (strcmp(bound_type, "UP") == 0) + { + state->var_upper_bounds[col_idx] = value; + } + else if (strcmp(bound_type, "FX") == 0) + { + state->var_lower_bounds[col_idx] = value; + state->var_upper_bounds[col_idx] = value; + } + else if (strcmp(bound_type, "FR") == 0) + { + state->var_lower_bounds[col_idx] = -INFINITY; + state->var_upper_bounds[col_idx] = INFINITY; } - for (size_t i = 1; i <= num_rows; ++i) { - csr->row_ptr[i] += csr->row_ptr[i - 1]; + else if (strcmp(bound_type, "MI") == 0) + { + state->var_lower_bounds[col_idx] = -INFINITY; } + else if (strcmp(bound_type, "PL") == 0) + { + state->var_upper_bounds[col_idx] = INFINITY; + } + else if (strcmp(bound_type, "BV") == 0) + { + state->var_lower_bounds[col_idx] = 0.0; + state->var_upper_bounds[col_idx] = 1.0; + } + return 0; +} - int *row_pos = (int *)safe_malloc((num_rows + 1) * sizeof(int)); - memcpy(row_pos, csr->row_ptr, (num_rows + 1) * sizeof(int)); +static int coo_to_csr_component(CsrComponent *csr, CooMatrix *coo, size_t num_rows) +{ + csr->row_ptr = (int *)safe_calloc(num_rows + 1, sizeof(int)); + if (!csr->row_ptr) + return -1; + + if (coo->nnz > 0) + { + csr->col_ind = (int *)safe_malloc(coo->nnz * sizeof(int)); + csr->val = (double *)safe_malloc(coo->nnz * sizeof(double)); + + if (!csr->col_ind || !csr->val) + return -1; + for (size_t i = 0; i < coo->nnz; ++i) + { + csr->row_ptr[coo->row_indices[i] + 1]++; + } + for (size_t i = 1; i <= num_rows; ++i) + { + csr->row_ptr[i] += csr->row_ptr[i - 1]; + } - for (size_t i = 0; i < coo->nnz; ++i) { - int row = coo->row_indices[i]; - int dest_idx = row_pos[row]; + int *row_pos = (int *)safe_malloc((num_rows + 1) * sizeof(int)); + memcpy(row_pos, csr->row_ptr, (num_rows + 1) * sizeof(int)); - csr->col_ind[dest_idx] = coo->col_indices[i]; - csr->val[dest_idx] = coo->values[i]; + for (size_t i = 0; i < coo->nnz; ++i) + { + int row = coo->row_indices[i]; + int dest_idx = row_pos[row]; - row_pos[row]++; + csr->col_ind[dest_idx] = coo->col_indices[i]; + csr->val[dest_idx] = coo->values[i]; + + row_pos[row]++; + } + free(row_pos); + } + else + { + csr->col_ind = NULL; + csr->val = NULL; } - free(row_pos); - } else { - csr->col_ind = NULL; - csr->val = NULL; - } - return 0; + return 0; } -static void free_parser_state(MpsParserState *state) { - if (!state) - return; - - namemap_free(&state->row_map); - namemap_free(&state->col_map); - - if (state->buffered_rows) { - for (size_t i = 0; i < state->num_buffered_rows; ++i) { - free(state->buffered_rows[i].name); - } - free(state->buffered_rows); - } - - free(state->coo_matrix.row_indices); - free(state->coo_matrix.col_indices); - free(state->coo_matrix.values); - - free(state->coo_matrix_q.row_indices); - free(state->coo_matrix_q.col_indices); - free(state->coo_matrix_q.values); - - free(state->constraint_types); - free(state->objective_coeffs); - free(state->var_lower_bounds); - free(state->var_upper_bounds); - free(state->constraint_lower_bounds); - free(state->constraint_upper_bounds); - free(state->objective_row_name); - free(state->current_col_name); -} \ No newline at end of file +static void free_parser_state(MpsParserState *state) +{ + if (!state) + return; + + namemap_free(&state->row_map); + namemap_free(&state->col_map); + + if (state->buffered_rows) + { + for (size_t i = 0; i < state->num_buffered_rows; ++i) + { + free(state->buffered_rows[i].name); + } + free(state->buffered_rows); + } + + free(state->coo_matrix.row_indices); + free(state->coo_matrix.col_indices); + free(state->coo_matrix.values); + + free(state->coo_matrix_q.row_indices); + free(state->coo_matrix_q.col_indices); + free(state->coo_matrix_q.values); + + if (state->qc_accums) + { + for (size_t i = 0; i < state->num_qc_accums; ++i) + { + free(state->qc_accums[i].coo.row_indices); + free(state->qc_accums[i].coo.col_indices); + free(state->qc_accums[i].coo.values); + } + free(state->qc_accums); + } + + free(state->constraint_types); + free(state->objective_coeffs); + free(state->var_lower_bounds); + free(state->var_upper_bounds); + free(state->constraint_lower_bounds); + free(state->constraint_upper_bounds); + free(state->objective_row_name); + free(state->current_col_name); +} diff --git a/src/partition_utils.c b/src/partition_utils.c new file mode 100644 index 0000000..91a7b72 --- /dev/null +++ b/src/partition_utils.c @@ -0,0 +1,248 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "partition_utils.h" +#include "utils.h" + +#include +#include + +typedef struct +{ + int first; + int last; +} forbidden_boundary_interval_t; + +static int compare_boundary_intervals(const void *left, const void *right) +{ + const forbidden_boundary_interval_t *a = (const forbidden_boundary_interval_t *)left; + const forbidden_boundary_interval_t *b = (const forbidden_boundary_interval_t *)right; + return (a->first > b->first) - (a->first < b->first); +} + +static int compare_ints(const void *left, const void *right) +{ + int a = *(const int *)left; + int b = *(const int *)right; + return (a > b) - (a < b); +} + +static int containing_boundary_interval(const forbidden_boundary_interval_t *intervals, int count, int boundary) +{ + int low = 0; + int high = count - 1; + while (low <= high) + { + int middle = low + (high - low) / 2; + if (boundary < intervals[middle].first) + high = middle - 1; + else if (boundary > intervals[middle].last) + low = middle + 1; + else + return middle; + } + return -1; +} + +static int project_to_legal_boundary( + const forbidden_boundary_interval_t *intervals, int count, int total_dimension, int boundary, int direction) +{ + if (total_dimension <= 1) + return -1; + if (boundary < 1) + { + if (direction < 0) + return -1; + boundary = 1; + } + if (boundary >= total_dimension) + { + if (direction > 0) + return -1; + boundary = total_dimension - 1; + } + + int interval = containing_boundary_interval(intervals, count, boundary); + if (interval >= 0) + boundary = direction < 0 ? intervals[interval].first - 1 : intervals[interval].last + 1; + return boundary > 0 && boundary < total_dimension ? boundary : -1; +} + +static void add_cut_candidate(int *candidates, int *count, int candidate) +{ + if (candidate > 0) + candidates[(*count)++] = candidate; +} + +bool optimize_partition_cuts(int total_dimension, + int num_partitions, + const int *forbidden_starts, + const int *forbidden_ends, + int num_forbidden_intervals, + int *cuts) +{ + if (total_dimension < 0 || num_partitions <= 0 || num_forbidden_intervals < 0 || !cuts || + (num_forbidden_intervals > 0 && (!forbidden_starts || !forbidden_ends))) + return false; + + int cuts_needed = num_partitions - 1; + if (cuts_needed <= 0) + return true; + if (total_dimension < num_partitions) + return false; + + forbidden_boundary_interval_t *intervals = num_forbidden_intervals > 0 + ? (forbidden_boundary_interval_t *)safe_malloc((size_t)num_forbidden_intervals * sizeof(*intervals)) + : NULL; + for (int interval = 0; interval < num_forbidden_intervals; ++interval) + { + int first = forbidden_starts[interval]; + int last = forbidden_ends[interval]; + if (first < 1 || last < first || last >= total_dimension) + { + free(intervals); + return false; + } + intervals[interval].first = first; + intervals[interval].last = last; + } + if (num_forbidden_intervals > 1) + qsort(intervals, (size_t)num_forbidden_intervals, sizeof(*intervals), compare_boundary_intervals); + + int merged_count = 0; + for (int interval = 0; interval < num_forbidden_intervals; ++interval) + { + if (merged_count == 0 || intervals[interval].first > intervals[merged_count - 1].last + 1) + { + intervals[merged_count++] = intervals[interval]; + } + else if (intervals[interval].last > intervals[merged_count - 1].last) + { + intervals[merged_count - 1].last = intervals[interval].last; + } + } + + size_t candidate_capacity = 4u * (size_t)num_partitions * (size_t)num_partitions + 4u * num_partitions; + int *candidates = (int *)safe_malloc(candidate_capacity * sizeof(int)); + int candidate_count = 0; + + /* An optimum with P-1 cuts cannot skip P unused legal boundaries on either side of a target. */ + for (int cut = 1; cut < num_partitions; ++cut) + { + int lower = project_to_legal_boundary(intervals, merged_count, total_dimension, cuts[cut], -1); + int upper = project_to_legal_boundary(intervals, merged_count, total_dimension, cuts[cut], 1); + for (int step = 0; step < num_partitions && lower > 0; ++step) + { + add_cut_candidate(candidates, &candidate_count, lower); + lower = project_to_legal_boundary(intervals, merged_count, total_dimension, lower - 1, -1); + } + for (int step = 0; step < num_partitions && upper > 0; ++step) + { + add_cut_candidate(candidates, &candidate_count, upper); + upper = project_to_legal_boundary(intervals, merged_count, total_dimension, upper + 1, 1); + } + } + + int earliest = project_to_legal_boundary(intervals, merged_count, total_dimension, 1, 1); + int latest = project_to_legal_boundary(intervals, merged_count, total_dimension, total_dimension - 1, -1); + for (int step = 0; step < num_partitions && earliest > 0; ++step) + { + add_cut_candidate(candidates, &candidate_count, earliest); + earliest = project_to_legal_boundary(intervals, merged_count, total_dimension, earliest + 1, 1); + } + for (int step = 0; step < num_partitions && latest > 0; ++step) + { + add_cut_candidate(candidates, &candidate_count, latest); + latest = project_to_legal_boundary(intervals, merged_count, total_dimension, latest - 1, -1); + } + free(intervals); + + qsort(candidates, (size_t)candidate_count, sizeof(int), compare_ints); + int unique_count = 0; + for (int candidate = 0; candidate < candidate_count; ++candidate) + { + if (unique_count == 0 || candidates[candidate] != candidates[unique_count - 1]) + candidates[unique_count++] = candidates[candidate]; + } + if (unique_count < cuts_needed) + { + free(candidates); + return false; + } + + size_t table_size = (size_t)cuts_needed * (size_t)unique_count; + long double *cost = (long double *)safe_malloc(table_size * sizeof(long double)); + int *predecessor = (int *)safe_malloc(table_size * sizeof(int)); + for (int candidate = 0; candidate < unique_count; ++candidate) + { + long double delta = (long double)candidates[candidate] - cuts[1]; + cost[candidate] = delta * delta; + predecessor[candidate] = -1; + } + + for (int cut = 1; cut < cuts_needed; ++cut) + { + long double best_prefix_cost = LDBL_MAX; + int best_prefix = -1; + for (int candidate = 0; candidate < unique_count; ++candidate) + { + if (candidate > 0) + { + long double previous = cost[(size_t)(cut - 1) * unique_count + candidate - 1]; + if (previous < best_prefix_cost) + { + best_prefix_cost = previous; + best_prefix = candidate - 1; + } + } + size_t entry = (size_t)cut * unique_count + candidate; + if (best_prefix < 0) + { + cost[entry] = LDBL_MAX; + predecessor[entry] = -1; + continue; + } + long double delta = (long double)candidates[candidate] - cuts[cut + 1]; + cost[entry] = best_prefix_cost + delta * delta; + predecessor[entry] = best_prefix; + } + } + + int best = -1; + long double best_cost = LDBL_MAX; + size_t final_row = (size_t)(cuts_needed - 1) * unique_count; + for (int candidate = 0; candidate < unique_count; ++candidate) + { + if (cost[final_row + candidate] < best_cost) + { + best_cost = cost[final_row + candidate]; + best = candidate; + } + } + if (best >= 0) + { + for (int cut = cuts_needed - 1; cut >= 0; --cut) + { + cuts[cut + 1] = candidates[best]; + best = predecessor[(size_t)cut * unique_count + best]; + } + } + + free(cost); + free(predecessor); + free(candidates); + return best_cost < LDBL_MAX; +} diff --git a/src/pdhcg.c b/src/pdhcg.c index 2299e69..1d3d266 100644 --- a/src/pdhcg.c +++ b/src/pdhcg.c @@ -16,303 +16,420 @@ limitations under the License. */ #include "pdhcg.h" +#include "cone_utils.h" +#include "distributed_interface.h" #include "solver.h" #include "utils.h" +#include +#include #include #include #include #include #include -#ifdef PDHCG_COMPILE_DISTRIBUTED -#include "distributed_solver.h" -#endif - volatile sig_atomic_t g_pdhcg_cancel_request = 0; -// create an qp_problem_t from a matrix -qp_problem_t *create_qp_problem(const double *objective_c, - const matrix_desc_t *Q_desc, - const matrix_desc_t *R_desc, - const matrix_desc_t *D_desc, - const matrix_desc_t *A_desc, - const double *con_lb, - const double *con_ub, - const double *var_lb, - const double *var_ub, - const double *objective_constant) -{ - qp_problem_t *prob = (qp_problem_t *)safe_malloc(sizeof(qp_problem_t)); - prob->primal_start = NULL; - prob->dual_start = NULL; - - int n = 0; - int m = 0; +static void csr_component_free(CsrComponent *csr); - if (A_desc) - { - n = A_desc->n; - m = A_desc->m; - } - else if (Q_desc) - { - n = Q_desc->n; // Infer variables from Quadratic term if A is missing - } - else if (R_desc) - { - n = R_desc->n; // Infer variables from Low-Rank term if others missing - } - else +static int validate_matrix_descriptor(const matrix_desc_t *desc, const char *name) +{ + if (!desc) + return 0; + if (desc->m < 0 || desc->n < 0) { - fprintf(stderr, - "[interface] Error: At least one matrix (A, Q, or R) must " - "be provided for non-trivil optimization problem.\n"); - free(prob); - return NULL; + fprintf(stderr, "[create_qp_problem] %s matrix has negative shape (%d, %d).\n", name, desc->m, desc->n); + return -1; } - if (n == 0 && (Q_desc || R_desc || A_desc)) + switch (desc->fmt) { - fprintf(stderr, - "[interface] Warning: Matrix dimensions seem to be zero or " - "inconsistent.\n"); - } - - prob->num_variables = n; - prob->num_constraints = m; + case matrix_dense: + if (desc->m > 0 && desc->n > INT_MAX / desc->m) + { + fprintf(stderr, "[create_qp_problem] %s dense matrix is too large to index with int.\n", name); + return -1; + } + if (desc->m > 0 && desc->n > 0 && !desc->data.dense.A) + { + fprintf(stderr, "[create_qp_problem] %s dense matrix data is NULL.\n", name); + return -1; + } + return 0; - // --- 2. Process Constraint Matrix (A) [OPTIONAL] --- - prob->constraint_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); - if (A_desc) - { - switch (A_desc->fmt) + case matrix_csr: { - case matrix_dense: - dense_to_csr(A_desc, - &prob->constraint_matrix->row_ptr, - &prob->constraint_matrix->col_ind, - &prob->constraint_matrix->val, - &prob->constraint_matrix_num_nonzeros); - break; - case matrix_csc: + int nnz = desc->data.csr.nnz; + const int *row_ptr = desc->data.csr.row_ptr; + if (nnz < 0 || !row_ptr || (nnz > 0 && (!desc->data.csr.col_ind || !desc->data.csr.vals))) { - int *row_ptr = NULL, *col_ind = NULL; - double *vals = NULL; - int nnz = 0; - if (csc_to_csr(A_desc, &row_ptr, &col_ind, &vals, &nnz) != 0) + fprintf(stderr, "[create_qp_problem] %s CSR storage is incomplete.\n", name); + return -1; + } + if (row_ptr[0] != 0 || row_ptr[desc->m] != nnz) + { + fprintf(stderr, "[create_qp_problem] %s CSR row pointers do not span [0, nnz].\n", name); + return -1; + } + for (int row = 0; row < desc->m; ++row) + { + if (row_ptr[row] > row_ptr[row + 1] || row_ptr[row] < 0 || row_ptr[row + 1] > nnz) { - fprintf(stderr, "[interface] A matrix CSC->CSR failed.\n"); - free(prob); - return NULL; + fprintf(stderr, "[create_qp_problem] %s CSR row pointers are invalid at row %d.\n", name, row); + return -1; } - prob->constraint_matrix_num_nonzeros = nnz; - prob->constraint_matrix->row_ptr = row_ptr; - prob->constraint_matrix->col_ind = col_ind; - prob->constraint_matrix->val = vals; - break; } - case matrix_coo: + for (int entry = 0; entry < nnz; ++entry) { - int *row_ptr = NULL, *col_ind = NULL; - double *vals = NULL; - int nnz = 0; - if (coo_to_csr(A_desc, &row_ptr, &col_ind, &vals, &nnz) != 0) + int column = desc->data.csr.col_ind[entry]; + if (column < 0 || column >= desc->n) { - fprintf(stderr, "[interface] A matrix COO->CSR failed.\n"); - free(prob); - return NULL; + fprintf(stderr, + "[create_qp_problem] %s CSR column index %d is out of range [0, %d).\n", + name, + column, + desc->n); + return -1; } - prob->constraint_matrix_num_nonzeros = nnz; - prob->constraint_matrix->row_ptr = row_ptr; - prob->constraint_matrix->col_ind = col_ind; - prob->constraint_matrix->val = vals; - break; } - case matrix_csr: - prob->constraint_matrix_num_nonzeros = A_desc->data.csr.nnz; - prob->constraint_matrix->row_ptr = (int *)safe_malloc((size_t)(A_desc->m + 1) * sizeof(int)); - prob->constraint_matrix->col_ind = (int *)safe_malloc((size_t)A_desc->data.csr.nnz * sizeof(int)); - prob->constraint_matrix->val = (double *)safe_malloc((size_t)A_desc->data.csr.nnz * sizeof(double)); - memcpy( - prob->constraint_matrix->row_ptr, A_desc->data.csr.row_ptr, (size_t)(A_desc->m + 1) * sizeof(int)); - memcpy(prob->constraint_matrix->col_ind, - A_desc->data.csr.col_ind, - (size_t)A_desc->data.csr.nnz * sizeof(int)); - memcpy( - prob->constraint_matrix->val, A_desc->data.csr.vals, (size_t)A_desc->data.csr.nnz * sizeof(double)); - break; - default: - fprintf(stderr, "[interface] A matrix: unsupported format %d.\n", A_desc->fmt); - free(prob); - return NULL; + return 0; } - } - else - { - // Handle missing A: Initialize empty 0xN matrix - prob->constraint_matrix_num_nonzeros = 0; - prob->constraint_matrix->row_ptr = (int *)safe_calloc(m + 1, sizeof(int)); - prob->constraint_matrix->col_ind = NULL; - prob->constraint_matrix->val = NULL; - } - // --- 3. Process Sparse Objective Matrix (Q) [OPTIONAL] --- - prob->objective_sparse_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); - if (Q_desc) - { - switch (Q_desc->fmt) + case matrix_csc: { - case matrix_dense: - dense_to_csr(Q_desc, - &prob->objective_sparse_matrix->row_ptr, - &prob->objective_sparse_matrix->col_ind, - &prob->objective_sparse_matrix->val, - &prob->objective_sparse_matrix_num_nonzeros); - break; - case matrix_csc: + int nnz = desc->data.csc.nnz; + const int *col_ptr = desc->data.csc.col_ptr; + if (nnz < 0 || !col_ptr || (nnz > 0 && (!desc->data.csc.row_ind || !desc->data.csc.vals))) + { + fprintf(stderr, "[create_qp_problem] %s CSC storage is incomplete.\n", name); + return -1; + } + if (col_ptr[0] != 0 || col_ptr[desc->n] != nnz) + { + fprintf(stderr, "[create_qp_problem] %s CSC column pointers do not span [0, nnz].\n", name); + return -1; + } + for (int column = 0; column < desc->n; ++column) { - int *row_ptr = NULL, *col_ind = NULL; - double *vals = NULL; - int nnz = 0; - if (csc_to_csr(Q_desc, &row_ptr, &col_ind, &vals, &nnz) != 0) + if (col_ptr[column] > col_ptr[column + 1] || col_ptr[column] < 0 || col_ptr[column + 1] > nnz) { - fprintf(stderr, "[interface] Q matrix CSC->CSR failed.\n"); - free(prob); - return NULL; + fprintf( + stderr, "[create_qp_problem] %s CSC column pointers are invalid at column %d.\n", name, column); + return -1; } - prob->objective_sparse_matrix_num_nonzeros = nnz; - prob->objective_sparse_matrix->row_ptr = row_ptr; - prob->objective_sparse_matrix->col_ind = col_ind; - prob->objective_sparse_matrix->val = vals; - break; } - case matrix_coo: + for (int entry = 0; entry < nnz; ++entry) { - int *row_ptr = NULL, *col_ind = NULL; - double *vals = NULL; - int nnz = 0; - if (coo_to_csr(Q_desc, &row_ptr, &col_ind, &vals, &nnz) != 0) + int row = desc->data.csc.row_ind[entry]; + if (row < 0 || row >= desc->m) { - fprintf(stderr, "[interface] Q matrix COO->CSR failed.\n"); - free(prob); - return NULL; + fprintf(stderr, + "[create_qp_problem] %s CSC row index %d is out of range [0, %d).\n", + name, + row, + desc->m); + return -1; } - prob->objective_sparse_matrix_num_nonzeros = nnz; - prob->objective_sparse_matrix->row_ptr = row_ptr; - prob->objective_sparse_matrix->col_ind = col_ind; - prob->objective_sparse_matrix->val = vals; - break; } - case matrix_csr: - prob->objective_sparse_matrix_num_nonzeros = Q_desc->data.csr.nnz; - prob->objective_sparse_matrix->row_ptr = (int *)safe_malloc((size_t)(Q_desc->m + 1) * sizeof(int)); - prob->objective_sparse_matrix->col_ind = (int *)safe_malloc((size_t)Q_desc->data.csr.nnz * sizeof(int)); - prob->objective_sparse_matrix->val = - (double *)safe_malloc((size_t)Q_desc->data.csr.nnz * sizeof(double)); - memcpy(prob->objective_sparse_matrix->row_ptr, - Q_desc->data.csr.row_ptr, - (size_t)(Q_desc->m + 1) * sizeof(int)); - memcpy(prob->objective_sparse_matrix->col_ind, - Q_desc->data.csr.col_ind, - (size_t)Q_desc->data.csr.nnz * sizeof(int)); - memcpy(prob->objective_sparse_matrix->val, - Q_desc->data.csr.vals, - (size_t)Q_desc->data.csr.nnz * sizeof(double)); - break; - default: - fprintf(stderr, "[interface] Q matrix: unsupported format %d.\n", Q_desc->fmt); - free(prob); - return NULL; + return 0; } - } - else - { - // For empty Q matrix, allocate row_ptr with n+1 elements (all zeros) - prob->objective_sparse_matrix->row_ptr = (int *)safe_calloc(n + 1, sizeof(int)); - prob->objective_sparse_matrix->col_ind = NULL; - prob->objective_sparse_matrix->val = NULL; - prob->objective_sparse_matrix_num_nonzeros = 0; - } - // --- 4. Process Low-Rank Objective Matrix (R) [OPTIONAL] --- - prob->objective_lowrank_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); - prob->num_rank_lowrank_obj = 0; - if (R_desc) - { - prob->num_rank_lowrank_obj = R_desc->m; - switch (R_desc->fmt) + case matrix_coo: { - case matrix_dense: - dense_to_csr(R_desc, - &prob->objective_lowrank_matrix->row_ptr, - &prob->objective_lowrank_matrix->col_ind, - &prob->objective_lowrank_matrix->val, - &prob->objective_lowrank_matrix_num_nonzeros); - break; - case matrix_csc: + int nnz = desc->data.coo.nnz; + if (nnz < 0 || (nnz > 0 && (!desc->data.coo.row_ind || !desc->data.coo.col_ind || !desc->data.coo.vals))) { - int *row_ptr = NULL, *col_ind = NULL; - double *vals = NULL; - int nnz = 0; - if (csc_to_csr(R_desc, &row_ptr, &col_ind, &vals, &nnz) != 0) - { - fprintf(stderr, "[interface] R matrix CSC->CSR failed.\n"); - free(prob); - return NULL; - } - prob->objective_lowrank_matrix_num_nonzeros = nnz; - prob->objective_lowrank_matrix->row_ptr = row_ptr; - prob->objective_lowrank_matrix->col_ind = col_ind; - prob->objective_lowrank_matrix->val = vals; - break; + fprintf(stderr, "[create_qp_problem] %s COO storage is incomplete.\n", name); + return -1; } - case matrix_coo: + for (int entry = 0; entry < nnz; ++entry) { - int *row_ptr = NULL, *col_ind = NULL; - double *vals = NULL; - int nnz = 0; - if (coo_to_csr(R_desc, &row_ptr, &col_ind, &vals, &nnz) != 0) + int row = desc->data.coo.row_ind[entry]; + int column = desc->data.coo.col_ind[entry]; + if (row < 0 || row >= desc->m || column < 0 || column >= desc->n) { - fprintf(stderr, "[interface] R matrix COO->CSR failed.\n"); - free(prob); - return NULL; + fprintf(stderr, + "[create_qp_problem] %s COO index (%d, %d) is out of range for shape (%d, %d).\n", + name, + row, + column, + desc->m, + desc->n); + return -1; } - prob->objective_lowrank_matrix_num_nonzeros = nnz; - prob->objective_lowrank_matrix->row_ptr = row_ptr; - prob->objective_lowrank_matrix->col_ind = col_ind; - prob->objective_lowrank_matrix->val = vals; - break; } - case matrix_csr: - prob->objective_lowrank_matrix_num_nonzeros = R_desc->data.csr.nnz; - prob->objective_lowrank_matrix->row_ptr = (int *)safe_malloc((size_t)(R_desc->m + 1) * sizeof(int)); - prob->objective_lowrank_matrix->col_ind = - (int *)safe_malloc((size_t)R_desc->data.csr.nnz * sizeof(int)); - prob->objective_lowrank_matrix->val = - (double *)safe_malloc((size_t)R_desc->data.csr.nnz * sizeof(double)); - memcpy(prob->objective_lowrank_matrix->row_ptr, - R_desc->data.csr.row_ptr, - (size_t)(R_desc->m + 1) * sizeof(int)); - memcpy(prob->objective_lowrank_matrix->col_ind, - R_desc->data.csr.col_ind, - (size_t)R_desc->data.csr.nnz * sizeof(int)); - memcpy(prob->objective_lowrank_matrix->val, - R_desc->data.csr.vals, - (size_t)R_desc->data.csr.nnz * sizeof(double)); - break; - default: - fprintf(stderr, "[interface] R matrix: unsupported format %d.\n", R_desc->fmt); - free(prob); - return NULL; + return 0; + } + + default: + fprintf(stderr, "[create_qp_problem] %s matrix has unsupported format %d.\n", name, (int)desc->fmt); + return -1; + } +} + +static int validate_problem_matrix_shapes(const matrix_desc_t *A_desc, + const matrix_desc_t *F_desc, + const matrix_desc_t *Q_desc, + const matrix_desc_t *R_desc, + const matrix_desc_t *D_desc, + int *num_variables, + int *num_scalar_constraints, + int *num_affine_constraints) +{ + if (!A_desc && !F_desc && !Q_desc && !R_desc) + { + fprintf(stderr, "[create_qp_problem] at least one of A, F, Q, or R must be provided.\n"); + return -1; + } + if (validate_matrix_descriptor(A_desc, "A") != 0 || validate_matrix_descriptor(F_desc, "F") != 0 || + validate_matrix_descriptor(Q_desc, "Q") != 0 || validate_matrix_descriptor(R_desc, "R") != 0) + return -1; + + int n = A_desc ? A_desc->n : (F_desc ? F_desc->n : (Q_desc ? Q_desc->n : R_desc->n)); + int m = A_desc ? A_desc->m : 0; + int p = F_desc ? F_desc->m : 0; + if (F_desc && F_desc->n != n) + { + fprintf(stderr, "[create_qp_problem] F matrix shape (%d, %d) must have %d columns.\n", F_desc->m, F_desc->n, n); + return -1; + } + if (Q_desc && (Q_desc->m != n || Q_desc->n != n)) + { + fprintf(stderr, "[create_qp_problem] Q matrix shape (%d, %d) must be (%d, %d).\n", Q_desc->m, Q_desc->n, n, n); + return -1; + } + if (R_desc && R_desc->n != n) + { + fprintf(stderr, "[create_qp_problem] R matrix shape (%d, %d) must have %d columns.\n", R_desc->m, R_desc->n, n); + return -1; + } + if (D_desc && R_desc && R_desc->m > 0) + { + int rank = R_desc->m; + if (validate_matrix_descriptor(D_desc, "D") != 0 || D_desc->m != rank || D_desc->n != rank) + { + fprintf(stderr, + "[create_qp_problem] D matrix shape (%d, %d) must be (%d, %d).\n", + D_desc->m, + D_desc->n, + rank, + rank); + return -1; + } + } + if (p > INT_MAX - m) + { + fprintf(stderr, "[create_qp_problem] combined A and F matrices have too many rows.\n"); + return -1; + } + + *num_variables = n; + *num_scalar_constraints = m; + *num_affine_constraints = p; + return 0; +} + +static int +copy_matrix_desc_to_csr(const matrix_desc_t *desc, const char *name, CsrComponent *destination, int *num_nonzeros) +{ + int rc = 0; + switch (desc->fmt) + { + case matrix_dense: + rc = dense_to_csr(desc, &destination->row_ptr, &destination->col_ind, &destination->val, num_nonzeros); + break; + case matrix_csc: + rc = csc_to_csr(desc, &destination->row_ptr, &destination->col_ind, &destination->val, num_nonzeros); + break; + case matrix_coo: + rc = coo_to_csr(desc, &destination->row_ptr, &destination->col_ind, &destination->val, num_nonzeros); + break; + case matrix_csr: + { + int nnz = desc->data.csr.nnz; + destination->row_ptr = (int *)safe_malloc((size_t)(desc->m + 1) * sizeof(int)); + memcpy(destination->row_ptr, desc->data.csr.row_ptr, (size_t)(desc->m + 1) * sizeof(int)); + if (nnz > 0) + { + destination->col_ind = (int *)safe_malloc((size_t)nnz * sizeof(int)); + destination->val = (double *)safe_malloc((size_t)nnz * sizeof(double)); + memcpy(destination->col_ind, desc->data.csr.col_ind, (size_t)nnz * sizeof(int)); + memcpy(destination->val, desc->data.csr.vals, (size_t)nnz * sizeof(double)); + } + *num_nonzeros = nnz; + break; } + default: + rc = -1; + break; + } + if (rc != 0) + { + fprintf(stderr, "[create_qp_problem] failed to convert %s matrix to CSR.\n", name); + csr_component_free(destination); + } + return rc; +} + +static void initialize_empty_csr(CsrComponent *component, int num_rows, int *num_nonzeros) +{ + component->row_ptr = (int *)safe_calloc((size_t)num_rows + 1, sizeof(int)); + *num_nonzeros = 0; +} + +static int append_matrix_desc_to_csr( + const matrix_desc_t *desc, const char *name, int current_rows, CsrComponent *destination, int *num_nonzeros) +{ + if (!desc) + return 0; + + CsrComponent converted = {0}; + const int *suffix_row_ptr = NULL; + const int *suffix_col_ind = NULL; + const double *suffix_values = NULL; + int suffix_nonzeros = 0; + if (desc->fmt == matrix_csr) + { + suffix_row_ptr = desc->data.csr.row_ptr; + suffix_col_ind = desc->data.csr.col_ind; + suffix_values = desc->data.csr.vals; + suffix_nonzeros = desc->data.csr.nnz; } else { - prob->objective_lowrank_matrix->row_ptr = (int *)safe_calloc(1, sizeof(int)); - prob->objective_lowrank_matrix->col_ind = NULL; - prob->objective_lowrank_matrix->val = NULL; - prob->objective_lowrank_matrix_num_nonzeros = 0; + if (copy_matrix_desc_to_csr(desc, name, &converted, &suffix_nonzeros) != 0) + return -1; + suffix_row_ptr = converted.row_ptr; + suffix_col_ind = converted.col_ind; + suffix_values = converted.val; } + if (suffix_nonzeros > INT_MAX - *num_nonzeros) + { + fprintf(stderr, "[create_qp_problem] combined A and F matrices have too many nonzeros.\n"); + csr_component_free(&converted); + return -1; + } + + int initial_nonzeros = *num_nonzeros; + int total_nonzeros = initial_nonzeros + suffix_nonzeros; + size_t total_rows = (size_t)current_rows + (size_t)desc->m; + destination->row_ptr = (int *)safe_realloc(destination->row_ptr, (total_rows + 1) * sizeof(int)); + for (int row = 1; row <= desc->m; ++row) + destination->row_ptr[current_rows + row] = initial_nonzeros + suffix_row_ptr[row]; + + if (suffix_nonzeros > 0) + { + destination->col_ind = (int *)safe_realloc(destination->col_ind, (size_t)total_nonzeros * sizeof(int)); + destination->val = (double *)safe_realloc(destination->val, (size_t)total_nonzeros * sizeof(double)); + memcpy(destination->col_ind + initial_nonzeros, suffix_col_ind, (size_t)suffix_nonzeros * sizeof(int)); + memcpy(destination->val + initial_nonzeros, suffix_values, (size_t)suffix_nonzeros * sizeof(double)); + } + *num_nonzeros = total_nonzeros; + csr_component_free(&converted); + return 0; +} + +static int initialize_affine_cones(qp_problem_t *prob, + int num_scalar_constraints, + int num_affine_constraints, + int num_affine_cones, + const cone_spec_t *affine_cones, + const double *affine_cone_offset) +{ + if (cone_blocks_init_from_specs( + &prob->affine_cones, num_affine_cones, affine_cones, num_affine_constraints, false, "affine") != 0) + return -1; + + int covered_rows = 0; + for (int cone = 0; cone < num_affine_cones; ++cone) + { + int length = cone_block_length(&prob->affine_cones, cone); + covered_rows += length; + prob->affine_cones.start_idx[cone] += num_scalar_constraints; + } + if (covered_rows != num_affine_constraints) + { + fprintf(stderr, + "[create_qp_problem] affine cone blocks cover %d of %d rows of F.\n", + covered_rows, + num_affine_constraints); + return -1; + } + if (affine_cone_offset && num_affine_constraints > 0) + memcpy(prob->affine_cone_offset + num_scalar_constraints, + affine_cone_offset, + (size_t)num_affine_constraints * sizeof(double)); + return 0; +} + +qp_problem_t *create_qp_problem(const double *objective_c, + const matrix_desc_t *Q_desc, + const matrix_desc_t *R_desc, + const matrix_desc_t *D_desc, + const matrix_desc_t *A_desc, + const double *con_lb, + const double *con_ub, + const double *var_lb, + const double *var_ub, + const double *objective_constant, + int num_var_cones, + const cone_spec_t *var_cones, + const matrix_desc_t *affine_cone_matrix_desc, + const double *affine_cone_offset, + int num_affine_cones, + const cone_spec_t *affine_cones) +{ + qp_problem_t *prob = (qp_problem_t *)safe_calloc(1, sizeof(qp_problem_t)); + int n = 0; + int m = 0; + int p = 0; + if (!affine_cone_matrix_desc && (affine_cone_offset || num_affine_cones != 0 || affine_cones)) + { + fprintf(stderr, "[create_qp_problem] affine cone data requires affine_cone_matrix_desc.\n"); + goto failure; + } + if (validate_problem_matrix_shapes(A_desc, affine_cone_matrix_desc, Q_desc, R_desc, D_desc, &n, &m, &p) != 0) + goto failure; + + prob->num_variables = n; + prob->num_constraints = m + p; + prob->affine_cone_offset = + prob->num_constraints > 0 ? (double *)safe_calloc((size_t)prob->num_constraints, sizeof(double)) : NULL; + + prob->constraint_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + if (A_desc) + { + if (copy_matrix_desc_to_csr(A_desc, "A", prob->constraint_matrix, &prob->constraint_matrix_num_nonzeros) != 0) + goto failure; + } + else + initialize_empty_csr(prob->constraint_matrix, m, &prob->constraint_matrix_num_nonzeros); + if (append_matrix_desc_to_csr( + affine_cone_matrix_desc, "F", m, prob->constraint_matrix, &prob->constraint_matrix_num_nonzeros) != 0) + goto failure; + + prob->objective_sparse_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + if (Q_desc) + { + if (copy_matrix_desc_to_csr( + Q_desc, "Q", prob->objective_sparse_matrix, &prob->objective_sparse_matrix_num_nonzeros) != 0) + goto failure; + } + else + initialize_empty_csr(prob->objective_sparse_matrix, n, &prob->objective_sparse_matrix_num_nonzeros); + + prob->objective_lowrank_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + prob->num_rank_lowrank_obj = 0; + + if (R_desc) + { + prob->num_rank_lowrank_obj = R_desc->m; + if (copy_matrix_desc_to_csr( + R_desc, "R", prob->objective_lowrank_matrix, &prob->objective_lowrank_matrix_num_nonzeros) != 0) + goto failure; + } + else + initialize_empty_csr(prob->objective_lowrank_matrix, 0, &prob->objective_lowrank_matrix_num_nonzeros); prob->objective_lowrank_middle_matrix = NULL; prob->objective_lowrank_middle_matrix_num_nonzeros = 0; @@ -320,71 +437,74 @@ qp_problem_t *create_qp_problem(const double *objective_c, { int k = prob->num_rank_lowrank_obj; if (k <= 0) - { fprintf(stderr, "[interface] D matrix ignored: problem has no low-rank component.\n"); - } - else if (D_desc->m != k || D_desc->n != k) - { - fprintf(stderr, "[interface] D matrix shape (%d, %d) must be (%d, %d).\n", D_desc->m, D_desc->n, k, k); - qp_problem_free(prob); - return NULL; - } else { prob->objective_lowrank_middle_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); - int *rp = NULL, *ci = NULL; - double *vv = NULL; - int nnz = 0; - int rc = 0; - switch (D_desc->fmt) - { - case matrix_dense: - rc = dense_to_csr(D_desc, &rp, &ci, &vv, &nnz); - break; - case matrix_csc: - rc = csc_to_csr(D_desc, &rp, &ci, &vv, &nnz); - break; - case matrix_coo: - rc = coo_to_csr(D_desc, &rp, &ci, &vv, &nnz); - break; - case matrix_csr: - nnz = D_desc->data.csr.nnz; - rp = (int *)safe_malloc((size_t)(k + 1) * sizeof(int)); - ci = (int *)safe_malloc((size_t)nnz * sizeof(int)); - vv = (double *)safe_malloc((size_t)nnz * sizeof(double)); - memcpy(rp, D_desc->data.csr.row_ptr, (size_t)(k + 1) * sizeof(int)); - memcpy(ci, D_desc->data.csr.col_ind, (size_t)nnz * sizeof(int)); - memcpy(vv, D_desc->data.csr.vals, (size_t)nnz * sizeof(double)); - break; - default: - rc = -1; - fprintf(stderr, "[interface] D matrix: unsupported format %d.\n", D_desc->fmt); - break; - } - if (rc != 0) - { - free(rp); - free(ci); - free(vv); - qp_problem_free(prob); - return NULL; - } - prob->objective_lowrank_middle_matrix->row_ptr = rp; - prob->objective_lowrank_middle_matrix->col_ind = ci; - prob->objective_lowrank_middle_matrix->val = vv; - prob->objective_lowrank_middle_matrix_num_nonzeros = nnz; + if (copy_matrix_desc_to_csr(D_desc, + "D", + prob->objective_lowrank_middle_matrix, + &prob->objective_lowrank_middle_matrix_num_nonzeros) != 0) + goto failure; } } - // default fill values prob->objective_constant = objective_constant ? *objective_constant : 0.0; fill_or_copy(&prob->objective_vector, prob->num_variables, objective_c, 0.0); fill_or_copy(&prob->variable_lower_bound, prob->num_variables, var_lb, -INFINITY); fill_or_copy(&prob->variable_upper_bound, prob->num_variables, var_ub, INFINITY); - fill_or_copy(&prob->constraint_lower_bound, prob->num_constraints, con_lb, -INFINITY); - fill_or_copy(&prob->constraint_upper_bound, prob->num_constraints, con_ub, INFINITY); + fill_or_copy(&prob->constraint_lower_bound, prob->num_constraints, NULL, -INFINITY); + fill_or_copy(&prob->constraint_upper_bound, prob->num_constraints, NULL, INFINITY); + if (m > 0 && con_lb) + memcpy(prob->constraint_lower_bound, con_lb, (size_t)m * sizeof(double)); + if (m > 0 && con_ub) + memcpy(prob->constraint_upper_bound, con_ub, (size_t)m * sizeof(double)); + + if (initialize_affine_cones(prob, m, p, num_affine_cones, affine_cones, affine_cone_offset) != 0) + goto failure; + if (cone_blocks_init_from_specs(&prob->cones, num_var_cones, var_cones, n, true, "variable") != 0) + goto failure; + + /* A finite var bound on a cone slot makes proj_K ∘ proj_Box != proj_{K ∩ Box}; the + caller must lift such variables with an auxiliary (x_cone = x_box) so cone slots + stay free. Treat |bound| >= 1e30 as "free" (matches the +/- INFINITY sentinel and + the 1e30 used in tests). */ + int n_orig = n; + for (int cone = 0; cone < prob->cones.num_cones; ++cone) + { + int start = prob->cones.start_idx[cone]; + int length = cone_block_length(&prob->cones, cone); + for (int variable = start; variable < start + length; ++variable) + { + double lo = prob->variable_lower_bound[variable]; + double hi = prob->variable_upper_bound[variable]; + int lo_finite = isfinite(lo) && lo > -1e30; + int hi_finite = isfinite(hi) && hi < 1e30; + if (lo_finite || hi_finite) + { + fprintf(stderr, + "[create_qp_problem] cone %d slot %d has a finite box bound " + "(lb=%.6g, ub=%.6g); cone variables must be free. Introduce an " + "auxiliary x_cone with x_cone = x_box and put the box on the " + "non-cone copy.\n", + cone, + variable, + lo, + hi); + goto failure; + } + } + if (start < n_orig) + n_orig = start; + } + if (prob->cones.num_cones > 0) + prob->num_original_variables = n_orig; return prob; + +failure: + qp_problem_free(prob); + return NULL; } void pdhcg_result_free(pdhcg_result_t *results) @@ -396,9 +516,11 @@ void pdhcg_result_free(pdhcg_result_t *results) free(results->primal_solution); free(results->dual_solution); + free(results->reduced_cost); free(results); } -void csr_component_free(CsrComponent *csr) + +static void csr_component_free(CsrComponent *csr) { if (!csr) return; @@ -412,17 +534,34 @@ void qp_problem_free(qp_problem_t *prob) if (!prob) return; csr_component_free(prob->objective_sparse_matrix); + free(prob->objective_sparse_matrix); csr_component_free(prob->objective_lowrank_matrix); + free(prob->objective_lowrank_matrix); csr_component_free(prob->constraint_matrix); + free(prob->constraint_matrix); free(prob->variable_lower_bound); free(prob->variable_upper_bound); free(prob->objective_vector); free(prob->constraint_lower_bound); free(prob->constraint_upper_bound); + free(prob->affine_cone_offset); free(prob->primal_start); free(prob->dual_start); csr_component_free(prob->objective_lowrank_middle_matrix); free(prob->objective_lowrank_middle_matrix); + if (prob->quadratic_constraint_matrices) + { + for (int i = 0; i < prob->num_quadratic_constraints; ++i) + { + csr_component_free(prob->quadratic_constraint_matrices[i]); + free(prob->quadratic_constraint_matrices[i]); + } + free(prob->quadratic_constraint_matrices); + } + free(prob->quadratic_constraint_row_indices); + free(prob->quadratic_constraint_matrix_num_nonzeros); + cone_blocks_free(&prob->cones); + cone_blocks_free(&prob->affine_cones); memset(prob, 0, sizeof(*prob)); free(prob); } @@ -435,40 +574,328 @@ void set_start_values(qp_problem_t *prob, const double *primal, const double *du int n = prob->num_variables; int m = prob->num_constraints; - // Free previous if any - if (prob->primal_start) + if (primal && prob->cones.is_fixed && prob->primal_start) { - free(prob->primal_start); - prob->primal_start = NULL; - } - if (prob->dual_start) - { - free(prob->dual_start); - prob->dual_start = NULL; + for (int i = 0; i < n; ++i) + { + if (prob->cones.is_fixed[i] && primal[i] != prob->primal_start[i]) + { + fprintf(stderr, + "[set_start_values] slot %d is fixed at %.17g but caller provided %.17g; " + "rejecting (use set_cone_fixed to change fixed value).\n", + i, + prob->primal_start[i], + primal[i]); + return; + } + } } + double *new_primal_start = NULL; + double *new_dual_start = NULL; if (primal) { - prob->primal_start = (double *)safe_malloc(n * sizeof(double)); - memcpy(prob->primal_start, primal, n * sizeof(double)); + new_primal_start = (double *)safe_malloc((size_t)n * sizeof(double)); + memcpy(new_primal_start, primal, (size_t)n * sizeof(double)); + } + else if (prob->cones.is_fixed && prob->primal_start) + { + new_primal_start = (double *)safe_calloc((size_t)n, sizeof(double)); + for (int i = 0; i < n; ++i) + if (prob->cones.is_fixed[i]) + new_primal_start[i] = prob->primal_start[i]; } if (dual) { - prob->dual_start = (double *)safe_malloc(m * sizeof(double)); - memcpy(prob->dual_start, dual, m * sizeof(double)); + new_dual_start = (double *)safe_malloc((size_t)m * sizeof(double)); + memcpy(new_dual_start, dual, (size_t)m * sizeof(double)); } + + free(prob->primal_start); + free(prob->dual_start); + prob->primal_start = new_primal_start; + prob->dual_start = new_dual_start; +} + +int set_cone_fixed(qp_problem_t *prob, int cone_idx, int slot, double value) +{ + if (!prob) + { + fprintf(stderr, "[set_cone_fixed] prob is NULL\n"); + return -1; + } + if (cone_idx < 0 || cone_idx >= prob->cones.num_cones) + { + fprintf(stderr, "[set_cone_fixed] cone_idx %d out of range [0, %d)\n", cone_idx, prob->cones.num_cones); + return -1; + } + int len = cone_block_length(&prob->cones, cone_idx); + if (slot < 0 || slot >= len) + { + fprintf(stderr, "[set_cone_fixed] slot %d out of range [0, %d) for cone %d\n", slot, len, cone_idx); + return -1; + } + int idx = prob->cones.start_idx[cone_idx] + slot; + if (idx < 0 || idx >= prob->num_variables) + { + fprintf(stderr, "[set_cone_fixed] computed index %d out of range [0, %d)\n", idx, prob->num_variables); + return -1; + } + if (!isfinite(value)) + { + fprintf(stderr, "[set_cone_fixed] fixed value must be finite; got %.17g\n", value); + return -1; + } + + if (!prob->cones.is_fixed) + { + prob->cones.is_fixed = (char *)safe_calloc(prob->num_variables, sizeof(char)); + prob->cones.fixed_mask_size = prob->num_variables; + } + prob->cones.is_fixed[idx] = 1; + + if (!prob->primal_start) + prob->primal_start = (double *)safe_calloc(prob->num_variables, sizeof(double)); + prob->primal_start[idx] = value; + return 0; +} + +static double fixed_vector_norm(const qp_problem_t *problem, int start, int length) +{ + double norm = 0.0; + for (int slot = 0; slot < length; ++slot) + { + int index = start + slot; + if (problem->cones.is_fixed[index]) + { + double value = problem->primal_start ? problem->primal_start[index] : 0.0; + norm = hypot(norm, value); + } + } + return norm; +} + +static int fixed_exp_section_is_nonempty(const qp_problem_t *problem, int start) +{ + int fixed_x = problem->cones.is_fixed[start + 0] != 0; + int fixed_y = problem->cones.is_fixed[start + 1] != 0; + int fixed_z = problem->cones.is_fixed[start + 2] != 0; + double x = problem->primal_start ? problem->primal_start[start + 0] : 0.0; + double y = problem->primal_start ? problem->primal_start[start + 1] : 0.0; + double z = problem->primal_start ? problem->primal_start[start + 2] : 0.0; + + if ((fixed_x && !isfinite(x)) || (fixed_y && !isfinite(y)) || (fixed_z && !isfinite(z))) + return 0; + + if (fixed_y) + { + if (y < 0.0) + return 0; + if (y == 0.0) + return (!fixed_x || x <= 0.0) && (!fixed_z || z >= 0.0); + if (fixed_z && !(z > 0.0)) + return 0; + if (fixed_x && fixed_z) + { + double log_bound = log(y) + x / y; + double log_z = log(z); + double tolerance = 64.0 * DBL_EPSILON * (1.0 + fabs(log_bound) + fabs(log_z)); + return log_bound <= log_z + tolerance; + } + return 1; + } + + if (!fixed_z) + return 1; + if (z < 0.0) + return 0; + if (z == 0.0) + return !fixed_x || x <= 0.0; + if (!fixed_x || x <= 0.0) + return 1; + + /* min_{y > 0} y exp(x / y) = e x for x > 0. */ + double log_minimum = 1.0 + log(x); + double log_z = log(z); + double tolerance = 64.0 * DBL_EPSILON * (1.0 + fabs(log_minimum) + fabs(log_z)); + return log_minimum <= log_z + tolerance; +} + +int pdhcg_validate_fixed_cone_sections(const qp_problem_t *problem) +{ + if (!problem || !problem->cones.is_fixed) + return 0; + if (problem->cones.fixed_mask_size != problem->num_variables) + { + fprintf(stderr, + "[solve_qp_problem] variable cone fixed mask has size %d; expected %d.\n", + problem->cones.fixed_mask_size, + problem->num_variables); + return -1; + } + + for (int cone = 0; cone < problem->cones.num_cones; ++cone) + { + int start = problem->cones.start_idx[cone]; + int vector_dimension = problem->cones.v_dim[cone]; + int length = cone_block_length(&problem->cones, cone); + int any_fixed = 0; + for (int slot = 0; slot < length; ++slot) + any_fixed |= problem->cones.is_fixed[start + slot] != 0; + if (!any_fixed) + continue; + + if (problem->cones.type[cone] == CONE_EXPONENTIAL) + { + if (!fixed_exp_section_is_nonempty(problem, start)) + { + fprintf( + stderr, "[solve_qp_problem] exponential cone %d has an empty or non-finite fixed section.\n", cone); + return -1; + } + continue; + } + + if (problem->cones.type[cone] == CONE_STANDARD_SOC) + { + int w_index = start + vector_dimension; + int z_index = w_index + 1; + int fixed_z = problem->cones.is_fixed[z_index] != 0; + double z = problem->primal_start ? problem->primal_start[z_index] : 0.0; + for (int slot = 0; slot < vector_dimension + 2; ++slot) + { + int index = start + slot; + double value = problem->primal_start ? problem->primal_start[index] : 0.0; + if (problem->cones.is_fixed[index] && !isfinite(value)) + { + fprintf(stderr, "[solve_qp_problem] standard SOC %d has a non-finite fixed value.\n", cone); + return -1; + } + } + double fixed_norm = fixed_vector_norm(problem, start, vector_dimension + 1); + if (fixed_z && (!(z >= 0.0) || fixed_norm > z)) + { + fprintf(stderr, + "[solve_qp_problem] standard SOC %d has an empty fixed section " + "(fixed vector norm=%.17g, fixed z=%.17g).\n", + cone, + fixed_norm, + z); + return -1; + } + continue; + } + + if (problem->cones.type[cone] == CONE_ROTATED_SOC) + { + int s_index = start + vector_dimension; + int t_index = s_index + 1; + int fixed_s = problem->cones.is_fixed[s_index] != 0; + int fixed_t = problem->cones.is_fixed[t_index] != 0; + double s = problem->primal_start ? problem->primal_start[s_index] : 0.0; + double t = problem->primal_start ? problem->primal_start[t_index] : 0.0; + for (int slot = 0; slot < vector_dimension + 2; ++slot) + { + int index = start + slot; + double value = problem->primal_start ? problem->primal_start[index] : 0.0; + if (problem->cones.is_fixed[index] && !isfinite(value)) + { + fprintf(stderr, "[solve_qp_problem] rotated SOC %d has a non-finite fixed value.\n", cone); + return -1; + } + } + double fixed_norm = fixed_vector_norm(problem, start, vector_dimension); + int empty = (fixed_s && s < 0.0) || (fixed_t && t < 0.0); + if (!empty && fixed_s && fixed_t) + empty = fixed_norm > 1.41421356237309504880 * sqrt(s) * sqrt(t); + else if (fixed_s && s == 0.0) + empty |= fixed_norm > 0.0; + else if (fixed_t && t == 0.0) + empty |= fixed_norm > 0.0; + if (empty) + { + fprintf(stderr, + "[solve_qp_problem] rotated SOC %d has an empty fixed section " + "(fixed vector norm=%.17g, s=%.17g%s, t=%.17g%s).\n", + cone, + fixed_norm, + s, + fixed_s ? " fixed" : "", + t, + fixed_t ? " fixed" : ""); + return -1; + } + continue; + } + + if (problem->cones.type[cone] != CONE_POWER) + continue; + + int fixed_x = problem->cones.is_fixed[start + 0] != 0; + int fixed_y = problem->cones.is_fixed[start + 1] != 0; + int fixed_z = problem->cones.is_fixed[start + 2] != 0; + double x = problem->primal_start ? problem->primal_start[start + 0] : 0.0; + double y = problem->primal_start ? problem->primal_start[start + 1] : 0.0; + double z = problem->primal_start ? problem->primal_start[start + 2] : 0.0; + + if ((fixed_x && (!isfinite(x) || x < 0.0)) || (fixed_y && (!isfinite(y) || y < 0.0)) || + (fixed_z && !isfinite(z))) + { + fprintf(stderr, + "[solve_qp_problem] power cone %d has an invalid fixed value " + "(x=%.17g%s, y=%.17g%s, z=%.17g%s).\n", + cone, + x, + fixed_x ? " fixed" : "", + y, + fixed_y ? " fixed" : "", + z, + fixed_z ? " fixed" : ""); + return -1; + } + + if (fixed_z && z != 0.0 && ((fixed_x && x == 0.0) || (fixed_y && y == 0.0))) + { + fprintf(stderr, + "[solve_qp_problem] power cone %d has an empty fixed section: " + "|z| is positive while a fixed nonnegative axis is zero.\n", + cone); + return -1; + } + + if (fixed_x && fixed_y && fixed_z && z != 0.0) + { + double alpha = problem->cones.power_alpha[cone]; + double log_bound = (x > 0.0 && y > 0.0) ? alpha * log(x) + (1.0 - alpha) * log(y) : -INFINITY; + double log_abs_z = log(fabs(z)); + double roundoff_tolerance = 64.0 * DBL_EPSILON * (1.0 + fabs(log_bound) + fabs(log_abs_z)); + if (log_bound + roundoff_tolerance < log_abs_z) + { + fprintf(stderr, + "[solve_qp_problem] power cone %d has an infeasible fully fixed point " + "(x=%.17g, y=%.17g, z=%.17g, alpha=%.17g).\n", + cone, + x, + y, + z, + alpha); + return -1; + } + } + } + return 0; } pdhcg_result_t *solve_qp_problem(const qp_problem_t *prob, const pdhg_parameters_t *params) { - // argument checks if (!prob) { fprintf(stderr, "[interface] solve_qp_problem: invalid arguments.\n"); return NULL; } + if (pdhcg_validate_fixed_cone_sections(prob) != 0) + return NULL; - // prepare parameters: use defaults if not provided pdhg_parameters_t local_params; if (params) { @@ -479,7 +906,6 @@ pdhcg_result_t *solve_qp_problem(const qp_problem_t *prob, const pdhg_parameters set_default_parameters(&local_params); } - // call optimizer pdhcg_result_t *res = optimize(&local_params, prob); if (!res) { @@ -490,9 +916,7 @@ pdhcg_result_t *solve_qp_problem(const qp_problem_t *prob, const pdhg_parameters return res; } -#ifdef PDHCG_COMPILE_DISTRIBUTED pdhcg_result_t *solve_qp_problem_distributed(const pdhg_parameters_t *params, const qp_problem_t *original_problem) { - return distributed_optimize(params, original_problem); + return pdhcg_distributed_optimize(params, original_problem); } -#endif diff --git a/src/pdhg_core_op.cu b/src/pdhg_core_op.cu index abb532f..44650bb 100644 --- a/src/pdhg_core_op.cu +++ b/src/pdhg_core_op.cu @@ -15,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +#include "cone_dispatch.h" +#include "distributed_conic.h" #include "distributed_interface.h" #include "internal_types.h" #include "pdhcg.h" @@ -33,9 +35,230 @@ limitations under the License. #include #include -#ifdef PDHCG_COMPILE_DISTRIBUTED -#include "distributed_types.h" -#endif +static const double *cone_dual_residual_effective_obj(pdhg_solver_state_t *state) +{ + if (state->cones.effective_objective_gradient) + return state->cones.effective_objective_gradient; + return state->objective_vector; +} + +static double cone_residual_norm(pdhg_solver_state_t *state, int count, const double *values, norm_type_t norm) +{ + if (count <= 0) + return 0.0; + if (norm == NORM_TYPE_L_INF) + return get_vector_inf_norm(state->blas_handle, count, values); + + double result = 0.0; + CUBLAS_CHECK(cublasDnrm2_v2_64(state->blas_handle, count, values, 1, &result)); + return result; +} + +/* + * Cone membership plus dual-cone membership does not enforce complementarity. + * This gradient mapping is zero exactly when the current point is feasible and + * the reduced gradient belongs to the negative normal cone, including for + * fixed cone cross-sections. + */ +static void augment_conic_projected_gradient_residual(pdhg_solver_state_t *state, const double *effective_obj) +{ + double step_size = state->step_size / state->primal_weight; + if (!(step_size > 0.0) || !isfinite(step_size)) + step_size = 1.0; + + prepare_projected_gradient_point_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( + state->delta_primal_solution, + state->pdhg_primal_solution, + effective_obj, + state->dual_product, + state->variable_lower_bound, + state->variable_upper_bound, + step_size, + state->num_variables); + project_cone_runtime(state, &state->cones, state->delta_primal_solution, state->cones.residual_warm_start); + augment_projected_gradient_residual_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( + state->dual_residual, + state->pdhg_primal_solution, + state->delta_primal_solution, + state->variable_rescaling, + step_size, + state->num_variables); +} + +static double compute_cone_complementarity_norm(pdhg_solver_state_t *state, norm_type_t norm) +{ + double residual_norm = + cone_residual_norm(state, state->cones.num_blocks, state->cones.complementarity_residual, norm); + + double distributed_norm = get_split_cone_complementarity_norm(state, norm); + residual_norm = + norm == NORM_TYPE_L_INF ? fmax(residual_norm, distributed_norm) : hypot(residual_norm, distributed_norm); + return residual_norm; +} + +static bool has_affine_cone_constraints(const pdhg_solver_state_t *state) +{ + return state->affine_cones.num_blocks > 0 || state->affine_cones.split != NULL || + pdhcg_get_global_num_affine_cones(state->grid_context) > 0; +} + +static void compute_affine_cone_residuals(pdhg_solver_state_t *state, + norm_type_t norm, + double *dual_membership_norm, + double *complementarity_norm) +{ + *dual_membership_norm = 0.0; + *complementarity_norm = 0.0; + + if (!has_affine_cone_constraints(state)) + return; + + int rows = state->num_constraints; + int blocks = (rows + THREADS_PER_BLOCK - 1) / THREADS_PER_BLOCK; + double *projection_point = state->delta_dual_solution; + if (state->affine_cones.num_blocks > 0) + { + int threads = THREADS_PER_BLOCK; + for (int bucket_idx = 0; bucket_idx < state->affine_cones.num_buckets; ++bucket_idx) + { + const cone_bucket_t *bucket = &state->affine_cones.buckets[bucket_idx]; + double *complementarity = state->affine_cones.complementarity_residual + bucket->offset; + const int *start_idx = state->affine_cones.start_idx + bucket->offset; + const int *v_dim = state->affine_cones.v_dim + bucket->offset; + if (bucket->method == PROJ_METHOD_GRID || bucket->method == PROJ_METHOD_GRID_WEIGHTED) + { + int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + CUDA_CHECK(cudaMemsetAsync(complementarity, 0, (size_t)bucket->count * sizeof(double))); + prepare_affine_cone_residuals_grid_kernel<<count * blocks_per_cone, + threads, + (size_t)threads * sizeof(double)>>>( + projection_point, + complementarity, + state->primal_product, + state->affine_cone_offset, + state->pdhg_dual_solution, + start_idx, + v_dim, + bucket->count, + blocks_per_cone); + finish_affine_cone_complementarity_kernel<<<(bucket->count + threads - 1) / threads, threads>>>( + complementarity, state->constraint_bound_rescaling, bucket->count); + } + else + { + prepare_affine_cone_residuals_kernel<<count, threads, (size_t)threads * sizeof(double)>>>( + projection_point, + complementarity, + state->primal_product, + state->affine_cone_offset, + state->pdhg_dual_solution, + start_idx, + v_dim, + state->constraint_bound_rescaling, + bucket->count); + } + } + } + prepare_split_affine_cone_residuals( + state, projection_point, state->primal_product, state->affine_cone_offset, state->pdhg_dual_solution); + + project_cone_runtime(state, &state->affine_cones, projection_point, state->affine_cones.residual_warm_start); + if (rows > 0) + { + finish_affine_cone_residuals_kernel<<>>(state->primal_residual, + state->primal_product, + state->affine_cone_offset, + state->constraint_rescaling, + projection_point, + state->affine_cones.coordinate_rescaling, + rows); + } + finalize_split_affine_cone_complementarity(state); + + if (norm == NORM_TYPE_L_INF) + { + *dual_membership_norm = cone_residual_norm(state, rows, projection_point, norm); + *complementarity_norm = cone_residual_norm( + state, state->affine_cones.num_blocks, state->affine_cones.complementarity_residual, norm); + *complementarity_norm = fmax(*complementarity_norm, get_split_affine_cone_complementarity_norm(state, norm)); + pdhcg_all_reduce_scalar(state->grid_context, dual_membership_norm, PDHCG_OP_MAX, PDHCG_SCOPE_COL, false); + pdhcg_all_reduce_scalar(state->grid_context, complementarity_norm, PDHCG_OP_MAX, PDHCG_SCOPE_COL, false); + } + else + { + *dual_membership_norm = cone_residual_norm(state, rows, projection_point, norm); + *complementarity_norm = cone_residual_norm( + state, state->affine_cones.num_blocks, state->affine_cones.complementarity_residual, norm); + double membership_squared = *dual_membership_norm * *dual_membership_norm; + double complementarity_squared = *complementarity_norm * *complementarity_norm; + double split_norm = get_split_affine_cone_complementarity_norm(state, norm); + complementarity_squared += split_norm * split_norm; + pdhcg_all_reduce_scalar(state->grid_context, &membership_squared, PDHCG_OP_SUM, PDHCG_SCOPE_COL, false); + pdhcg_all_reduce_scalar(state->grid_context, &complementarity_squared, PDHCG_OP_SUM, PDHCG_SCOPE_COL, false); + *dual_membership_norm = sqrt(membership_squared); + *complementarity_norm = sqrt(complementarity_squared); + } +} + +static void compute_power_cone_primal_violation(pdhg_solver_state_t *state, + norm_type_t optimality_norm, + double *absolute_violation, + double *relative_violation) +{ + *absolute_violation = 0.0; + *relative_violation = 0.0; + if (!state->cones.has_power_cones) + return; + + double absolute_accumulator = 0.0; + double relative_accumulator = 0.0; + int threads = THREADS_PER_BLOCK; + for (int b = 0; b < state->cones.num_buckets; ++b) + { + const cone_bucket_t *bucket = &state->cones.buckets[b]; + if (bucket->type != CONE_POWER) + continue; + int blocks = (bucket->count + threads - 1) / threads; + double *absolute_workspace = state->cones.power_violation_workspace + bucket->offset; + double *relative_workspace = state->cones.power_violation_workspace + state->cones.num_blocks + bucket->offset; + compute_power_cone_primal_violation_kernel<<>>(absolute_workspace, + relative_workspace, + state->pdhg_primal_solution, + state->variable_rescaling, + state->cones.start_idx + bucket->offset, + state->cones.power_alpha + bucket->offset, + state->constraint_bound_rescaling, + bucket->count); + if (optimality_norm == NORM_TYPE_L_INF) + { + absolute_accumulator = fmax(absolute_accumulator, + cone_residual_norm(state, bucket->count, absolute_workspace, optimality_norm)); + relative_accumulator = fmax(relative_accumulator, + cone_residual_norm(state, bucket->count, relative_workspace, optimality_norm)); + } + else + { + double bucket_absolute_norm = cone_residual_norm(state, bucket->count, absolute_workspace, optimality_norm); + double bucket_relative_norm = cone_residual_norm(state, bucket->count, relative_workspace, optimality_norm); + absolute_accumulator += bucket_absolute_norm * bucket_absolute_norm; + relative_accumulator += bucket_relative_norm * bucket_relative_norm; + } + } + if (optimality_norm == NORM_TYPE_L_INF) + { + pdhcg_all_reduce_scalar(state->grid_context, &absolute_accumulator, PDHCG_OP_MAX, PDHCG_SCOPE_ROW, false); + pdhcg_all_reduce_scalar(state->grid_context, &relative_accumulator, PDHCG_OP_MAX, PDHCG_SCOPE_ROW, false); + } + else + { + pdhcg_all_reduce_scalar(state->grid_context, &absolute_accumulator, PDHCG_OP_SUM, PDHCG_SCOPE_ROW, false); + pdhcg_all_reduce_scalar(state->grid_context, &relative_accumulator, PDHCG_OP_SUM, PDHCG_SCOPE_ROW, false); + absolute_accumulator = sqrt(absolute_accumulator); + relative_accumulator = sqrt(relative_accumulator); + } + *absolute_violation = absolute_accumulator / state->constraint_bound_rescaling; + *relative_violation = relative_accumulator; +} static void apply_lowrank_middle(pdhg_solver_state_t *state) { @@ -77,6 +300,14 @@ void update_obj_product(pdhg_solver_state_t *state, double *primal_solution) case PDHCG_NON_Q: return; + case PDHCG_DIAG_Q: + element_wise_mul_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( + state->quadratic_objective_term->diagonal_objective_matrix, + primal_solution, + state->quadratic_objective_term->primal_obj_product, + state->num_variables); + break; + case PDHCG_SPARSE_Q: pdhcg_spmv_execute(state->sparse_handle, state->quadratic_objective_term->spmv_ctx_Q, @@ -91,15 +322,7 @@ void update_obj_product(pdhg_solver_state_t *state, double *primal_solution) PDHCG_OP_SUM, PDHCG_SCOPE_ROW, 0); - return; - - case PDHCG_DIAG_Q: - element_wise_mul_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( - state->quadratic_objective_term->diagonal_objective_matrix, - primal_solution, - state->quadratic_objective_term->primal_obj_product, - state->num_variables); - return; + break; case PDHCG_LOW_RANK_Q: pdhcg_spmv_execute(state->sparse_handle, @@ -124,7 +347,7 @@ void update_obj_product(pdhg_solver_state_t *state, double *primal_solution) &HOST_ZERO, state->quadratic_objective_term->Rx_product, state->quadratic_objective_term->primal_obj_product); - return; + break; case PDHCG_LOW_RANK_PLUS_SPARSE_Q: pdhcg_spmv_execute(state->sparse_handle, @@ -163,12 +386,21 @@ void update_obj_product(pdhg_solver_state_t *state, double *primal_solution) &HOST_ONE, state->quadratic_objective_term->Rx_product, state->quadratic_objective_term->primal_obj_product); - return; + break; default: fprintf(stderr, "Error: Unknown Quadratic Objective Type detected.\n"); exit(EXIT_FAILURE); } + + if (state->cones.effective_objective_gradient) + { + vector_add_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( + state->objective_vector, + state->quadratic_objective_term->primal_obj_product, + state->cones.effective_objective_gradient, + state->num_variables); + } } double compute_xQx(pdhg_solver_state_t *state, double *primal_sol, double *primal_obj_product) @@ -184,7 +416,9 @@ double compute_xQx(pdhg_solver_state_t *state, double *primal_sol, double *prima void lp_primal_update(pdhg_solver_state_t *state, double step_size) { - if (state->is_this_major_iteration || ((state->total_count + 2) % get_print_frequency(state->total_count + 2)) == 0) + bool force_major_for_cone = state->has_variable_cones; + if (state->is_this_major_iteration || force_major_for_cone || + ((state->total_count + 2) % get_print_frequency(state->total_count + 2)) == 0) { compute_lp_next_pdhg_primal_solution_major_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( state->current_primal_solution, @@ -214,7 +448,9 @@ void lp_primal_update(pdhg_solver_state_t *state, double step_size) void diag_q_primal_update(pdhg_solver_state_t *state, double step_size) { - if (state->is_this_major_iteration || ((state->total_count + 2) % get_print_frequency(state->total_count + 2)) == 0) + bool force_major_for_cone = state->has_variable_cones; + if (state->is_this_major_iteration || + ((state->total_count + 2) % get_print_frequency(state->total_count + 2)) == 0 || force_major_for_cone) { compute_diagonal_q_next_pdhg_primal_solution_major_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( state->current_primal_solution, @@ -313,6 +549,13 @@ void primal_BB_step_size_update(pdhg_solver_state_t *state, double step_size) state->num_variables); } + if (state->has_variable_cones) + { + project_cone_runtime(state, &state->cones, state->pdhg_primal_solution, state->cones.projection_warm_start); + vector_sub_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( + bb->direction, state->pdhg_primal_solution, state->current_primal_solution, state->num_variables); + } + cublasSetPointerMode(state->blas_handle, CUBLAS_POINTER_MODE_DEVICE); int check_frequency = 1; @@ -361,6 +604,14 @@ void primal_BB_step_size_update(pdhg_solver_state_t *state, double step_size) pdhcg_all_reduce_scalar(state->grid_context, d_tmp, PDHCG_OP_SUM, PDHCG_SCOPE_ROW, true); + if (state->has_variable_cones && state->cones.bb_primal_snapshot) + { + CUDA_CHECK(cudaMemcpyAsync(state->cones.bb_primal_snapshot, + state->pdhg_primal_solution, + (size_t)state->num_variables * sizeof(double), + cudaMemcpyDeviceToDevice)); + } + if (precond) { compute_bb_alpha_M_kernel<<<1, 1>>>(d_stMs, d_tmp, d_alpha); @@ -388,6 +639,14 @@ void primal_BB_step_size_update(pdhg_solver_state_t *state, double step_size) d_alpha, state->num_variables); } + + if (state->has_variable_cones && state->cones.bb_primal_snapshot) + { + project_cone_runtime(state, &state->cones, state->pdhg_primal_solution, state->cones.projection_warm_start); + vector_sub_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( + bb->direction, state->pdhg_primal_solution, state->cones.bb_primal_snapshot, state->num_variables); + } + inner_solver_iter++; } @@ -477,6 +736,24 @@ void pdhg_update(pdhg_solver_state_t *state) fprintf(stderr, "Error: Unknown Quadratic Objective Type detected.\n"); exit(EXIT_FAILURE); } + + if (state->has_variable_cones) + { + quad_obj_type_t qt = state->quadratic_objective_term->quad_obj_type; + if (qt == PDHCG_DIAG_Q) + { + project_cone_runtime_diag_q(state, &state->cones, primal_step_size); + } + else if (qt == PDHCG_SPARSE_Q || qt == PDHCG_LOW_RANK_Q || qt == PDHCG_LOW_RANK_PLUS_SPARSE_Q) + { + } + else + { + project_cone_runtime(state, &state->cones, state->pdhg_primal_solution, state->cones.projection_warm_start); + recompute_cone_reflection(state); + } + } + state->inner_solver->total_count++; pdhcg_spmv_execute(state->sparse_handle, @@ -489,29 +766,62 @@ void pdhg_update(pdhg_solver_state_t *state) pdhcg_all_reduce_array( state->grid_context, state->primal_product, state->num_constraints, PDHCG_OP_SUM, PDHCG_SCOPE_ROW, 0); - if (state->is_this_major_iteration || ((state->total_count + 2) % get_print_frequency(state->total_count + 2)) == 0) - { - compute_next_pdhg_dual_solution_major_kernel<<num_blocks_dual, THREADS_PER_BLOCK>>>( - state->current_dual_solution, - state->pdhg_dual_solution, - state->reflected_dual_solution, - state->primal_product, - state->constraint_lower_bound, - state->constraint_upper_bound, - state->num_constraints, - dual_step_size); - } - else + if (state->num_constraints == 0) + return; + + bool store_pdhg_dual = + state->is_this_major_iteration || ((state->total_count + 2) % get_print_frequency(state->total_count + 2)) == 0; + bool has_local_affine_cones = state->affine_cones.num_blocks > 0 || state->affine_cones.split; + if (!has_local_affine_cones) { - compute_next_pdhg_dual_solution_kernel<<num_blocks_dual, THREADS_PER_BLOCK>>>( - state->current_dual_solution, - state->reflected_dual_solution, - state->primal_product, - state->constraint_lower_bound, - state->constraint_upper_bound, - state->num_constraints, - dual_step_size); + if (store_pdhg_dual) + { + compute_next_pdhg_dual_solution_major_kernel<<num_blocks_dual, THREADS_PER_BLOCK>>>( + state->current_dual_solution, + state->pdhg_dual_solution, + state->reflected_dual_solution, + state->primal_product, + state->affine_cone_offset, + state->constraint_lower_bound, + state->constraint_upper_bound, + state->num_constraints, + dual_step_size); + } + else + { + compute_next_pdhg_dual_solution_kernel<<num_blocks_dual, THREADS_PER_BLOCK>>>( + state->current_dual_solution, + state->reflected_dual_solution, + state->primal_product, + state->affine_cone_offset, + state->constraint_lower_bound, + state->constraint_upper_bound, + state->num_constraints, + dual_step_size); + } + return; } + + /* reflected_dual is scratch until the post-projection kernel on non-major iterations. */ + double *projection_point = store_pdhg_dual ? state->pdhg_dual_solution : state->reflected_dual_solution; + prepare_constraint_dual_update_kernel<<num_blocks_dual, THREADS_PER_BLOCK>>>(state->current_dual_solution, + state->primal_product, + state->affine_cone_offset, + state->constraint_lower_bound, + state->constraint_upper_bound, + projection_point, + state->num_constraints, + dual_step_size); + project_cone_runtime(state, &state->affine_cones, projection_point, state->affine_cones.projection_warm_start); + finish_constraint_dual_update_kernel<<num_blocks_dual, THREADS_PER_BLOCK>>>( + state->current_dual_solution, + state->primal_product, + state->affine_cone_offset, + projection_point, + store_pdhg_dual ? state->pdhg_dual_solution : NULL, + state->reflected_dual_solution, + state->num_constraints, + dual_step_size); } void halpern_update(pdhg_solver_state_t *state, double reflection_coefficient) @@ -617,7 +927,11 @@ void perform_restart(pdhg_solver_state_t *state, const pdhg_parameters_t *params void initialize_step_size_and_primal_weight(pdhg_solver_state_t *state, const pdhg_parameters_t *params) { - if (state->constraint_matrix->num_nonzeros == 0) + bool constraint_matrix_is_zero = state->constraint_matrix->num_nonzeros == 0; + double has_nonzero_tile = constraint_matrix_is_zero ? 0.0 : 1.0; + pdhcg_all_reduce_scalar(state->grid_context, &has_nonzero_tile, PDHCG_OP_MAX, PDHCG_SCOPE_GLOBAL, false); + constraint_matrix_is_zero = has_nonzero_tile == 0.0; + if (constraint_matrix_is_zero) { state->step_size = 1.0; } @@ -719,6 +1033,12 @@ void compute_fixed_point_error(pdhg_solver_state_t *state) void compute_residual(pdhg_solver_state_t *state, norm_type_t optimality_norm) { + double linear_absolute_primal_residual = 0.0; + double power_cone_absolute_violation = 0.0; + double power_cone_relative_violation = 0.0; + double affine_dual_membership_norm = 0.0; + double affine_complementarity_norm = 0.0; + bool has_affine_cones = has_affine_cone_constraints(state); pdhcg_spmv_execute(state->sparse_handle, state->spmv_ctx_A, &HOST_ONE, @@ -746,6 +1066,7 @@ void compute_residual(pdhg_solver_state_t *state, norm_type_t optimality_norm) compute_lp_residual_kernel<<num_blocks_primal_dual, THREADS_PER_BLOCK>>>( state->primal_residual, state->primal_product, + state->affine_cone_offset, state->constraint_lower_bound, state->constraint_upper_bound, state->pdhg_dual_solution, @@ -755,17 +1076,27 @@ void compute_residual(pdhg_solver_state_t *state, norm_type_t optimality_norm) state->objective_vector, state->constraint_rescaling, state->variable_rescaling, + state->delta_dual_solution, state->primal_slack, state->constraint_lower_bound_finite_val, state->constraint_upper_bound_finite_val, + has_affine_cones, state->num_constraints, state->num_variables); + + if (state->has_variable_cones) + { + const double *effective_obj = cone_dual_residual_effective_obj(state); + compute_cone_dual_residual(state, effective_obj); + augment_conic_projected_gradient_residual(state, effective_obj); + } } else if (state->problem_type == CONVEX_QP) { compute_qp_residual_kernel<<num_blocks_primal_dual, THREADS_PER_BLOCK>>>( state->primal_residual, state->primal_product, + state->affine_cone_offset, state->quadratic_objective_term->primal_obj_product, state->pdhg_primal_solution, state->constraint_lower_bound, @@ -779,14 +1110,28 @@ void compute_residual(pdhg_solver_state_t *state, norm_type_t optimality_norm) state->objective_vector, state->constraint_rescaling, state->variable_rescaling, + state->delta_dual_solution, state->primal_slack, state->constraint_lower_bound_finite_val, state->constraint_upper_bound_finite_val, state->step_size / state->primal_weight, + has_affine_cones, state->num_constraints, state->num_variables); - } + if (state->has_variable_cones) + { + const double *effective_obj = cone_dual_residual_effective_obj(state); + compute_cone_dual_residual(state, effective_obj); + augment_conic_projected_gradient_residual(state, effective_obj); + } + } + if (state->affine_cones.num_blocks > 0 || state->affine_cones.split) + { + project_cone_runtime( + state, &state->affine_cones, state->primal_residual, state->affine_cones.residual_warm_start); + } + compute_affine_cone_residuals(state, optimality_norm, &affine_dual_membership_norm, &affine_complementarity_norm); if (optimality_norm == NORM_TYPE_L_INF) { state->absolute_primal_residual = @@ -804,21 +1149,39 @@ void compute_residual(pdhg_solver_state_t *state, norm_type_t optimality_norm) state->absolute_primal_residual = sqrt(state->absolute_primal_residual); } state->absolute_primal_residual /= state->constraint_bound_rescaling; + linear_absolute_primal_residual = state->absolute_primal_residual; + if (state->has_variable_cones) + { + compute_power_cone_primal_violation( + state, optimality_norm, &power_cone_absolute_violation, &power_cone_relative_violation); + if (optimality_norm == NORM_TYPE_L_INF) + state->absolute_primal_residual = fmax(state->absolute_primal_residual, power_cone_absolute_violation); + else + state->absolute_primal_residual = hypot(state->absolute_primal_residual, power_cone_absolute_violation); + } if (optimality_norm == NORM_TYPE_L_INF) { state->absolute_dual_residual = get_vector_inf_norm(state->blas_handle, state->num_variables, state->dual_residual); + state->absolute_dual_residual = + fmax(state->absolute_dual_residual, compute_cone_complementarity_norm(state, optimality_norm)); pdhcg_all_reduce_scalar( state->grid_context, &state->absolute_dual_residual, PDHCG_OP_MAX, PDHCG_SCOPE_ROW, false); + state->absolute_dual_residual = fmax(state->absolute_dual_residual, affine_dual_membership_norm); + state->absolute_dual_residual = fmax(state->absolute_dual_residual, affine_complementarity_norm); } else { CUBLAS_CHECK(cublasDnrm2_v2_64( state->blas_handle, state->num_variables, state->dual_residual, 1, &state->absolute_dual_residual)); state->absolute_dual_residual *= state->absolute_dual_residual; + double complementarity_norm = compute_cone_complementarity_norm(state, optimality_norm); + state->absolute_dual_residual += complementarity_norm * complementarity_norm; pdhcg_all_reduce_scalar( state->grid_context, &state->absolute_dual_residual, PDHCG_OP_SUM, PDHCG_SCOPE_ROW, false); + state->absolute_dual_residual += affine_dual_membership_norm * affine_dual_membership_norm; + state->absolute_dual_residual += affine_complementarity_norm * affine_complementarity_norm; state->absolute_dual_residual = sqrt(state->absolute_dual_residual); } state->absolute_dual_residual /= state->objective_vector_rescaling; @@ -840,6 +1203,12 @@ void compute_residual(pdhg_solver_state_t *state, norm_type_t optimality_norm) (state->constraint_bound_rescaling * state->objective_vector_rescaling) + state->objective_constant; + if (state->has_variable_cones) + { + const double *effective_obj = cone_dual_residual_effective_obj(state); + set_cone_dual_slack(state, effective_obj); + } + double base_dual_objective; CUBLAS_CHECK(cublasDdot(state->blas_handle, state->num_variables, @@ -852,7 +1221,7 @@ void compute_residual(pdhg_solver_state_t *state, norm_type_t optimality_norm) pdhcg_all_reduce_scalar(state->grid_context, &base_dual_objective, PDHCG_OP_SUM, PDHCG_SCOPE_ROW, false); double dual_slack_sum = - get_vector_sum(state->blas_handle, state->num_constraints, state->ones_dual_d, state->primal_slack); + get_vector_sum(state->blas_handle, state->num_constraints, state->ones_dual, state->primal_slack); pdhcg_all_reduce_scalar(state->grid_context, &dual_slack_sum, PDHCG_OP_SUM, PDHCG_SCOPE_COL, false); state->dual_objective_value = (base_dual_objective + dual_slack_sum - half_xQx) / @@ -860,7 +1229,11 @@ void compute_residual(pdhg_solver_state_t *state, norm_type_t optimality_norm) state->objective_constant; double relative_primal_dominator = 1.0 + state->constraint_bound_norm; - state->relative_primal_residual = state->absolute_primal_residual / relative_primal_dominator; + state->relative_primal_residual = linear_absolute_primal_residual / relative_primal_dominator; + if (optimality_norm == NORM_TYPE_L_INF) + state->relative_primal_residual = fmax(state->relative_primal_residual, power_cone_relative_violation); + else + state->relative_primal_residual = hypot(state->relative_primal_residual, power_cone_relative_violation); double relative_dual_dominator; if (state->problem_type == LP) @@ -977,6 +1350,7 @@ void compute_infeasibility_information(pdhg_solver_state_t *state) dual_solution_dual_objective_contribution_kernel<<num_blocks_dual, THREADS_PER_BLOCK>>>( state->constraint_lower_bound_finite_val, state->constraint_upper_bound_finite_val, + state->affine_cone_offset, state->delta_dual_solution, state->num_constraints, state->primal_slack); @@ -989,12 +1363,12 @@ void compute_infeasibility_information(pdhg_solver_state_t *state) state->num_variables); double sum_primal_slack = - get_vector_sum(state->blas_handle, state->num_constraints, state->ones_dual_d, state->primal_slack); + get_vector_sum(state->blas_handle, state->num_constraints, state->ones_dual, state->primal_slack); pdhcg_all_reduce_scalar(state->grid_context, &sum_primal_slack, PDHCG_OP_SUM, PDHCG_SCOPE_COL, false); double sum_dual_slack = - get_vector_sum(state->blas_handle, state->num_variables, state->ones_primal_d, state->dual_slack); + get_vector_sum(state->blas_handle, state->num_variables, state->ones_primal, state->dual_slack); pdhcg_all_reduce_scalar(state->grid_context, &sum_dual_slack, PDHCG_OP_SUM, PDHCG_SCOPE_ROW, false); diff --git a/src/permute.cu b/src/permute.cu index 6ad398d..ce83d01 100644 --- a/src/permute.cu +++ b/src/permute.cu @@ -13,22 +13,33 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -#include "pdhcg_types.h" +#include "cone_utils.h" +#include "pdhcg.h" #include "permute.h" #include "utils.h" #include #include +#include #ifndef MIN #define MIN(a, b) (((a) < (b)) ? (a) : (b)) #endif -int cmp_tuples(const void *a, const void *b) +typedef struct +{ + int new_col; + double val; +} permute_tuple_t; + +static void generate_random_permutation(int n, int *perm); +static void generate_block_permutation(int n, int block_size, int *perm); + +static int cmp_tuples(const void *a, const void *b) { return ((permute_tuple_t *)a)->new_col - ((permute_tuple_t *)b)->new_col; } -void col_permute_in_place(int m, int *Ap, int *Aj, double *Ax, const int *old_col_to_new) +static void col_permute_in_place(int m, int *Ap, int *Aj, double *Ax, const int *old_col_to_new) { int max_row_nnz = 0; for (int i = 0; i < m; i++) @@ -74,7 +85,7 @@ void col_permute_in_place(int m, int *Ap, int *Aj, double *Ax, const int *old_co free(buffer); } -void permute_csr_rows_structural(CsrComponent *csr, int num_rows, int nnz, const int *row_perm) +static void permute_csr_rows_structural(CsrComponent *csr, int num_rows, int nnz, const int *row_perm) { if (!csr || nnz == 0) return; @@ -112,7 +123,7 @@ void permute_csr_rows_structural(CsrComponent *csr, int num_rows, int nnz, const csr->val = new_Ax; } -void permute_double_array(double *arr, int n, const int *perm) +static void permute_double_array(double *arr, int n, const int *perm) { if (!arr) return; @@ -123,17 +134,30 @@ void permute_double_array(double *arr, int n, const int *perm) free(tmp); } -void compute_inv_perm(int n, const int *perm, int *inv_perm) +static void compute_inv_perm(int n, const int *perm, int *inv_perm) { for (int i = 0; i < n; i++) inv_perm[perm[i]] = i; } -void permute_problem(qp_problem_t *qp, int *row_perm, int *col_perm) +bool permute_problem(qp_problem_t *qp, int *row_perm, int *col_perm) { + if (!qp || (qp->num_constraints > 0 && !row_perm) || (qp->num_variables > 0 && !col_perm)) + return false; int m = qp->num_constraints; int n = qp->num_variables; + if (!validate_cone_permutation(qp, col_perm)) + { + fprintf(stderr, "Error: column permutation splits or reorders a cone block.\n"); + return false; + } + if (!validate_affine_cone_row_permutation(qp, row_perm)) + { + fprintf(stderr, "Error: row permutation splits or reorders an affine cone block.\n"); + return false; + } + permute_double_array(qp->objective_vector, n, col_perm); permute_double_array(qp->variable_lower_bound, n, col_perm); permute_double_array(qp->variable_upper_bound, n, col_perm); @@ -142,9 +166,18 @@ void permute_problem(qp_problem_t *qp, int *row_perm, int *col_perm) permute_double_array(qp->constraint_lower_bound, m, row_perm); permute_double_array(qp->constraint_upper_bound, m, row_perm); + permute_double_array(qp->affine_cone_offset, m, row_perm); if (qp->dual_start) permute_double_array(qp->dual_start, m, row_perm); + int *inv_row_perm = (int *)malloc((size_t)m * sizeof(int)); + compute_inv_perm(m, row_perm, inv_row_perm); + for (int cone = 0; cone < qp->affine_cones.num_cones; ++cone) + { + int old_start = qp->affine_cones.start_idx[cone]; + qp->affine_cones.start_idx[cone] = inv_row_perm[old_start]; + } + int *inv_col_perm = (int *)malloc(n * sizeof(int)); compute_inv_perm(n, col_perm, inv_col_perm); @@ -177,7 +210,205 @@ void permute_problem(qp_problem_t *qp, int *row_perm, int *col_perm) inv_col_perm); } + if (qp->cones.num_cones > 0) + { + for (int cone = 0; cone < qp->cones.num_cones; ++cone) + qp->cones.start_idx[cone] = inv_col_perm[qp->cones.start_idx[cone]]; + + if (qp->cones.is_fixed) + { + char *tmp = (char *)malloc((size_t)n * sizeof(char)); + for (int i = 0; i < n; ++i) + tmp[i] = qp->cones.is_fixed[col_perm[i]]; + memcpy(qp->cones.is_fixed, tmp, (size_t)n * sizeof(char)); + free(tmp); + } + } + + free(inv_row_perm); free(inv_col_perm); + return true; +} + +typedef struct +{ + int start; + int length; +} permutation_unit_t; + +static int compare_units_by_start(const void *a, const void *b) +{ + const permutation_unit_t *ua = (const permutation_unit_t *)a; + const permutation_unit_t *ub = (const permutation_unit_t *)b; + return (ua->start > ub->start) - (ua->start < ub->start); +} + +static bool build_checked_inverse_permutation(int size, const int *permutation, int **inverse_out) +{ + *inverse_out = NULL; + if (size <= 0) + return true; + if (!permutation) + return false; + + int *inverse = (int *)malloc((size_t)size * sizeof(int)); + if (!inverse) + return false; + for (int index = 0; index < size; ++index) + inverse[index] = -1; + for (int index = 0; index < size; ++index) + { + int value = permutation[index]; + if (value < 0 || value >= size || inverse[value] >= 0) + { + free(inverse); + return false; + } + inverse[value] = index; + } + *inverse_out = inverse; + return true; +} + +bool validate_cone_permutation(const qp_problem_t *qp, const int *col_perm) +{ + if (!qp) + return false; + int *inverse = NULL; + if (!build_checked_inverse_permutation(qp->num_variables, col_perm, &inverse)) + return false; + if (qp->cones.num_cones <= 0) + { + free(inverse); + return true; + } + + bool valid = true; + for (int cone = 0; cone < qp->cones.num_cones && valid; ++cone) + { + int old_start = qp->cones.start_idx[cone]; + int length = cone_block_length(&qp->cones, cone); + int new_start = inverse[old_start]; + for (int slot = 1; slot < length; ++slot) + { + if (inverse[old_start + slot] != new_start + slot) + { + valid = false; + break; + } + } + } + free(inverse); + return valid; +} + +bool validate_affine_cone_row_permutation(const qp_problem_t *qp, const int *row_perm) +{ + if (!qp) + return false; + int *inverse = NULL; + if (!build_checked_inverse_permutation(qp->num_constraints, row_perm, &inverse)) + return false; + if (qp->affine_cones.num_cones <= 0) + { + free(inverse); + return true; + } + bool valid = true; + for (int cone = 0; cone < qp->affine_cones.num_cones && valid; ++cone) + { + int old_start = qp->affine_cones.start_idx[cone]; + int length = cone_block_length(&qp->affine_cones, cone); + int new_start = inverse[old_start]; + for (int slot = 1; slot < length; ++slot) + { + if (inverse[old_start + slot] != new_start + slot) + { + valid = false; + break; + } + } + } + free(inverse); + return valid; +} + +static void generate_cone_aware_vector_permutation( + int n, const cone_blocks_t *cones, permute_method_t method, int block_size, int *perm) +{ + if (method == NO_PERMUTATION || n <= 1) + { + for (int i = 0; i < n; ++i) + perm[i] = i; + return; + } + + if (cones->num_cones <= 0) + { + if (method == FULL_RANDOM_PERMUTATION) + generate_random_permutation(n, perm); + else + generate_block_permutation(n, block_size, perm); + return; + } + + int K = cones->num_cones; + permutation_unit_t *cone_units = (permutation_unit_t *)malloc((size_t)K * sizeof(permutation_unit_t)); + for (int cone = 0; cone < K; ++cone) + { + cone_units[cone].start = cones->start_idx[cone]; + cone_units[cone].length = cone_block_length(cones, cone); + } + qsort(cone_units, (size_t)K, sizeof(permutation_unit_t), compare_units_by_start); + + std::vector units; + int cursor = 0; + int free_block = (method == FULL_RANDOM_PERMUTATION) ? 1 : ((block_size > 0) ? block_size : 1); + for (int cone = 0; cone < K; ++cone) + { + int cone_start = cone_units[cone].start; + while (cursor < cone_start) + { + int length = MIN(free_block, cone_start - cursor); + units.push_back({cursor, length}); + cursor += length; + } + units.push_back(cone_units[cone]); + cursor = cone_start + cone_units[cone].length; + } + while (cursor < n) + { + int length = MIN(free_block, n - cursor); + units.push_back({cursor, length}); + cursor += length; + } + free(cone_units); + + for (int i = (int)units.size() - 1; i > 0; --i) + { + int j = rand() % (i + 1); + permutation_unit_t tmp = units[i]; + units[i] = units[j]; + units[j] = tmp; + } + + int out = 0; + for (const permutation_unit_t &unit : units) + for (int slot = 0; slot < unit.length; ++slot) + perm[out++] = unit.start + slot; +} + +void generate_cone_aware_permutation(const qp_problem_t *qp, permute_method_t method, int block_size, int *perm) +{ + generate_cone_aware_vector_permutation(qp->num_variables, &qp->cones, method, block_size, perm); +} + +void generate_affine_cone_aware_row_permutation(const qp_problem_t *qp, + permute_method_t method, + int block_size, + int *perm) +{ + generate_cone_aware_vector_permutation(qp->num_constraints, &qp->affine_cones, method, block_size, perm); } qp_problem_t *permute_problem_return_new(const qp_problem_t *qp, int *row_perm, int *col_perm) @@ -187,12 +418,16 @@ qp_problem_t *permute_problem_return_new(const qp_problem_t *qp, int *row_perm, qp_problem_t *new_qp = deepcopy_problem(qp); - permute_problem(new_qp, row_perm, col_perm); + if (!permute_problem(new_qp, row_perm, col_perm)) + { + qp_problem_free(new_qp); + return NULL; + } return new_qp; } -void generate_random_permutation(int n, int *perm) +static void generate_random_permutation(int n, int *perm) { for (int i = 0; i < n; i++) perm[i] = i; @@ -205,24 +440,7 @@ void generate_random_permutation(int n, int *perm) } } -void randomly_permute_problem(qp_problem_t *qp, int **out_row_perm, int **out_col_perm) -{ - int m = qp->num_constraints; - int n = qp->num_variables; - - int *row_perm = (int *)malloc(m * sizeof(int)); - int *col_perm = (int *)malloc(n * sizeof(int)); - - generate_random_permutation(m, row_perm); - generate_random_permutation(n, col_perm); - - permute_problem(qp, row_perm, col_perm); - - *out_row_perm = row_perm; - *out_col_perm = col_perm; -} - -void generate_block_permutation(int n, int block_size, int *perm) +static void generate_block_permutation(int n, int block_size, int *perm) { if (block_size <= 0) block_size = 1; @@ -262,24 +480,6 @@ void generate_block_permutation(int n, int block_size, int *perm) free(block_indices); } -void randomly_block_permute_problem( - qp_problem_t *qp, int row_block_size, int col_block_size, int **out_row_perm, int **out_col_perm) -{ - int m = qp->num_constraints; - int n = qp->num_variables; - - int *row_perm = (int *)malloc(m * sizeof(int)); - int *col_perm = (int *)malloc(n * sizeof(int)); - - generate_block_permutation(m, row_block_size, row_perm); - generate_block_permutation(n, col_block_size, col_perm); - - permute_problem(qp, row_perm, col_perm); - - *out_row_perm = row_perm; - *out_col_perm = col_perm; -} - void repermute_solution(pdhcg_result_t *result, int *row_perm, int *col_perm) { int *inv_col_perm = (int *)malloc(result->num_variables * sizeof(int)); diff --git a/src/preconditioner.c b/src/preconditioner.c index 035be99..dec8b60 100644 --- a/src/preconditioner.c +++ b/src/preconditioner.c @@ -16,6 +16,7 @@ limitations under the License. */ #include "preconditioner.h" +#include "cone_utils.h" #include "utils.h" #include #include @@ -23,15 +24,77 @@ limitations under the License. #include #define SCALING_EPSILON 1e-12 +#define CURTIS_REID_MIN_ABS 1e-300 +#define CURTIS_REID_LOG_SCALE_LIMIT 69.07755278982137 +#define PHASE_TAPER_CONE_THRESHOLD 8 + +typedef enum +{ + CONE_SCALING_RUIZ, + CONE_SCALING_POCK_CHAMBOLLE, +} cone_scaling_phase_t; + +/* + * Cone-block aggregation follows HPR-SOCP's :phase_taper strategy: + * https://github.com/PolyU-IOR/HPR-SOCP + */ +static void apply_cone_preserving_scaling(double *scaling, const cone_blocks_t *cones, cone_scaling_phase_t phase) +{ + for (int block = 0; block < cones->num_cones; ++block) + { + int start = cones->start_idx[block]; + int length = cone_block_length(cones, block); + double block_max = 0.0; + double sum_sq = 0.0; + for (int index = start; index < start + length; ++index) + { + block_max = fmax(block_max, scaling[index]); + sum_sq += scaling[index] * scaling[index]; + } + + double rms = sqrt(sum_sq / (double)length); + double block_scale = phase == CONE_SCALING_RUIZ + ? (length <= PHASE_TAPER_CONE_THRESHOLD ? block_max : rms) + : (length <= PHASE_TAPER_CONE_THRESHOLD ? rms : sqrt(block_max * rms)); + for (int index = start; index < start + length; ++index) + scaling[index] = block_scale; + } +} + +static double curtis_reid_exp_clamped(double value) +{ + if (isnan(value)) + return 1.0; + if (value > CURTIS_REID_LOG_SCALE_LIMIT) + value = CURTIS_REID_LOG_SCALE_LIMIT; + else if (value < -CURTIS_REID_LOG_SCALE_LIMIT) + value = -CURTIS_REID_LOG_SCALE_LIMIT; + return exp(value); +} static void scale_problem(qp_problem_t *problem, const double *con_rescale, const double *var_rescale); -static void ruiz_rescaling(qp_problem_t *problem, int num_iters, double *cum_con_rescale, double *cum_var_rescale); -static void -pock_chambolle_rescaling(qp_problem_t *problem, double alpha, double *cum_con_rescale, double *cum_var_rescale); +static void pin_scaled_fixed_cone_bounds(qp_problem_t *scaled_problem, + const qp_problem_t *source_problem, + const rescale_info_t *rescale_info); +static void curtis_reid_rescaling(qp_problem_t *problem, + int num_iters, + bool use_cone_preserving_scaling, + double *cum_con_rescale, + double *cum_var_rescale); +static void ruiz_rescaling(qp_problem_t *problem, + int num_iters, + bool use_cone_preserving_scaling, + double *cum_con_rescale, + double *cum_var_rescale); +static void pock_chambolle_rescaling(qp_problem_t *problem, + double alpha, + bool use_cone_preserving_scaling, + double *cum_con_rescale, + double *cum_var_rescale); qp_problem_t *deepcopy_problem(const qp_problem_t *prob) { - qp_problem_t *new_prob = (qp_problem_t *)safe_malloc(sizeof(qp_problem_t)); + qp_problem_t *new_prob = (qp_problem_t *)safe_calloc(1, sizeof(qp_problem_t)); new_prob->num_variables = prob->num_variables; new_prob->num_constraints = prob->num_constraints; @@ -49,12 +112,14 @@ qp_problem_t *deepcopy_problem(const qp_problem_t *prob) new_prob->objective_vector = safe_malloc(var_bytes); new_prob->constraint_lower_bound = safe_malloc(con_bytes); new_prob->constraint_upper_bound = safe_malloc(con_bytes); + new_prob->affine_cone_offset = safe_malloc(con_bytes); memcpy(new_prob->variable_lower_bound, prob->variable_lower_bound, var_bytes); memcpy(new_prob->variable_upper_bound, prob->variable_upper_bound, var_bytes); memcpy(new_prob->objective_vector, prob->objective_vector, var_bytes); memcpy(new_prob->constraint_lower_bound, prob->constraint_lower_bound, con_bytes); memcpy(new_prob->constraint_upper_bound, prob->constraint_upper_bound, con_bytes); + memcpy(new_prob->affine_cone_offset, prob->affine_cone_offset, con_bytes); new_prob->constraint_matrix = deepcopy_csr_component(prob->constraint_matrix, prob->num_constraints, prob->constraint_matrix_num_nonzeros); new_prob->objective_sparse_matrix = deepcopy_csr_component( @@ -73,20 +138,36 @@ qp_problem_t *deepcopy_problem(const qp_problem_t *prob) new_prob->primal_start = safe_malloc(var_bytes); memcpy(new_prob->primal_start, prob->primal_start, var_bytes); } - else - { - new_prob->primal_start = NULL; - } if (prob->dual_start) { new_prob->dual_start = safe_malloc(con_bytes); memcpy(new_prob->dual_start, prob->dual_start, con_bytes); } - else + + new_prob->num_quadratic_constraints = prob->num_quadratic_constraints; + if (prob->num_quadratic_constraints > 0) { - new_prob->dual_start = NULL; + int K = prob->num_quadratic_constraints; + new_prob->quadratic_constraint_row_indices = safe_malloc(K * sizeof(int)); + new_prob->quadratic_constraint_matrix_num_nonzeros = safe_malloc(K * sizeof(int)); + new_prob->quadratic_constraint_matrices = safe_malloc(K * sizeof(CsrComponent *)); + memcpy(new_prob->quadratic_constraint_row_indices, prob->quadratic_constraint_row_indices, K * sizeof(int)); + memcpy(new_prob->quadratic_constraint_matrix_num_nonzeros, + prob->quadratic_constraint_matrix_num_nonzeros, + K * sizeof(int)); + for (int i = 0; i < K; ++i) + { + new_prob->quadratic_constraint_matrices[i] = + deepcopy_csr_component(prob->quadratic_constraint_matrices[i], + prob->num_variables, + prob->quadratic_constraint_matrix_num_nonzeros[i]); + } } + new_prob->num_original_variables = prob->num_original_variables; + cone_blocks_clone(&new_prob->cones, &prob->cones); + cone_blocks_clone(&new_prob->affine_cones, &prob->affine_cones); + return new_prob; } @@ -102,6 +183,7 @@ static void scale_problem(qp_problem_t *problem, const double *constraint_rescal { problem->constraint_lower_bound[i] /= constraint_rescaling[i]; problem->constraint_upper_bound[i] /= constraint_rescaling[i]; + problem->affine_cone_offset[i] /= constraint_rescaling[i]; } for (int row = 0; row < problem->num_constraints; ++row) @@ -144,8 +226,168 @@ static void scale_problem(qp_problem_t *problem, const double *constraint_rescal } } +static void pin_scaled_fixed_cone_bounds(qp_problem_t *scaled_problem, + const qp_problem_t *source_problem, + const rescale_info_t *rescale_info) +{ + if (!source_problem->cones.is_fixed) + return; + + for (int variable = 0; variable < source_problem->num_variables; ++variable) + { + if (!source_problem->cones.is_fixed[variable]) + continue; + + double value = source_problem->primal_start ? source_problem->primal_start[variable] : 0.0; + double scaled_value = value * rescale_info->var_rescale[variable] * rescale_info->con_bound_rescale; + scaled_problem->variable_lower_bound[variable] = scaled_value; + scaled_problem->variable_upper_bound[variable] = scaled_value; + } +} + +/* + * A. R. Curtis and J. K. Reid, "On the Automatic Scaling of Matrices + * for Gaussian Elimination", IMA J. Appl. Math. 10(1), 118-124 (1972). + */ +static void curtis_reid_rescaling(qp_problem_t *problem, + int num_iterations, + bool use_cone_preserving_scaling, + double *cum_constraint_rescaling, + double *cum_variable_rescaling) +{ + const int num_cons = problem->num_constraints; + const int num_vars = problem->num_variables; + const int num_nonzeros = problem->constraint_matrix_num_nonzeros; + double *con_rescale = safe_malloc((size_t)num_cons * sizeof(double)); + double *var_rescale = safe_malloc((size_t)num_vars * sizeof(double)); + + for (int row = 0; row < num_cons; ++row) + con_rescale[row] = 1.0; + for (int col = 0; col < num_vars; ++col) + var_rescale[col] = 1.0; + + if (num_iterations > 0 && num_cons > 0 && num_vars > 0 && num_nonzeros > 0) + { + const CsrComponent *matrix = problem->constraint_matrix; + double *row_log_scale = safe_calloc((size_t)num_cons, sizeof(double)); + double *col_log_scale = safe_calloc((size_t)num_vars, sizeof(double)); + double *row_log_abs_sum = safe_calloc((size_t)num_cons, sizeof(double)); + double *col_log_abs_sum = safe_calloc((size_t)num_vars, sizeof(double)); + double *col_sum = safe_calloc((size_t)num_vars, sizeof(double)); + int *col_count = safe_calloc((size_t)num_vars, sizeof(int)); + + for (int row = 0; row < num_cons; ++row) + { + for (int nz = matrix->row_ptr[row]; nz < matrix->row_ptr[row + 1]; ++nz) + { + const int col = matrix->col_ind[nz]; + const double log_abs = log(fmax(fabs(matrix->val[nz]), CURTIS_REID_MIN_ABS)); + row_log_abs_sum[row] += log_abs; + col_log_abs_sum[col] += log_abs; + ++col_count[col]; + } + } + + /* + * Minimize sum_(i,j) in nz(A) (log|A_ij| - r_i - c_j)^2 by + * alternating exact row and column least-squares updates. + */ + for (int iter = 0; iter < num_iterations; ++iter) + { + for (int row = 0; row < num_cons; ++row) + { + const int begin = matrix->row_ptr[row]; + const int end = matrix->row_ptr[row + 1]; + double sum = row_log_abs_sum[row]; + for (int nz = begin; nz < end; ++nz) + sum -= col_log_scale[matrix->col_ind[nz]]; + row_log_scale[row] = (end > begin) ? sum / (double)(end - begin) : 0.0; + } + + if (use_cone_preserving_scaling) + { + for (int block = 0; block < problem->affine_cones.num_cones; ++block) + { + int start = problem->affine_cones.start_idx[block]; + int length = cone_block_length(&problem->affine_cones, block); + double block_sum = 0.0; + int block_count = 0; + for (int row = start; row < start + length; ++row) + { + int begin = matrix->row_ptr[row]; + int end = matrix->row_ptr[row + 1]; + block_sum += row_log_abs_sum[row]; + block_count += end - begin; + for (int nz = begin; nz < end; ++nz) + block_sum -= col_log_scale[matrix->col_ind[nz]]; + } + double block_log_scale = block_count > 0 ? block_sum / (double)block_count : 0.0; + for (int row = start; row < start + length; ++row) + row_log_scale[row] = block_log_scale; + } + } + + memcpy(col_sum, col_log_abs_sum, (size_t)num_vars * sizeof(double)); + for (int row = 0; row < num_cons; ++row) + { + for (int nz = matrix->row_ptr[row]; nz < matrix->row_ptr[row + 1]; ++nz) + col_sum[matrix->col_ind[nz]] -= row_log_scale[row]; + } + for (int col = 0; col < num_vars; ++col) + col_log_scale[col] = col_count[col] > 0 ? col_sum[col] / (double)col_count[col] : 0.0; + + if (use_cone_preserving_scaling) + { + /* + * Adding c_j = c_B for all j in cone block B gives the exact + * block minimizer below. With cone-preserving scaling disabled, + * the independent column minimizers above are retained. + */ + for (int block = 0; block < problem->cones.num_cones; ++block) + { + const int start = problem->cones.start_idx[block]; + const int length = cone_block_length(&problem->cones, block); + double block_sum = 0.0; + int block_count = 0; + for (int col = start; col < start + length; ++col) + { + block_sum += col_sum[col]; + block_count += col_count[col]; + } + const double block_log_scale = block_count > 0 ? block_sum / (double)block_count : 0.0; + for (int col = start; col < start + length; ++col) + col_log_scale[col] = block_log_scale; + } + } + } + + for (int row = 0; row < num_cons; ++row) + con_rescale[row] = curtis_reid_exp_clamped(row_log_scale[row]); + for (int col = 0; col < num_vars; ++col) + var_rescale[col] = curtis_reid_exp_clamped(col_log_scale[col]); + + free(row_log_scale); + free(col_log_scale); + free(row_log_abs_sum); + free(col_log_abs_sum); + free(col_sum); + free(col_count); + } + + scale_problem(problem, con_rescale, var_rescale); + + for (int row = 0; row < num_cons; ++row) + cum_constraint_rescaling[row] *= con_rescale[row]; + for (int col = 0; col < num_vars; ++col) + cum_variable_rescaling[col] *= var_rescale[col]; + + free(con_rescale); + free(var_rescale); +} + static void ruiz_rescaling(qp_problem_t *problem, int num_iterations, + bool use_cone_preserving_scaling, double *cum_constraint_rescaling, double *cum_variable_rescaling) { @@ -185,33 +427,17 @@ static void ruiz_rescaling(qp_problem_t *problem, con_rescale[row] = val; } } - // for (int q_row = 0; problem->objective_sparse_matrix && q_row < num_vars; ++q_row) - // { - // for (int nz_idx = problem->objective_sparse_matrix->row_ptr[q_row]; - // nz_idx < problem->objective_sparse_matrix->row_ptr[q_row + 1]; - // ++nz_idx) - // { - // int q_col = problem->objective_sparse_matrix->col_ind[nz_idx]; - // if (q_col < 0 || q_col >= num_vars) - // { - // fprintf(stderr, - // "Error: Invalid column index %d at nz_idx %d for q_row %d. " - // "Must be in [0, %d).\n", - // q_col, - // nz_idx, - // q_row, - // num_vars); - // } - // double val = fabs(problem->objective_sparse_matrix->val[nz_idx]); - // if (val > var_rescale[q_col]) - // var_rescale[q_col] = val; - // } - // } for (int i = 0; i < num_vars; ++i) var_rescale[i] = (var_rescale[i] < SCALING_EPSILON) ? 1.0 : sqrt(var_rescale[i]); for (int i = 0; i < num_cons; ++i) con_rescale[i] = (con_rescale[i] < SCALING_EPSILON) ? 1.0 : sqrt(con_rescale[i]); + if (use_cone_preserving_scaling) + { + apply_cone_preserving_scaling(var_rescale, &problem->cones, CONE_SCALING_RUIZ); + apply_cone_preserving_scaling(con_rescale, &problem->affine_cones, CONE_SCALING_RUIZ); + } + scale_problem(problem, con_rescale, var_rescale); for (int i = 0; i < num_vars; ++i) cum_variable_rescaling[i] *= var_rescale[i]; @@ -224,6 +450,7 @@ static void ruiz_rescaling(qp_problem_t *problem, static void pock_chambolle_rescaling(qp_problem_t *problem, double alpha, + bool use_cone_preserving_scaling, double *cum_constraint_rescaling, double *cum_variable_rescaling) { @@ -245,26 +472,17 @@ static void pock_chambolle_rescaling(qp_problem_t *problem, } } - // if (problem->objective_sparse_matrix) - // { - // for (int q_row = 0; q_row < num_vars; ++q_row) - // { - // for (int nz_idx = problem->objective_sparse_matrix->row_ptr[q_row]; - // nz_idx < problem->objective_sparse_matrix->row_ptr[q_row + 1]; - // ++nz_idx) - // { - // int q_col = problem->objective_sparse_matrix->col_ind[nz_idx]; - // double val = fabs(problem->objective_sparse_matrix->val[nz_idx]); - // var_rescale[q_col] += pow(val, 2.0 - alpha); - // } - // } - // } - for (int i = 0; i < num_vars; ++i) var_rescale[i] = (var_rescale[i] < SCALING_EPSILON) ? 1.0 : sqrt(var_rescale[i]); for (int i = 0; i < num_cons; ++i) con_rescale[i] = (con_rescale[i] < SCALING_EPSILON) ? 1.0 : sqrt(con_rescale[i]); + if (use_cone_preserving_scaling) + { + apply_cone_preserving_scaling(var_rescale, &problem->cones, CONE_SCALING_POCK_CHAMBOLLE); + apply_cone_preserving_scaling(con_rescale, &problem->affine_cones, CONE_SCALING_POCK_CHAMBOLLE); + } + scale_problem(problem, con_rescale, var_rescale); for (int i = 0; i < num_vars; ++i) cum_variable_rescaling[i] *= var_rescale[i]; @@ -290,6 +508,8 @@ static void bound_obj_rescaling(qp_problem_t *problem, rescale_info_t *rescale_i b_norm_sq += problem->constraint_upper_bound[i] * problem->constraint_upper_bound[i]; } } + for (int i = 0; i < problem->num_constraints; ++i) + b_norm_sq += problem->affine_cone_offset[i] * problem->affine_cone_offset[i]; double c_norm_sq = 0.0; for (int i = 0; i < problem->num_variables; ++i) { @@ -302,6 +522,7 @@ static void bound_obj_rescaling(qp_problem_t *problem, rescale_info_t *rescale_i { problem->constraint_lower_bound[i] *= rescale_info->con_bound_rescale; problem->constraint_upper_bound[i] *= rescale_info->con_bound_rescale; + problem->affine_cone_offset[i] *= rescale_info->con_bound_rescale; } for (int i = 0; i < problem->num_variables; ++i) { @@ -345,10 +566,20 @@ rescale_info_t *rescale_problem(const pdhg_parameters_t *params, const qp_proble for (int i = 0; i < num_vars; ++i) rescale_info->var_rescale[i] = 1.0; + bool use_cone_preserving_scaling = params->use_cone_preserving_scaling; + if (params->curtis_reid_iterations > 0) + { + curtis_reid_rescaling(rescale_info->scaled_problem, + params->curtis_reid_iterations, + use_cone_preserving_scaling, + rescale_info->con_rescale, + rescale_info->var_rescale); + } if (params->l_inf_ruiz_iterations > 0) { ruiz_rescaling(rescale_info->scaled_problem, params->l_inf_ruiz_iterations, + use_cone_preserving_scaling, rescale_info->con_rescale, rescale_info->var_rescale); } @@ -356,6 +587,7 @@ rescale_info_t *rescale_problem(const pdhg_parameters_t *params, const qp_proble { pock_chambolle_rescaling(rescale_info->scaled_problem, params->pock_chambolle_alpha, + use_cone_preserving_scaling, rescale_info->con_rescale, rescale_info->var_rescale); } @@ -368,6 +600,7 @@ rescale_info_t *rescale_problem(const pdhg_parameters_t *params, const qp_proble rescale_info->con_bound_rescale = 1.0; rescale_info->obj_vec_rescale = 1.0; } + pin_scaled_fixed_cone_bounds(rescale_info->scaled_problem, working_problem, rescale_info); rescale_info->processed_problem = preprocess_qp_problem(rescale_info->scaled_problem); rescale_info->rescaling_time_sec = (double)(clock() - start_rescaling) / CLOCKS_PER_SEC; return rescale_info; diff --git a/src/presolve_wrapper.c b/src/presolve_wrapper.c index 4098085..cc3c922 100644 --- a/src/presolve_wrapper.c +++ b/src/presolve_wrapper.c @@ -1,413 +1,1031 @@ /* - * PDHCG-II PSQP Presolve Wrapper - */ +Copyright 2026 Hongpei Li -#ifdef PSQP_AVAILABLE +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -#include + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "presolve_wrapper.h" +#include "cone_utils.h" + +#ifdef PREFOS_AVAILABLE + +#include + +#include #include +#include #include #include #include #include -#include "PSQP_API.h" -#include "PSQP_sol.h" -#include "PSQP_status.h" -#include "presolve_wrapper.h" -#include "utils.h" +typedef struct +{ + PreFOSProblemData problem; + int *owned_A_row_pointers; + int *owned_A_column_indices; + double *owned_A_values; + double *owned_constraint_lower; + double *owned_constraint_upper; + int *owned_Q_row_pointers; + int *owned_Q_column_indices; + double *owned_Q_values; + double *owned_D; +} PreFOSInputAdapter; + +#define PDHCG_INFINITY_SENTINEL 1e20 + +static double normalize_lower_bound(double value) +{ + return value <= -PDHCG_INFINITY_SENTINEL ? -INFINITY : value; +} -#ifndef PSQP_VERSION -#define PSQP_VERSION "unknown" -#endif +static double normalize_upper_bound(double value) +{ + return value >= PDHCG_INFINITY_SENTINEL ? INFINITY : value; +} -const char *pdhcg_get_presolve_status_str(int status) +static double shifted_constraint_bound(double bound, double constant, int is_lower) { - switch (status) - { - case UNCHANGED: - return "UNCHANGED"; - case REDUCED: - return "REDUCED"; - case INFEASIBLE: - return "INFEASIBLE"; - case UNBNDORINFEAS: - return "INFEASIBLE_OR_UNBOUNDED"; - default: - return "UNKNOWN_STATUS"; - } + if (isfinite(bound)) + bound -= constant; + return is_lower ? normalize_lower_bound(bound) : normalize_upper_bound(bound); } -static qp_problem_t *convert_psqp_to_pdhcg(PresolvedProblem *reduced_prob, double original_obj_constant) +static void *allocate_array(size_t count, size_t element_size) { - if (!reduced_prob) + if (count == 0) return NULL; + if (element_size != 0 && count > SIZE_MAX / element_size) + return NULL; + return malloc(count * element_size); +} - qp_problem_t *pdhcg_prob = (qp_problem_t *)safe_calloc(1, sizeof(qp_problem_t)); - - pdhcg_prob->objective_constant = original_obj_constant + reduced_prob->obj_offset; - pdhcg_prob->objective_vector = reduced_prob->c; - - pdhcg_prob->constraint_lower_bound = reduced_prob->lhs; - pdhcg_prob->constraint_upper_bound = reduced_prob->rhs; - pdhcg_prob->variable_lower_bound = reduced_prob->lbs; - pdhcg_prob->variable_upper_bound = reduced_prob->ubs; +static PreFOSCsrMatrix csr_view(const CsrComponent *matrix, size_t rows, size_t cols, size_t nnz) +{ + PreFOSCsrMatrix view; + view.rows = rows; + view.cols = cols; + view.nnz = nnz; + view.values = matrix ? matrix->val : NULL; + view.column_indices = matrix ? matrix->col_ind : NULL; + view.row_pointers = matrix ? matrix->row_ptr : NULL; + return view; +} - pdhcg_prob->num_variables = (int)reduced_prob->n; - pdhcg_prob->num_constraints = (int)reduced_prob->m; - pdhcg_prob->constraint_matrix_num_nonzeros = (int)reduced_prob->nnz; +static void free_input_adapter(PreFOSInputAdapter *adapter) +{ + size_t cone; + if (!adapter) + return; + for (cone = 0; cone < adapter->problem.n_cones; ++cone) + free(adapter->problem.cones[cone].indices); + free(adapter->problem.cones); + free(adapter->problem.box_indices); + free(adapter->problem.box_lower); + free(adapter->problem.box_upper); + free(adapter->owned_A_row_pointers); + free(adapter->owned_A_column_indices); + free(adapter->owned_A_values); + free(adapter->owned_constraint_lower); + free(adapter->owned_constraint_upper); + free(adapter->owned_Q_row_pointers); + free(adapter->owned_Q_column_indices); + free(adapter->owned_Q_values); + free(adapter->owned_D); + memset(adapter, 0, sizeof(*adapter)); +} - if (reduced_prob->nnz > 0 && reduced_prob->Ap) +static int initialize_full_q(const qp_problem_t *source, PreFOSInputAdapter *adapter) +{ + const CsrComponent *Q = source->objective_sparse_matrix; + size_t n = (size_t)source->num_variables; + size_t nnz = + source->objective_sparse_matrix_num_nonzeros > 0 ? (size_t)source->objective_sparse_matrix_num_nonzeros : 0; + int all_upper = 1; + int all_lower = 1; + int has_off_diagonal = 0; + size_t expanded_nnz = nnz; + size_t row; + + adapter->problem.Q = csr_view(Q, n, n, nnz); + adapter->problem.q_storage = PREFOS_Q_FULL; + if (nnz == 0) + return 1; + if (!Q || !Q->row_ptr || !Q->col_ind || !Q->val) + return 0; + + for (row = 0; row < n; ++row) { - pdhcg_prob->constraint_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); - pdhcg_prob->constraint_matrix->row_ptr = reduced_prob->Ap; - pdhcg_prob->constraint_matrix->col_ind = reduced_prob->Ai; - pdhcg_prob->constraint_matrix->val = reduced_prob->Ax; + int p; + for (p = Q->row_ptr[row]; p < Q->row_ptr[row + 1]; ++p) + { + int column = Q->col_ind[p]; + if (column < 0 || (size_t)column >= n) + return 0; + if (column < (int)row) + all_upper = 0; + if (column > (int)row) + all_lower = 0; + if (column != (int)row) + { + has_off_diagonal = 1; + ++expanded_nnz; + } + } } - if (reduced_prob->Qnnz > 0 && reduced_prob->Qp) + if (!has_off_diagonal || (!all_upper && !all_lower)) + return 1; + if (expanded_nnz > (size_t)INT_MAX) + return 0; + + adapter->owned_Q_row_pointers = (int *)calloc(n + 1, sizeof(int)); + adapter->owned_Q_column_indices = (int *)allocate_array(expanded_nnz, sizeof(int)); + adapter->owned_Q_values = (double *)allocate_array(expanded_nnz, sizeof(double)); + if (!adapter->owned_Q_row_pointers || + (expanded_nnz > 0 && (!adapter->owned_Q_column_indices || !adapter->owned_Q_values))) + return 0; + + for (row = 0; row < n; ++row) { - pdhcg_prob->objective_sparse_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); - pdhcg_prob->objective_sparse_matrix->row_ptr = reduced_prob->Qp; - pdhcg_prob->objective_sparse_matrix->col_ind = reduced_prob->Qi; - pdhcg_prob->objective_sparse_matrix->val = reduced_prob->Qx; - pdhcg_prob->objective_sparse_matrix_num_nonzeros = (int)reduced_prob->Qnnz; + int p; + for (p = Q->row_ptr[row]; p < Q->row_ptr[row + 1]; ++p) + { + int column = Q->col_ind[p]; + ++adapter->owned_Q_row_pointers[row + 1]; + if (column != (int)row) + ++adapter->owned_Q_row_pointers[(size_t)column + 1]; + } } + for (row = 0; row < n; ++row) + adapter->owned_Q_row_pointers[row + 1] += adapter->owned_Q_row_pointers[row]; - if (reduced_prob->Rnnz > 0 && reduced_prob->Rp) { - pdhcg_prob->objective_lowrank_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); - pdhcg_prob->objective_lowrank_matrix->row_ptr = reduced_prob->Rp; - pdhcg_prob->objective_lowrank_matrix->col_ind = reduced_prob->Ri; - pdhcg_prob->objective_lowrank_matrix->val = reduced_prob->Rx; - pdhcg_prob->objective_lowrank_matrix_num_nonzeros = (int)reduced_prob->Rnnz; - pdhcg_prob->num_rank_lowrank_obj = (int)reduced_prob->k; + int *next = (int *)allocate_array(n, sizeof(int)); + if (n > 0 && !next) + return 0; + if (n > 0) + memcpy(next, adapter->owned_Q_row_pointers, n * sizeof(int)); + for (row = 0; row < n; ++row) + { + int p; + for (p = Q->row_ptr[row]; p < Q->row_ptr[row + 1]; ++p) + { + int column = Q->col_ind[p]; + int position = next[row]++; + adapter->owned_Q_column_indices[position] = column; + adapter->owned_Q_values[position] = Q->val[p]; + if (column != (int)row) + { + position = next[column]++; + adapter->owned_Q_column_indices[position] = (int)row; + adapter->owned_Q_values[position] = Q->val[p]; + } + } + } + free(next); } - return pdhcg_prob; + adapter->problem.Q.rows = n; + adapter->problem.Q.cols = n; + adapter->problem.Q.nnz = expanded_nnz; + adapter->problem.Q.row_pointers = adapter->owned_Q_row_pointers; + adapter->problem.Q.column_indices = adapter->owned_Q_column_indices; + adapter->problem.Q.values = adapter->owned_Q_values; + return 1; } -static void free_converted_problem(qp_problem_t *prob) +static int initialize_diagonal_d(const qp_problem_t *source, PreFOSInputAdapter *adapter) { - if (!prob) - return; + const CsrComponent *middle = source->objective_lowrank_middle_matrix; + size_t rank = source->num_rank_lowrank_obj > 0 ? (size_t)source->num_rank_lowrank_obj : 0; + size_t row; - // Only free the wrapper structs. The internal arrays are freed by free_presolver() - if (prob->constraint_matrix) - free(prob->constraint_matrix); - if (prob->objective_sparse_matrix) - free(prob->objective_sparse_matrix); + if (rank == 0) + return 1; + adapter->owned_D = (double *)allocate_array(rank, sizeof(double)); + if (!adapter->owned_D) + return 0; - free(prob); + if (!middle || !middle->row_ptr || source->objective_lowrank_middle_matrix_num_nonzeros == 0) + { + for (row = 0; row < rank; ++row) + adapter->owned_D[row] = 1.0; + } + else + { + memset(adapter->owned_D, 0, rank * sizeof(double)); + for (row = 0; row < rank; ++row) + { + int p; + for (p = middle->row_ptr[row]; p < middle->row_ptr[row + 1]; ++p) + { + if (middle->col_ind[p] != (int)row) + return -1; + adapter->owned_D[row] = middle->val[p]; + } + } + } + adapter->problem.D = adapter->owned_D; + return 1; } -pdhcg_presolve_info_t *pdhcg_presolve(const qp_problem_t *original_prob, const pdhg_parameters_t *params) +static int append_fixed_cone_rows(const qp_problem_t *source, PreFOSInputAdapter *adapter, int *prefos_rows) { - if (!original_prob) - return NULL; - - if (original_prob->num_constraints == 0) + const CsrComponent *A = source->constraint_matrix; + size_t original_rows = (size_t)source->num_constraints; + size_t original_nnz = + source->constraint_matrix_num_nonzeros > 0 ? (size_t)source->constraint_matrix_num_nonzeros : 0; + size_t fixed_count = 0; + int normalize_original_bounds = 0; + size_t variable; + size_t row; + + if (source->cones.is_fixed) + for (variable = 0; variable < (size_t)source->num_variables; ++variable) + if (source->cones.is_fixed[variable]) + ++fixed_count; + + for (row = 0; row < original_rows; ++row) + if (source->affine_cone_offset[row] != 0.0 || + normalize_lower_bound(source->constraint_lower_bound[row]) != source->constraint_lower_bound[row] || + normalize_upper_bound(source->constraint_upper_bound[row]) != source->constraint_upper_bound[row]) + normalize_original_bounds = 1; + + if (fixed_count == 0 && !normalize_original_bounds) + { + adapter->problem.A = csr_view(A, original_rows, (size_t)source->num_variables, original_nnz); + adapter->problem.constraint_lower = source->constraint_lower_bound; + adapter->problem.constraint_upper = source->constraint_upper_bound; + *prefos_rows = source->num_constraints; + return 1; + } + if (original_rows + fixed_count > (size_t)INT_MAX || original_nnz + fixed_count > (size_t)INT_MAX) + return 0; + if (original_rows > 0 && (!A || !A->row_ptr)) + return 0; + + adapter->owned_A_row_pointers = (int *)calloc(original_rows + fixed_count + 1, sizeof(int)); + adapter->owned_A_column_indices = (int *)allocate_array(original_nnz + fixed_count, sizeof(int)); + adapter->owned_A_values = (double *)allocate_array(original_nnz + fixed_count, sizeof(double)); + adapter->owned_constraint_lower = (double *)allocate_array(original_rows + fixed_count, sizeof(double)); + adapter->owned_constraint_upper = (double *)allocate_array(original_rows + fixed_count, sizeof(double)); + if (!adapter->owned_A_row_pointers || + (original_nnz + fixed_count > 0 && (!adapter->owned_A_column_indices || !adapter->owned_A_values)) || + !adapter->owned_constraint_lower || !adapter->owned_constraint_upper) + return 0; + + if (original_rows > 0) { - if (params->verbose > 1) + memcpy(adapter->owned_A_row_pointers, A->row_ptr, (original_rows + 1) * sizeof(int)); + for (row = 0; row < original_rows; ++row) { - printf("Note: Problem has no constraints, skipping presolve.\n"); + adapter->owned_constraint_lower[row] = + shifted_constraint_bound(source->constraint_lower_bound[row], source->affine_cone_offset[row], 1); + adapter->owned_constraint_upper[row] = + shifted_constraint_bound(source->constraint_upper_bound[row], source->affine_cone_offset[row], 0); } - return NULL; + } + if (original_nnz > 0) + { + memcpy(adapter->owned_A_column_indices, A->col_ind, original_nnz * sizeof(int)); + memcpy(adapter->owned_A_values, A->val, original_nnz * sizeof(double)); } - clock_t start_time = clock(); + row = original_rows; + for (variable = 0; variable < (size_t)source->num_variables; ++variable) + { + size_t position; + double value; + if (!source->cones.is_fixed[variable]) + continue; + position = original_nnz + row - original_rows; + value = source->primal_start ? source->primal_start[variable] : 0.0; + adapter->owned_A_column_indices[position] = (int)variable; + adapter->owned_A_values[position] = 1.0; + adapter->owned_A_row_pointers[row + 1] = (int)(position + 1); + adapter->owned_constraint_lower[row] = value; + adapter->owned_constraint_upper[row] = value; + ++row; + } - pdhcg_presolve_info_t *info = (pdhcg_presolve_info_t *)safe_calloc(1, sizeof(pdhcg_presolve_info_t)); - if (!info) - return NULL; + adapter->problem.A.rows = original_rows + fixed_count; + adapter->problem.A.cols = (size_t)source->num_variables; + adapter->problem.A.nnz = original_nnz + fixed_count; + adapter->problem.A.row_pointers = adapter->owned_A_row_pointers; + adapter->problem.A.column_indices = adapter->owned_A_column_indices; + adapter->problem.A.values = adapter->owned_A_values; + adapter->problem.constraint_lower = adapter->owned_constraint_lower; + adapter->problem.constraint_upper = adapter->owned_constraint_upper; + *prefos_rows = (int)(original_rows + fixed_count); + return 1; +} - info->settings = default_settings(); - ((Settings *)info->settings)->verbose = true; - ((Settings *)info->settings)->dual_fix = false; - - bool has_q = (original_prob->objective_sparse_matrix != NULL); - bool has_r = (original_prob->objective_lowrank_matrix != NULL); - - Presolver *presolver = NULL; - size_t m = (size_t)original_prob->num_constraints; - size_t n = (size_t)original_prob->num_variables; - size_t nnz = (size_t)original_prob->constraint_matrix_num_nonzeros; - - if (has_r) - { - size_t Qnnz = (has_q && original_prob->objective_sparse_matrix) - ? (size_t)original_prob->objective_sparse_matrix_num_nonzeros - : 0; - size_t Rnnz = (size_t)original_prob->objective_lowrank_matrix_num_nonzeros; - size_t k_rank = (size_t)original_prob->num_rank_lowrank_obj; - - presolver = new_qp_presolver_qr( - original_prob->constraint_matrix ? original_prob->constraint_matrix->val : NULL, - original_prob->constraint_matrix ? original_prob->constraint_matrix->col_ind : NULL, - original_prob->constraint_matrix ? original_prob->constraint_matrix->row_ptr : NULL, - m, - n, - nnz, - original_prob->constraint_lower_bound, - original_prob->constraint_upper_bound, - original_prob->variable_lower_bound, - original_prob->variable_upper_bound, - original_prob->objective_vector, - (has_q && original_prob->objective_sparse_matrix) ? original_prob->objective_sparse_matrix->val : NULL, - (has_q && original_prob->objective_sparse_matrix) ? original_prob->objective_sparse_matrix->col_ind : NULL, - (has_q && original_prob->objective_sparse_matrix) ? original_prob->objective_sparse_matrix->row_ptr : NULL, - Qnnz, - original_prob->objective_lowrank_matrix->val, - original_prob->objective_lowrank_matrix->col_ind, - original_prob->objective_lowrank_matrix->row_ptr, - Rnnz, - k_rank, - info->settings); - } - else if (has_q) - { - size_t Qnnz = (size_t)original_prob->objective_sparse_matrix_num_nonzeros; - presolver = - new_qp_presolver_qr(original_prob->constraint_matrix ? original_prob->constraint_matrix->val : NULL, - original_prob->constraint_matrix ? original_prob->constraint_matrix->col_ind : NULL, - original_prob->constraint_matrix ? original_prob->constraint_matrix->row_ptr : NULL, - m, - n, - nnz, - original_prob->constraint_lower_bound, - original_prob->constraint_upper_bound, - original_prob->variable_lower_bound, - original_prob->variable_upper_bound, - original_prob->objective_vector, - original_prob->objective_sparse_matrix->val, - original_prob->objective_sparse_matrix->col_ind, - original_prob->objective_sparse_matrix->row_ptr, - Qnnz, - NULL, // no low-rank component - NULL, - NULL, - 0, - 0, - info->settings); - } - else +static int initialize_domains(const qp_problem_t *source, PreFOSInputAdapter *adapter) +{ + size_t n = (size_t)source->num_variables; + size_t cone_count = source->cones.num_cones > 0 ? (size_t)source->cones.num_cones : 0; + unsigned char *owner = (unsigned char *)calloc(n, sizeof(unsigned char)); + size_t cone; + size_t cone_variables = 0; + size_t box_write = 0; + size_t variable; + + if (n > 0 && !owner) + return 0; + if (cone_count > 0 && (!source->cones.start_idx || !source->cones.v_dim || !source->cones.type)) { - presolver = new_presolver(original_prob->constraint_matrix ? original_prob->constraint_matrix->val : NULL, - original_prob->constraint_matrix ? original_prob->constraint_matrix->col_ind : NULL, - original_prob->constraint_matrix ? original_prob->constraint_matrix->row_ptr : NULL, - m, - n, - nnz, - original_prob->constraint_lower_bound, - original_prob->constraint_upper_bound, - original_prob->variable_lower_bound, - original_prob->variable_upper_bound, - original_prob->objective_vector, - info->settings); + free(owner); + return 0; } - - if (!presolver) + adapter->problem.n_cones = cone_count; + adapter->problem.cones = (PreFOSConeBlock *)calloc(cone_count, sizeof(PreFOSConeBlock)); + if (cone_count > 0 && !adapter->problem.cones) { - free_settings(info->settings); - free(info); - return NULL; + free(owner); + return 0; } - info->presolver = presolver; - - PresolveStatus status = run_presolver(presolver); - info->presolve_time = (double)(clock() - start_time) / CLOCKS_PER_SEC; - info->presolve_status = (int)status; - if (params->verbose > 1) + for (cone = 0; cone < cone_count; ++cone) { - printf("\nRunning presolver (PSQP %s)...\n", PSQP_VERSION); - printf(" %-15s : %s\n", "status", pdhcg_get_presolve_status_str(status)); - printf(" %-15s : %.3g sec\n", "presolve time", info->presolve_time); - if (presolver->reduced_prob) + PreFOSConeBlock *target = &adapter->problem.cones[cone]; + int start = source->cones.start_idx[cone]; + int vector_dimension = source->cones.v_dim[cone]; + int block_length = cone_block_length(&source->cones, (int)cone); + size_t dimension; + size_t index; + + if (block_length <= 0) + { + free(owner); + return 0; + } + dimension = (size_t)block_length; + if (start < 0 || (size_t)start > n || dimension > n - (size_t)start) + { + free(owner); + return 0; + } + + target->dimension = dimension; + target->matrix_order = 0; + target->indices = (int *)allocate_array(dimension, sizeof(int)); + if (!target->indices) + { + free(owner); + return 0; + } + switch (source->cones.type[cone]) + { + case CONE_STANDARD_SOC: + target->type = PREFOS_CONE_SECOND_ORDER; + target->indices[0] = start + vector_dimension + 1; + for (index = 1; index < dimension; ++index) + target->indices[index] = start + (int)index - 1; + break; + case CONE_ROTATED_SOC: + target->type = PREFOS_CONE_ROTATED_SECOND_ORDER; + target->indices[0] = start + vector_dimension; + target->indices[1] = start + vector_dimension + 1; + for (index = 2; index < dimension; ++index) + target->indices[index] = start + (int)index - 2; + break; + case CONE_EXPONENTIAL: + target->type = PREFOS_CONE_EXPONENTIAL; + target->indices[0] = start; + target->indices[1] = start + 1; + target->indices[2] = start + 2; + break; + case CONE_POWER: + target->type = PREFOS_CONE_POWER; + target->power_alpha = source->cones.power_alpha ? source->cones.power_alpha[cone] : 0.0; + target->indices[0] = start; + target->indices[1] = start + 1; + target->indices[2] = start + 2; + break; + default: + free(owner); + return 0; + } + for (index = 0; index < dimension; ++index) { - printf(" %-15s : %zu rows, %zu columns, %zu nonzeros\n", - "reduced problem", - presolver->reduced_prob->m, - presolver->reduced_prob->n, - presolver->reduced_prob->nnz); + int column = target->indices[index]; + double lower = normalize_lower_bound(source->variable_lower_bound[column]); + double upper = normalize_upper_bound(source->variable_upper_bound[column]); + if (owner[column] || isfinite(lower) || isfinite(upper)) + { + free(owner); + return 0; + } + owner[column] = 1; + ++cone_variables; } } - if ((status & INFEASIBLE) || (status & UNBNDORINFEAS) || - (presolver->reduced_prob && presolver->reduced_prob->n == 0)) + adapter->problem.n_box = n - cone_variables; + adapter->problem.box_indices = (int *)allocate_array(adapter->problem.n_box, sizeof(int)); + adapter->problem.box_lower = (double *)allocate_array(adapter->problem.n_box, sizeof(double)); + adapter->problem.box_upper = (double *)allocate_array(adapter->problem.n_box, sizeof(double)); + if (adapter->problem.n_box > 0 && + (!adapter->problem.box_indices || !adapter->problem.box_lower || !adapter->problem.box_upper)) { - info->problem_solved_during_presolve = true; - info->reduced_problem = NULL; + free(owner); + return 0; } - else + for (variable = 0; variable < n; ++variable) { - info->problem_solved_during_presolve = false; - info->reduced_problem = convert_psqp_to_pdhcg(presolver->reduced_prob, original_prob->objective_constant); + if (owner[variable]) + continue; + adapter->problem.box_indices[box_write] = (int)variable; + adapter->problem.box_lower[box_write] = normalize_lower_bound(source->variable_lower_bound[variable]); + adapter->problem.box_upper[box_write] = normalize_upper_bound(source->variable_upper_bound[variable]); + ++box_write; } + free(owner); + return box_write == adapter->problem.n_box; +} - return info; +static int initialize_prefos_input(const qp_problem_t *source, PreFOSInputAdapter *adapter, int *prefos_rows) +{ + int d_status; + memset(adapter, 0, sizeof(*adapter)); + if (!source || source->num_variables < 0 || source->num_constraints < 0 || source->num_rank_lowrank_obj < 0 || + source->cones.num_cones < 0 || source->constraint_matrix_num_nonzeros < 0 || + source->objective_sparse_matrix_num_nonzeros < 0 || source->objective_lowrank_matrix_num_nonzeros < 0 || + source->objective_lowrank_middle_matrix_num_nonzeros < 0 || + (source->num_variables > 0 && + (!source->objective_vector || !source->variable_lower_bound || !source->variable_upper_bound)) || + (source->num_constraints > 0 && + (!source->constraint_matrix || !source->constraint_lower_bound || !source->constraint_upper_bound || + !source->affine_cone_offset))) + return 0; + + adapter->problem.n = (size_t)source->num_variables; + adapter->problem.c = source->objective_vector; + adapter->problem.objective_offset = source->objective_constant; + adapter->problem.R = csr_view( + source->objective_lowrank_matrix, + source->num_rank_lowrank_obj > 0 ? (size_t)source->num_rank_lowrank_obj : 0, + (size_t)source->num_variables, + source->objective_lowrank_matrix_num_nonzeros > 0 ? (size_t)source->objective_lowrank_matrix_num_nonzeros : 0); + + if (!initialize_full_q(source, adapter) || !initialize_domains(source, adapter) || + !append_fixed_cone_rows(source, adapter, prefos_rows)) + return 0; + d_status = initialize_diagonal_d(source, adapter); + if (d_status <= 0) + return d_status; + return 1; } -pdhcg_result_t *pdhcg_create_result_from_presolve(const pdhcg_presolve_info_t *info, const qp_problem_t *original_prob) +static CsrComponent *wrap_csr(const PreFOSCsrMatrix *matrix) { - if (!info || !info->presolver) + CsrComponent *wrapper = (CsrComponent *)calloc(1, sizeof(CsrComponent)); + if (!wrapper) return NULL; + wrapper->row_ptr = matrix->row_pointers; + wrapper->col_ind = matrix->column_indices; + wrapper->val = matrix->values; + return wrapper; +} - Presolver *presolver = (Presolver *)info->presolver; - pdhcg_result_t *result = (pdhcg_result_t *)safe_calloc(1, sizeof(pdhcg_result_t)); - - result->num_variables = original_prob->num_variables; - result->num_constraints = original_prob->num_constraints; - result->num_nonzeros = original_prob->constraint_matrix_num_nonzeros; +static int convert_cone_to_pdhcg(const PreFOSConeBlock *source, cone_blocks_t *target, size_t cone) +{ + size_t dimension = source->dimension; + size_t i; + int start; + if (!source->indices || dimension < 2) + return 0; - if (presolver->reduced_prob) + switch (source->type) { - result->num_reduced_variables = (int)presolver->reduced_prob->n; - result->num_reduced_constraints = (int)presolver->reduced_prob->m; - result->num_reduced_nonzeros = (int)presolver->reduced_prob->nnz; + case PREFOS_CONE_SECOND_ORDER: + start = source->indices[1]; + for (i = 1; i < dimension; ++i) + if (source->indices[i] != start + (int)i - 1) + return 0; + if (source->indices[0] != start + (int)dimension - 1) + return 0; + target->type[cone] = CONE_STANDARD_SOC; + target->v_dim[cone] = (int)dimension - 2; + break; + case PREFOS_CONE_ROTATED_SECOND_ORDER: + if (dimension < 3) + return 0; + start = source->indices[2]; + for (i = 2; i < dimension; ++i) + if (source->indices[i] != start + (int)i - 2) + return 0; + if (source->indices[0] != start + (int)dimension - 2 || source->indices[1] != start + (int)dimension - 1) + return 0; + target->type[cone] = CONE_ROTATED_SOC; + target->v_dim[cone] = (int)dimension - 2; + break; + case PREFOS_CONE_EXPONENTIAL: + case PREFOS_CONE_POWER: + if (dimension != 3 || source->indices[1] != source->indices[0] + 1 || + source->indices[2] != source->indices[0] + 2) + return 0; + start = source->indices[0]; + target->type[cone] = source->type == PREFOS_CONE_EXPONENTIAL ? CONE_EXPONENTIAL : CONE_POWER; + target->v_dim[cone] = 1; + if (source->type == PREFOS_CONE_POWER) + target->power_alpha[cone] = source->power_alpha; + break; + default: + return 0; } + target->start_idx[cone] = start; + return 1; +} - result->presolve_status = info->presolve_status; - result->presolve_time = info->presolve_time; +static qp_problem_t *convert_prefos_to_pdhcg(const PreFOSPresolvedProblem *source) +{ + qp_problem_t *target; + size_t variable; + size_t box; + size_t cone; + size_t rank; + + if (!source || source->n > (size_t)INT_MAX || source->A.rows > (size_t)INT_MAX || source->A.nnz > (size_t)INT_MAX || + source->Q.nnz > (size_t)INT_MAX || source->R.rows > (size_t)INT_MAX || source->R.nnz > (size_t)INT_MAX || + source->n_cones > (size_t)INT_MAX || source->n_affine_cones > 0 || source->affine_cone_matrix.rows > 0 || + source->q_storage != PREFOS_Q_FULL) + return NULL; - if (info->presolve_status == INFEASIBLE) + target = (qp_problem_t *)calloc(1, sizeof(qp_problem_t)); + if (!target) + return NULL; + target->num_variables = (int)source->n; + target->num_constraints = (int)source->A.rows; + target->affine_cone_offset = + target->num_constraints > 0 ? (double *)calloc((size_t)target->num_constraints, sizeof(double)) : NULL; + if (target->num_constraints > 0 && !target->affine_cone_offset) { - result->termination_reason = TERMINATION_REASON_PRIMAL_INFEASIBLE; - result->absolute_primal_residual = INFINITY; - result->relative_primal_residual = INFINITY; - result->absolute_dual_residual = INFINITY; - result->relative_dual_residual = INFINITY; - result->primal_objective_value = INFINITY; - result->dual_objective_value = -INFINITY; - result->objective_gap = INFINITY; - result->relative_objective_gap = INFINITY; + free(target); + return NULL; } - else if (info->presolve_status == UNBNDORINFEAS) + target->constraint_matrix_num_nonzeros = (int)source->A.nnz; + target->objective_sparse_matrix_num_nonzeros = (int)source->Q.nnz; + target->objective_lowrank_matrix_num_nonzeros = (int)source->R.nnz; + target->num_rank_lowrank_obj = (int)source->R.rows; + target->objective_constant = source->objective_offset; + target->objective_vector = source->c; + target->constraint_lower_bound = source->constraint_lower; + target->constraint_upper_bound = source->constraint_upper; + target->constraint_matrix = wrap_csr(&source->A); + target->objective_sparse_matrix = wrap_csr(&source->Q); + target->objective_lowrank_matrix = wrap_csr(&source->R); + if (!target->constraint_matrix || !target->objective_sparse_matrix || !target->objective_lowrank_matrix) + goto failure; + + target->variable_lower_bound = (double *)allocate_array(source->n, sizeof(double)); + target->variable_upper_bound = (double *)allocate_array(source->n, sizeof(double)); + if (source->n > 0 && (!target->variable_lower_bound || !target->variable_upper_bound)) + goto failure; + for (variable = 0; variable < source->n; ++variable) { - result->termination_reason = TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED; - result->absolute_primal_residual = INFINITY; - result->relative_primal_residual = INFINITY; - result->absolute_dual_residual = INFINITY; - result->relative_dual_residual = INFINITY; - result->primal_objective_value = INFINITY; - result->dual_objective_value = -INFINITY; - result->objective_gap = INFINITY; - result->relative_objective_gap = INFINITY; + target->variable_lower_bound[variable] = -INFINITY; + target->variable_upper_bound[variable] = INFINITY; } - else if (presolver->reduced_prob && presolver->reduced_prob->n == 0) + for (box = 0; box < source->n_box; ++box) { - result->termination_reason = TERMINATION_REASON_OPTIMAL; - // Delegate cleanly to postsolve just like LP wrapper does - pdhcg_postsolve(info, result, original_prob); - return result; + int index = source->box_indices[box]; + if (index < 0 || (size_t)index >= source->n) + goto failure; + target->variable_lower_bound[index] = source->box_lower[box]; + target->variable_upper_bound[index] = source->box_upper[box]; } - else + + target->cones.num_cones = (int)source->n_cones; + target->cones.start_idx = (int *)allocate_array(source->n_cones, sizeof(int)); + target->cones.v_dim = (int *)allocate_array(source->n_cones, sizeof(int)); + target->cones.type = (cone_type_t *)allocate_array(source->n_cones, sizeof(cone_type_t)); + target->cones.power_alpha = (double *)calloc(source->n_cones, sizeof(double)); + if (source->n_cones > 0 && + (!target->cones.start_idx || !target->cones.v_dim || !target->cones.type || !target->cones.power_alpha)) + goto failure; + for (cone = 0; cone < source->n_cones; ++cone) + if (!convert_cone_to_pdhcg(&source->cones[cone], &target->cones, cone)) + goto failure; + + rank = source->R.rows; + if (rank > 0) { - result->termination_reason = TERMINATION_REASON_UNSPECIFIED; + CsrComponent *middle = (CsrComponent *)calloc(1, sizeof(CsrComponent)); + if (!middle) + goto failure; + target->objective_lowrank_middle_matrix = middle; + target->objective_lowrank_middle_matrix_num_nonzeros = (int)rank; + middle->row_ptr = (int *)allocate_array(rank + 1, sizeof(int)); + middle->col_ind = (int *)allocate_array(rank, sizeof(int)); + middle->val = (double *)allocate_array(rank, sizeof(double)); + if (!middle->row_ptr || !middle->col_ind || !middle->val) + goto failure; + for (variable = 0; variable < rank; ++variable) + { + middle->row_ptr[variable] = (int)variable; + middle->col_ind[variable] = (int)variable; + middle->val[variable] = source->D[variable]; + } + middle->row_ptr[rank] = (int)rank; } - if (result->num_variables > 0) + target->num_original_variables = target->num_variables; + return target; + +failure: + if (target) { - result->primal_solution = (double *)safe_calloc(result->num_variables, sizeof(double)); - result->reduced_cost = (double *)safe_calloc(result->num_variables, sizeof(double)); + free(target->constraint_matrix); + free(target->objective_sparse_matrix); + free(target->objective_lowrank_matrix); + if (target->objective_lowrank_middle_matrix) + { + free(target->objective_lowrank_middle_matrix->row_ptr); + free(target->objective_lowrank_middle_matrix->col_ind); + free(target->objective_lowrank_middle_matrix->val); + free(target->objective_lowrank_middle_matrix); + } + free(target->variable_lower_bound); + free(target->variable_upper_bound); + free(target->affine_cone_offset); + cone_blocks_free(&target->cones); + free(target); } - if (result->num_constraints > 0) + return NULL; +} + +static void free_converted_problem(qp_problem_t *problem) +{ + if (!problem) + return; + free(problem->constraint_matrix); + free(problem->objective_sparse_matrix); + free(problem->objective_lowrank_matrix); + if (problem->objective_lowrank_middle_matrix) { - result->dual_solution = (double *)safe_calloc(result->num_constraints, sizeof(double)); + free(problem->objective_lowrank_middle_matrix->row_ptr); + free(problem->objective_lowrank_middle_matrix->col_ind); + free(problem->objective_lowrank_middle_matrix->val); + free(problem->objective_lowrank_middle_matrix); } - - return result; + free(problem->variable_lower_bound); + free(problem->variable_upper_bound); + free(problem->affine_cone_offset); + cone_blocks_free(&problem->cones); + free(problem); } -void pdhcg_postsolve(const pdhcg_presolve_info_t *info, pdhcg_result_t *result, const qp_problem_t *original_prob) +static pdhcg_presolve_status_t map_prefos_status(PreFOSStatus status) { - if (!info || !info->presolver || !result) - return; + switch (status) + { + case PREFOS_STATUS_OK: + return PDHCG_PRESOLVE_STATUS_UNCHANGED; + case PREFOS_STATUS_REDUCED: + return PDHCG_PRESOLVE_STATUS_REDUCED; + case PREFOS_STATUS_PRIMAL_INFEASIBLE: + return PDHCG_PRESOLVE_STATUS_PRIMAL_INFEASIBLE; + default: + return PDHCG_PRESOLVE_STATUS_ERROR; + } +} - Presolver *presolver = (Presolver *)info->presolver; +const char *pdhcg_get_presolve_status_str(int status) +{ + switch ((pdhcg_presolve_status_t)status) + { + case PDHCG_PRESOLVE_STATUS_UNCHANGED: + return "UNCHANGED"; + case PDHCG_PRESOLVE_STATUS_REDUCED: + return "REDUCED"; + case PDHCG_PRESOLVE_STATUS_PRIMAL_INFEASIBLE: + return "PRIMAL_INFEASIBLE"; + case PDHCG_PRESOLVE_STATUS_ERROR: + return "ERROR"; + case PDHCG_PRESOLVE_STATUS_NOT_AVAILABLE: + return "NOT_AVAILABLE"; + default: + return "UNKNOWN_STATUS"; + } +} - postsolve(presolver, result->primal_solution, result->dual_solution, result->reduced_cost); +pdhcg_presolve_info_t *pdhcg_presolve(const qp_problem_t *original_problem, const pdhg_parameters_t *parameters) +{ + PreFOSInputAdapter adapter; + PreFOSSettings settings = prefos_default_settings(); + PreFOSPresolver *presolver = NULL; + const PreFOSPresolvedProblem *reduced; + pdhcg_presolve_info_t *info; + PreFOSStatus status; + clock_t start; + int adapter_status; + int prefos_rows = 0; + + if (!original_problem) + return NULL; + start = clock(); + adapter_status = initialize_prefos_input(original_problem, &adapter, &prefos_rows); + if (adapter_status <= 0) + { + if (!parameters || parameters->verbose > 0) + { + if (adapter_status < 0) + fprintf(stderr, "PreFOS presolve skipped: R^T D R currently requires diagonal D.\n"); + else + fprintf(stderr, "PreFOS presolve skipped: the PDHCG model could not be adapted safely.\n"); + } + free_input_adapter(&adapter); + return NULL; + } - double *full_primal = (double *)safe_calloc(original_prob->num_variables, sizeof(double)); - double *full_dual = (double *)safe_calloc(original_prob->num_constraints, sizeof(double)); - double *full_rc = (double *)safe_calloc(original_prob->num_variables, sizeof(double)); + /* PDHCG reports standard row/domain multipliers. Keep transformations whose + postsolve maps back to standard original-cone normals. */ + settings.rsoc_face_reduction = 0; + settings.psd_face_reduction = 0; + settings.exponential_face_reduction = 0; + settings.power_face_reduction = 0; + settings.affine_cone_coordinate_aggregation = 0; + settings.propagated_bound_policy = PREFOS_PROPAGATED_BOUND_POLICY_FIRST_ORDER; +#ifdef PDHCG_PREFOS_CUDA_ENABLED + settings.linear_propagation_gpu = 1; +#endif - if (presolver->sol) + info = (pdhcg_presolve_info_t *)calloc(1, sizeof(pdhcg_presolve_info_t)); + if (!info) { - if (presolver->sol->x) - memcpy(full_primal, presolver->sol->x, original_prob->num_variables * sizeof(double)); - if (presolver->sol->y) - memcpy(full_dual, presolver->sol->y, original_prob->num_constraints * sizeof(double)); - if (presolver->sol->z) - memcpy(full_rc, presolver->sol->z, original_prob->num_variables * sizeof(double)); + free_input_adapter(&adapter); + return NULL; } + info->prefos_original_rows = prefos_rows; + info->postsolve_tolerance = 1e-8; + if (parameters && parameters->termination_criteria.eps_feasible_relative > 0.0) + info->postsolve_tolerance = fmax(1e-10, parameters->termination_criteria.eps_feasible_relative); + + status = prefos_create_presolver(&adapter.problem, &settings, &presolver); + free_input_adapter(&adapter); + if (status == PREFOS_STATUS_OK && presolver) + status = prefos_run_presolve(presolver); + else if (status == PREFOS_STATUS_OK) + status = PREFOS_STATUS_OUT_OF_MEMORY; + info->presolve_time = (double)(clock() - start) / CLOCKS_PER_SEC; + info->presolve_status = map_prefos_status(status); + info->presolver = presolver; - if (result->primal_solution) - free(result->primal_solution); - if (result->dual_solution) - free(result->dual_solution); - if (result->reduced_cost) - free(result->reduced_cost); - - result->primal_solution = full_primal; - result->dual_solution = full_dual; - result->reduced_cost = full_rc; + if (status == PREFOS_STATUS_PRIMAL_INFEASIBLE) + { + info->problem_solved_during_presolve = true; + return info; + } + if (status != PREFOS_STATUS_OK && status != PREFOS_STATUS_REDUCED) + { + if (!parameters || parameters->verbose > 0) + fprintf(stderr, "PreFOS presolve failed: %s. Continuing without presolve.\n", prefos_status_string(status)); + pdhcg_presolve_info_free(info); + return NULL; + } - for (int i = 0; i < original_prob->num_variables; i++) + reduced = prefos_get_reduced_problem(presolver); + if (!reduced) { - if (!isfinite(original_prob->variable_lower_bound[i])) - { - result->reduced_cost[i] = fmin(result->reduced_cost[i], 0.0); - } - if (!isfinite(original_prob->variable_upper_bound[i])) + pdhcg_presolve_info_free(info); + return NULL; + } + if (status == PREFOS_STATUS_REDUCED && reduced->n == 0) + { + info->problem_solved_during_presolve = true; + } + else if (status == PREFOS_STATUS_REDUCED) + { + info->reduced_problem = convert_prefos_to_pdhcg(reduced); + if (!info->reduced_problem) { - result->reduced_cost[i] = fmax(result->reduced_cost[i], 0.0); + if (!parameters || parameters->verbose > 0) + fprintf(stderr, + "PreFOS reduced model is not representable by the PDHCG direct-cone interface; " + "continuing without presolve.\n"); + pdhcg_presolve_info_free(info); + return NULL; } } - if (presolver->reduced_prob && presolver->reduced_prob->n == 0) + if (parameters && parameters->verbose > 1) { - double obj = original_prob->objective_constant + presolver->reduced_prob->obj_offset; - result->primal_objective_value = obj; - result->dual_objective_value = obj; + const PreFOSStats *stats = prefos_get_stats(presolver); + printf("\nRunning presolver (PreFOS %s)...\n", PREFOS_VERSION); + printf(" %-15s : %s\n", "status", pdhcg_get_presolve_status_str(info->presolve_status)); + printf(" %-15s : %.3g sec\n", "presolve time", info->presolve_time); + if (stats && settings.linear_propagation_gpu) + { + printf(" %-15s : %zu rounds, %zu fallbacks\n", + "GPU propagation", + stats->linear_gpu_rounds, + stats->linear_gpu_fallbacks); + printf(" %-15s : %.3g setup, %.3g transfer, %.3g kernel sec\n", + "GPU time", + stats->linear_gpu_setup_milliseconds * 1e-3, + stats->linear_gpu_transfer_milliseconds * 1e-3, + stats->linear_gpu_kernel_milliseconds * 1e-3); + } + printf(" %-15s : %zu rows, %zu columns, %zu nonzeros\n", + "reduced problem", + reduced->A.rows, + reduced->n, + reduced->A.nnz); } + return info; +} - if (presolver->reduced_prob) +static void initialize_result_dimensions(pdhcg_result_t *result, + const pdhcg_presolve_info_t *info, + const qp_problem_t *original_problem) +{ + const PreFOSPresolvedProblem *reduced = + info->presolver ? prefos_get_reduced_problem((const PreFOSPresolver *)info->presolver) : NULL; + result->num_variables = original_problem->num_variables; + result->num_constraints = original_problem->num_constraints; + result->num_nonzeros = original_problem->constraint_matrix_num_nonzeros; + if (reduced) { - result->num_reduced_variables = (int)presolver->reduced_prob->n; - result->num_reduced_constraints = (int)presolver->reduced_prob->m; - result->num_reduced_nonzeros = (int)presolver->reduced_prob->nnz; + result->num_reduced_variables = (int)reduced->n; + result->num_reduced_constraints = (int)reduced->A.rows; + result->num_reduced_nonzeros = (int)reduced->A.nnz; } - result->presolve_status = info->presolve_status; + result->presolve_status = (int)info->presolve_status; result->presolve_time = info->presolve_time; } -void pdhcg_presolve_info_free(pdhcg_presolve_info_t *info) +pdhcg_result_t *pdhcg_create_result_from_presolve(const pdhcg_presolve_info_t *info, + const qp_problem_t *original_problem) { - if (!info) - return; + pdhcg_result_t *result; + if (!info || !original_problem) + return NULL; + result = (pdhcg_result_t *)calloc(1, sizeof(pdhcg_result_t)); + if (!result) + return NULL; + initialize_result_dimensions(result, info, original_problem); - if (info->reduced_problem) + if (info->presolve_status == PDHCG_PRESOLVE_STATUS_PRIMAL_INFEASIBLE) { - free_converted_problem(info->reduced_problem); + result->termination_reason = TERMINATION_REASON_PRIMAL_INFEASIBLE; + result->absolute_primal_residual = INFINITY; + result->relative_primal_residual = INFINITY; + result->absolute_dual_residual = INFINITY; + result->relative_dual_residual = INFINITY; + result->primal_objective_value = INFINITY; + result->dual_objective_value = -INFINITY; + result->objective_gap = INFINITY; + result->relative_objective_gap = INFINITY; + return result; } - if (info->presolver) + result->termination_reason = TERMINATION_REASON_OPTIMAL; + if (!pdhcg_postsolve(info, result, original_problem)) + result->termination_reason = TERMINATION_REASON_UNSPECIFIED; + return result; +} + +int pdhcg_postsolve(const pdhcg_presolve_info_t *info, pdhcg_result_t *result, const qp_problem_t *original_problem) +{ + const PreFOSPresolver *presolver; + const PreFOSPresolvedProblem *reduced; + double *reduced_y = NULL; + double *reduced_z = NULL; + double *original_x = NULL; + double *prefos_y = NULL; + double *prefos_z = NULL; + double *original_y = NULL; + double *original_z = NULL; + PreFOSStatus status; + size_t i; + int dual_recovered = 1; + + if (!info || !info->presolver || !result || !original_problem) + return 0; + presolver = (const PreFOSPresolver *)info->presolver; + reduced = prefos_get_reduced_problem(presolver); + if (!reduced) + return 0; + if ((reduced->n > 0 && (!result->primal_solution || !result->reduced_cost)) || + (reduced->A.rows > 0 && !result->dual_solution)) + return 0; + + reduced_y = (double *)allocate_array(reduced->A.rows, sizeof(double)); + reduced_z = (double *)allocate_array(reduced->n, sizeof(double)); + original_x = (double *)calloc((size_t)original_problem->num_variables, sizeof(double)); + prefos_y = (double *)calloc((size_t)info->prefos_original_rows, sizeof(double)); + prefos_z = (double *)calloc((size_t)original_problem->num_variables, sizeof(double)); + if ((reduced->A.rows > 0 && !reduced_y) || (reduced->n > 0 && !reduced_z) || + (original_problem->num_variables > 0 && (!original_x || !prefos_z)) || + (info->prefos_original_rows > 0 && !prefos_y)) + goto failure; + + for (i = 0; i < reduced->A.rows; ++i) + reduced_y[i] = -result->dual_solution[i]; + for (i = 0; i < reduced->n; ++i) + reduced_z[i] = -result->reduced_cost[i]; + + status = prefos_postsolve_primal_dual(presolver, + result->primal_solution, + reduced_y, + reduced_z, + info->postsolve_tolerance, + original_x, + prefos_y, + prefos_z); + if (status == PREFOS_STATUS_DUAL_RECOVERY_UNAVAILABLE) + status = prefos_postsolve_extended_dual(presolver, + result->primal_solution, + reduced_y, + reduced_z, + info->postsolve_tolerance, + original_x, + prefos_y, + prefos_z); + if (status != PREFOS_STATUS_OK) { - free_presolver((Presolver *)info->presolver); + dual_recovered = 0; + if (original_problem->num_variables > 0) + memset(original_x, 0, (size_t)original_problem->num_variables * sizeof(double)); + status = prefos_postsolve_primal(presolver, result->primal_solution, original_x); + if (status != PREFOS_STATUS_OK) + goto failure; + if (info->prefos_original_rows > 0) + memset(prefos_y, 0, (size_t)info->prefos_original_rows * sizeof(double)); + if (original_problem->num_variables > 0) + memset(prefos_z, 0, (size_t)original_problem->num_variables * sizeof(double)); } - if (info->settings) + original_y = (double *)allocate_array((size_t)original_problem->num_constraints, sizeof(double)); + original_z = (double *)allocate_array((size_t)original_problem->num_variables, sizeof(double)); + if ((original_problem->num_constraints > 0 && !original_y) || (original_problem->num_variables > 0 && !original_z)) + goto failure; + for (i = 0; i < (size_t)original_problem->num_constraints; ++i) + original_y[i] = -prefos_y[i]; + for (i = 0; i < (size_t)original_problem->num_variables; ++i) + original_z[i] = -prefos_z[i]; + + free(result->primal_solution); + free(result->dual_solution); + free(result->reduced_cost); + result->primal_solution = original_x; + result->dual_solution = original_y; + result->reduced_cost = original_z; + original_x = NULL; + original_y = NULL; + original_z = NULL; + + initialize_result_dimensions(result, info, original_problem); + if (reduced->n == 0) { - free_settings((Settings *)info->settings); + result->primal_objective_value = reduced->objective_offset; + result->dual_objective_value = reduced->objective_offset; } + free(reduced_y); + free(reduced_z); + free(prefos_y); + free(prefos_z); + if (!dual_recovered) + fprintf(stderr, "Warning: PreFOS recovered the primal solution but not a valid original dual solution.\n"); + return dual_recovered; + +failure: + free(reduced_y); + free(reduced_z); + free(original_x); + free(prefos_y); + free(prefos_z); + free(original_y); + free(original_z); + return 0; +} +void pdhcg_presolve_info_free(pdhcg_presolve_info_t *info) +{ + if (!info) + return; + free_converted_problem(info->reduced_problem); + if (info->presolver) + prefos_free_presolver((PreFOSPresolver *)info->presolver); free(info); } const char *pdhcg_presolve_version(void) { - return "PSQP " PSQP_VERSION; + return "PreFOS " PREFOS_VERSION; } int pdhcg_presolve_available(void) @@ -415,38 +1033,38 @@ int pdhcg_presolve_available(void) return 1; } -#else /* PSQP_AVAILABLE */ +#else -#include "presolve_wrapper.h" #include -#include const char *pdhcg_get_presolve_status_str(int status) { (void)status; - return "PSQP_NOT_AVAILABLE"; + return "NOT_AVAILABLE"; } -pdhcg_presolve_info_t *pdhcg_presolve(const qp_problem_t *original_prob, const pdhg_parameters_t *params) +pdhcg_presolve_info_t *pdhcg_presolve(const qp_problem_t *original_problem, const pdhg_parameters_t *parameters) { - (void)original_prob; - (void)params; - fprintf(stderr, "Warning: PSQP not available, presolving disabled.\n"); + (void)original_problem; + (void)parameters; + fprintf(stderr, "Warning: PreFOS not available; presolving disabled.\n"); return NULL; } -pdhcg_result_t *pdhcg_create_result_from_presolve(const pdhcg_presolve_info_t *info, const qp_problem_t *original_prob) +pdhcg_result_t *pdhcg_create_result_from_presolve(const pdhcg_presolve_info_t *info, + const qp_problem_t *original_problem) { (void)info; - (void)original_prob; + (void)original_problem; return NULL; } -void pdhcg_postsolve(const pdhcg_presolve_info_t *info, pdhcg_result_t *result, const qp_problem_t *original_prob) +int pdhcg_postsolve(const pdhcg_presolve_info_t *info, pdhcg_result_t *result, const qp_problem_t *original_problem) { (void)info; (void)result; - (void)original_prob; + (void)original_problem; + return 0; } void pdhcg_presolve_info_free(pdhcg_presolve_info_t *info) @@ -456,7 +1074,7 @@ void pdhcg_presolve_info_free(pdhcg_presolve_info_t *info) const char *pdhcg_presolve_version(void) { - return "PSQP not available"; + return "PreFOS not available"; } int pdhcg_presolve_available(void) @@ -464,4 +1082,4 @@ int pdhcg_presolve_available(void) return 0; } -#endif /* PSQP_AVAILABLE */ +#endif diff --git a/src/qcqp_transform.c b/src/qcqp_transform.c new file mode 100644 index 0000000..24d6e30 --- /dev/null +++ b/src/qcqp_transform.c @@ -0,0 +1,552 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +#include "qcqp_transform.h" +#include "pdhcg.h" +#include "utils.h" +#include +#include +#include +#include + +void restore_qcqp_result_dimensions(pdhcg_result_t *result, const qp_problem_t *original) +{ + if (!result || !original) + return; + + int n_orig = original->num_variables; + int m_orig = original->num_constraints; + + if (n_orig >= 0 && n_orig < result->num_variables) + { + if (result->primal_solution) + { + double *new_primal = n_orig > 0 ? (double *)safe_malloc((size_t)n_orig * sizeof(double)) : NULL; + if (n_orig > 0) + memcpy(new_primal, result->primal_solution, (size_t)n_orig * sizeof(double)); + free(result->primal_solution); + result->primal_solution = new_primal; + } + + if (result->reduced_cost) + { + double *new_rc = n_orig > 0 ? (double *)safe_malloc((size_t)n_orig * sizeof(double)) : NULL; + if (n_orig > 0) + memcpy(new_rc, result->reduced_cost, (size_t)n_orig * sizeof(double)); + free(result->reduced_cost); + result->reduced_cost = new_rc; + } + result->num_variables = n_orig; + } + if (m_orig >= 0 && m_orig < result->num_constraints) + { + if (result->dual_solution) + { + double *new_dual = m_orig > 0 ? (double *)safe_malloc((size_t)m_orig * sizeof(double)) : NULL; + if (m_orig > 0) + memcpy(new_dual, result->dual_solution, (size_t)m_orig * sizeof(double)); + free(result->dual_solution); + result->dual_solution = new_dual; + } + result->num_constraints = m_orig; + } + result->num_nonzeros = original->constraint_matrix_num_nonzeros; +} + +static int +extract_diag_signed(const CsrComponent *Q, int n, int nnz_max, int *out_cols, double *out_vals, int *sign_out) +{ + int count = 0; + int sign = 0; + for (int row = 0; row < n; ++row) + { + int start = Q->row_ptr[row]; + int end = Q->row_ptr[row + 1]; + for (int k = start; k < end; ++k) + { + int col = Q->col_ind[k]; + double val = Q->val[k]; + if (col != row) + return -1; + if (val == 0.0) + continue; + int s = (val > 0.0) ? +1 : -1; + if (sign == 0) + sign = s; + else if (sign != s) + return -1; + if (count >= nnz_max) + return -1; + out_cols[count] = col; + out_vals[count] = (val > 0.0) ? val : -val; + count++; + } + } + *sign_out = sign; + return count; +} + +qp_problem_t *qcqp_to_socp_qp(const qp_problem_t *orig, cone_type_t default_type) +{ + if (!orig) + return NULL; + if (orig->num_quadratic_constraints == 0) + { + fprintf(stderr, "[qcqp_to_socp_qp] no quadratic constraints; nothing to do.\n"); + return NULL; + } + if (orig->affine_cones.num_cones > 0) + { + fprintf(stderr, + "[qcqp_to_socp_qp] native affine cones combined with quadratic constraints " + "are not supported by this transform.\n"); + return NULL; + } + + int n_orig = orig->num_variables; + int m_orig = orig->num_constraints; + int K = orig->num_quadratic_constraints; + const bool is_std = (default_type == CONE_STANDARD_SOC); + const double SQRT2 = 1.4142135623730951; + + int *block_k = (int *)safe_malloc(K * sizeof(int)); + int **block_cols = (int **)safe_malloc(K * sizeof(int *)); + double **block_sqrt = (double **)safe_malloc(K * sizeof(double *)); + int *block_flip = (int *)safe_calloc(K, sizeof(int)); + double *block_b = (double *)safe_malloc(K * sizeof(double)); + long total_v = 0; + for (int i = 0; i < K; ++i) + { + CsrComponent *Q = orig->quadratic_constraint_matrices[i]; + int nnz = orig->quadratic_constraint_matrix_num_nonzeros[i]; + int *cols = (int *)safe_malloc((nnz > 0 ? nnz : 1) * sizeof(int)); + double *qjj = (double *)safe_malloc((nnz > 0 ? nnz : 1) * sizeof(double)); + int sign = 0; + int k = extract_diag_signed(Q, n_orig, nnz, cols, qjj, &sign); + if (k < 0) + { + fprintf(stderr, + "[qcqp_to_socp_qp] Q_%d is non-diagonal or has mixed signs; " + "diagonal PSD (or all-NSD with >= sense) required.\n", + i); + free(cols); + free(qjj); + for (int j = 0; j < i; ++j) + { + free(block_cols[j]); + free(block_sqrt[j]); + } + free(block_k); + free(block_cols); + free(block_sqrt); + free(block_flip); + free(block_b); + return NULL; + } + + int row = orig->quadratic_constraint_row_indices[i]; + double lhs = orig->constraint_lower_bound[row]; + double rhs = orig->constraint_upper_bound[row]; + int flip = 0; + double b_eff = 0.0; + if (sign >= 0) + { + if (isfinite(lhs) || !isfinite(rhs)) + { + fprintf(stderr, + "[qcqp_to_socp_qp] QC row %d (Q PSD) requires one-sided <= " + "(lhs=-inf, rhs finite); got lhs=%.3g rhs=%.3g.\n", + row, + lhs, + rhs); + free(cols); + free(qjj); + for (int j = 0; j < i; ++j) + { + free(block_cols[j]); + free(block_sqrt[j]); + } + free(block_k); + free(block_cols); + free(block_sqrt); + free(block_flip); + free(block_b); + return NULL; + } + b_eff = rhs; + } + else + { + if (!isfinite(lhs) || isfinite(rhs)) + { + fprintf(stderr, + "[qcqp_to_socp_qp] QC row %d (Q NSD) requires one-sided >= " + "(lhs finite, rhs=+inf); got lhs=%.3g rhs=%.3g.\n", + row, + lhs, + rhs); + free(cols); + free(qjj); + for (int j = 0; j < i; ++j) + { + free(block_cols[j]); + free(block_sqrt[j]); + } + free(block_k); + free(block_cols); + free(block_sqrt); + free(block_flip); + free(block_b); + return NULL; + } + flip = 1; + b_eff = -lhs; + } + + for (int m = 0; m < k; ++m) + qjj[m] = sqrt(2.0 * qjj[m]); + block_k[i] = k; + block_cols[i] = cols; + block_sqrt[i] = qjj; + block_flip[i] = flip; + block_b[i] = b_eff; + total_v += k; + } + + const CsrComponent *A_orig_pre = orig->constraint_matrix; + char *blk_pin = (char *)safe_calloc(K, sizeof(char)); + int num_pin = 0; + for (int i = 0; i < K; ++i) + { + int row = orig->quadratic_constraint_row_indices[i]; + if (A_orig_pre->row_ptr[row + 1] - A_orig_pre->row_ptr[row] == 0) + { + blk_pin[i] = 1; + num_pin++; + } + } + + long n_ext = (long)n_orig + total_v + 2L * K; + long m_ext = (long)m_orig + total_v + K; + long extras_per_block = is_std ? 2L : 1L; + long nnz_ext = (long)orig->constraint_matrix_num_nonzeros + extras_per_block * (K - num_pin) + 2L * total_v + + extras_per_block * (K - num_pin); + if (n_ext > INT32_MAX || m_ext > INT32_MAX || nnz_ext > INT32_MAX) + { + fprintf(stderr, "[qcqp_to_socp_qp] extended problem size overflows int32.\n"); + goto fail_free_blocks; + } + + qp_problem_t *out = (qp_problem_t *)safe_calloc(1, sizeof(qp_problem_t)); + out->num_variables = (int)n_ext; + out->num_constraints = (int)m_ext; + out->affine_cone_offset = (double *)safe_calloc((size_t)m_ext, sizeof(double)); + out->constraint_matrix_num_nonzeros = (int)nnz_ext; + out->objective_constant = orig->objective_constant; + + out->objective_vector = (double *)safe_calloc(n_ext, sizeof(double)); + out->variable_lower_bound = (double *)safe_malloc(n_ext * sizeof(double)); + out->variable_upper_bound = (double *)safe_malloc(n_ext * sizeof(double)); + memcpy(out->objective_vector, orig->objective_vector, n_orig * sizeof(double)); + memcpy(out->variable_lower_bound, orig->variable_lower_bound, n_orig * sizeof(double)); + memcpy(out->variable_upper_bound, orig->variable_upper_bound, n_orig * sizeof(double)); + + out->cones.num_cones = K; + out->cones.start_idx = (int *)safe_malloc(K * sizeof(int)); + out->cones.v_dim = (int *)safe_malloc(K * sizeof(int)); + out->cones.type = (cone_type_t *)safe_malloc(K * sizeof(cone_type_t)); + out->num_original_variables = n_orig; + { + long idx = n_orig; + for (int i = 0; i < K; ++i) + { + int k = block_k[i]; + out->cones.start_idx[i] = (int)idx; + out->cones.v_dim[i] = k; + out->cones.type[i] = default_type; + for (int m = 0; m < k; ++m) + { + out->variable_lower_bound[idx] = -INFINITY; + out->variable_upper_bound[idx] = INFINITY; + idx++; + } + out->variable_lower_bound[idx] = -INFINITY; + out->variable_upper_bound[idx] = INFINITY; + idx++; + out->variable_lower_bound[idx] = -INFINITY; + out->variable_upper_bound[idx] = INFINITY; + idx++; + } + } + + out->constraint_lower_bound = (double *)safe_malloc(m_ext * sizeof(double)); + out->constraint_upper_bound = (double *)safe_malloc(m_ext * sizeof(double)); + memcpy(out->constraint_lower_bound, orig->constraint_lower_bound, m_orig * sizeof(double)); + memcpy(out->constraint_upper_bound, orig->constraint_upper_bound, m_orig * sizeof(double)); + memcpy(out->affine_cone_offset, orig->affine_cone_offset, m_orig * sizeof(double)); + for (int i = 0; i < K; ++i) + { + int row = orig->quadratic_constraint_row_indices[i]; + double rhs = blk_pin[i] ? 0.0 : (is_std ? block_b[i] * SQRT2 : block_b[i]); + out->constraint_lower_bound[row] = rhs; + out->constraint_upper_bound[row] = rhs; + } + for (long r = m_orig; r < m_orig + total_v; ++r) + { + out->constraint_lower_bound[r] = 0.0; + out->constraint_upper_bound[r] = 0.0; + } + { + double last_rhs = is_std ? SQRT2 : 1.0; + long r = m_orig + total_v; + for (int i = 0; i < K; ++i, ++r) + { + double v = blk_pin[i] ? 0.0 : last_rhs; + out->constraint_lower_bound[r] = v; + out->constraint_upper_bound[r] = v; + } + } + + int *qc_row_to_block = (int *)safe_malloc(m_orig * sizeof(int)); + for (int r = 0; r < m_orig; ++r) + qc_row_to_block[r] = -1; + for (int i = 0; i < K; ++i) + qc_row_to_block[orig->quadratic_constraint_row_indices[i]] = i; + + out->constraint_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + int *row_ptr_ext = (int *)safe_calloc(m_ext + 1, sizeof(int)); + int *col_ind_ext = (int *)safe_malloc(nnz_ext * sizeof(int)); + double *val_ext = (double *)safe_malloc(nnz_ext * sizeof(double)); + + const CsrComponent *A_orig = orig->constraint_matrix; + for (int r = 0; r < m_orig; ++r) + { + int orig_nnz = A_orig->row_ptr[r + 1] - A_orig->row_ptr[r]; + int blk_r = qc_row_to_block[r]; + int extra = (blk_r >= 0 && !blk_pin[blk_r]) ? (is_std ? 2 : 1) : 0; + row_ptr_ext[r + 1] = orig_nnz + extra; + } + long extra_row = m_orig; + for (int i = 0; i < K; ++i) + { + for (int m = 0; m < block_k[i]; ++m) + { + row_ptr_ext[extra_row + 1] = 2; + extra_row++; + } + } + for (int i = 0; i < K; ++i) + { + row_ptr_ext[extra_row + 1] = blk_pin[i] ? 0 : (is_std ? 2 : 1); + extra_row++; + } + for (long r = 1; r <= m_ext; ++r) + row_ptr_ext[r] += row_ptr_ext[r - 1]; + + for (int r = 0; r < m_orig; ++r) + { + int dst = row_ptr_ext[r]; + int s = A_orig->row_ptr[r]; + int e = A_orig->row_ptr[r + 1]; + int blk = qc_row_to_block[r]; + double scale = (blk >= 0 && block_flip[blk]) ? -1.0 : 1.0; + double xscale = (is_std && blk >= 0) ? scale * SQRT2 : scale; + for (int k = s; k < e; ++k) + { + col_ind_ext[dst] = A_orig->col_ind[k]; + val_ext[dst] = xscale * A_orig->val[k]; + dst++; + } + if (blk >= 0 && !blk_pin[blk]) + { + int aux0 = out->cones.start_idx[blk] + out->cones.v_dim[blk]; + col_ind_ext[dst] = aux0; + val_ext[dst] = 1.0; + dst++; + if (is_std) + { + col_ind_ext[dst] = aux0 + 1; + val_ext[dst] = 1.0; + dst++; + } + } + } + extra_row = m_orig; + for (int i = 0; i < K; ++i) + { + int v_start = out->cones.start_idx[i]; + for (int m = 0; m < block_k[i]; ++m) + { + int dst = row_ptr_ext[extra_row]; + col_ind_ext[dst] = block_cols[i][m]; + val_ext[dst] = -block_sqrt[i][m]; + dst++; + col_ind_ext[dst] = v_start + m; + val_ext[dst] = 1.0; + extra_row++; + } + } + for (int i = 0; i < K; ++i) + { + if (blk_pin[i]) + { + extra_row++; + continue; + } + int aux0 = out->cones.start_idx[i] + out->cones.v_dim[i]; + int dst = row_ptr_ext[extra_row]; + if (is_std) + { + col_ind_ext[dst] = aux0; + val_ext[dst] = -1.0; + dst++; + col_ind_ext[dst] = aux0 + 1; + val_ext[dst] = 1.0; + } + else + { + col_ind_ext[dst] = aux0 + 1; + val_ext[dst] = 1.0; + } + extra_row++; + } + + out->constraint_matrix->row_ptr = row_ptr_ext; + out->constraint_matrix->col_ind = col_ind_ext; + out->constraint_matrix->val = val_ext; + + out->num_rank_lowrank_obj = orig->num_rank_lowrank_obj; + out->objective_sparse_matrix_num_nonzeros = orig->objective_sparse_matrix_num_nonzeros; + out->objective_lowrank_matrix_num_nonzeros = orig->objective_lowrank_matrix_num_nonzeros; + out->objective_lowrank_middle_matrix_num_nonzeros = orig->objective_lowrank_middle_matrix_num_nonzeros; + + if (orig->objective_sparse_matrix) + { + int nz = orig->objective_sparse_matrix_num_nonzeros; + int total_rows = (int)n_ext; + out->objective_sparse_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + out->objective_sparse_matrix->row_ptr = (int *)safe_malloc((size_t)(total_rows + 1) * sizeof(int)); + memcpy(out->objective_sparse_matrix->row_ptr, + orig->objective_sparse_matrix->row_ptr, + (size_t)(n_orig + 1) * sizeof(int)); + int last = orig->objective_sparse_matrix->row_ptr[n_orig]; + for (int r = n_orig + 1; r <= total_rows; ++r) + out->objective_sparse_matrix->row_ptr[r] = last; + if (nz > 0) + { + out->objective_sparse_matrix->col_ind = (int *)safe_malloc((size_t)nz * sizeof(int)); + out->objective_sparse_matrix->val = (double *)safe_malloc((size_t)nz * sizeof(double)); + memcpy(out->objective_sparse_matrix->col_ind, + orig->objective_sparse_matrix->col_ind, + (size_t)nz * sizeof(int)); + memcpy(out->objective_sparse_matrix->val, orig->objective_sparse_matrix->val, (size_t)nz * sizeof(double)); + } + } + + if (orig->objective_lowrank_matrix) + { + int nr = orig->num_rank_lowrank_obj; + int nz = orig->objective_lowrank_matrix_num_nonzeros; + out->objective_lowrank_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + out->objective_lowrank_matrix->row_ptr = (int *)safe_malloc((size_t)(nr + 1) * sizeof(int)); + memcpy(out->objective_lowrank_matrix->row_ptr, + orig->objective_lowrank_matrix->row_ptr, + (size_t)(nr + 1) * sizeof(int)); + if (nz > 0) + { + out->objective_lowrank_matrix->col_ind = (int *)safe_malloc((size_t)nz * sizeof(int)); + out->objective_lowrank_matrix->val = (double *)safe_malloc((size_t)nz * sizeof(double)); + memcpy(out->objective_lowrank_matrix->col_ind, + orig->objective_lowrank_matrix->col_ind, + (size_t)nz * sizeof(int)); + memcpy( + out->objective_lowrank_matrix->val, orig->objective_lowrank_matrix->val, (size_t)nz * sizeof(double)); + } + } + + if (orig->objective_lowrank_middle_matrix) + { + int nr = orig->num_rank_lowrank_obj; + int nz = orig->objective_lowrank_middle_matrix_num_nonzeros; + out->objective_lowrank_middle_matrix = (CsrComponent *)safe_calloc(1, sizeof(CsrComponent)); + out->objective_lowrank_middle_matrix->row_ptr = (int *)safe_malloc((size_t)(nr + 1) * sizeof(int)); + memcpy(out->objective_lowrank_middle_matrix->row_ptr, + orig->objective_lowrank_middle_matrix->row_ptr, + (size_t)(nr + 1) * sizeof(int)); + if (nz > 0) + { + out->objective_lowrank_middle_matrix->col_ind = (int *)safe_malloc((size_t)nz * sizeof(int)); + out->objective_lowrank_middle_matrix->val = (double *)safe_malloc((size_t)nz * sizeof(double)); + memcpy(out->objective_lowrank_middle_matrix->col_ind, + orig->objective_lowrank_middle_matrix->col_ind, + (size_t)nz * sizeof(int)); + memcpy(out->objective_lowrank_middle_matrix->val, + orig->objective_lowrank_middle_matrix->val, + (size_t)nz * sizeof(double)); + } + } + + out->num_quadratic_constraints = 0; + + if (num_pin > 0) + { + out->cones.fixed_mask_size = (int)n_ext; + out->cones.is_fixed = (char *)safe_calloc(n_ext, sizeof(char)); + out->primal_start = (double *)safe_calloc(n_ext, sizeof(double)); + for (int i = 0; i < K; ++i) + { + if (!blk_pin[i]) + continue; + int s_slot = out->cones.start_idx[i] + out->cones.v_dim[i]; + int t_slot = s_slot + 1; + out->cones.is_fixed[s_slot] = 1; + out->cones.is_fixed[t_slot] = 1; + if (is_std) + { + out->primal_start[s_slot] = (block_b[i] - 1.0) * SQRT2 * 0.5; + out->primal_start[t_slot] = (block_b[i] + 1.0) * SQRT2 * 0.5; + } + else + { + out->primal_start[s_slot] = block_b[i]; + out->primal_start[t_slot] = 1.0; + } + } + } + + free(blk_pin); + free(qc_row_to_block); + for (int i = 0; i < K; ++i) + { + free(block_cols[i]); + free(block_sqrt[i]); + } + free(block_k); + free(block_cols); + free(block_sqrt); + free(block_flip); + free(block_b); + + return out; + +fail_free_blocks: + for (int j = 0; j < K; ++j) + { + free(block_cols[j]); + free(block_sqrt[j]); + } + free(block_k); + free(block_cols); + free(block_sqrt); + free(block_flip); + free(block_b); + free(blk_pin); + return NULL; +} diff --git a/src/solver.cu b/src/solver.cu index 34e6d34..b1b12ab 100644 --- a/src/solver.cu +++ b/src/solver.cu @@ -20,29 +20,57 @@ limitations under the License. #include "pdhg_core_op.h" #include "preconditioner.h" #include "presolve_wrapper.h" +#include "qcqp_transform.h" #include "solver.h" #include "solver_state.h" #include "utils.h" +#include #include #include #include #include #include #include -#include pdhcg_result_t *optimize(const pdhg_parameters_t *input_params, const qp_problem_t *original_problem) { pdhg_parameters_t copyed_params = *input_params; pdhg_parameters_t *params = ©ed_params; + const qp_problem_t *input_problem = original_problem; print_initial_info(input_params, original_problem); + qp_problem_t *transformed = NULL; + if (original_problem->num_quadratic_constraints > 0) + { + transformed = qcqp_to_socp_qp(original_problem, params->default_cone_type); + if (!transformed) + { + fprintf(stderr, "Error: QCQP -> SOCP transformation failed; cannot solve.\n"); + return NULL; + } + if (params->verbose >= 1) + { + const char *form_name = (params->default_cone_type == CONE_STANDARD_SOC) ? "standard" : "rotated"; + fprintf(stderr, + "[QCQP] %d quadratic constraint(s) reformulated as %d " + "%s SOC block(s); extended problem: %d vars, " + "%d rows, %d nnz.\n", + original_problem->num_quadratic_constraints, + transformed->cones.num_cones, + form_name, + transformed->num_variables, + transformed->num_constraints, + transformed->constraint_matrix_num_nonzeros); + } + original_problem = transformed; + } + pdhcg_presolve_info_t *presolve_info = NULL; const qp_problem_t *working_problem = original_problem; bool working_problem_needs_free = false; - if (params->presolve && pdhcg_presolve_available()) + if (params->presolve && original_problem->affine_cones.num_cones == 0 && pdhcg_presolve_available()) { presolve_info = pdhcg_presolve(original_problem, params); if (presolve_info) @@ -50,11 +78,16 @@ pdhcg_result_t *optimize(const pdhg_parameters_t *input_params, const qp_problem if (presolve_info->problem_solved_during_presolve) { pdhcg_result_t *result = pdhcg_create_result_from_presolve(presolve_info, original_problem); + restore_qcqp_result_dimensions(result, transformed ? input_problem : NULL); if (result) { pdhg_final_log(result, params); } pdhcg_presolve_info_free(presolve_info); + if (transformed) + { + qp_problem_free(transformed); + } return result; } @@ -67,7 +100,7 @@ pdhcg_result_t *optimize(const pdhg_parameters_t *input_params, const qp_problem if (working_problem->num_constraints == 0 || working_problem->constraint_matrix == NULL) { - working_problem = create_problem_with_dummy_constraint(original_problem); + working_problem = create_problem_with_dummy_constraint(working_problem); working_problem_needs_free = true; } @@ -82,7 +115,7 @@ pdhcg_result_t *optimize(const pdhg_parameters_t *input_params, const qp_problem rescale_info_free(rescale_info); initialize_step_size_and_primal_weight(state, params); - clock_t start_time = clock(); + const auto start_time = std::chrono::steady_clock::now(); bool do_restart = false; while (state->total_count < params->termination_criteria.iteration_limit) @@ -91,12 +124,14 @@ pdhcg_result_t *optimize(const pdhg_parameters_t *input_params, const qp_problem (state->total_count % get_print_frequency(state->total_count) == 0)) { compute_residual(state, params->optimality_norm); - if (state->is_this_major_iteration && state->total_count < 3 * params->termination_evaluation_frequency) + if (!state->has_variable_cones && state->affine_cones.num_blocks == 0 && state->is_this_major_iteration && + state->total_count < 3 * params->termination_evaluation_frequency) { compute_infeasibility_information(state); } - state->cumulative_time_sec = (double)(clock() - start_time) / CLOCKS_PER_SEC; + state->cumulative_time_sec = + std::chrono::duration(std::chrono::steady_clock::now() - start_time).count(); check_termination_criteria(state, ¶ms->termination_criteria); display_iteration_stats(state, params->verbose); @@ -150,15 +185,26 @@ pdhcg_result_t *optimize(const pdhg_parameters_t *input_params, const qp_problem if (presolve_info && presolve_info->reduced_problem) { - pdhcg_postsolve(presolve_info, result, original_problem); + if (!pdhcg_postsolve(presolve_info, result, original_problem)) + { + fprintf(stderr, "Error: PreFOS primal-dual postsolve failed.\n"); + result->termination_reason = TERMINATION_REASON_UNSPECIFIED; + } } if (working_problem_needs_free) { qp_problem_free((qp_problem_t *)working_problem); } + + restore_qcqp_result_dimensions(result, transformed ? input_problem : NULL); + pdhg_final_log(result, params); pdhg_solver_state_free(state); pdhcg_presolve_info_free(presolve_info); + if (transformed) + { + qp_problem_free(transformed); + } CUDA_CHECK(cudaGetLastError()); return result; } diff --git a/src/solver_state.cu b/src/solver_state.cu index 2829323..79be4b0 100644 --- a/src/solver_state.cu +++ b/src/solver_state.cu @@ -15,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +#include "cone_dispatch.h" +#include "distributed_conic.h" #include "internal_types.h" #include "pdhcg.h" #include "pdhcg_kernels.cuh" @@ -30,37 +32,18 @@ limitations under the License. #include #include #include +#include #include -#ifdef PDHCG_COMPILE_DISTRIBUTED -#include "distributed_types.h" -#endif - int get_global_n(pdhg_solver_state_t *state) { - int n = state->num_variables; -#ifdef PDHCG_COMPILE_DISTRIBUTED - if (state->grid_context != NULL && state->grid_context->global_num_variables > 0) - { - n = state->grid_context->global_num_variables; - } -#endif - (void)state; - return n; + int global_n = pdhcg_get_global_num_variables(state->grid_context); + return global_n > 0 ? global_n : state->num_variables; } int get_n_start(grid_context_t *ctx) { - int start = 0; -#ifdef PDHCG_COMPILE_DISTRIBUTED - if (ctx != NULL) - { - start = ctx->n_start; - } -#else - (void)ctx; -#endif - return start; + return pdhcg_get_variable_start(ctx); } static void initialize_sparse_component_obj(pdhg_solver_state_t *state, const processed_qp_problem_t *problem) @@ -458,6 +441,254 @@ void initialize_quadratic_term_information(pdhg_solver_state_t *state, const pdh } } +static cone_proj_method_t +pick_cone_proj_method(const cone_blocks_t *cones, int cone, const double *coordinate_rescaling) +{ + cone_type_t type = cones->type[cone]; + int v_dim = cones->v_dim[cone]; + if (type == CONE_EXPONENTIAL || type == CONE_POWER) + return PROJ_METHOD_THREAD; + if (v_dim < 32) + return PROJ_METHOD_THREAD; + if (type != CONE_STANDARD_SOC && type != CONE_ROTATED_SOC) + return PROJ_METHOD_WARP; + + int start = cones->start_idx[cone]; + if (cones->is_fixed) + { + for (int slot = 0; slot < v_dim + 2; ++slot) + if (cones->is_fixed[start + slot]) + return v_dim >= PDHCG_LARGE_CONE_MIN_VDIM ? PROJ_METHOD_GRID_WEIGHTED : PROJ_METHOD_BLOCK; + } + if (v_dim < PDHCG_LARGE_CONE_MIN_VDIM || !coordinate_rescaling) + return PROJ_METHOD_WARP; + + int endpoint0 = start + v_dim; + int endpoint1 = endpoint0 + 1; + + double d0 = coordinate_rescaling[endpoint0]; + double d1 = coordinate_rescaling[endpoint1]; + if (!(d0 > 0.0) || !(d1 > 0.0) || !isfinite(d0) || !isfinite(d1)) + return PROJ_METHOD_WARP; + + double d_vector = d1; + if (type == CONE_STANDARD_SOC) + { + if (d0 != d1) + return PROJ_METHOD_GRID_WEIGHTED; + } + else + { + double d_ref = coordinate_rescaling[start]; + bool scalar_uniform = d0 == d_ref && d1 == d_ref; + for (int i = 1; i < v_dim && scalar_uniform; ++i) + scalar_uniform = coordinate_rescaling[start + i] == d_ref; + if (scalar_uniform) + return PROJ_METHOD_GRID; + d_vector = sqrt(d0) * sqrt(d1); + } + + for (int i = 0; i < v_dim; ++i) + { + if (coordinate_rescaling[start + i] != d_vector) + return PROJ_METHOD_GRID_WEIGHTED; + } + return PROJ_METHOD_GRID; +} + +static void +initialize_cone_layout(cone_runtime_t *runtime, const cone_blocks_t *cones, const double *coordinate_rescaling) +{ + runtime->num_blocks = cones->num_cones; + if (runtime->num_blocks == 0) + return; + + int K = runtime->num_blocks; + + cone_proj_method_t *methods = (cone_proj_method_t *)safe_malloc(K * sizeof(cone_proj_method_t)); + for (int i = 0; i < K; ++i) + methods[i] = pick_cone_proj_method(cones, i, coordinate_rescaling); + + int bucket_count[NUM_CONE_TYPES][NUM_PROJ_METHODS] = {{0}}; + for (int i = 0; i < K; ++i) + bucket_count[cones->type[i]][methods[i]]++; + for (int method = 0; method < NUM_PROJ_METHODS; ++method) + runtime->has_power_cones |= bucket_count[CONE_POWER][method] > 0; + + cone_bucket_t buckets_tmp[NUM_CONE_TYPES * NUM_PROJ_METHODS]; + int num_buckets = 0; + int offset = 0; + int bucket_offset[NUM_CONE_TYPES][NUM_PROJ_METHODS]; + for (int t = 0; t < NUM_CONE_TYPES; ++t) + { + for (int m = 0; m < NUM_PROJ_METHODS; ++m) + { + bucket_offset[t][m] = offset; + if (bucket_count[t][m] > 0) + { + buckets_tmp[num_buckets].type = (cone_type_t)t; + buckets_tmp[num_buckets].method = (cone_proj_method_t)m; + buckets_tmp[num_buckets].offset = offset; + buckets_tmp[num_buckets].count = bucket_count[t][m]; + num_buckets++; + } + offset += bucket_count[t][m]; + } + } + + runtime->num_buckets = num_buckets; + runtime->buckets = (cone_bucket_t *)safe_malloc((size_t)num_buckets * sizeof(cone_bucket_t)); + memcpy(runtime->buckets, buckets_tmp, (size_t)num_buckets * sizeof(cone_bucket_t)); + + size_t cb = (size_t)K * sizeof(int); + int *start_perm = (int *)safe_malloc(cb); + int *vdim_perm = (int *)safe_malloc(cb); + double *alpha_perm = NULL; + if (cones->power_alpha) + alpha_perm = (double *)safe_malloc((size_t)K * sizeof(double)); + int write_pos[NUM_CONE_TYPES][NUM_PROJ_METHODS]; + memcpy(write_pos, bucket_offset, sizeof(write_pos)); + for (int i = 0; i < K; ++i) + { + int t = cones->type[i]; + int m = methods[i]; + int p = write_pos[t][m]++; + start_perm[p] = cones->start_idx[i]; + vdim_perm[p] = cones->v_dim[i]; + if (alpha_perm) + alpha_perm[p] = cones->power_alpha[i]; + } + + CUDA_CHECK(cudaMalloc(&runtime->start_idx, cb)); + CUDA_CHECK(cudaMemcpy(runtime->start_idx, start_perm, cb, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMalloc(&runtime->v_dim, cb)); + CUDA_CHECK(cudaMemcpy(runtime->v_dim, vdim_perm, cb, cudaMemcpyHostToDevice)); + if (alpha_perm) + { + size_t ab = (size_t)K * sizeof(double); + CUDA_CHECK(cudaMalloc(&runtime->power_alpha, ab)); + CUDA_CHECK(cudaMemcpy(runtime->power_alpha, alpha_perm, ab, cudaMemcpyHostToDevice)); + free(alpha_perm); + } + free(start_perm); + free(vdim_perm); + free(methods); + + size_t scalar_bytes = (size_t)K * sizeof(double); + size_t workspace_bytes = PDHCG_CONE_WORKSPACE_STRIDE * scalar_bytes; + CUDA_CHECK(cudaMalloc(&runtime->projection_warm_start, workspace_bytes)); + CUDA_CHECK(cudaMemset(runtime->projection_warm_start, 0, workspace_bytes)); + CUDA_CHECK(cudaMalloc(&runtime->residual_warm_start, workspace_bytes)); + CUDA_CHECK(cudaMemset(runtime->residual_warm_start, 0, workspace_bytes)); + CUDA_CHECK(cudaMalloc(&runtime->complementarity_residual, scalar_bytes)); + CUDA_CHECK(cudaMemset(runtime->complementarity_residual, 0, scalar_bytes)); + if (runtime->axis == CONE_AXIS_VARIABLE && runtime->has_power_cones) + CUDA_CHECK(cudaMalloc(&runtime->power_violation_workspace, 2 * scalar_bytes)); +} + +static void initialize_cone_runtime(pdhg_solver_state_t *state, + const qp_problem_t *working_problem, + const rescale_info_t *rescale_info) +{ + memset(&state->cones, 0, sizeof(state->cones)); + memset(&state->affine_cones, 0, sizeof(state->affine_cones)); + state->cones.axis = CONE_AXIS_VARIABLE; + state->affine_cones.axis = CONE_AXIS_CONSTRAINT; + + initialize_split_cones(state, rescale_info); + + bool has_global_cones = working_problem->cones.num_cones > 0 || state->cones.split != NULL || + pdhcg_get_global_num_cones(state->grid_context) > 0; + state->has_variable_cones = has_global_cones; + + if (working_problem->cones.is_fixed) + { + size_t fb = (size_t)state->num_variables * sizeof(char); + CUDA_CHECK(cudaMalloc(&state->cones.is_fixed, fb)); + CUDA_CHECK(cudaMemcpy(state->cones.is_fixed, working_problem->cones.is_fixed, fb, cudaMemcpyHostToDevice)); + } + + if (state->has_variable_cones) + { + quad_obj_type_t qt = rescale_info->processed_problem ? rescale_info->processed_problem->quad_type : PDHCG_NON_Q; + size_t vb = (size_t)state->num_variables * sizeof(double); + if (qt != PDHCG_NON_Q) + CUDA_CHECK(cudaMalloc(&state->cones.effective_objective_gradient, vb)); + if (qt == PDHCG_SPARSE_Q || qt == PDHCG_LOW_RANK_Q || qt == PDHCG_LOW_RANK_PLUS_SPARSE_Q) + CUDA_CHECK(cudaMalloc(&state->cones.bb_primal_snapshot, vb)); + } + + initialize_cone_layout(&state->cones, &working_problem->cones, rescale_info->var_rescale); + double global_has_power_cones = state->cones.has_power_cones ? 1.0 : 0.0; + pdhcg_all_reduce_scalar(state->grid_context, &global_has_power_cones, PDHCG_OP_MAX, PDHCG_SCOPE_ROW, false); + state->cones.has_power_cones = global_has_power_cones != 0.0; + + bool has_affine_cones = working_problem->affine_cones.num_cones > 0 || state->affine_cones.split != NULL || + pdhcg_get_global_num_affine_cones(state->grid_context) > 0; + + int constraint_rows = state->num_constraints; + if (has_affine_cones && constraint_rows > 0) + { + double *inverse_constraint_rescaling = (double *)safe_malloc((size_t)constraint_rows * sizeof(double)); + for (int i = 0; i < constraint_rows; ++i) + inverse_constraint_rescaling[i] = 1.0 / rescale_info->con_rescale[i]; + size_t constraint_bytes = (size_t)constraint_rows * sizeof(double); + CUDA_CHECK(cudaMalloc(&state->affine_cones.coordinate_rescaling, constraint_bytes)); + CUDA_CHECK(cudaMemcpy(state->affine_cones.coordinate_rescaling, + inverse_constraint_rescaling, + constraint_bytes, + cudaMemcpyHostToDevice)); + initialize_cone_layout(&state->affine_cones, &working_problem->affine_cones, inverse_constraint_rescaling); + free(inverse_constraint_rescaling); + } + + const double INV_SQRT2 = 0.7071067811865475; + for (int i = 0; i < working_problem->cones.num_cones; ++i) + { + const cone_blocks_t *cones = &working_problem->cones; + int aux0 = cones->start_idx[i] + cones->v_dim[i]; + if (cones->type[i] == CONE_STANDARD_SOC) + { + int w_idx = aux0; + int z_idx = aux0 + 1; + bool w_pinned = (cones->is_fixed && cones->is_fixed[w_idx]); + bool z_pinned = (cones->is_fixed && cones->is_fixed[z_idx]); + double w_val = -INV_SQRT2 * rescale_info->con_bound_rescale * rescale_info->var_rescale[w_idx]; + double z_val = INV_SQRT2 * rescale_info->con_bound_rescale * rescale_info->var_rescale[z_idx]; + for (int which = 0; which < 4; ++which) + { + double *dst = (which == 0 ? state->initial_primal_solution + : which == 1 ? state->current_primal_solution + : which == 2 ? state->pdhg_primal_solution + : state->reflected_primal_solution); + if (!w_pinned) + CUDA_CHECK(cudaMemcpy(dst + w_idx, &w_val, sizeof(double), cudaMemcpyHostToDevice)); + if (!z_pinned) + CUDA_CHECK(cudaMemcpy(dst + z_idx, &z_val, sizeof(double), cudaMemcpyHostToDevice)); + } + } + else if (cones->type[i] == CONE_EXPONENTIAL || cones->type[i] == CONE_POWER) + { + /* Rely on the ALLOC_ZERO default (0, 0, 0), which is in-cone for both. */ + } + else + { + int t_idx = aux0 + 1; + bool t_pinned = (cones->is_fixed && cones->is_fixed[t_idx]); + double t_val = rescale_info->con_bound_rescale * rescale_info->var_rescale[t_idx]; + for (int which = 0; which < 4; ++which) + { + double *dst = (which == 0 ? state->initial_primal_solution + : which == 1 ? state->current_primal_solution + : which == 2 ? state->pdhg_primal_solution + : state->reflected_primal_solution); + if (!t_pinned) + CUDA_CHECK(cudaMemcpy(dst + t_idx, &t_val, sizeof(double), cudaMemcpyHostToDevice)); + } + } + } +} + pdhg_solver_state_t *initialize_solver_state(const pdhg_parameters_t *params, const qp_problem_t *working_problem, const rescale_info_t *rescale_info, @@ -554,6 +785,7 @@ pdhg_solver_state_t *initialize_solver_state(const pdhg_parameters_t *params, ALLOC_AND_COPY(state->objective_vector, rescale_info->scaled_problem->objective_vector, var_bytes); ALLOC_AND_COPY(state->constraint_lower_bound, rescale_info->scaled_problem->constraint_lower_bound, con_bytes); ALLOC_AND_COPY(state->constraint_upper_bound, rescale_info->scaled_problem->constraint_upper_bound, con_bytes); + ALLOC_AND_COPY(state->affine_cone_offset, rescale_info->scaled_problem->affine_cone_offset, con_bytes); ALLOC_AND_COPY(state->constraint_rescaling, rescale_info->con_rescale, con_bytes); ALLOC_AND_COPY(state->variable_rescaling, rescale_info->var_rescale, var_bytes); @@ -687,6 +919,15 @@ pdhg_solver_state_t *initialize_solver_state(const pdhg_parameters_t *params, } } + for (int i = 0; i < working_problem->num_constraints; ++i) + { + double constant = working_problem->affine_cone_offset[i]; + if (params->optimality_norm == NORM_TYPE_L_INF) + max_val = fmax(max_val, fabs(constant)); + else + sum_of_squares += constant * constant; + } + if (params->optimality_norm == NORM_TYPE_L_INF) { state->constraint_bound_norm = max_val; @@ -734,25 +975,48 @@ pdhg_solver_state_t *initialize_solver_state(const pdhg_parameters_t *params, state->vec_dual_sol, state->vec_dual_prod); + state->num_original_variables = working_problem->num_original_variables; + initialize_cone_runtime(state, working_problem, rescale_info); + if (state->has_variable_cones) + { + project_cone_runtime(state, &state->cones, state->initial_primal_solution, state->cones.projection_warm_start); + CUDA_CHECK(cudaGetLastError()); + } + if (state->num_variables > 0) + { + project_primal_onto_bounds_kernel<<num_blocks_primal, THREADS_PER_BLOCK>>>( + state->initial_primal_solution, + state->variable_lower_bound, + state->variable_upper_bound, + state->num_variables); + CUDA_CHECK(cudaGetLastError()); + } + CUDA_CHECK(cudaMemcpy( + state->current_primal_solution, state->initial_primal_solution, var_bytes, cudaMemcpyDeviceToDevice)); + CUDA_CHECK( + cudaMemcpy(state->pdhg_primal_solution, state->initial_primal_solution, var_bytes, cudaMemcpyDeviceToDevice)); + CUDA_CHECK(cudaMemcpy( + state->reflected_primal_solution, state->initial_primal_solution, var_bytes, cudaMemcpyDeviceToDevice)); + initialize_quadratic_obj_term(state, rescale_info->processed_problem); initialize_quadratic_term_information(state, params); initialize_inner_solver(state, params); - CUDA_CHECK(cudaMalloc(&state->ones_primal_d, state->num_variables * sizeof(double))); - CUDA_CHECK(cudaMalloc(&state->ones_dual_d, state->num_constraints * sizeof(double))); + CUDA_CHECK(cudaMalloc(&state->ones_primal, state->num_variables * sizeof(double))); + CUDA_CHECK(cudaMalloc(&state->ones_dual, state->num_constraints * sizeof(double))); double *ones_primal_h = (double *)safe_malloc(state->num_variables * sizeof(double)); for (int i = 0; i < state->num_variables; ++i) ones_primal_h[i] = 1.0; CUDA_CHECK( - cudaMemcpy(state->ones_primal_d, ones_primal_h, state->num_variables * sizeof(double), cudaMemcpyHostToDevice)); + cudaMemcpy(state->ones_primal, ones_primal_h, state->num_variables * sizeof(double), cudaMemcpyHostToDevice)); free(ones_primal_h); double *ones_dual_h = (double *)safe_malloc(state->num_constraints * sizeof(double)); for (int i = 0; i < state->num_constraints; ++i) ones_dual_h[i] = 1.0; CUDA_CHECK( - cudaMemcpy(state->ones_dual_d, ones_dual_h, state->num_constraints * sizeof(double), cudaMemcpyHostToDevice)); + cudaMemcpy(state->ones_dual, ones_dual_h, state->num_constraints * sizeof(double), cudaMemcpyHostToDevice)); decide_problem_type(state); free(ones_dual_h); if (params->verbose >= 2) @@ -826,6 +1090,8 @@ void pdhg_solver_state_free(pdhg_solver_state_t *state) CUDA_CHECK(cudaFree(state->constraint_lower_bound)); if (state->constraint_upper_bound) CUDA_CHECK(cudaFree(state->constraint_upper_bound)); + if (state->affine_cone_offset) + CUDA_CHECK(cudaFree(state->affine_cone_offset)); if (state->constraint_lower_bound_finite_val) CUDA_CHECK(cudaFree(state->constraint_lower_bound_finite_val)); if (state->constraint_upper_bound_finite_val) @@ -870,10 +1136,10 @@ void pdhg_solver_state_free(pdhg_solver_state_t *state) CUDA_CHECK(cudaFree(state->delta_primal_solution)); if (state->delta_dual_solution) CUDA_CHECK(cudaFree(state->delta_dual_solution)); - if (state->ones_primal_d) - CUDA_CHECK(cudaFree(state->ones_primal_d)); - if (state->ones_dual_d) - CUDA_CHECK(cudaFree(state->ones_dual_d)); + if (state->ones_primal) + CUDA_CHECK(cudaFree(state->ones_primal)); + if (state->ones_dual) + CUDA_CHECK(cudaFree(state->ones_dual)); if (state->quadratic_objective_term) { @@ -960,6 +1226,37 @@ void pdhg_solver_state_free(pdhg_solver_state_t *state) free(state->inner_solver); } + cone_runtime_t *runtimes[] = {&state->cones, &state->affine_cones}; + for (int runtime_idx = 0; runtime_idx < 2; ++runtime_idx) + { + cone_runtime_t *runtime = runtimes[runtime_idx]; + if (runtime->start_idx) + CUDA_CHECK(cudaFree(runtime->start_idx)); + if (runtime->v_dim) + CUDA_CHECK(cudaFree(runtime->v_dim)); + if (runtime->power_alpha) + CUDA_CHECK(cudaFree(runtime->power_alpha)); + if (runtime->is_fixed) + CUDA_CHECK(cudaFree(runtime->is_fixed)); + if (runtime->buckets) + free(runtime->buckets); + if (runtime->projection_warm_start) + CUDA_CHECK(cudaFree(runtime->projection_warm_start)); + if (runtime->residual_warm_start) + CUDA_CHECK(cudaFree(runtime->residual_warm_start)); + if (runtime->complementarity_residual) + CUDA_CHECK(cudaFree(runtime->complementarity_residual)); + if (runtime->power_violation_workspace) + CUDA_CHECK(cudaFree(runtime->power_violation_workspace)); + if (runtime->coordinate_rescaling) + CUDA_CHECK(cudaFree(runtime->coordinate_rescaling)); + if (runtime->effective_objective_gradient) + CUDA_CHECK(cudaFree(runtime->effective_objective_gradient)); + if (runtime->bb_primal_snapshot) + CUDA_CHECK(cudaFree(runtime->bb_primal_snapshot)); + } + free_split_cones(state); + free(state); } diff --git a/src/utils.cu b/src/utils.cu index 133bef4..e9bab4d 100644 --- a/src/utils.cu +++ b/src/utils.cu @@ -93,6 +93,12 @@ qp_problem_t *create_problem_with_dummy_constraint(const qp_problem_t *prob) { new_prob->constraint_matrix = (CsrComponent *)malloc(sizeof(CsrComponent)); } + else + { + free(new_prob->constraint_matrix->row_ptr); + free(new_prob->constraint_matrix->col_ind); + free(new_prob->constraint_matrix->val); + } new_prob->constraint_matrix->row_ptr = (int *)malloc(2 * sizeof(int)); new_prob->constraint_matrix->row_ptr[0] = 0; @@ -115,6 +121,9 @@ qp_problem_t *create_problem_with_dummy_constraint(const qp_problem_t *prob) new_prob->constraint_upper_bound = (double *)malloc(1 * sizeof(double)); new_prob->constraint_upper_bound[0] = INFINITY; + free(new_prob->affine_cone_offset); + new_prob->affine_cone_offset = (double *)calloc(1, sizeof(double)); + if (new_prob->dual_start != NULL) { free(new_prob->dual_start); @@ -209,8 +218,13 @@ const char *quad_obj_type_to_string(quad_obj_type_t type) bool optimality_criteria_met(const pdhg_solver_state_t *state, double rel_opt_tol, double rel_feas_tol) { +#ifdef PDHCG_ABSOLUTE_ONLY_TERMINATION + return state->absolute_dual_residual < rel_feas_tol && state->absolute_primal_residual < rel_feas_tol && + state->objective_gap < rel_opt_tol; +#else return state->relative_dual_residual < rel_feas_tol && state->relative_primal_residual < rel_feas_tol && state->relative_objective_gap < rel_opt_tol; +#endif } bool primal_infeasibility_criteria_met(const pdhg_solver_state_t *state, double eps) @@ -246,15 +260,21 @@ void check_termination_criteria(pdhg_solver_state_t *solver_state, const termina solver_state->termination_reason = TERMINATION_REASON_OPTIMAL; return; } - if (primal_infeasibility_criteria_met(solver_state, criteria->eps_infeasible)) + /* The current ray projection handles box recession directions only. Direct + cones require cone/dual-cone membership checks before either certificate + is valid. */ + if (!solver_state->has_variable_cones) { - solver_state->termination_reason = TERMINATION_REASON_PRIMAL_INFEASIBLE; - return; - } - if (dual_infeasibility_criteria_met(solver_state, criteria->eps_infeasible)) - { - solver_state->termination_reason = TERMINATION_REASON_DUAL_INFEASIBLE; - return; + if (primal_infeasibility_criteria_met(solver_state, criteria->eps_infeasible)) + { + solver_state->termination_reason = TERMINATION_REASON_PRIMAL_INFEASIBLE; + return; + } + if (dual_infeasibility_criteria_met(solver_state, criteria->eps_infeasible)) + { + solver_state->termination_reason = TERMINATION_REASON_DUAL_INFEASIBLE; + return; + } } if (solver_state->total_count >= criteria->iteration_limit) { @@ -303,10 +323,12 @@ bool should_do_adaptive_restart(pdhg_solver_state_t *solver_state, void set_default_parameters(pdhg_parameters_t *params) { + params->curtis_reid_iterations = 0; params->l_inf_ruiz_iterations = 10; params->has_pock_chambolle_alpha = true; params->pock_chambolle_alpha = 1.0; params->bound_objective_rescaling = true; + params->use_cone_preserving_scaling = true; params->verbose = 1; params->termination_evaluation_frequency = 200; params->feasibility_polishing = false; @@ -318,7 +340,7 @@ void set_default_parameters(pdhg_parameters_t *params) params->termination_criteria.eps_optimal_relative = 1e-4; params->termination_criteria.eps_feasible_relative = 1e-4; - params->termination_criteria.eps_infeasible = 1e-10; + params->termination_criteria.eps_infeasible = 1e-12; params->termination_criteria.time_sec_limit = 3600.0; params->termination_criteria.iteration_limit = INT32_MAX; params->termination_criteria.eps_feas_polish_relative = 1e-6; @@ -342,6 +364,7 @@ void set_default_parameters(pdhg_parameters_t *params) params->permute_method = BLOCK_RANDOM_PERMUTATION; params->diag_jacobi_precond = true; + params->default_cone_type = CONE_ROTATED_SOC; } #define PRINT_DIFF_INT(name, current, default_val) \ @@ -393,7 +416,7 @@ void print_initial_info(const pdhg_parameters_t *params, const qp_problem_t *pro printf("%*s\n", padding, text); }; - print_centered("PDHCG-II"); + print_centered("PDHCG"); print_centered("A GPU-Accelerated First-Order Solver for Convex QPs"); print_centered("(c) Hongpei Li, 2026"); print_centered("Contact: ishongpeili@gmail.com"); @@ -405,6 +428,27 @@ void print_initial_info(const pdhg_parameters_t *params, const qp_problem_t *pro problem->num_constraints, problem->num_variables, problem->constraint_matrix_num_nonzeros); + if (problem->cones.num_cones > 0) + { + int fixed_slots = 0; + if (problem->cones.is_fixed) + { + for (int i = 0; i < problem->num_variables; ++i) + fixed_slots += problem->cones.is_fixed[i] != 0; + } + printf(" + %d cone block(s), %d fixed slot(s)\n", problem->cones.num_cones, fixed_slots); + } + if (problem->num_quadratic_constraints > 0) + { + long total_q_nnz = 0; + for (int i = 0; i < problem->num_quadratic_constraints; ++i) + { + total_q_nnz += problem->quadratic_constraint_matrix_num_nonzeros[i]; + } + printf(" + %d quadratic constraint(s), %ld Q-nnz total\n", + problem->num_quadratic_constraints, + total_q_nnz); + } printf("settings:\n"); printf(" iter_limit : %d\n", params->termination_criteria.iteration_limit); @@ -418,11 +462,14 @@ void print_initial_info(const pdhg_parameters_t *params, const qp_problem_t *pro printf(" optimality_norm : %s\n", params->optimality_norm == NORM_TYPE_L_INF ? "L_inf" : "L2"); } + PRINT_DIFF_INT("curtis_reid_iter", params->curtis_reid_iterations, default_params.curtis_reid_iterations); PRINT_DIFF_INT("l_inf_ruiz_iter", params->l_inf_ruiz_iterations, default_params.l_inf_ruiz_iterations); PRINT_DIFF_DBL("pock_chambolle_alpha", params->pock_chambolle_alpha, default_params.pock_chambolle_alpha); PRINT_DIFF_BOOL( "has_pock_chambolle_alpha", params->has_pock_chambolle_alpha, default_params.has_pock_chambolle_alpha); PRINT_DIFF_BOOL("bound_obj_rescaling", params->bound_objective_rescaling, default_params.bound_objective_rescaling); + PRINT_DIFF_BOOL( + "use_cone_preserving_scaling", params->use_cone_preserving_scaling, default_params.use_cone_preserving_scaling); PRINT_DIFF_INT("sv_max_iter", params->sv_max_iter, default_params.sv_max_iter); PRINT_DIFF_DBL("sv_tol", params->sv_tol, default_params.sv_tol); PRINT_DIFF_INT( diff --git a/test/export_qcqp_socp.c b/test/export_qcqp_socp.c new file mode 100644 index 0000000..8dd5e8f --- /dev/null +++ b/test/export_qcqp_socp.c @@ -0,0 +1,337 @@ +#include "mps_parser.h" +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include +#include + +static void *xcalloc(size_t count, size_t size) +{ + void *p = calloc(count ? count : 1, size ? size : 1); + if (!p) + { + fprintf(stderr, "out of memory\n"); + exit(1); + } + return p; +} + +static void *xmalloc(size_t size) +{ + void *p = malloc(size ? size : 1); + if (!p) + { + fprintf(stderr, "out of memory\n"); + exit(1); + } + return p; +} + +static int write_all(FILE *f, const void *ptr, size_t size, size_t count) +{ + if (count == 0) + return 0; + return fwrite(ptr, size, count, f) == count ? 0 : -1; +} + +static int write_csr(FILE *f, const CsrComponent *csr, int rows, int nnz) +{ + if (write_all(f, csr && csr->row_ptr ? csr->row_ptr : NULL, sizeof(int32_t), (size_t)rows + 1) != 0) + return -1; + if (write_all(f, csr && csr->col_ind ? csr->col_ind : NULL, sizeof(int32_t), (size_t)nnz) != 0) + return -1; + if (write_all(f, csr && csr->val ? csr->val : NULL, sizeof(double), (size_t)nnz) != 0) + return -1; + return 0; +} + +static int32_t one_if_nonnull(const void *p) +{ + return p ? 1 : 0; +} + +static CsrComponent *copy_csr_with_rows(const CsrComponent *src, int old_rows, int new_rows, int nnz, double scale) +{ + CsrComponent *dst = (CsrComponent *)xcalloc(1, sizeof(CsrComponent)); + dst->row_ptr = (int *)xcalloc((size_t)new_rows + 1, sizeof(int)); + if (src && src->row_ptr) + { + memcpy(dst->row_ptr, src->row_ptr, ((size_t)old_rows + 1) * sizeof(int)); + int last = src->row_ptr[old_rows]; + for (int r = old_rows + 1; r <= new_rows; ++r) + dst->row_ptr[r] = last; + } + if (nnz > 0) + { + dst->col_ind = (int *)xmalloc((size_t)nnz * sizeof(int)); + dst->val = (double *)xmalloc((size_t)nnz * sizeof(double)); + memcpy(dst->col_ind, src->col_ind, (size_t)nnz * sizeof(int)); + for (int i = 0; i < nnz; ++i) + dst->val[i] = scale * src->val[i]; + } + return dst; +} + +static qp_problem_t *epigraph_objective_q_to_qc(const qp_problem_t *orig) +{ + if (!orig) + return NULL; + if (orig->objective_sparse_matrix_num_nonzeros <= 0) + return NULL; + if (orig->num_rank_lowrank_obj > 0 || orig->objective_lowrank_matrix_num_nonzeros > 0 || + orig->objective_lowrank_middle_matrix_num_nonzeros > 0) + { + fprintf(stderr, "objective epigraph for low-rank Q is not implemented\n"); + return NULL; + } + + const int n_old = orig->num_variables; + const int m_old = orig->num_constraints; + const int n_new = n_old + 1; + const int m_new = m_old + 1; + const int eta_col = n_old; + + int lin_nnz = 0; + for (int j = 0; j < n_old; ++j) + { + if (orig->objective_vector[j] != 0.0) + lin_nnz++; + } + + qp_problem_t *out = (qp_problem_t *)xcalloc(1, sizeof(qp_problem_t)); + out->num_variables = n_new; + out->num_constraints = m_new; + out->num_rank_lowrank_obj = 0; + out->objective_sparse_matrix_num_nonzeros = 0; + out->objective_lowrank_matrix_num_nonzeros = 0; + out->objective_lowrank_middle_matrix_num_nonzeros = 0; + out->objective_constant = 0.0; + out->num_original_variables = orig->num_original_variables > 0 ? orig->num_original_variables : n_old; + + out->objective_vector = (double *)xcalloc((size_t)n_new, sizeof(double)); + out->objective_vector[eta_col] = 1.0; + out->variable_lower_bound = (double *)xmalloc((size_t)n_new * sizeof(double)); + out->variable_upper_bound = (double *)xmalloc((size_t)n_new * sizeof(double)); + memcpy(out->variable_lower_bound, orig->variable_lower_bound, (size_t)n_old * sizeof(double)); + memcpy(out->variable_upper_bound, orig->variable_upper_bound, (size_t)n_old * sizeof(double)); + out->variable_lower_bound[eta_col] = -INFINITY; + out->variable_upper_bound[eta_col] = INFINITY; + + out->constraint_lower_bound = (double *)xmalloc((size_t)m_new * sizeof(double)); + out->constraint_upper_bound = (double *)xmalloc((size_t)m_new * sizeof(double)); + memcpy(out->constraint_lower_bound, orig->constraint_lower_bound, (size_t)m_old * sizeof(double)); + memcpy(out->constraint_upper_bound, orig->constraint_upper_bound, (size_t)m_old * sizeof(double)); + out->constraint_lower_bound[m_old] = -INFINITY; + out->constraint_upper_bound[m_old] = -orig->objective_constant; + + int a_nnz_old = orig->constraint_matrix_num_nonzeros; + out->constraint_matrix_num_nonzeros = a_nnz_old + lin_nnz + 1; + out->constraint_matrix = (CsrComponent *)xcalloc(1, sizeof(CsrComponent)); + out->constraint_matrix->row_ptr = (int *)xcalloc((size_t)m_new + 1, sizeof(int)); + memcpy(out->constraint_matrix->row_ptr, orig->constraint_matrix->row_ptr, ((size_t)m_old + 1) * sizeof(int)); + out->constraint_matrix->row_ptr[m_new] = out->constraint_matrix_num_nonzeros; + out->constraint_matrix->col_ind = (int *)xmalloc((size_t)out->constraint_matrix_num_nonzeros * sizeof(int)); + out->constraint_matrix->val = (double *)xmalloc((size_t)out->constraint_matrix_num_nonzeros * sizeof(double)); + if (a_nnz_old > 0) + { + memcpy(out->constraint_matrix->col_ind, orig->constraint_matrix->col_ind, (size_t)a_nnz_old * sizeof(int)); + memcpy(out->constraint_matrix->val, orig->constraint_matrix->val, (size_t)a_nnz_old * sizeof(double)); + } + int dst = a_nnz_old; + for (int j = 0; j < n_old; ++j) + { + double cj = orig->objective_vector[j]; + if (cj == 0.0) + continue; + out->constraint_matrix->col_ind[dst] = j; + out->constraint_matrix->val[dst] = cj; + dst++; + } + out->constraint_matrix->col_ind[dst] = eta_col; + out->constraint_matrix->val[dst] = -1.0; + + out->objective_sparse_matrix = (CsrComponent *)xcalloc(1, sizeof(CsrComponent)); + out->objective_sparse_matrix->row_ptr = (int *)xcalloc((size_t)n_new + 1, sizeof(int)); + out->objective_lowrank_matrix = (CsrComponent *)xcalloc(1, sizeof(CsrComponent)); + out->objective_lowrank_matrix->row_ptr = (int *)xcalloc(1, sizeof(int)); + out->objective_lowrank_middle_matrix = NULL; + + const int k_old = orig->num_quadratic_constraints; + const int k_new = k_old + 1; + out->num_quadratic_constraints = k_new; + out->quadratic_constraint_row_indices = (int *)xcalloc((size_t)k_new, sizeof(int)); + out->quadratic_constraint_matrices = (CsrComponent **)xcalloc((size_t)k_new, sizeof(CsrComponent *)); + out->quadratic_constraint_matrix_num_nonzeros = (int *)xcalloc((size_t)k_new, sizeof(int)); + for (int k = 0; k < k_old; ++k) + { + out->quadratic_constraint_row_indices[k] = orig->quadratic_constraint_row_indices[k]; + out->quadratic_constraint_matrix_num_nonzeros[k] = orig->quadratic_constraint_matrix_num_nonzeros[k]; + out->quadratic_constraint_matrices[k] = copy_csr_with_rows(orig->quadratic_constraint_matrices[k], + n_old, + n_new, + orig->quadratic_constraint_matrix_num_nonzeros[k], + 1.0); + } + out->quadratic_constraint_row_indices[k_old] = m_old; + out->quadratic_constraint_matrix_num_nonzeros[k_old] = orig->objective_sparse_matrix_num_nonzeros; + out->quadratic_constraint_matrices[k_old] = copy_csr_with_rows( + orig->objective_sparse_matrix, n_old, n_new, orig->objective_sparse_matrix_num_nonzeros, 0.5); + + out->cones.num_cones = 0; + out->cones.start_idx = NULL; + out->cones.v_dim = NULL; + out->cones.type = NULL; + out->cones.is_fixed = NULL; + out->primal_start = NULL; + out->dual_start = NULL; + return out; +} + +int main(int argc, char **argv) +{ + if (argc < 3) + { + fprintf(stderr, "usage: %s INPUT.mps[.gz] OUTPUT.bin [rotated|standard] [epigraph]\n", argv[0]); + return 2; + } + const char *input = argv[1]; + const char *output = argv[2]; + cone_type_t form = CONE_ROTATED_SOC; + int epigraph_objective = 0; + for (int i = 3; i < argc; ++i) + { + if (strcmp(argv[i], "standard") == 0) + form = CONE_STANDARD_SOC; + else if (strcmp(argv[i], "rotated") == 0) + form = CONE_ROTATED_SOC; + else if (strcmp(argv[i], "epigraph") == 0 || strcmp(argv[i], "--epigraph-objective") == 0 || + strcmp(argv[i], "epigraph-objective") == 0) + epigraph_objective = 1; + else + { + fprintf(stderr, "unknown option '%s'\n", argv[i]); + return 2; + } + } + + qp_problem_t *orig = read_mps_file(input); + if (!orig) + { + fprintf(stderr, "read_mps_file failed: %s\n", input); + return 1; + } + qp_problem_t *base = orig; + qp_problem_t *epig = NULL; + int epigraph_done = 0; + if (epigraph_objective && orig->objective_sparse_matrix_num_nonzeros > 0) + { + epig = epigraph_objective_q_to_qc(orig); + if (!epig) + { + qp_problem_free(orig); + fprintf(stderr, "objective epigraph failed: %s\n", input); + return 1; + } + base = epig; + epigraph_done = 1; + } + + qp_problem_t *prob = base; + int transformed = 0; + if (base->num_quadratic_constraints > 0) + { + prob = qcqp_to_socp_qp(base, form); + if (!prob) + { + if (epig) + qp_problem_free(epig); + qp_problem_free(orig); + fprintf(stderr, "qcqp_to_socp_qp failed: %s\n", input); + return 1; + } + transformed = 1; + } + + FILE *f = fopen(output, "wb"); + if (!f) + { + perror(output); + if (transformed) + qp_problem_free(prob); + if (epig) + qp_problem_free(epig); + qp_problem_free(orig); + return 1; + } + + const char magic[8] = {'P', 'D', 'H', 'Q', 'C', 'Q', '1', '\0'}; + int32_t header[18]; + memset(header, 0, sizeof(header)); + header[0] = 1; + header[1] = prob->num_variables; + header[2] = prob->num_constraints; + header[3] = prob->constraint_matrix_num_nonzeros; + header[4] = prob->objective_sparse_matrix_num_nonzeros; + header[5] = prob->cones.num_cones; + header[6] = prob->num_original_variables; + header[7] = transformed; + header[8] = orig->num_variables; + header[9] = orig->num_constraints; + header[10] = orig->constraint_matrix_num_nonzeros; + header[11] = orig->objective_sparse_matrix_num_nonzeros; + header[12] = orig->num_quadratic_constraints; + header[13] = one_if_nonnull(prob->cones.is_fixed); + header[14] = one_if_nonnull(prob->primal_start); + header[15] = (int32_t)form; + header[16] = prob->num_rank_lowrank_obj; + header[17] = prob->objective_lowrank_matrix_num_nonzeros + prob->objective_lowrank_middle_matrix_num_nonzeros; + + int rc = 0; + rc |= write_all(f, magic, sizeof(char), sizeof(magic)); + rc |= write_all(f, header, sizeof(int32_t), 18); + rc |= write_all(f, &prob->objective_constant, sizeof(double), 1); + rc |= write_all(f, prob->objective_vector, sizeof(double), (size_t)prob->num_variables); + rc |= write_all(f, prob->variable_lower_bound, sizeof(double), (size_t)prob->num_variables); + rc |= write_all(f, prob->variable_upper_bound, sizeof(double), (size_t)prob->num_variables); + rc |= write_all(f, prob->constraint_lower_bound, sizeof(double), (size_t)prob->num_constraints); + rc |= write_all(f, prob->constraint_upper_bound, sizeof(double), (size_t)prob->num_constraints); + rc |= write_csr(f, prob->constraint_matrix, prob->num_constraints, prob->constraint_matrix_num_nonzeros); + rc |= write_csr(f, prob->objective_sparse_matrix, prob->num_variables, prob->objective_sparse_matrix_num_nonzeros); + rc |= write_all(f, prob->cones.start_idx, sizeof(int32_t), (size_t)prob->cones.num_cones); + rc |= write_all(f, prob->cones.v_dim, sizeof(int32_t), (size_t)prob->cones.num_cones); + rc |= write_all(f, prob->cones.type, sizeof(int32_t), (size_t)prob->cones.num_cones); + if (prob->cones.is_fixed) + rc |= write_all(f, prob->cones.is_fixed, sizeof(char), (size_t)prob->num_variables); + if (prob->primal_start) + rc |= write_all(f, prob->primal_start, sizeof(double), (size_t)prob->num_variables); + + if (fclose(f) != 0) + rc = -1; + + fprintf(stderr, + "{\"input\":\"%s\",\"output\":\"%s\",\"n\":%d,\"m\":%d,\"A_nnz\":%d," + "\"Q_nnz\":%d,\"cones\":%d,\"orig_n\":%d,\"orig_m\":%d," + "\"orig_qc\":%d,\"orig_Q_nnz\":%d,\"epigraph_objective\":%d,\"lowrank_nnz\":%d}\n", + input, + output, + prob->num_variables, + prob->num_constraints, + prob->constraint_matrix_num_nonzeros, + prob->objective_sparse_matrix_num_nonzeros, + prob->cones.num_cones, + orig->num_variables, + orig->num_constraints, + orig->num_quadratic_constraints, + orig->objective_sparse_matrix_num_nonzeros, + epigraph_done, + header[17]); + + if (transformed) + qp_problem_free(prob); + if (epig) + qp_problem_free(epig); + qp_problem_free(orig); + return rc == 0 ? 0 : 1; +} diff --git a/test/inspect_qcqp_exportability.c b/test/inspect_qcqp_exportability.c new file mode 100644 index 0000000..3bfe6d2 --- /dev/null +++ b/test/inspect_qcqp_exportability.c @@ -0,0 +1,173 @@ +#include "mps_parser.h" +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +static int extract_diag_signed(const CsrComponent *Q, int n, int nnz_max, int *sign_out) +{ + int count = 0; + int sign = 0; + if (!Q || !Q->row_ptr) + return -1; + for (int row = 0; row < n; ++row) + { + int start = Q->row_ptr[row]; + int end = Q->row_ptr[row + 1]; + for (int k = start; k < end; ++k) + { + int col = Q->col_ind[k]; + double val = Q->val[k]; + if (col != row) + return -1; + if (val == 0.0) + continue; + int s = (val > 0.0) ? +1 : -1; + if (sign == 0) + sign = s; + else if (sign != s) + return -1; + if (count >= nnz_max) + return -1; + count++; + } + } + *sign_out = sign; + return count; +} + +static long count_nonzero_obj(const qp_problem_t *p) +{ + long nnz = 0; + for (int j = 0; j < p->num_variables; ++j) + if (p->objective_vector[j] != 0.0) + nnz++; + return nnz; +} + +static int inspect_qc(const qp_problem_t *p, + int idx, + int base_n, + int base_m, + int base_a_nnz, + long *total_v, + long *num_pin, + int *first_bad, + const char **bad_reason) +{ + const int orig_k = p->num_quadratic_constraints; + const int has_obj_q = p->objective_sparse_matrix_num_nonzeros > 0; + const int is_obj_q = (has_obj_q && idx == orig_k); + CsrComponent *Q = is_obj_q ? p->objective_sparse_matrix : p->quadratic_constraint_matrices[idx]; + int q_nnz = is_obj_q ? p->objective_sparse_matrix_num_nonzeros : p->quadratic_constraint_matrix_num_nonzeros[idx]; + int sign = 0; + int diag_nnz = extract_diag_signed(Q, p->num_variables, q_nnz, &sign); + if (diag_nnz < 0) + { + *first_bad = idx; + *bad_reason = "non_diagonal_or_mixed_sign"; + return -1; + } + + int row = is_obj_q ? p->num_constraints : p->quadratic_constraint_row_indices[idx]; + double lhs = is_obj_q ? -INFINITY : p->constraint_lower_bound[row]; + double rhs = is_obj_q ? -p->objective_constant : p->constraint_upper_bound[row]; + if (sign >= 0) + { + if (isfinite(lhs) || !isfinite(rhs)) + { + *first_bad = idx; + *bad_reason = "psd_requires_le_constraint"; + return -1; + } + } + else + { + if (!isfinite(lhs) || isfinite(rhs)) + { + *first_bad = idx; + *bad_reason = "nsd_requires_ge_constraint"; + return -1; + } + } + + int row_nnz = 0; + if (is_obj_q) + row_nnz = (int)count_nonzero_obj(p) + 1; + else + row_nnz = p->constraint_matrix->row_ptr[row + 1] - p->constraint_matrix->row_ptr[row]; + if (row_nnz == 0) + (*num_pin)++; + (void)base_n; + (void)base_m; + (void)base_a_nnz; + *total_v += diag_nnz; + return 0; +} + +int main(int argc, char **argv) +{ + if (argc != 2) + { + fprintf(stderr, "usage: %s INPUT.mps[.gz]\n", argv[0]); + return 2; + } + qp_problem_t *p = read_mps_file(argv[1]); + if (!p) + { + fprintf(stderr, "read_mps_file failed: %s\n", argv[1]); + return 1; + } + + int has_obj_q = p->objective_sparse_matrix_num_nonzeros > 0; + int base_n = p->num_variables + (has_obj_q ? 1 : 0); + int base_m = p->num_constraints + (has_obj_q ? 1 : 0); + long obj_lin_nnz = has_obj_q ? count_nonzero_obj(p) : 0; + long base_a_nnz = p->constraint_matrix_num_nonzeros + (has_obj_q ? obj_lin_nnz + 1 : 0); + int K = p->num_quadratic_constraints + (has_obj_q ? 1 : 0); + + long total_v = 0; + long num_pin = 0; + int first_bad = -1; + const char *bad_reason = ""; + for (int i = 0; i < K; ++i) + { + if (inspect_qc(p, i, base_n, base_m, (int)base_a_nnz, &total_v, &num_pin, &first_bad, &bad_reason) != 0) + break; + } + + long n_ext = (long)base_n + total_v + 2L * K; + long m_ext = (long)base_m + total_v + K; + long a_ext = base_a_nnz + 2L * (K - num_pin) + 2L * total_v; + long bytes = 8 + 72 + 8 + 8 * n_ext * 3 + 8 * m_ext * 2 + 4 * (m_ext + 1) + 12 * a_ext + 4 * (n_ext + 1) + 12 * 0 + + 12 * K + n_ext + 8 * n_ext; + + printf("{\"input\":\"%s\",\"n\":%d,\"m\":%d,\"A_nnz\":%d,\"obj_Q_nnz\":%d," + "\"qc\":%d,\"base_n\":%d,\"base_m\":%d,\"base_A_nnz\":%ld," + "\"supported\":%s,\"first_bad_qc\":%d,\"bad_reason\":\"%s\"," + "\"total_v\":%ld,\"num_pin\":%ld,\"n_ext_est\":%ld,\"m_ext_est\":%ld," + "\"A_nnz_ext_est\":%ld,\"bin_bytes_est\":%ld}\n", + argv[1], + p->num_variables, + p->num_constraints, + p->constraint_matrix_num_nonzeros, + p->objective_sparse_matrix_num_nonzeros, + K, + base_n, + base_m, + base_a_nnz, + first_bad < 0 ? "true" : "false", + first_bad, + bad_reason, + total_v, + num_pin, + n_ext, + m_ext, + a_ext, + bytes); + + qp_problem_free(p); + return first_bad < 0 ? 0 : 3; +} diff --git a/test/qcqp_probe.c b/test/qcqp_probe.c new file mode 100644 index 0000000..4a2c5cf --- /dev/null +++ b/test/qcqp_probe.c @@ -0,0 +1,171 @@ +#include "mps_parser.h" +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +static int diag_signed_count(const CsrComponent *Q, int n, int *sign_out) +{ + int count = 0; + int sign = 0; + if (!Q || !Q->row_ptr) + return -1; + for (int row = 0; row < n; ++row) + { + for (int k = Q->row_ptr[row]; k < Q->row_ptr[row + 1]; ++k) + { + int col = Q->col_ind[k]; + double val = Q->val[k]; + if (col != row) + return -1; + if (val == 0.0) + continue; + int s = (val > 0.0) ? 1 : -1; + if (sign == 0) + sign = s; + else if (sign != s) + return -1; + count++; + } + } + *sign_out = sign; + return count; +} + +static int row_nnz(const CsrComponent *A, int row) +{ + if (!A || !A->row_ptr) + return 0; + return A->row_ptr[row + 1] - A->row_ptr[row]; +} + +static int objective_linear_nnz(const qp_problem_t *p) +{ + int nnz = 0; + for (int j = 0; j < p->num_variables; ++j) + if (p->objective_vector && p->objective_vector[j] != 0.0) + nnz++; + return nnz; +} + +static void inspect_qc(const qp_problem_t *p, + const CsrComponent *Q, + int q_nnz, + int row, + int is_epigraph_obj, + long long *total_v, + int *num_pin, + int *unsupported_diag, + int *unsupported_bound) +{ + (void)q_nnz; + int sign = 0; + int k = diag_signed_count(Q, p->num_variables + (is_epigraph_obj ? 1 : 0), &sign); + if (k < 0) + { + (*unsupported_diag)++; + return; + } + + double lhs = is_epigraph_obj ? -INFINITY : p->constraint_lower_bound[row]; + double rhs = is_epigraph_obj ? -p->objective_constant : p->constraint_upper_bound[row]; + if (sign >= 0) + { + if (isfinite(lhs) || !isfinite(rhs)) + (*unsupported_bound)++; + } + else + { + if (!isfinite(lhs) || isfinite(rhs)) + (*unsupported_bound)++; + } + + *total_v += k; + if (!is_epigraph_obj && row_nnz(p->constraint_matrix, row) == 0) + (*num_pin)++; +} + +int main(int argc, char **argv) +{ + if (argc < 2) + { + fprintf(stderr, "usage: %s INPUT.mps[.gz] [epigraph]\n", argv[0]); + return 2; + } + int epigraph = argc >= 3; + qp_problem_t *p = read_mps_file(argv[1]); + if (!p) + { + fprintf(stderr, "read_mps_file failed\n"); + return 1; + } + + long long total_v = 0; + int num_pin = 0; + int unsupported_diag = 0; + int unsupported_bound = 0; + for (int i = 0; i < p->num_quadratic_constraints; ++i) + { + inspect_qc(p, + p->quadratic_constraint_matrices[i], + p->quadratic_constraint_matrix_num_nonzeros[i], + p->quadratic_constraint_row_indices[i], + 0, + &total_v, + &num_pin, + &unsupported_diag, + &unsupported_bound); + } + + int add_obj_q = epigraph && p->objective_sparse_matrix_num_nonzeros > 0; + if (add_obj_q) + { + int sign = 0; + int k = diag_signed_count(p->objective_sparse_matrix, p->num_variables, &sign); + if (k < 0) + unsupported_diag++; + else + { + if (sign < 0) + unsupported_bound++; + total_v += k; + } + } + + long long n_base = (long long)p->num_variables + (add_obj_q ? 1 : 0); + long long m_base = (long long)p->num_constraints + (add_obj_q ? 1 : 0); + long long a_base = p->constraint_matrix_num_nonzeros; + if (add_obj_q) + a_base += objective_linear_nnz(p) + 1; + int K = p->num_quadratic_constraints + add_obj_q; + long long n_ext = n_base + total_v + 2LL * K; + long long m_ext = m_base + total_v + K; + long long nnz_ext = a_base + 2LL * total_v + 2LL * (K - num_pin); + + printf("{\"input\":\"%s\",\"n\":%d,\"m\":%d,\"A_nnz\":%d," + "\"obj_Q_nnz\":%d,\"lowrank_nnz\":%d,\"qc\":%d," + "\"epigraph_obj\":%d,\"K_eff\":%d,\"total_v\":%lld," + "\"num_pin\":%d,\"unsupported_diag\":%d,\"unsupported_bound\":%d," + "\"n_ext_est\":%lld,\"m_ext_est\":%lld,\"A_nnz_ext_est\":%lld}\n", + argv[1], + p->num_variables, + p->num_constraints, + p->constraint_matrix_num_nonzeros, + p->objective_sparse_matrix_num_nonzeros, + p->objective_lowrank_matrix_num_nonzeros + p->objective_lowrank_middle_matrix_num_nonzeros, + p->num_quadratic_constraints, + add_obj_q, + K, + total_v, + num_pin, + unsupported_diag, + unsupported_bound, + n_ext, + m_ext, + nnz_ext); + + qp_problem_free(p); + return 0; +} diff --git a/test/solve_exported_qcqp_pdhcg.c b/test/solve_exported_qcqp_pdhcg.c new file mode 100644 index 0000000..2803d7f --- /dev/null +++ b/test/solve_exported_qcqp_pdhcg.c @@ -0,0 +1,337 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include +#include +#include + +static void *checked_calloc(size_t count, size_t size) +{ + void *ptr = calloc(count ? count : 1, size ? size : 1); + if (!ptr) + { + fprintf(stderr, "out of memory\n"); + exit(1); + } + return ptr; +} + +static int read_exact(FILE *stream, void *ptr, size_t size, size_t count) +{ + return count == 0 || fread(ptr, size, count, stream) == count; +} + +static const char *termination_name(termination_reason_t reason) +{ + switch (reason) + { + case TERMINATION_REASON_OPTIMAL: + return "OPTIMAL"; + case TERMINATION_REASON_PRIMAL_INFEASIBLE: + return "PRIMAL_INFEASIBLE"; + case TERMINATION_REASON_DUAL_INFEASIBLE: + return "DUAL_INFEASIBLE"; + case TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED: + return "INFEASIBLE_OR_UNBOUNDED"; + case TERMINATION_REASON_TIME_LIMIT: + return "TIME_LIMIT"; + case TERMINATION_REASON_ITERATION_LIMIT: + return "ITERATION_LIMIT"; + case TERMINATION_REASON_USER_INTERRUPT: + return "USER_INTERRUPT"; + case TERMINATION_REASON_FEAS_POLISH_SUCCESS: + return "FEAS_POLISH_SUCCESS"; + default: + return "UNSPECIFIED"; + } +} + +static double monotonic_seconds(void) +{ + struct timespec value; + clock_gettime(CLOCK_MONOTONIC, &value); + return (double)value.tv_sec + 1.0e-9 * (double)value.tv_nsec; +} + +static void print_host_sanity(const qp_problem_t *problem) +{ + double max_row_violation = 0.0; + double max_bound_violation = 0.0; + double linear_objective = problem->objective_constant; + const double *x = problem->primal_start; + + for (int row = 0; row < problem->num_constraints; ++row) + { + double activity = 0.0; + if (x) + { + for (int p = problem->constraint_matrix->row_ptr[row]; p < problem->constraint_matrix->row_ptr[row + 1]; + ++p) + { + activity += problem->constraint_matrix->val[p] * x[problem->constraint_matrix->col_ind[p]]; + } + } + double projected = + fmax(problem->constraint_lower_bound[row], fmin(activity, problem->constraint_upper_bound[row])); + max_row_violation = fmax(max_row_violation, fabs(activity - projected)); + } + for (int col = 0; col < problem->num_variables; ++col) + { + double value = x ? x[col] : 0.0; + double projected = fmax(problem->variable_lower_bound[col], fmin(value, problem->variable_upper_bound[col])); + max_bound_violation = fmax(max_bound_violation, fabs(value - projected)); + linear_objective += problem->objective_vector[col] * value; + } + fprintf(stderr, + "host sanity: row_violation=%.17g bound_violation=%.17g " + "linear_objective=%.17g first_row=[%.17g,%.17g] last_row=[%.17g,%.17g]\n", + max_row_violation, + max_bound_violation, + linear_objective, + problem->constraint_lower_bound[0], + problem->constraint_upper_bound[0], + problem->constraint_lower_bound[problem->num_constraints - 1], + problem->constraint_upper_bound[problem->num_constraints - 1]); +} + +int main(int argc, char **argv) +{ + if (argc < 4 || argc > 5) + { + fprintf(stderr, "usage: %s MODEL.bin EPS TIME_LIMIT [VERBOSE]\n", argv[0]); + return 2; + } + + const char *path = argv[1]; + double eps = strtod(argv[2], NULL); + double time_limit = strtod(argv[3], NULL); + int verbose = argc == 5 ? atoi(argv[4]) : 1; + + FILE *stream = fopen(path, "rb"); + if (!stream) + { + perror(path); + return 1; + } + + char magic[8]; + int32_t header[18]; + double objective_constant; + if (!read_exact(stream, magic, 1, sizeof(magic)) || memcmp(magic, "PDHQCQ1", 7) != 0 || + !read_exact(stream, header, sizeof(int32_t), 18) || !read_exact(stream, &objective_constant, sizeof(double), 1)) + { + fprintf(stderr, "invalid or truncated model header: %s\n", path); + fclose(stream); + return 1; + } + + int n = header[1]; + int m = header[2]; + int a_nnz = header[3]; + int q_nnz = header[4]; + int num_cones = header[5]; + int has_fixed = header[13]; + int has_primal = header[14]; + if (n <= 0 || m < 0 || a_nnz < 0 || q_nnz < 0 || num_cones < 0) + { + fprintf(stderr, "unsupported model dimensions\n"); + fclose(stream); + return 1; + } + + double *c = checked_calloc((size_t)n, sizeof(double)); + double *lbx = checked_calloc((size_t)n, sizeof(double)); + double *ubx = checked_calloc((size_t)n, sizeof(double)); + double *lbc = checked_calloc((size_t)m, sizeof(double)); + double *ubc = checked_calloc((size_t)m, sizeof(double)); + int *a_row = checked_calloc((size_t)m + 1, sizeof(int)); + int *a_col = checked_calloc((size_t)a_nnz, sizeof(int)); + double *a_val = checked_calloc((size_t)a_nnz, sizeof(double)); + int *q_row = checked_calloc((size_t)n + 1, sizeof(int)); + int *q_col = checked_calloc((size_t)q_nnz, sizeof(int)); + double *q_val = checked_calloc((size_t)q_nnz, sizeof(double)); + int *cone_start = checked_calloc((size_t)num_cones, sizeof(int)); + int *cone_vdim = checked_calloc((size_t)num_cones, sizeof(int)); + cone_type_t *cone_type = checked_calloc((size_t)num_cones, sizeof(cone_type_t)); + char *fixed = has_fixed ? checked_calloc((size_t)n, sizeof(char)) : NULL; + double *primal = has_primal ? checked_calloc((size_t)n, sizeof(double)) : NULL; + + int ok = read_exact(stream, c, sizeof(double), (size_t)n) && read_exact(stream, lbx, sizeof(double), (size_t)n) && + read_exact(stream, ubx, sizeof(double), (size_t)n) && read_exact(stream, lbc, sizeof(double), (size_t)m) && + read_exact(stream, ubc, sizeof(double), (size_t)m) && + read_exact(stream, a_row, sizeof(int32_t), (size_t)m + 1) && + read_exact(stream, a_col, sizeof(int32_t), (size_t)a_nnz) && + read_exact(stream, a_val, sizeof(double), (size_t)a_nnz) && + read_exact(stream, q_row, sizeof(int32_t), (size_t)n + 1) && + read_exact(stream, q_col, sizeof(int32_t), (size_t)q_nnz) && + read_exact(stream, q_val, sizeof(double), (size_t)q_nnz) && + read_exact(stream, cone_start, sizeof(int32_t), (size_t)num_cones) && + read_exact(stream, cone_vdim, sizeof(int32_t), (size_t)num_cones) && + read_exact(stream, cone_type, sizeof(int32_t), (size_t)num_cones) && + (!has_fixed || read_exact(stream, fixed, sizeof(char), (size_t)n)) && + (!has_primal || read_exact(stream, primal, sizeof(double), (size_t)n)); + fclose(stream); + if (!ok) + { + fprintf(stderr, "truncated model body: %s\n", path); + return 1; + } + if (getenv("PDHCG_PROJECT_BOX_START")) + { + if (!primal) + primal = checked_calloc((size_t)n, sizeof(double)); + for (int i = 0; i < n; ++i) + primal[i] = fmax(lbx[i], fmin(primal[i], ubx[i])); + } + + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = m; + A.n = n; + A.fmt = matrix_csr; + A.data.csr.nnz = a_nnz; + A.data.csr.row_ptr = a_row; + A.data.csr.col_ind = a_col; + A.data.csr.vals = a_val; + + matrix_desc_t Q; + memset(&Q, 0, sizeof(Q)); + Q.m = n; + Q.n = n; + Q.fmt = matrix_csr; + Q.data.csr.nnz = q_nnz; + Q.data.csr.row_ptr = q_row; + Q.data.csr.col_ind = q_col; + Q.data.csr.vals = q_val; + + cone_spec_t *specs = checked_calloc((size_t)num_cones, sizeof(cone_spec_t)); + for (int i = 0; i < num_cones; ++i) + { + specs[i].type = cone_type[i]; + specs[i].start_idx = cone_start[i]; + specs[i].v_dim = cone_vdim[i]; + specs[i].is_fixed = (fixed && !getenv("PDHCG_IGNORE_FIXED_MASK")) ? fixed + cone_start[i] : NULL; + } + + int effective_num_cones = getenv("PDHCG_IGNORE_CONES") ? 0 : num_cones; + qp_problem_t *problem = create_qp_problem(c, + q_nnz > 0 ? &Q : NULL, + NULL, + NULL, + &A, + lbc, + ubc, + lbx, + ubx, + &objective_constant, + effective_num_cones, + specs, + NULL, + NULL, + 0, + NULL); + if (!problem) + { + fprintf(stderr, "create_qp_problem failed\n"); + return 1; + } + if (primal && !getenv("PDHCG_IGNORE_START")) + set_start_values(problem, primal, NULL); + if (verbose >= 2) + print_host_sanity(problem); + + free(c); + free(lbx); + free(ubx); + free(lbc); + free(ubc); + free(a_row); + free(a_col); + free(a_val); + free(q_row); + free(q_col); + free(q_val); + free(cone_start); + free(cone_vdim); + free(cone_type); + free(fixed); + free(primal); + free(specs); + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = verbose; + params.termination_criteria.eps_feasible_relative = eps; + params.termination_criteria.eps_optimal_relative = eps; + params.termination_criteria.time_sec_limit = time_limit; + if (getenv("PDHCG_NO_BOUND_OBJ_RESCALING")) + params.bound_objective_rescaling = false; + if (getenv("PDHCG_NO_SCALING")) + { + params.l_inf_ruiz_iterations = 0; + params.has_pock_chambolle_alpha = false; + params.curtis_reid_iterations = 0; + params.bound_objective_rescaling = false; + } + + double wall_start = monotonic_seconds(); + pdhcg_result_t *result = solve_qp_problem(problem, ¶ms); + double wall_time = monotonic_seconds() - wall_start; + if (!result) + { + fprintf(stderr, "solve_qp_problem failed\n"); + qp_problem_free(problem); + return 1; + } + + printf("{\"status\":\"%s\",\"runtime_sec\":%.17g,\"wall_time_sec\":%.17g," + "\"iterations\":%d,\"inner_iterations\":%d," + "\"primal_objective\":%.17g,\"dual_objective\":%.17g," + "\"absolute_primal_residual\":%.17g,\"absolute_dual_residual\":%.17g," + "\"absolute_objective_gap\":%.17g," + "\"relative_primal_residual\":%.17g,\"relative_dual_residual\":%.17g," + "\"relative_objective_gap\":%.17g,\"n\":%d,\"m\":%d,\"A_nnz\":%d," + "\"Q_nnz\":%d," + "\"num_cones\":%d}\n", + termination_name(result->termination_reason), + result->cumulative_time_sec, + wall_time, + result->total_count, + result->total_inner_count, + result->primal_objective_value, + result->dual_objective_value, + result->absolute_primal_residual, + result->absolute_dual_residual, + result->objective_gap, + result->relative_primal_residual, + result->relative_dual_residual, + result->relative_objective_gap, + problem->num_variables, + problem->num_constraints, + problem->constraint_matrix_num_nonzeros, + problem->objective_sparse_matrix_num_nonzeros, + problem->cones.num_cones); + fflush(stdout); + + pdhcg_result_free(result); + qp_problem_free(problem); + return 0; +} diff --git a/test/test_affine_cones.c b/test/test_affine_cones.c new file mode 100644 index 0000000..355934d --- /dev/null +++ b/test/test_affine_cones.c @@ -0,0 +1,285 @@ +#include "pdhcg.h" +#include "pdhcg_types.h" + +#include +#include +#include + +static qp_problem_t *make_one_variable_problem(double objective, + const matrix_desc_t *A, + const double *offset, + const cone_spec_t *affine_cone) +{ + static const int q_row_ptr[] = {0, 0}; + matrix_desc_t Q; + memset(&Q, 0, sizeof(Q)); + Q.m = 1; + Q.n = 1; + Q.fmt = matrix_csr; + Q.data.csr.row_ptr = q_row_ptr; + int num_affine_cones = affine_cone ? 1 : 0; + return create_qp_problem(&objective, + &Q, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + 0, + NULL, + A, + offset, + num_affine_cones, + affine_cone); +} + +static qp_problem_t *make_one_variable_quadratic_problem( + double objective, double quadratic, const matrix_desc_t *A, const double *offset, const cone_spec_t *affine_cone) +{ + static const int q_row_ptr[] = {0, 1}; + static const int q_col_ind[] = {0}; + matrix_desc_t Q; + memset(&Q, 0, sizeof(Q)); + Q.m = 1; + Q.n = 1; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 1; + Q.data.csr.row_ptr = q_row_ptr; + Q.data.csr.col_ind = q_col_ind; + Q.data.csr.vals = &quadratic; + int num_affine_cones = affine_cone ? 1 : 0; + return create_qp_problem(&objective, + &Q, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + 0, + NULL, + A, + offset, + num_affine_cones, + affine_cone); +} + +static pdhcg_result_t *solve_tiny(qp_problem_t *problem, norm_type_t norm, bool use_cone_preserving_scaling) +{ + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.verbose = 0; + parameters.presolve = false; + parameters.optimality_norm = norm; + parameters.use_cone_preserving_scaling = use_cone_preserving_scaling; + parameters.termination_evaluation_frequency = 10; + parameters.termination_criteria.eps_optimal_relative = 1e-7; + parameters.termination_criteria.eps_feasible_relative = 1e-7; + parameters.termination_criteria.iteration_limit = 1000000; + parameters.termination_criteria.time_sec_limit = 30.0; + return solve_qp_problem(problem, ¶meters); +} + +static int check_result(const char *name, pdhcg_result_t *result, double expected) +{ + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL) + { + fprintf(stderr, "%s: expected OPTIMAL, got %d\n", name, result ? (int)result->termination_reason : -1); + return 0; + } + double error = fabs(result->primal_solution[0] - expected); + double objective_gap = fabs(result->primal_objective_value - result->dual_objective_value); + if (error > 2e-4 * (1.0 + fabs(expected)) || result->relative_primal_residual > 2e-6 || + result->relative_dual_residual > 2e-6 || objective_gap > 2e-4 * (1.0 + fabs(expected))) + { + fprintf(stderr, + "%s: x=%.17g expected=%.17g, rel_pr=%.3e rel_du=%.3e gap=%.3e\n", + name, + result->primal_solution[0], + expected, + result->relative_primal_residual, + result->relative_dual_residual, + objective_gap); + return 0; + } + return 1; +} + +static int run_soc(void) +{ + static const int row_ptr[] = {0, 1, 1, 2}; + static const int col_ind[] = {0, 0}; + static const double values[] = {10.0, 1.0}; + static const double offset[] = {0.0, 0.0, 9.0}; + matrix_desc_t F; + memset(&F, 0, sizeof(F)); + F.m = 3; + F.n = 1; + F.fmt = matrix_csr; + F.data.csr.nnz = 2; + F.data.csr.row_ptr = row_ptr; + F.data.csr.col_ind = col_ind; + F.data.csr.vals = values; + cone_spec_t cone = {.type = CONE_STANDARD_SOC, .start_idx = 0, .v_dim = 1}; + + qp_problem_t *problem = make_one_variable_quadratic_problem(-2.0, 1.0, &F, offset, &cone); + int setup_ok = problem != NULL; + pdhcg_result_t *result = setup_ok ? solve_tiny(problem, NORM_TYPE_L_INF, false) : NULL; + int passed = setup_ok && check_result("affine SOC", result, 1.0); + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int run_exp(void) +{ + static const int row_ptr[] = {0, 1, 1, 1}; + static const int col_ind[] = {0}; + static const double values[] = {1.0}; + static const double offset[] = {0.0, 1.0, 2.0}; + matrix_desc_t F; + memset(&F, 0, sizeof(F)); + F.m = 3; + F.n = 1; + F.fmt = matrix_csr; + F.data.csr.nnz = 1; + F.data.csr.row_ptr = row_ptr; + F.data.csr.col_ind = col_ind; + F.data.csr.vals = values; + cone_spec_t cone = {.type = CONE_EXPONENTIAL, .start_idx = 0, .v_dim = 1}; + + qp_problem_t *problem = make_one_variable_problem(-1.0, &F, offset, &cone); + int setup_ok = problem != NULL; + pdhcg_result_t *result = setup_ok ? solve_tiny(problem, NORM_TYPE_L_INF, true) : NULL; + int passed = setup_ok && check_result("affine exponential cone", result, log(2.0)); + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int run_power(void) +{ + static const int row_ptr[] = {0, 0, 0, 1}; + static const int col_ind[] = {0}; + static const double values[] = {1.0}; + static const double offset[] = {1.0, 1.0, 0.0}; + matrix_desc_t F; + memset(&F, 0, sizeof(F)); + F.m = 3; + F.n = 1; + F.fmt = matrix_csr; + F.data.csr.nnz = 1; + F.data.csr.row_ptr = row_ptr; + F.data.csr.col_ind = col_ind; + F.data.csr.vals = values; + cone_spec_t cone = {.type = CONE_POWER, .start_idx = 0, .v_dim = 1, .power_alpha = 0.3}; + + qp_problem_t *problem = make_one_variable_problem(-1.0, &F, offset, &cone); + int setup_ok = problem != NULL; + pdhcg_result_t *result = setup_ok ? solve_tiny(problem, NORM_TYPE_L2, true) : NULL; + int passed = setup_ok && check_result("affine power cone", result, 1.0); + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int run_rsoc(void) +{ + static const int row_ptr[] = {0, 1, 1, 1}; + static const int col_ind[] = {0}; + static const double values[] = {1.0}; + static const double offset[] = {0.0, 1.0, 1.0}; + matrix_desc_t F; + memset(&F, 0, sizeof(F)); + F.m = 3; + F.n = 1; + F.fmt = matrix_csr; + F.data.csr.nnz = 1; + F.data.csr.row_ptr = row_ptr; + F.data.csr.col_ind = col_ind; + F.data.csr.vals = values; + cone_spec_t cone = {.type = CONE_ROTATED_SOC, .start_idx = 0, .v_dim = 1}; + + qp_problem_t *problem = make_one_variable_problem(-1.0, &F, offset, &cone); + int setup_ok = problem != NULL; + pdhcg_result_t *result = setup_ok ? solve_tiny(problem, NORM_TYPE_L2, true) : NULL; + int passed = setup_ok && check_result("affine rotated SOC", result, sqrt(2.0)); + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int run_scalar_bound_infeasible(void) +{ + static const int row_ptr[] = {0, 0}; + static const double objective[] = {0.0}; + static const double equality[] = {-1.0}; + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = 1; + A.n = 1; + A.fmt = matrix_csr; + A.data.csr.row_ptr = row_ptr; + + qp_problem_t *problem = create_qp_problem( + objective, NULL, NULL, NULL, &A, equality, equality, NULL, NULL, NULL, 0, NULL, NULL, NULL, 0, NULL); + if (!problem) + return 0; + + pdhcg_result_t *result = solve_tiny(problem, NORM_TYPE_L_INF, true); + int passed = result && result->termination_reason == TERMINATION_REASON_PRIMAL_INFEASIBLE; + if (!passed) + { + fprintf(stderr, + "scalar bound: expected PRIMAL_INFEASIBLE, got %d\n", + result ? (int)result->termination_reason : -1); + } + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int run_reject_fixed_slots(void) +{ + static const int row_ptr[] = {0, 0, 0, 0}; + static const char is_fixed[] = {1, 0, 0}; + matrix_desc_t F; + memset(&F, 0, sizeof(F)); + F.m = 3; + F.n = 1; + F.fmt = matrix_csr; + F.data.csr.row_ptr = row_ptr; + cone_spec_t cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 0, + .v_dim = 1, + .is_fixed = is_fixed, + }; + + qp_problem_t *problem = make_one_variable_problem(0.0, &F, NULL, &cone); + int rejected = problem == NULL; + qp_problem_free(problem); + return rejected; +} + +int main(void) +{ + int soc = run_soc(); + int rsoc = run_rsoc(); + int exp = run_exp(); + int power = run_power(); + int scalar_bound = run_scalar_bound_infeasible(); + int fixed_slots = run_reject_fixed_slots(); + printf("affine SOC: %s\n", soc ? "PASS" : "FAIL"); + printf("affine RSOC: %s\n", rsoc ? "PASS" : "FAIL"); + printf("affine Exp: %s\n", exp ? "PASS" : "FAIL"); + printf("affine Power: %s\n", power ? "PASS" : "FAIL"); + printf("scalar bound infeasibility: %s\n", scalar_bound ? "PASS" : "FAIL"); + printf("affine fixed-slot rejection: %s\n", fixed_slots ? "PASS" : "FAIL"); + return (soc && rsoc && exp && power && scalar_bound && fixed_slots) ? 0 : 1; +} diff --git a/test/test_c_api_working.c b/test/test_c_api_working.c new file mode 100644 index 0000000..ac74754 --- /dev/null +++ b/test/test_c_api_working.c @@ -0,0 +1,341 @@ +/* + * Test program to verify C API works correctly. + * + * This demonstrates that the C API works while Python binding hangs. + * + * Compile: + * gcc -o test_c_api test_c_api.c -I../include -L../build -lpdhcg -Wl,-rpath,../build -lm + * + * Run: + * ./test_c_api + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +int main() +{ + printf("=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=\n"); + printf("Testing PDHCG C API\n"); + printf("=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=\n\n"); + + /* Problem: min x + 2y s.t. x + y = 1, x,y >= 0 */ + printf("Problem: min x + 2y s.t. x + y = 1\n"); + printf("Expected: x ≈ 1, y ≈ 0, obj ≈ 1\n\n"); + + /* A = [1, 1] (CSR format) */ + double val[] = {1.0, 1.0}; + int col_ind[] = {0, 1}; + int row_ptr[] = {0, 2}; + + matrix_desc_t A_desc; + A_desc.m = 1; + A_desc.n = 2; + A_desc.fmt = matrix_csr; + A_desc.zero_tolerance = 0.0; + A_desc.data.csr.nnz = 2; + A_desc.data.csr.row_ptr = row_ptr; + A_desc.data.csr.col_ind = col_ind; + A_desc.data.csr.vals = val; + + double c[] = {1.0, 2.0}; + double lb[] = {0.0, 0.0}; + double ub[] = {1e30, 1e30}; /* Use large numbers instead of inf */ + double cl[] = {1.0}; + double cu[] = {1.0}; + + printf("Creating problem...\n"); + qp_problem_t *prob = + create_qp_problem(c, NULL, NULL, NULL, &A_desc, cl, cu, lb, ub, NULL, 0, NULL, NULL, NULL, 0, NULL); + if (!prob) + { + printf("FAIL: create_qp_problem failed\n"); + return 1; + } + printf("Problem created successfully\n"); + printf(" num_variables: %d\n", prob->num_variables); + printf(" num_constraints: %d\n", prob->num_constraints); + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.presolve = true; /* Test with presolve enabled */ + params.verbose = 1; + + printf("\nCalling solve_qp_problem (with presolve)...\n"); + clock_t start = clock(); + pdhcg_result_t *result = solve_qp_problem(prob, ¶ms); + clock_t end = clock(); + double elapsed = (double)(end - start) / CLOCKS_PER_SEC; + + if (!result) + { + printf("FAIL: solve_qp_problem returned NULL\n"); + qp_problem_free(prob); + return 1; + } + + printf("\n"); + printf("=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=\n"); + printf("Results (completed in %.3f seconds)\n", elapsed); + printf("=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=\n"); + printf("Status: %d (OPTIMAL=%d)\n", result->termination_reason, TERMINATION_REASON_OPTIMAL); + + if (result->primal_solution) + { + printf("Primal X: [%.6f, %.6f]\n", result->primal_solution[0], result->primal_solution[1]); + } + if (result->dual_solution) + { + printf("Dual Y: [%.6f]\n", result->dual_solution[0]); + } + printf("Objective: %.6f\n", result->primal_objective_value); + + /* Verification */ + printf("\n"); + printf("=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=\n"); + printf("Verification\n"); + printf("=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=\n"); + + int success = (result->termination_reason == TERMINATION_REASON_OPTIMAL); + double x0 = result->primal_solution ? result->primal_solution[0] : -1; + double x1 = result->primal_solution ? result->primal_solution[1] : -1; + double y = result->dual_solution ? result->dual_solution[0] : -1; + + /* Check primal solution */ + if (x0 > 0.9 && x0 < 1.1 && x1 >= 0.0 && x1 < 0.1) + { + printf("Primal solution: PASS (x≈1, y≈0)\n"); + } + else + { + printf("Primal solution: FAIL (expected x≈1, y≈0, got x=%.4f, y=%.4f)\n", x0, x1); + success = 0; + } + + /* Check dual solution */ + if (y > 0.9 && y < 1.1) + { + printf("Dual solution: PASS (y≈1)\n"); + } + else + { + printf("Dual solution: FAIL (expected y≈1, got y=%.4f)\n", y); + success = 0; + } + + /* Check objective */ + if (result->primal_objective_value > 0.9 && result->primal_objective_value < 1.1) + { + printf("Objective: PASS (obj≈1)\n"); + } + else + { + printf("Objective: FAIL (expected obj≈1, got obj=%.4f)\n", result->primal_objective_value); + success = 0; + } + + pdhcg_result_free(result); + qp_problem_free(prob); + + printf("\n"); + printf("=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=\n"); + if (success) + { + printf("OVERALL: PASS - C API works correctly!\n"); + } + else + { + printf("OVERALL: FAIL - C API has issues\n"); + } + printf("=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=" + "=\n"); + + return success ? 0 : 1; +} diff --git a/test/test_cbf_affine_cones.c b/test/test_cbf_affine_cones.c new file mode 100644 index 0000000..99dc4e5 --- /dev/null +++ b/test/test_cbf_affine_cones.c @@ -0,0 +1,99 @@ +#include "cbf_parser.h" +#include "pdhcg.h" + +#include +#include + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + return 1; \ + } \ + } while (0) + +static int write_test_cbf(const char *path) +{ + static const char model[] = "VER\n" + "1\n\n" + "OBJSENSE\n" + "MIN\n\n" + "VAR\n" + "1 1\n" + "F 1\n\n" + "CON\n" + "4 2\n" + "Q 3\n" + "L+ 1\n\n" + "OBJACOORD\n" + "1\n" + "0 1\n\n" + "ACOORD\n" + "2\n" + "0 0 1\n" + "3 0 1\n\n" + "BCOORD\n" + "1\n" + "1 1\n"; + + FILE *file = fopen(path, "w"); + if (!file) + return 0; + return fputs(model, file) >= 0 && fclose(file) == 0; +} + +int main(void) +{ + const char *path = "test_cbf_affine_cones_tmp.cbf"; + CHECK(write_test_cbf(path)); + qp_problem_t *problem = read_cbf_file(path); + remove(path); + CHECK(problem != NULL); + + CHECK(problem->num_variables == 1); + CHECK(problem->num_constraints == 4); + CHECK(problem->cones.num_cones == 0); + CHECK(problem->affine_cones.num_cones == 1); + CHECK(problem->affine_cones.type[0] == CONE_STANDARD_SOC); + CHECK(problem->affine_cones.start_idx[0] == 0); + CHECK(problem->affine_cones.v_dim[0] == 1); + + /* CBF Q is (z,v,w), while the runtime order is (v,w,z). */ + CHECK(problem->constraint_matrix_num_nonzeros == 2); + CHECK(problem->constraint_matrix->row_ptr[0] == 0); + CHECK(problem->constraint_matrix->row_ptr[1] == 0); + CHECK(problem->constraint_matrix->row_ptr[2] == 0); + CHECK(problem->constraint_matrix->row_ptr[3] == 1); + CHECK(problem->constraint_matrix->row_ptr[4] == 2); + CHECK(problem->constraint_matrix->col_ind[0] == 0); + CHECK(problem->constraint_matrix->val[0] == 1.0); + CHECK(problem->constraint_matrix->col_ind[1] == 0); + CHECK(problem->constraint_matrix->val[1] == 1.0); + CHECK(problem->constraint_lower_bound[3] == 0.0); + CHECK(isinf(problem->constraint_upper_bound[3]) && problem->constraint_upper_bound[3] > 0.0); + CHECK(problem->affine_cone_offset[0] == 1.0); + CHECK(problem->affine_cone_offset[1] == 0.0); + CHECK(problem->affine_cone_offset[2] == 0.0); + CHECK(problem->affine_cone_offset[3] == 0.0); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.verbose = 0; + parameters.presolve = false; + parameters.termination_evaluation_frequency = 10; + parameters.termination_criteria.eps_optimal_relative = 1e-7; + parameters.termination_criteria.eps_feasible_relative = 1e-7; + parameters.termination_criteria.iteration_limit = 1000000; + parameters.termination_criteria.time_sec_limit = 30.0; + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + CHECK(result != NULL); + CHECK(result->termination_reason == TERMINATION_REASON_OPTIMAL); + CHECK(fabs(result->primal_solution[0] - 1.0) <= 2e-4); + CHECK(fabs(result->primal_objective_value - result->dual_objective_value) <= 2e-4); + + pdhcg_result_free(result); + qp_problem_free(problem); + return 0; +} diff --git a/test/test_cbf_fixed_slots.c b/test/test_cbf_fixed_slots.c new file mode 100644 index 0000000..ee2d5fe --- /dev/null +++ b/test/test_cbf_fixed_slots.c @@ -0,0 +1,109 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "cbf_parser.h" +#include "pdhcg.h" +#include +#include + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + return 1; \ + } \ + } while (0) + +static int write_test_cbf(const char *path) +{ + static const char model[] = "VER\n" + "1\n\n" + "OBJSENSE\n" + "MIN\n\n" + "VAR\n" + "3 1\n" + "Q 3\n\n" + "CON\n" + "1 1\n" + "L= 1\n\n" + "OBJACOORD\n" + "1\n" + "1 -1\n\n" + "ACOORD\n" + "1\n" + "0 0 -1\n\n" + "BCOORD\n" + "1\n" + "0 1\n"; + + FILE *file = fopen(path, "w"); + if (!file) + return 0; + int ok = fputs(model, file) >= 0 && fclose(file) == 0; + return ok; +} + +int main(void) +{ + const char *path = "test_cbf_fixed_slots_tmp.cbf"; + CHECK(write_test_cbf(path)); + + qp_problem_t *problem = read_cbf_file(path); + remove(path); + CHECK(problem != NULL); + CHECK(problem->num_variables == 3); + CHECK(problem->num_constraints == 1); + CHECK(problem->cones.num_cones == 1); + CHECK(problem->cones.type[0] == CONE_STANDARD_SOC); + CHECK(problem->cones.start_idx[0] == 0); + CHECK(problem->cones.v_dim[0] == 1); + + /* CBF Q ordering is (z, v, w); internal ordering is (v, w, z). */ + CHECK(problem->constraint_matrix_num_nonzeros == 1); + CHECK(problem->constraint_matrix->row_ptr[0] == 0); + CHECK(problem->constraint_matrix->row_ptr[1] == 1); + CHECK(problem->constraint_matrix->col_ind[0] == 2); + CHECK(problem->constraint_matrix->val[0] == -1.0); + CHECK(problem->cones.is_fixed && problem->cones.is_fixed[2]); + CHECK(problem->primal_start && problem->primal_start[2] == 1.0); + CHECK(isinf(problem->variable_lower_bound[2]) && problem->variable_lower_bound[2] < 0.0); + CHECK(isinf(problem->variable_upper_bound[2]) && problem->variable_upper_bound[2] > 0.0); + CHECK(problem->constraint_lower_bound[0] == -1.0); + CHECK(problem->constraint_upper_bound[0] == -1.0); + CHECK(problem->affine_cone_offset[0] == 0.0); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.verbose = 0; + parameters.termination_criteria.eps_optimal_relative = 1e-7; + parameters.termination_criteria.eps_feasible_relative = 1e-7; + parameters.termination_criteria.iteration_limit = 100000; + parameters.termination_criteria.time_sec_limit = 30.0; + + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + CHECK(result != NULL); + CHECK(result->termination_reason == TERMINATION_REASON_OPTIMAL); + CHECK(fabs(result->primal_solution[0] - 1.0) <= 1e-5); + CHECK(fabs(result->primal_solution[1]) <= 1e-5); + CHECK(fabs(result->primal_solution[2] - 1.0) <= 1e-12); + CHECK(fabs(result->primal_objective_value + 1.0) <= 1e-5); + + pdhcg_result_free(result); + qp_problem_free(problem); + return 0; +} diff --git a/test/test_cone_box_reject.c b/test/test_cone_box_reject.c new file mode 100644 index 0000000..1d24709 --- /dev/null +++ b/test/test_cone_box_reject.c @@ -0,0 +1,41 @@ +/* + * create_qp_problem must reject a finite box bound on a cone slot. + * Lift such variables manually with an auxiliary x_cone = x_box. + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include + +int main(void) +{ + double aval[] = {1.0}; + int acol[] = {1}; + int arow[] = {0, 1}; + matrix_desc_t A = {0}; + A.m = 1; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + double c[] = {0.0, 0.0, 1.0}; + double con_lb[] = {4.0}, con_ub[] = {4.0}; + double var_lb[] = {0.0, -1e30, -1e30}; /* finite lower bound on v slot (index 0) */ + double var_ub[] = {1e30, 1e30, 1e30}; + + cone_spec_t cones[] = {{.type = CONE_STANDARD_SOC, .start_idx = 0, .v_dim = 1, .is_fixed = NULL}}; + + fprintf(stderr, "(expecting error on next line)\n"); + qp_problem_t *prob = + create_qp_problem(c, NULL, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + int pass = (prob == NULL); + if (prob) + qp_problem_free(prob); + printf("create_qp_problem returned %s -> %s\n", prob ? "non-NULL" : "NULL", pass ? "PASS" : "FAIL"); + return pass ? 0 : 1; +} diff --git a/test/test_cone_permutation.c b/test/test_cone_permutation.c new file mode 100644 index 0000000..b89bc19 --- /dev/null +++ b/test/test_cone_permutation.c @@ -0,0 +1,237 @@ +#include "pdhcg.h" +#include "permute.h" +#include +#include +#include +#include + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + return 1; \ + } \ + } while (0) + +int main(void) +{ + const int n = 16; + static const int scalar_row_ptr[] = {0, 0}; + static const int affine_row_ptr[] = {0, 0, 0, 0, 0, 0, 0}; + static const int col_ind[] = {0}; + static const double values[] = {0.0}; + double objective[n]; + for (int i = 0; i < n; ++i) + objective[i] = (double)i; + + matrix_desc_t A = {0}; + A.m = 1; + A.n = n; + A.fmt = matrix_csr; + A.data.csr.nnz = 0; + A.data.csr.row_ptr = scalar_row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + matrix_desc_t F = {0}; + F.m = 6; + F.n = n; + F.fmt = matrix_csr; + F.data.csr.nnz = 0; + F.data.csr.row_ptr = affine_row_ptr; + F.data.csr.col_ind = col_ind; + F.data.csr.vals = values; + const double constraint_lower[] = {0.0}; + const double constraint_upper[] = {0.0}; + static const double affine_offset[] = {10.0, 11.0, 12.0, 20.0, 21.0, 22.0}; + const char fixed0[] = {0, 0, 1}; + const char fixed1[] = {1, 0, 0}; + const char fixed2[] = {0, 1, 0}; + const char fixed3[] = {0, 0, 1, 1}; + const cone_spec_t cones[] = { + {.type = CONE_STANDARD_SOC, .start_idx = 1, .v_dim = 1, .power_alpha = 0.0, .is_fixed = fixed0}, + {.type = CONE_POWER, .start_idx = 5, .v_dim = 1, .power_alpha = 0.3, .is_fixed = fixed1}, + {.type = CONE_EXPONENTIAL, .start_idx = 9, .v_dim = 1, .power_alpha = 0.0, .is_fixed = fixed2}, + {.type = CONE_ROTATED_SOC, .start_idx = 12, .v_dim = 2, .power_alpha = 0.0, .is_fixed = fixed3}, + }; + const cone_spec_t affine_cones[] = { + {.type = CONE_STANDARD_SOC, .start_idx = 0, .v_dim = 1}, + {.type = CONE_EXPONENTIAL, .start_idx = 3, .v_dim = 1}, + }; + + qp_problem_t *problem = create_qp_problem(objective, + NULL, + NULL, + NULL, + &A, + constraint_lower, + constraint_upper, + NULL, + NULL, + NULL, + 4, + cones, + &F, + affine_offset, + 2, + affine_cones); + CHECK(problem != NULL); + + int permutation[n]; + int row_permutation[] = {0, 1, 2, 3, 4, 5, 6}; + srand(7); + generate_cone_aware_permutation(problem, FULL_RANDOM_PERMUTATION, 1, permutation); + CHECK(validate_cone_permutation(problem, permutation)); + + qp_problem_t *permuted = permute_problem_return_new(problem, row_permutation, permutation); + CHECK(permuted != NULL); + for (int cone = 0; cone < 4; ++cone) + { + int old_start = problem->cones.start_idx[cone]; + int new_start = permuted->cones.start_idx[cone]; + int length = (problem->cones.type[cone] == CONE_EXPONENTIAL || problem->cones.type[cone] == CONE_POWER) + ? 3 + : problem->cones.v_dim[cone] + 2; + CHECK(permuted->cones.type[cone] == problem->cones.type[cone]); + if (problem->cones.type[cone] == CONE_POWER) + CHECK(permuted->cones.power_alpha[cone] == problem->cones.power_alpha[cone]); + for (int slot = 0; slot < length; ++slot) + { + CHECK(permuted->objective_vector[new_start + slot] == objective[old_start + slot]); + CHECK(permuted->cones.is_fixed[new_start + slot] == problem->cones.is_fixed[old_start + slot]); + } + } + + int block_permutation[n]; + srand(11); + generate_cone_aware_permutation(problem, BLOCK_RANDOM_PERMUTATION, 2, block_permutation); + CHECK(validate_cone_permutation(problem, block_permutation)); + + int row_cone_permutation[7]; + int identity_columns[n]; + for (int col = 0; col < n; ++col) + identity_columns[col] = col; + srand(13); + generate_affine_cone_aware_row_permutation(problem, FULL_RANDOM_PERMUTATION, 1, row_cone_permutation); + CHECK(validate_affine_cone_row_permutation(problem, row_cone_permutation)); + qp_problem_t *affine_permuted = permute_problem_return_new(problem, row_cone_permutation, identity_columns); + CHECK(affine_permuted != NULL); + for (int cone = 0; cone < 2; ++cone) + { + int old_start = problem->affine_cones.start_idx[cone]; + int new_start = affine_permuted->affine_cones.start_idx[cone]; + for (int slot = 0; slot < 3; ++slot) + CHECK(affine_permuted->affine_cone_offset[new_start + slot] == + problem->affine_cone_offset[old_start + slot]); + } + int invalid_rows[] = {1, 0, 2, 3, 4, 5, 6}; + CHECK(!validate_affine_cone_row_permutation(problem, invalid_rows)); + CHECK(!permute_problem(problem, invalid_rows, identity_columns)); + int duplicate_columns[n]; + memcpy(duplicate_columns, identity_columns, sizeof(duplicate_columns)); + duplicate_columns[1] = duplicate_columns[0]; + CHECK(!validate_cone_permutation(problem, duplicate_columns)); + qp_problem_free(affine_permuted); + + qp_problem_free(permuted); + qp_problem_free(problem); + + const cone_spec_t overlapping[] = { + {.type = CONE_POWER, .start_idx = 0, .v_dim = 1, .power_alpha = 0.3, .is_fixed = NULL}, + {.type = CONE_EXPONENTIAL, .start_idx = 2, .v_dim = 1, .power_alpha = 0.0, .is_fixed = NULL}, + }; + problem = create_qp_problem(objective, + NULL, + NULL, + NULL, + &A, + constraint_lower, + constraint_upper, + NULL, + NULL, + NULL, + 2, + overlapping, + NULL, + NULL, + 0, + NULL); + CHECK(problem == NULL); + + { + static const int scalar_row_ptr[] = {0, 1}; + static const int affine_row_ptr[] = {0, 1, 1, 1, 2, 2, 2}; + static const int scalar_col_ind[] = {0}; + static const int affine_col_ind[] = {0, 0}; + static const double scalar_values[] = {1.0}; + static const double affine_values[] = {1.0, 1.0}; + static const double scalar_lower[] = {0.0}; + static const double scalar_upper[] = {INFINITY}; + static const double affine_offset[] = {0.0, 1.0, 2.0, 0.0, 1.0, 3.0}; + static const cone_spec_t row_cones[] = { + {.type = CONE_EXPONENTIAL, .start_idx = 0, .v_dim = 1}, + {.type = CONE_EXPONENTIAL, .start_idx = 3, .v_dim = 1}, + }; + double linear_objective = -1.0; + matrix_desc_t scalar_matrix = {0}; + scalar_matrix.m = 1; + scalar_matrix.n = 1; + scalar_matrix.fmt = matrix_csr; + scalar_matrix.data.csr.nnz = 1; + scalar_matrix.data.csr.row_ptr = scalar_row_ptr; + scalar_matrix.data.csr.col_ind = scalar_col_ind; + scalar_matrix.data.csr.vals = scalar_values; + matrix_desc_t affine_matrix = {0}; + affine_matrix.m = 6; + affine_matrix.n = 1; + affine_matrix.fmt = matrix_csr; + affine_matrix.data.csr.nnz = 2; + affine_matrix.data.csr.row_ptr = affine_row_ptr; + affine_matrix.data.csr.col_ind = affine_col_ind; + affine_matrix.data.csr.vals = affine_values; + + problem = create_qp_problem(&linear_objective, + NULL, + NULL, + NULL, + &scalar_matrix, + scalar_lower, + scalar_upper, + NULL, + NULL, + NULL, + 0, + NULL, + &affine_matrix, + affine_offset, + 2, + row_cones); + CHECK(problem != NULL); + int interleaved_rows[] = {1, 2, 3, 0, 4, 5, 6}; + int identity_column[] = {0}; + CHECK(validate_affine_cone_row_permutation(problem, interleaved_rows)); + CHECK(permute_problem(problem, interleaved_rows, identity_column)); + CHECK(problem->affine_cones.start_idx[0] == 0); + CHECK(problem->affine_cones.start_idx[1] == 4); + CHECK(problem->constraint_lower_bound[3] == 0.0); + CHECK(isinf(problem->constraint_upper_bound[3])); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.verbose = 0; + parameters.presolve = false; + parameters.termination_evaluation_frequency = 10; + parameters.termination_criteria.eps_optimal_relative = 1e-7; + parameters.termination_criteria.eps_feasible_relative = 1e-7; + parameters.termination_criteria.iteration_limit = 1000000; + parameters.termination_criteria.time_sec_limit = 30.0; + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + CHECK(result != NULL); + CHECK(result->termination_reason == TERMINATION_REASON_OPTIMAL); + CHECK(fabs(result->primal_solution[0] - log(2.0)) <= 2e-4); + pdhcg_result_free(result); + qp_problem_free(problem); + } + return 0; +} diff --git a/test/test_conic_e2e.c b/test/test_conic_e2e.c new file mode 100644 index 0000000..c04ff11 --- /dev/null +++ b/test/test_conic_e2e.c @@ -0,0 +1,227 @@ +/* + * E2E tests for create_qp_problem covering the three supported cone types: + * Test 1: standard SOC, recovers (v, w, z) = (3, 4, 5). + * Test 2: rotated SOC, recovers s = t = 3/sqrt(2) at v = 3. + * Test 3: exponential cone, recovers x = log 2 with y = 1, z = 2. + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +static int approx_eq(double a, double b, double tol) +{ + return fabs(a - b) <= tol * (1.0 + fabs(b)); +} + +static int run_test_standard_soc(void) +{ + printf("[Test 1] Standard SOC: min z s.t. v=3, w=4, (v,w,z) in K_soc\n"); + + double val[] = {1.0, 1.0}; + int col_ind[] = {0, 1}; + int row_ptr[] = {0, 1, 2}; + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = 2; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 2; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = val; + + double c[] = {0.0, 0.0, 1.0}; + double var_lb[] = {-1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30}; + double con_lb[] = {3.0, 4.0}; + double con_ub[] = {3.0, 4.0}; + + cone_spec_t cones[] = { + {.type = CONE_STANDARD_SOC, .start_idx = 0, .v_dim = 1}, + }; + qp_problem_t *prob = + create_qp_problem(c, NULL, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + if (!prob) + { + printf(" FAIL: create_qp_problem returned NULL\n"); + return 0; + } + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 1; + params.termination_criteria.eps_optimal_relative = 1e-7; + params.termination_criteria.eps_feasible_relative = 1e-7; + + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + printf(" FAIL: solve_qp_problem returned NULL\n"); + qp_problem_free(prob); + return 0; + } + + int ok = (res->termination_reason == TERMINATION_REASON_OPTIMAL); + double v = res->primal_solution[0], w = res->primal_solution[1], z = res->primal_solution[2]; + printf( + " status=%d obj=%.6f v=%.6f w=%.6f z=%.6f\n", res->termination_reason, res->primal_objective_value, v, w, z); + + ok = ok && approx_eq(v, 3.0, 1e-3) && approx_eq(w, 4.0, 1e-3) && approx_eq(z, 5.0, 1e-3); + printf(" %s (expected v=3, w=4, z=5)\n", ok ? "PASS" : "FAIL"); + + pdhcg_result_free(res); + qp_problem_free(prob); + return ok; +} + +static int run_test_rotated_soc(void) +{ + printf("\n[Test 2] Rotated SOC: min s+t s.t. v=3, (v,s,t) in K_rsoc\n"); + + double val[] = {1.0}; + int col_ind[] = {0}; + int row_ptr[] = {0, 1}; + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = 1; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = val; + + double c[] = {0.0, 1.0, 1.0}; + double var_lb[] = {-1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30}; + double con_lb[] = {3.0}; + double con_ub[] = {3.0}; + + cone_spec_t cones[] = { + {.type = CONE_ROTATED_SOC, .start_idx = 0, .v_dim = 1}, + }; + qp_problem_t *prob = + create_qp_problem(c, NULL, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + if (!prob) + { + printf(" FAIL: create_qp_problem returned NULL\n"); + return 0; + } + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 1; + params.termination_criteria.eps_optimal_relative = 1e-7; + params.termination_criteria.eps_feasible_relative = 1e-7; + + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + printf(" FAIL: solve_qp_problem returned NULL\n"); + qp_problem_free(prob); + return 0; + } + + int ok = (res->termination_reason == TERMINATION_REASON_OPTIMAL); + double v = res->primal_solution[0], s = res->primal_solution[1], t = res->primal_solution[2]; + double expected = 3.0 / sqrt(2.0); + printf(" status=%d obj=%.6f v=%.6f s=%.6f t=%.6f (expected s=t=%.6f, obj=%.6f)\n", + res->termination_reason, + res->primal_objective_value, + v, + s, + t, + expected, + 2.0 * expected); + + ok = ok && approx_eq(v, 3.0, 1e-3) && approx_eq(s, expected, 5e-3) && approx_eq(t, expected, 5e-3); + printf(" %s\n", ok ? "PASS" : "FAIL"); + + pdhcg_result_free(res); + qp_problem_free(prob); + return ok; +} + +static int run_test_exp_cone(void) +{ + printf("\n[Test 3] Exp cone (linear obj): min -x s.t. y=1, z=2, (x,y,z) in K_exp\n"); + + double val[] = {1.0, 1.0}; + int col_ind[] = {1, 2}; + int row_ptr[] = {0, 1, 2}; + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = 2; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 2; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = val; + + double c[] = {-1.0, 0.0, 0.0}; + double var_lb[] = {-1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30}; + double con_lb[] = {1.0, 2.0}; + double con_ub[] = {1.0, 2.0}; + + cone_spec_t cones[] = { + {.type = CONE_EXPONENTIAL, .start_idx = 0, .v_dim = 1}, + }; + qp_problem_t *prob = + create_qp_problem(c, NULL, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + if (!prob) + { + printf(" FAIL: create_qp_problem returned NULL\n"); + return 0; + } + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 1; + params.termination_criteria.eps_optimal_relative = 1e-6; + params.termination_criteria.eps_feasible_relative = 1e-6; + params.termination_criteria.time_sec_limit = 60.0; + + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + printf(" FAIL: solve_qp_problem returned NULL\n"); + qp_problem_free(prob); + return 0; + } + + double x = res->primal_solution[0], y = res->primal_solution[1], z = res->primal_solution[2]; + int ok = (res->termination_reason == TERMINATION_REASON_OPTIMAL); + double expected_x = log(2.0); + double feas_gap = (y > 0.0) ? (y * exp(x / y) - z) : INFINITY; + printf(" status=%d x=%.6f y=%.6f z=%.6f (expected x=%.6f, y=1, z=2)\n", + res->termination_reason, + x, + y, + z, + expected_x); + printf(" feasibility gap y*exp(x/y) - z = %.3e\n", feas_gap); + ok = ok && approx_eq(x, expected_x, 5e-3) && approx_eq(y, 1.0, 5e-3) && approx_eq(z, 2.0, 5e-3); + printf(" %s\n", ok ? "PASS" : "FAIL"); + + pdhcg_result_free(res); + qp_problem_free(prob); + return ok; +} + +int main(void) +{ + int p1 = run_test_standard_soc(); + int p2 = run_test_rotated_soc(); + int p3 = run_test_exp_cone(); + printf("\n=== Summary ===\n"); + printf("Test 1 (Standard SOC): %s\n", p1 ? "PASS" : "FAIL"); + printf("Test 2 (Rotated SOC): %s\n", p2 ? "PASS" : "FAIL"); + printf("Test 3 (Exp cone): %s\n", p3 ? "PASS" : "FAIL"); + return (p1 && p2 && p3) ? 0 : 1; +} diff --git a/test/test_conic_kkt_termination.c b/test/test_conic_kkt_termination.c new file mode 100644 index 0000000..a4800b6 --- /dev/null +++ b/test/test_conic_kkt_termination.c @@ -0,0 +1,603 @@ +#include "pdhcg.h" +#include "pdhcg_types.h" + +#include +#include +#include + +static qp_problem_t *make_empty_cone_problem(cone_type_t type) +{ + static const int row_ptr[] = {0}; + static const double objective[] = {0.0, 0.0, 0.0}; + matrix_desc_t A = {0}; + A.m = 0; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.row_ptr = row_ptr; + cone_spec_t cone = { + .type = type, + .start_idx = 0, + .v_dim = 1, + .power_alpha = type == CONE_POWER ? 0.5 : 0.0, + .is_fixed = NULL, + }; + return create_qp_problem( + objective, NULL, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); +} + +static int preserves_fixed_values_when_clearing_warm_start(void) +{ + static const double primal_start[] = {2.0, 1.25, 3.0}; + qp_problem_t *problem = make_empty_cone_problem(CONE_STANDARD_SOC); + if (!problem || set_cone_fixed(problem, 0, 1, primal_start[1]) != 0) + { + qp_problem_free(problem); + return 0; + } + + set_start_values(problem, primal_start, NULL); + set_start_values(problem, NULL, NULL); + int passed = problem->primal_start && problem->primal_start[0] == 0.0 && + problem->primal_start[1] == primal_start[1] && problem->primal_start[2] == 0.0; + if (!passed) + fprintf(stderr, "clearing warm starts changed a fixed cone value\n"); + qp_problem_free(problem); + return passed; +} + +static int accepts_supported_and_rejects_empty_fixed_sections(void) +{ + int passed = 1; + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.verbose = 0; + parameters.termination_criteria.iteration_limit = 1; + + qp_problem_t *problem = make_empty_cone_problem(CONE_STANDARD_SOC); + if (!problem || set_cone_fixed(problem, 0, 1, 1.0) != 0) + passed = 0; + pdhcg_result_t *result = problem ? solve_qp_problem(problem, ¶meters) : NULL; + passed &= result != NULL; + pdhcg_result_free(result); + qp_problem_free(problem); + + problem = make_empty_cone_problem(CONE_EXPONENTIAL); + if (!problem || set_cone_fixed(problem, 0, 0, 0.0) != 0) + passed = 0; + result = problem ? solve_qp_problem(problem, ¶meters) : NULL; + passed &= result != NULL; + pdhcg_result_free(result); + qp_problem_free(problem); + + problem = make_empty_cone_problem(CONE_ROTATED_SOC); + if (!problem || set_cone_fixed(problem, 0, 1, 1.0) != 0) + passed = 0; + result = problem ? solve_qp_problem(problem, ¶meters) : NULL; + passed &= result != NULL; + pdhcg_result_free(result); + qp_problem_free(problem); + + problem = make_empty_cone_problem(CONE_STANDARD_SOC); + if (!problem || set_cone_fixed(problem, 0, 0, 2.0) != 0 || set_cone_fixed(problem, 0, 2, 1.0) != 0) + passed = 0; + result = problem ? solve_qp_problem(problem, ¶meters) : NULL; + passed &= result == NULL; + pdhcg_result_free(result); + qp_problem_free(problem); + + problem = make_empty_cone_problem(CONE_EXPONENTIAL); + if (!problem || set_cone_fixed(problem, 0, 0, 1.0) != 0 || set_cone_fixed(problem, 0, 2, 1.0) != 0) + passed = 0; + result = problem ? solve_qp_problem(problem, ¶meters) : NULL; + passed &= result == NULL; + pdhcg_result_free(result); + qp_problem_free(problem); + + problem = make_empty_cone_problem(CONE_ROTATED_SOC); + if (!problem || set_cone_fixed(problem, 0, 0, 1.0) != 0 || set_cone_fixed(problem, 0, 1, 0.0) != 0) + passed = 0; + result = problem ? solve_qp_problem(problem, ¶meters) : NULL; + passed &= result == NULL; + pdhcg_result_free(result); + qp_problem_free(problem); + + if (!passed) + fprintf(stderr, "fixed cone section validation did not match nonempty-section semantics\n"); + return passed; +} + +static double initial_soc_dual_residual(double matrix_scale) +{ + static const int row_ptr[] = {0, 1}; + static const int col_ind[] = {3}; + static const double objective[] = {0.0, 0.0, 1.0, 0.0}; + static const double rhs[] = {0.0}; + static const double primal_start[] = {0.0, 0.0, 1.0, 0.0}; + static const double dual_start[] = {0.0}; + static const double var_lb[] = {-INFINITY, -INFINITY, -INFINITY, 0.0}; + static const double var_ub[] = {INFINITY, INFINITY, INFINITY, 0.0}; + const cone_spec_t cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 0, + .v_dim = 1, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 1; + A.n = 4; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = &matrix_scale; + + qp_problem_t *problem = create_qp_problem( + objective, NULL, NULL, NULL, &A, rhs, rhs, var_lb, var_ub, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem) + return NAN; + set_start_values(problem, primal_start, dual_start); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.verbose = 0; + parameters.l_inf_ruiz_iterations = 0; + parameters.has_pock_chambolle_alpha = false; + parameters.bound_objective_rescaling = false; + parameters.termination_criteria.iteration_limit = 0; + + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + double residual = result ? result->relative_dual_residual : NAN; + pdhcg_result_free(result); + qp_problem_free(problem); + return residual; +} + +static int projected_gradient_uses_adaptive_step(void) +{ + double moderate_step_residual = initial_soc_dual_residual(1.0); + double large_step_residual = initial_soc_dual_residual(1e-3); + int passed = isfinite(moderate_step_residual) && isfinite(large_step_residual) && moderate_step_residual > 0.1 && + large_step_residual < 0.01 * moderate_step_residual; + if (!passed) + { + fprintf(stderr, + "conic projected-gradient residual did not track the adaptive step: moderate=%.9g large=%.9g\n", + moderate_step_residual, + large_step_residual); + } + return passed; +} + +static int recognizes_soc_with_only_zero_w_fixed_as_optimal(norm_type_t optimality_norm, int v_dim) +{ + static const int row_ptr[] = {0}; + double *objective = (double *)calloc((size_t)v_dim + 2, sizeof(double)); + double *primal_start = (double *)calloc((size_t)v_dim + 2, sizeof(double)); + if (!objective || !primal_start) + { + free(objective); + free(primal_start); + return 0; + } + objective[v_dim] = 1.0; + + matrix_desc_t A = {0}; + A.m = 0; + A.n = v_dim + 2; + A.fmt = matrix_csr; + A.data.csr.row_ptr = row_ptr; + const cone_spec_t cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 0, + .v_dim = v_dim, + .is_fixed = NULL, + }; + + qp_problem_t *problem = + create_qp_problem(objective, NULL, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem || set_cone_fixed(problem, 0, v_dim, 0.0) != 0) + { + qp_problem_free(problem); + free(objective); + free(primal_start); + return 0; + } + set_start_values(problem, primal_start, NULL); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.optimality_norm = optimality_norm; + parameters.verbose = 0; + parameters.l_inf_ruiz_iterations = 0; + parameters.has_pock_chambolle_alpha = false; + parameters.bound_objective_rescaling = false; + parameters.termination_evaluation_frequency = 1; + parameters.termination_criteria.eps_optimal_relative = 1e-8; + parameters.termination_criteria.eps_feasible_relative = 1e-8; + parameters.termination_criteria.iteration_limit = 1; + + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + int passed = result && result->termination_reason == TERMINATION_REASON_OPTIMAL && result->total_count == 0; + if (!passed && result) + { + fprintf(stderr, + "SOC with only w=0 fixed was not recognized as optimal: norm=%d v_dim=%d " + "status=%d iter=%d primal=%.9g dual=%.9g gap=%.9g\n", + (int)optimality_norm, + v_dim, + (int)result->termination_reason, + result->total_count, + result->relative_primal_residual, + result->relative_dual_residual, + result->relative_objective_gap); + } + + pdhcg_result_free(result); + qp_problem_free(problem); + free(objective); + free(primal_start); + return passed; +} + +static int solves_fixed_rsoc_with_large_initial_step(norm_type_t optimality_norm) +{ + /* + * Fixing s=t=1 reduces this rotated SOC to |x| <= sqrt(2). The tiny + * equality coefficient makes the initial primal step very large. At the + * feasible interior warm start x=0.5, an adaptive projected-gradient + * mapping is small but the normal-cone KKT residual must remain nonzero. + */ + static const int row_ptr[] = {0, 1}; + static const int col_ind[] = {1}; + static const double values[] = {1e-9}; + static const double objective[] = {-1.0, 0.0, 0.0}; + static const double rhs[] = {1e-9}; + static const double primal_start[] = {0.5, 1.0, 1.0}; + static const double dual_start[] = {0.0}; + const cone_spec_t cone = { + .type = CONE_ROTATED_SOC, + .start_idx = 0, + .v_dim = 1, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 1; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + + qp_problem_t *problem = + create_qp_problem(objective, NULL, NULL, NULL, &A, rhs, rhs, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem || set_cone_fixed(problem, 0, 1, 1.0) != 0 || set_cone_fixed(problem, 0, 2, 1.0) != 0) + { + qp_problem_free(problem); + return 0; + } + set_start_values(problem, primal_start, dual_start); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.optimality_norm = optimality_norm; + parameters.verbose = 0; + parameters.l_inf_ruiz_iterations = 0; + parameters.has_pock_chambolle_alpha = false; + parameters.bound_objective_rescaling = false; + parameters.termination_evaluation_frequency = 1; + parameters.termination_criteria.eps_optimal_relative = 1e-8; + parameters.termination_criteria.eps_feasible_relative = 1e-8; + parameters.termination_criteria.iteration_limit = 1; + + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + int passed = result && result->termination_reason == TERMINATION_REASON_ITERATION_LIMIT && result->total_count == 1; + if (!passed && result) + { + fprintf(stderr, + "large-step fixed-RSOC warm start was accepted: status=%d iter=%d x=%.17g " + "primal=%.9g dual=%.9g gap=%.9g\n", + (int)result->termination_reason, + result->total_count, + result->primal_solution[0], + result->relative_primal_residual, + result->relative_dual_residual, + result->relative_objective_gap); + } + + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int solves_fixed_soc_with_large_initial_step(norm_type_t optimality_norm) +{ + /* Fix z=1; the free standard-SOC section is v^2 + w^2 <= 1. */ + static const int row_ptr[] = {0, 1}; + static const int col_ind[] = {2}; + static const double values[] = {1e-9}; + static const double objective[] = {-1.0, 0.0, 0.0}; + static const double rhs[] = {1e-9}; + static const double primal_start[] = {0.5, 0.0, 1.0}; + static const double dual_start[] = {0.0}; + const cone_spec_t cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 0, + .v_dim = 1, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 1; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + + qp_problem_t *problem = + create_qp_problem(objective, NULL, NULL, NULL, &A, rhs, rhs, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem || set_cone_fixed(problem, 0, 2, 1.0) != 0) + { + qp_problem_free(problem); + return 0; + } + set_start_values(problem, primal_start, dual_start); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.optimality_norm = optimality_norm; + parameters.verbose = 0; + parameters.l_inf_ruiz_iterations = 0; + parameters.has_pock_chambolle_alpha = false; + parameters.bound_objective_rescaling = false; + parameters.termination_evaluation_frequency = 1; + parameters.termination_criteria.eps_optimal_relative = 1e-8; + parameters.termination_criteria.eps_feasible_relative = 1e-8; + parameters.termination_criteria.iteration_limit = 1; + + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + int passed = result && result->termination_reason == TERMINATION_REASON_ITERATION_LIMIT && result->total_count == 1; + if (!passed && result) + { + fprintf(stderr, + "large-step fixed-SOC warm start was accepted: status=%d iter=%d v=%.17g " + "primal=%.9g dual=%.9g gap=%.9g\n", + (int)result->termination_reason, + result->total_count, + result->primal_solution[0], + result->relative_primal_residual, + result->relative_dual_residual, + result->relative_objective_gap); + } + + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int solves_fixed_power_with_large_initial_step(norm_type_t optimality_norm) +{ + /* With x=y=1 and alpha=0.5, the free section is simply |z| <= 1. */ + static const int row_ptr[] = {0, 1}; + static const int col_ind[] = {0}; + static const double values[] = {1e-9}; + static const double objective[] = {0.0, 0.0, -1.0}; + static const double rhs[] = {1e-9}; + static const double primal_start[] = {1.0, 1.0, 0.5}; + static const double dual_start[] = {0.0}; + const cone_spec_t cone = { + .type = CONE_POWER, + .start_idx = 0, + .v_dim = 1, + .power_alpha = 0.5, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 1; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + + qp_problem_t *problem = + create_qp_problem(objective, NULL, NULL, NULL, &A, rhs, rhs, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem || set_cone_fixed(problem, 0, 0, 1.0) != 0 || set_cone_fixed(problem, 0, 1, 1.0) != 0) + { + qp_problem_free(problem); + return 0; + } + set_start_values(problem, primal_start, dual_start); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.optimality_norm = optimality_norm; + parameters.verbose = 0; + parameters.l_inf_ruiz_iterations = 0; + parameters.has_pock_chambolle_alpha = false; + parameters.bound_objective_rescaling = false; + parameters.termination_evaluation_frequency = 1; + parameters.termination_criteria.eps_optimal_relative = 1e-8; + parameters.termination_criteria.eps_feasible_relative = 1e-8; + parameters.termination_criteria.iteration_limit = 1; + + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + int passed = result && result->termination_reason == TERMINATION_REASON_ITERATION_LIMIT && result->total_count == 1; + if (!passed && result) + { + fprintf(stderr, + "large-step fixed-power warm start was accepted: status=%d iter=%d z=%.17g " + "primal=%.9g dual=%.9g gap=%.9g\n", + (int)result->termination_reason, + result->total_count, + result->primal_solution[2], + result->relative_primal_residual, + result->relative_dual_residual, + result->relative_objective_gap); + } + + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int solves_fixed_exp_with_large_initial_step(norm_type_t optimality_norm) +{ + /* Fix y=1 and x=0; minimizing z over z >= exp(x) has solution z=1. */ + static const int row_ptr[] = {0, 1}; + static const int col_ind[] = {0}; + static const double values[] = {1e-10}; + static const double objective[] = {0.0, 0.0, 1.0}; + static const double rhs[] = {0.0}; + static const double primal_start[] = {0.0, 1.0, 2.0}; + static const double dual_start[] = {0.0}; + const cone_spec_t cone = { + .type = CONE_EXPONENTIAL, + .start_idx = 0, + .v_dim = 1, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 1; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + + qp_problem_t *problem = + create_qp_problem(objective, NULL, NULL, NULL, &A, rhs, rhs, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem || set_cone_fixed(problem, 0, 1, 1.0) != 0) + { + qp_problem_free(problem); + return 0; + } + set_start_values(problem, primal_start, dual_start); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.optimality_norm = optimality_norm; + parameters.verbose = 0; + parameters.l_inf_ruiz_iterations = 0; + parameters.has_pock_chambolle_alpha = false; + parameters.bound_objective_rescaling = false; + parameters.termination_evaluation_frequency = 1; + parameters.termination_criteria.eps_optimal_relative = 1e-8; + parameters.termination_criteria.eps_feasible_relative = 1e-8; + parameters.termination_criteria.iteration_limit = 1; + + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + int passed = result && result->termination_reason == TERMINATION_REASON_ITERATION_LIMIT && result->total_count == 1; + if (!passed && result) + { + fprintf(stderr, + "large-step fixed-exp warm start was accepted: status=%d iter=%d z=%.17g " + "primal=%.9g dual=%.9g gap=%.9g\n", + (int)result->termination_reason, + result->total_count, + result->primal_solution[2], + result->relative_primal_residual, + result->relative_dual_residual, + result->relative_objective_gap); + } + + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +int main(void) +{ + /* + * min -z + * s.t. t = 2, y = 1 (fixed cone slot), (z, y, t) in K_exp. + * + * The warm start (0, 1, 2) is primal feasible and its reduced gradient + * satisfies the recession-cone sign checks, but it is not stationary. + * A conic termination test must continue to z = log(2). + */ + const int row_ptr[] = {0, 1}; + const int col_ind[] = {2}; + const double values[] = {1e-12}; + const double objective[] = {-1.0, 0.0, 0.0}; + const double rhs[] = {2e-12}; + const double primal_start[] = {0.0, 1.0, 2.0}; + const double dual_start[] = {0.0}; + const cone_spec_t cone = { + .type = CONE_EXPONENTIAL, + .start_idx = 0, + .v_dim = 1, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 1; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + + qp_problem_t *problem = + create_qp_problem(objective, NULL, NULL, NULL, &A, rhs, rhs, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem || set_cone_fixed(problem, 0, 1, 1.0) != 0) + { + qp_problem_free(problem); + return 1; + } + set_start_values(problem, primal_start, dual_start); + + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.verbose = 0; + parameters.l_inf_ruiz_iterations = 0; + parameters.has_pock_chambolle_alpha = false; + parameters.bound_objective_rescaling = false; + parameters.termination_criteria.eps_optimal_relative = 1e-8; + parameters.termination_criteria.eps_feasible_relative = 1e-8; + + pdhcg_result_t *result = solve_qp_problem(problem, ¶meters); + if (!result) + { + qp_problem_free(problem); + return 1; + } + + double z = result->primal_solution[0]; + int passed = result->termination_reason == TERMINATION_REASON_OPTIMAL && result->total_count > 0 && + fabs(z - log(2.0)) <= 1e-7; + if (!passed) + { + fprintf(stderr, + "conic KKT termination failed: status=%d iter=%d z=%.17g expected=%.17g " + "primal=%.9g dual=%.9g gap=%.9g\n", + (int)result->termination_reason, + result->total_count, + z, + log(2.0), + result->relative_primal_residual, + result->relative_dual_residual, + result->relative_objective_gap); + } + passed &= projected_gradient_uses_adaptive_step(); + const norm_type_t norms[] = {NORM_TYPE_L_INF, NORM_TYPE_L2}; + for (int norm = 0; norm < 2; ++norm) + { + passed &= recognizes_soc_with_only_zero_w_fixed_as_optimal(norms[norm], 1); + passed &= recognizes_soc_with_only_zero_w_fixed_as_optimal(norms[norm], 32); + passed &= recognizes_soc_with_only_zero_w_fixed_as_optimal(norms[norm], 32768); + passed &= solves_fixed_rsoc_with_large_initial_step(norms[norm]); + passed &= solves_fixed_soc_with_large_initial_step(norms[norm]); + passed &= solves_fixed_power_with_large_initial_step(norms[norm]); + passed &= solves_fixed_exp_with_large_initial_step(norms[norm]); + } + passed &= accepts_supported_and_rejects_empty_fixed_sections(); + passed &= preserves_fixed_values_when_clearing_warm_start(); + + pdhcg_result_free(result); + qp_problem_free(problem); + return passed ? 0 : 1; +} diff --git a/test/test_curtis_reid_scaling.c b/test/test_curtis_reid_scaling.c new file mode 100644 index 0000000..0104099 --- /dev/null +++ b/test/test_curtis_reid_scaling.c @@ -0,0 +1,344 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "pdhcg.h" +#include "preconditioner.h" +#include "solver_state.h" +#include +#include +#include + +static int failures = 0; + +static void check_close(const char *name, double actual, double expected, double tolerance) +{ + const double scale = fmax(1.0, fmax(fabs(actual), fabs(expected))); + if (!isfinite(actual) || fabs(actual - expected) > tolerance * scale) + { + fprintf(stderr, "%s: got %.17g, expected %.17g\n", name, actual, expected); + ++failures; + } +} + +static qp_problem_t *make_plain_problem(void) +{ + static const int row_ptr[] = {0, 2, 4}; + static const int col_ind[] = {0, 1, 0, 1}; + static const double values[] = {1e-6, 1e-3, 1e3, 1e6}; + static const double objective[] = {3.0, -4.0}; + static const double con_lb[] = {2.0, -5.0}; + static const double con_ub[] = {4.0, 8.0}; + static const double var_lb[] = {-2.0, -3.0}; + static const double var_ub[] = {7.0, 9.0}; + matrix_desc_t A = {0}; + A.m = 2; + A.n = 2; + A.fmt = matrix_csr; + A.data.csr.nnz = 4; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + return create_qp_problem( + objective, NULL, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 0, NULL, NULL, NULL, 0, NULL); +} + +static qp_problem_t *make_cone_problem(void) +{ + static const int row_ptr[] = {0, 4}; + static const int col_ind[] = {0, 1, 2, 3}; + static const double values[] = {162754.79141900392, 1.0, 7.38905609893065, 54.598150033144236}; + static const double objective[] = {0.0, 0.0, 0.0, 0.0}; + static const double con_lb[] = {0.0}; + static const double con_ub[] = {0.0}; + const cone_spec_t cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 1, + .v_dim = 1, + .power_alpha = 0.0, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 1; + A.n = 4; + A.fmt = matrix_csr; + A.data.csr.nnz = 4; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + return create_qp_problem( + objective, NULL, NULL, NULL, &A, con_lb, con_ub, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); +} + +static pdhg_parameters_t curtis_reid_only_parameters(void) +{ + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.curtis_reid_iterations = 20; + params.l_inf_ruiz_iterations = 0; + params.has_pock_chambolle_alpha = false; + params.bound_objective_rescaling = false; + return params; +} + +static void test_plain_scaling(void) +{ + qp_problem_t *problem = make_plain_problem(); + if (!problem) + { + fprintf(stderr, "failed to create plain scaling problem\n"); + ++failures; + return; + } + + pdhg_parameters_t params = curtis_reid_only_parameters(); + rescale_info_t *info = rescale_problem(¶ms, problem); + if (!info) + { + fprintf(stderr, "plain Curtis-Reid scaling returned NULL\n"); + ++failures; + qp_problem_free(problem); + return; + } + + for (int row = 0; row < problem->num_constraints; ++row) + { + for (int nz = problem->constraint_matrix->row_ptr[row]; nz < problem->constraint_matrix->row_ptr[row + 1]; ++nz) + { + const int col = problem->constraint_matrix->col_ind[nz]; + const double expected = + problem->constraint_matrix->val[nz] / (info->con_rescale[row] * info->var_rescale[col]); + check_close("scaled A equivalence", info->scaled_problem->constraint_matrix->val[nz], expected, 1e-12); + check_close( + "Curtis-Reid unit magnitude", fabs(info->scaled_problem->constraint_matrix->val[nz]), 1.0, 1e-12); + } + check_close("constraint lower bound", + info->scaled_problem->constraint_lower_bound[row], + problem->constraint_lower_bound[row] / info->con_rescale[row], + 1e-12); + check_close("constraint upper bound", + info->scaled_problem->constraint_upper_bound[row], + problem->constraint_upper_bound[row] / info->con_rescale[row], + 1e-12); + } + for (int col = 0; col < problem->num_variables; ++col) + { + check_close("objective scaling", + info->scaled_problem->objective_vector[col], + problem->objective_vector[col] / info->var_rescale[col], + 1e-12); + check_close("variable lower bound", + info->scaled_problem->variable_lower_bound[col], + problem->variable_lower_bound[col] * info->var_rescale[col], + 1e-12); + check_close("variable upper bound", + info->scaled_problem->variable_upper_bound[col], + problem->variable_upper_bound[col] * info->var_rescale[col], + 1e-12); + } + + rescale_info_free(info); + qp_problem_free(problem); +} + +static void test_cone_block_scaling(void) +{ + qp_problem_t *problem = make_cone_problem(); + if (!problem) + { + fprintf(stderr, "failed to create cone scaling problem\n"); + ++failures; + return; + } + + pdhg_parameters_t params = curtis_reid_only_parameters(); + rescale_info_t *cone_preserving = rescale_problem(¶ms, problem); + if (!cone_preserving) + { + fprintf(stderr, "cone-preserving Curtis-Reid scaling returned NULL\n"); + ++failures; + qp_problem_free(problem); + return; + } + check_close("cone-preserving row scale", cone_preserving->con_rescale[0], exp(4.5), 1e-12); + check_close("cone-preserving non-cone scale", cone_preserving->var_rescale[0], exp(7.5), 1e-12); + check_close("cone-preserving block minimizer", cone_preserving->var_rescale[1], exp(-2.5), 1e-12); + check_close( + "cone-preserving scale slot 2", cone_preserving->var_rescale[2], cone_preserving->var_rescale[1], 1e-14); + check_close( + "cone-preserving scale slot 3", cone_preserving->var_rescale[3], cone_preserving->var_rescale[1], 1e-14); + + params.use_cone_preserving_scaling = false; + rescale_info_t *coordinatewise = rescale_problem(¶ms, problem); + if (!coordinatewise) + { + fprintf(stderr, "coordinate-wise cone Curtis-Reid scaling returned NULL\n"); + ++failures; + } + else + { + for (int nz = 0; nz < problem->constraint_matrix_num_nonzeros; ++nz) + { + check_close("coordinate-wise cone unit magnitude", + fabs(coordinatewise->scaled_problem->constraint_matrix->val[nz]), + 1.0, + 1e-12); + } + if (coordinatewise->var_rescale[1] == coordinatewise->var_rescale[3]) + { + fprintf(stderr, "coordinate-wise cone scaling unexpectedly tied all slots\n"); + ++failures; + } + rescale_info_free(coordinatewise); + } + + rescale_info_free(cone_preserving); + qp_problem_free(problem); +} + +static qp_problem_t *make_phase_taper_problem(int length, int affine) +{ + int *row_ptr = (int *)malloc((size_t)(length + 1) * sizeof(int)); + int *col_ind = (int *)malloc((size_t)length * sizeof(int)); + double *values = (double *)malloc((size_t)length * sizeof(double)); + double *objective = (double *)calloc((size_t)length, sizeof(double)); + if (!row_ptr || !col_ind || !values || !objective) + { + free(row_ptr); + free(col_ind); + free(values); + free(objective); + return NULL; + } + for (int index = 0; index < length; ++index) + { + row_ptr[index] = index; + col_ind[index] = index; + values[index] = (double)(index + 1) * (double)(index + 1); + } + row_ptr[length] = length; + + matrix_desc_t diagonal = {0}; + diagonal.m = length; + diagonal.n = length; + diagonal.fmt = matrix_csr; + diagonal.data.csr.nnz = length; + diagonal.data.csr.row_ptr = row_ptr; + diagonal.data.csr.col_ind = col_ind; + diagonal.data.csr.vals = values; + cone_spec_t cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 0, + .v_dim = length - 2, + }; + + qp_problem_t *problem = NULL; + if (!affine) + { + problem = create_qp_problem( + objective, NULL, NULL, NULL, &diagonal, NULL, NULL, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + } + else + { + problem = create_qp_problem( + objective, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, &diagonal, NULL, 1, &cone); + } + + free(row_ptr); + free(col_ind); + free(values); + free(objective); + return problem; +} + +static pdhg_parameters_t phase_taper_parameters(int ruiz) +{ + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.curtis_reid_iterations = 0; + params.l_inf_ruiz_iterations = ruiz ? 1 : 0; + params.has_pock_chambolle_alpha = !ruiz; + params.pock_chambolle_alpha = 1.0; + params.bound_objective_rescaling = false; + params.use_cone_preserving_scaling = true; + return params; +} + +static void test_phase_taper_case(int length, int affine, int ruiz) +{ + qp_problem_t *problem = make_phase_taper_problem(length, affine); + if (!problem) + { + fprintf( + stderr, "failed to create %s phase-taper problem of length %d\n", affine ? "affine" : "variable", length); + ++failures; + return; + } + + pdhg_parameters_t params = phase_taper_parameters(ruiz); + rescale_info_t *info = rescale_problem(¶ms, problem); + if (!info) + { + fprintf(stderr, "%s phase-taper scaling returned NULL\n", ruiz ? "Ruiz" : "Pock-Chambolle"); + ++failures; + qp_problem_free(problem); + return; + } + + double sum_sq = (double)length * (double)(length + 1) * (double)(2 * length + 1) / 6.0; + double rms = sqrt(sum_sq / (double)length); + double block_max = (double)length; + double expected = ruiz ? (length <= 8 ? block_max : rms) : (length <= 8 ? rms : sqrt(block_max * rms)); + int start = affine ? problem->affine_cones.start_idx[0] : problem->cones.start_idx[0]; + const double *scaling = affine ? info->con_rescale : info->var_rescale; + for (int index = start; index < start + length; ++index) + check_close("phase-taper cone scale", scaling[index], expected, 1e-12); + + rescale_info_free(info); + qp_problem_free(problem); +} + +static void test_phase_taper_scaling(void) +{ + for (int length = 8; length <= 9; ++length) + { + for (int affine = 0; affine <= 1; ++affine) + { + test_phase_taper_case(length, affine, 1); + test_phase_taper_case(length, affine, 0); + } + } +} + +int main(void) +{ + pdhg_parameters_t defaults; + set_default_parameters(&defaults); + if (defaults.curtis_reid_iterations != 0) + { + fprintf(stderr, "default Curtis-Reid iterations: got %d, expected 0\n", defaults.curtis_reid_iterations); + ++failures; + } + if (!defaults.use_cone_preserving_scaling) + { + fprintf(stderr, "cone-preserving scaling must be enabled by default\n"); + ++failures; + } + + test_plain_scaling(); + test_cone_block_scaling(); + test_phase_taper_scaling(); + return failures == 0 ? 0 : 1; +} diff --git a/test/test_diag_q_cones.c b/test/test_diag_q_cones.c new file mode 100644 index 0000000..1624578 --- /dev/null +++ b/test/test_diag_q_cones.c @@ -0,0 +1,293 @@ +/* + * E2E tests for diagonal-Q PDHG with cone constraints. + * Test 1: standard SOC with diag Q, boundary-active solution. + * Test 2: rotated SOC with symmetric diag Q, interior unconstrained solution. + * Test 4: rotated SOC with asymmetric diag Q (Q_s != Q_t), boundary-active. + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +static int approx_eq(double a, double b, double tol) +{ + return fabs(a - b) <= tol * (1.0 + fabs(b)); +} + +static int run_test_soc_diag_q(void) +{ + printf("\n[Test 1] Standard SOC + diag Q\n"); + printf(" min (1/2) v^2 + (1/2) z^2 - 3v - 4z s.t. w=4, (v,w,z) in K_soc\n"); + + double aval[] = {1.0}; + int acol[] = {1}; + int arow[] = {0, 1}; + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = 1; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + double qval[] = {1.0, 1.0}; + int qcol[] = {0, 2}; + int qrow[] = {0, 1, 1, 2}; + matrix_desc_t Q; + memset(&Q, 0, sizeof(Q)); + Q.m = 3; + Q.n = 3; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 2; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-3.0, 0.0, -4.0}; + double var_lb[] = {-1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30}; + double con_lb[] = {4.0}; + double con_ub[] = {4.0}; + + cone_spec_t cones[] = {{.type = CONE_STANDARD_SOC, .start_idx = 0, .v_dim = 1, .is_fixed = NULL}}; + + qp_problem_t *prob = + create_qp_problem(c, &Q, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + if (!prob) + { + fprintf(stderr, " create_qp_problem returned NULL\n"); + return 1; + } + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 1; + params.termination_criteria.eps_optimal_relative = 1e-8; + params.termination_criteria.eps_feasible_relative = 1e-8; + params.termination_criteria.iteration_limit = 100000; + params.termination_criteria.time_sec_limit = 30.0; + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + fprintf(stderr, " solve_qp_problem returned NULL\n"); + qp_problem_free(prob); + return 1; + } + + double v = res->primal_solution[0], w = res->primal_solution[1], z = res->primal_solution[2]; + double cone_lhs = v * v + w * w; + double cone_rhs = z * z; + double cone_viol = cone_lhs - cone_rhs; + double lam_v = (v > 1e-9) ? (3.0 / v - 1.0) / 2.0 : 0.0; + double lam_z = (z > 1e-9) ? (1.0 - 4.0 / z) / 2.0 : 0.0; + double lam_mismatch = fabs(lam_v - lam_z); + + printf(" status=%d iter=%d obj=%.6f v=%.6f w=%.6f z=%.6f\n", + (int)res->termination_reason, + res->total_count, + res->primal_objective_value, + v, + w, + z); + printf(" cone violation (v^2+w^2-z^2) = %.3e (expect 0)\n", cone_viol); + printf(" KKT mismatch |lam(v)-lam(z)| = %.3e (expect 0)\n", lam_mismatch); + printf(" dual y_w = %.6f (expect ~0.64 at KKT)\n", res->dual_solution[0]); + + int pass = (res->termination_reason == TERMINATION_REASON_OPTIMAL) && approx_eq(w, 4.0, 1e-5) && + fabs(cone_viol) < 1e-5 && lam_mismatch < 1e-4; + printf(" %s\n", pass ? "PASS" : "FAIL"); + + pdhcg_result_free(res); + qp_problem_free(prob); + return pass ? 0 : 1; +} + +static int run_test_rsoc_diag_q(void) +{ + printf("\n[Test 2] Rotated SOC + diag Q symmetric (Q_s == Q_t)\n"); + printf(" min (1/2) v^2 + (1/2) s^2 + (1/2) t^2 - v - 3s - 3t s.t. (v,s,t) in K_rsoc\n"); + + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + int arow_empty[] = {0}; + A.m = 0; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 0; + A.data.csr.row_ptr = arow_empty; + A.data.csr.col_ind = NULL; + A.data.csr.vals = NULL; + + double qval[] = {1.0, 1.0, 1.0}; + int qcol[] = {0, 1, 2}; + int qrow[] = {0, 1, 2, 3}; + matrix_desc_t Q; + memset(&Q, 0, sizeof(Q)); + Q.m = 3; + Q.n = 3; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 3; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-1.0, -3.0, -3.0}; + double var_lb[] = {-1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30}; + + cone_spec_t cones[] = {{.type = CONE_ROTATED_SOC, .start_idx = 0, .v_dim = 1, .is_fixed = NULL}}; + + qp_problem_t *prob = + create_qp_problem(c, &Q, NULL, NULL, &A, NULL, NULL, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + if (!prob) + { + fprintf(stderr, " create_qp_problem returned NULL\n"); + return 1; + } + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 1; + params.termination_criteria.eps_optimal_relative = 1e-8; + params.termination_criteria.eps_feasible_relative = 1e-8; + params.termination_criteria.iteration_limit = 100000; + params.termination_criteria.time_sec_limit = 30.0; + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + fprintf(stderr, " solve_qp_problem returned NULL\n"); + qp_problem_free(prob); + return 1; + } + + double v = res->primal_solution[0], s = res->primal_solution[1], t = res->primal_solution[2]; + printf(" status=%d iter=%d obj=%.6f v=%.6f s=%.6f t=%.6f\n", + (int)res->termination_reason, + res->total_count, + res->primal_objective_value, + v, + s, + t); + double cone_viol = v * v - 2.0 * s * t; + printf(" cone slack v^2 - 2st = %.3e (expect <= 0)\n", cone_viol); + + int pass = (res->termination_reason == TERMINATION_REASON_OPTIMAL) && approx_eq(v, 1.0, 1e-4) && + approx_eq(s, 3.0, 1e-4) && approx_eq(t, 3.0, 1e-4) && (cone_viol < 1e-6); + printf(" %s (expected v=1, s=t=3, obj=-9.5)\n", pass ? "PASS" : "FAIL"); + + pdhcg_result_free(res); + qp_problem_free(prob); + return pass ? 0 : 1; +} + +static int run_test_rsoc_diag_q_asymmetric(void) +{ + printf("\n[Test 4] Rotated SOC + diag Q asymmetric (Q_s != Q_t), boundary active\n"); + printf(" min (1/2)(v^2 + s^2 + 4 t^2) - 4v - 4s - 4t s.t. (v,s,t) in K_rsoc\n"); + + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + int arow_empty[] = {0}; + A.m = 0; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 0; + A.data.csr.row_ptr = arow_empty; + A.data.csr.col_ind = NULL; + A.data.csr.vals = NULL; + + double qval[] = {1.0, 1.0, 4.0}; + int qcol[] = {0, 1, 2}; + int qrow[] = {0, 1, 2, 3}; + matrix_desc_t Q; + memset(&Q, 0, sizeof(Q)); + Q.m = 3; + Q.n = 3; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 3; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-4.0, -4.0, -4.0}; + double var_lb[] = {-1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30}; + + cone_spec_t cones[] = {{.type = CONE_ROTATED_SOC, .start_idx = 0, .v_dim = 1, .is_fixed = NULL}}; + + qp_problem_t *prob = + create_qp_problem(c, &Q, NULL, NULL, &A, NULL, NULL, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + if (!prob) + { + fprintf(stderr, " create_qp_problem returned NULL\n"); + return 1; + } + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 1; + params.termination_criteria.eps_optimal_relative = 1e-8; + params.termination_criteria.eps_feasible_relative = 1e-8; + params.termination_criteria.iteration_limit = 200000; + params.termination_criteria.time_sec_limit = 60.0; + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + fprintf(stderr, " solve_qp_problem returned NULL\n"); + qp_problem_free(prob); + return 1; + } + + double v = res->primal_solution[0]; + double s = res->primal_solution[1]; + double t = res->primal_solution[2]; + double cone_viol = v * v - 2.0 * s * t; + + /* Reference solution computed offline by Newton-bisection on the KKT system. */ + const double xi_ref = 0.11320819470084; + const double v_ref = 3.26153501744331; + const double s_ref = 4.28128575585834; + const double t_ref = 1.24233831570958; + const double obj_ref = -17.57031817800; + + printf(" status=%d iter=%d obj=%.6f v=%.6f s=%.6f t=%.6f\n", + (int)res->termination_reason, + res->total_count, + res->primal_objective_value, + v, + s, + t); + printf(" cone violation (v^2 - 2st) = %+.3e (expect ~0, |.| <= 1e-5)\n", cone_viol); + + double xi = (v > 1e-9) ? (4.0 / v - 1.0) / 2.0 : 0.0; + double kkt_s = s - 4.0 - 2.0 * xi * t; + double kkt_t = 4.0 * t - 4.0 - 2.0 * xi * s; + printf(" recovered xi=%.6f (ref %.6f)\n", xi, xi_ref); + printf(" KKT residual s - 4 - 2 xi t = %+.3e\n", kkt_s); + printf(" KKT residual 4t - 4 - 2 xi s = %+.3e\n", kkt_t); + printf(" reference (v*,s*,t*) = (%.5f, %.5f, %.5f), obj* = %.5f\n", v_ref, s_ref, t_ref, obj_ref); + + int pass = (res->termination_reason == TERMINATION_REASON_OPTIMAL) && (fabs(cone_viol) < 1e-5) && (xi >= -1e-6) && + approx_eq(v, v_ref, 1e-3) && approx_eq(s, s_ref, 1e-3) && approx_eq(t, t_ref, 1e-3) && (fabs(kkt_s) < 1e-3) && + (fabs(kkt_t) < 1e-3); + printf(" %s\n", pass ? "PASS" : "FAIL"); + + pdhcg_result_free(res); + qp_problem_free(prob); + return pass ? 0 : 1; +} + +int main(void) +{ + int fails = 0; + fails += run_test_soc_diag_q(); + fails += run_test_rsoc_diag_q(); + fails += run_test_rsoc_diag_q_asymmetric(); + printf("\n=== Summary: %s ===\n", fails == 0 ? "ALL PASSED" : "SOME FAILED"); + return fails; +} diff --git a/test/test_distributed_conic.c b/test/test_distributed_conic.c new file mode 100644 index 0000000..6654a98 --- /dev/null +++ b/test/test_distributed_conic.c @@ -0,0 +1,812 @@ +#include "pdhcg.h" +#include +#include +#include + +#ifdef PDHCG_COMPILE_DISTRIBUTED +#include + +static qp_problem_t *make_problem(void) +{ + static const int row_ptr[] = {0, 1, 2, 3}; + static const int col_ind[] = {1, 2, 3}; + static const double values[] = {1.0, 1.0, 1.0}; + static const double objective[] = {0.0, 0.0, 0.0, 0.0, 1.0, 0.0}; + static const double var_lb[] = {0.0, -INFINITY, -INFINITY, -INFINITY, -INFINITY, 0.0}; + static const double var_ub[] = {0.0, INFINITY, INFINITY, INFINITY, INFINITY, 0.0}; + static const double rhs[] = {3.0, 4.0, 0.0}; + const cone_spec_t cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 1, + .v_dim = 2, + .power_alpha = 0.0, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 3; + A.n = 6; + A.fmt = matrix_csr; + A.data.csr.nnz = 3; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + return create_qp_problem( + objective, NULL, NULL, NULL, &A, rhs, rhs, var_lb, var_ub, NULL, 1, &cone, NULL, NULL, 0, NULL); +} + +static qp_problem_t *make_fixed_rsoc_problem(void) +{ + static const int row_ptr[] = {0, 0, 0}; + static const int col_ind[] = {0}; + static const double values[] = {0.0}; + static const double objective[] = {-1.0, 0.0, 0.0, 0.0}; + static const double var_lb[] = {-INFINITY, -INFINITY, -INFINITY, 0.0}; + static const double var_ub[] = {INFINITY, INFINITY, INFINITY, 0.0}; + static const double zero[] = {0.0, 0.0}; + const cone_spec_t cone = { + .type = CONE_ROTATED_SOC, + .start_idx = 0, + .v_dim = 1, + .power_alpha = 0.0, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 2; + A.n = 4; + A.fmt = matrix_csr; + A.data.csr.nnz = 0; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + qp_problem_t *problem = create_qp_problem( + objective, NULL, NULL, NULL, &A, zero, zero, var_lb, var_ub, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem || set_cone_fixed(problem, 0, 1, 1.0) != 0 || set_cone_fixed(problem, 0, 2, 1.0) != 0) + { + qp_problem_free(problem); + return NULL; + } + return problem; +} + +static qp_problem_t *make_large_step_fixed_rsoc_problem(void) +{ + static const int row_ptr[] = {0, 1, 2}; + static const int col_ind[] = {1, 2}; + static const double values[] = {1e-9, 1e-9}; + static const double objective[] = {-1.0, 0.0, 0.0, 0.0}; + static const double rhs[] = {1e-9, 1e-9}; + static const double primal_start[] = {0.5, 1.0, 1.0, 0.0}; + static const double dual_start[] = {0.0, 0.0}; + static const double var_lb[] = {-INFINITY, -INFINITY, -INFINITY, 0.0}; + static const double var_ub[] = {INFINITY, INFINITY, INFINITY, 0.0}; + const cone_spec_t cone = { + .type = CONE_ROTATED_SOC, + .start_idx = 0, + .v_dim = 1, + .power_alpha = 0.0, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 2; + A.n = 4; + A.fmt = matrix_csr; + A.data.csr.nnz = 2; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + qp_problem_t *problem = create_qp_problem( + objective, NULL, NULL, NULL, &A, rhs, rhs, var_lb, var_ub, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem || set_cone_fixed(problem, 0, 1, 1.0) != 0 || set_cone_fixed(problem, 0, 2, 1.0) != 0) + { + qp_problem_free(problem); + return NULL; + } + set_start_values(problem, primal_start, dual_start); + return problem; +} + +static qp_problem_t *make_atomic_soc_problem(void) +{ + static const int row_ptr[] = {0, 1, 2, 3}; + static const int col_ind[] = {3, 4, 5}; + static const double values[] = {1.0, 1.0, 1.0}; + static const double objective[] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0}; + static const double var_lb[] = {0.0, 0.0, 0.0, -INFINITY, -INFINITY, -INFINITY, -INFINITY, 0.0, 0.0, 0.0}; + static const double var_ub[] = {0.0, 0.0, 0.0, INFINITY, INFINITY, INFINITY, INFINITY, 0.0, 0.0, 0.0}; + static const double rhs[] = {3.0, 4.0, 0.0}; + const cone_spec_t cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 3, + .v_dim = 2, + .power_alpha = 0.0, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = 3; + A.n = 10; + A.fmt = matrix_csr; + A.data.csr.nnz = 3; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + return create_qp_problem( + objective, NULL, NULL, NULL, &A, rhs, rhs, var_lb, var_ub, NULL, 1, &cone, NULL, NULL, 0, NULL); +} + +static qp_problem_t *make_exponential_problem(void) +{ + static const int row_ptr[] = {0, 1, 2, 3, 4}; + static const int col_ind[] = {0, 3, 6, 9}; + static const double values[] = {1.0, 1.0, 1.0, 1.0}; + static const double objective[] = { + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + }; + static const double rhs[] = {0.0, 0.0, 0.0, 0.0}; + cone_spec_t cones[4]; + for (int cone = 0; cone < 4; ++cone) + { + cones[cone].type = CONE_EXPONENTIAL; + cones[cone].start_idx = 3 * cone; + cones[cone].v_dim = 1; + cones[cone].power_alpha = 0.0; + cones[cone].is_fixed = NULL; + } + + matrix_desc_t A = {0}; + A.m = 4; + A.n = 12; + A.fmt = matrix_csr; + A.data.csr.nnz = 4; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + qp_problem_t *problem = + create_qp_problem(objective, NULL, NULL, NULL, &A, rhs, rhs, NULL, NULL, NULL, 4, cones, NULL, NULL, 0, NULL); + if (!problem) + return NULL; + for (int cone = 0; cone < 4; ++cone) + { + if (set_cone_fixed(problem, cone, 1, 1.0) != 0) + { + qp_problem_free(problem); + return NULL; + } + } + return problem; +} + +static qp_problem_t *make_power_problem(void) +{ + static const int row_ptr[] = {0, 2, 4, 6, 8}; + static const int col_ind[] = {0, 1, 3, 4, 6, 7, 9, 10}; + static const double values[] = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; + static const double objective[] = { + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + -1.0, + }; + static const double rhs[] = {2.0, 2.0, 2.0, 2.0}; + static const double alpha[] = {0.2, 0.35, 0.65, 0.8}; + cone_spec_t cones[4]; + for (int cone = 0; cone < 4; ++cone) + { + cones[cone].type = CONE_POWER; + cones[cone].start_idx = 3 * cone; + cones[cone].v_dim = 1; + cones[cone].power_alpha = alpha[cone]; + cones[cone].is_fixed = NULL; + } + + matrix_desc_t A = {0}; + A.m = 4; + A.n = 12; + A.fmt = matrix_csr; + A.data.csr.nnz = 8; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + return create_qp_problem( + objective, NULL, NULL, NULL, &A, rhs, rhs, NULL, NULL, NULL, 4, cones, NULL, NULL, 0, NULL); +} + +enum fixed_soc_endpoint +{ + FIX_SOC_NONE = 0, + FIX_SOC_W = 1, + FIX_SOC_Z = 2, +}; + +static qp_problem_t *make_large_soc_problem(int v_dim, int fixed_endpoint, double fixed_value) +{ + int n = v_dim + 2; + int *row_ptr = (int *)malloc((size_t)(v_dim + 1) * sizeof(int)); + int *col_ind = (int *)malloc((size_t)v_dim * sizeof(int)); + double *values = (double *)malloc((size_t)v_dim * sizeof(double)); + double *objective = (double *)calloc((size_t)n, sizeof(double)); + double *rhs = (double *)malloc((size_t)v_dim * sizeof(double)); + if (!row_ptr || !col_ind || !values || !objective || !rhs) + { + free(row_ptr); + free(col_ind); + free(values); + free(objective); + free(rhs); + return NULL; + } + + double value = 1.0 / sqrt((double)v_dim); + for (int i = 0; i < v_dim; ++i) + { + row_ptr[i] = i; + col_ind[i] = i; + values[i] = 1.0; + rhs[i] = value; + } + row_ptr[v_dim] = v_dim; + if (fixed_endpoint == FIX_SOC_W) + objective[n - 2] = 100.0; + else if (fixed_endpoint == FIX_SOC_Z) + objective[n - 2] = -1.0; + if (fixed_endpoint != FIX_SOC_Z) + objective[n - 1] = 1.0; + + cone_spec_t cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 0, + .v_dim = v_dim, + .power_alpha = 0.0, + .is_fixed = NULL, + }; + matrix_desc_t A = {0}; + A.m = v_dim; + A.n = n; + A.fmt = matrix_csr; + A.data.csr.nnz = v_dim; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + qp_problem_t *problem = + create_qp_problem(objective, NULL, NULL, NULL, &A, rhs, rhs, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (problem && fixed_endpoint != FIX_SOC_NONE && + set_cone_fixed(problem, 0, v_dim + fixed_endpoint - 1, fixed_value) != 0) + { + qp_problem_free(problem); + problem = NULL; + } + + free(row_ptr); + free(col_ind); + free(values); + free(objective); + free(rhs); + return problem; +} + +static qp_problem_t *make_affine_soc_problem(void) +{ + static const int row_ptr[] = {0, 0, 0, 1}; + static const int col_ind[] = {0}; + static const double values[] = {1.0}; + static const double objective[] = {1.0}; + static const double offset[] = {1.0, 0.0, 0.0}; + matrix_desc_t F = {0}; + F.m = 3; + F.n = 1; + F.fmt = matrix_csr; + F.data.csr.nnz = 1; + F.data.csr.row_ptr = row_ptr; + F.data.csr.col_ind = col_ind; + F.data.csr.vals = values; + cone_spec_t cone = {.type = CONE_STANDARD_SOC, .start_idx = 0, .v_dim = 1}; + return create_qp_problem( + objective, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, &F, offset, 1, &cone); +} + +static qp_problem_t *make_local_affine_soc_problem(void) +{ + static const int scalar_row_ptr[] = {0, 0, 0, 0}; + static const int affine_row_ptr[] = {0, 0, 0, 1}; + static const int col_ind[] = {0}; + static const double values[] = {1.0}; + static const double objective[] = {1.0}; + static const double offset[] = {1.0, 0.0, 0.0}; + matrix_desc_t A = {0}; + A.m = 3; + A.n = 1; + A.fmt = matrix_csr; + A.data.csr.row_ptr = scalar_row_ptr; + matrix_desc_t F = {0}; + F.m = 3; + F.n = 1; + F.fmt = matrix_csr; + F.data.csr.nnz = 1; + F.data.csr.row_ptr = affine_row_ptr; + F.data.csr.col_ind = col_ind; + F.data.csr.vals = values; + cone_spec_t cone = {.type = CONE_STANDARD_SOC, .start_idx = 0, .v_dim = 1}; + return create_qp_problem( + objective, NULL, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, 0, NULL, &F, offset, 1, &cone); +} + +static qp_problem_t *make_qcqp_problem(void) +{ + static const int row_ptr[] = {0, 0}; + static const int col_ind[] = {0}; + static const double values[] = {0.0}; + static const double objective[] = {-1.0}; + static const double con_lb[] = {-INFINITY}; + static const double con_ub[] = {1.0}; + matrix_desc_t A = {0}; + A.m = 1; + A.n = 1; + A.fmt = matrix_csr; + A.data.csr.nnz = 0; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + qp_problem_t *problem = create_qp_problem( + objective, NULL, NULL, NULL, &A, con_lb, con_ub, NULL, NULL, NULL, 0, NULL, NULL, NULL, 0, NULL); + if (!problem) + return NULL; + + problem->num_quadratic_constraints = 1; + problem->quadratic_constraint_row_indices = (int *)calloc(1, sizeof(int)); + problem->quadratic_constraint_matrix_num_nonzeros = (int *)calloc(1, sizeof(int)); + problem->quadratic_constraint_matrices = (CsrComponent **)calloc(1, sizeof(CsrComponent *)); + CsrComponent *Q = (CsrComponent *)calloc(1, sizeof(CsrComponent)); + if (!problem->quadratic_constraint_row_indices || !problem->quadratic_constraint_matrix_num_nonzeros || + !problem->quadratic_constraint_matrices || !Q) + { + free(Q); + qp_problem_free(problem); + return NULL; + } + Q->row_ptr = (int *)malloc(2 * sizeof(int)); + Q->col_ind = (int *)malloc(sizeof(int)); + Q->val = (double *)malloc(sizeof(double)); + if (!Q->row_ptr || !Q->col_ind || !Q->val) + { + free(Q->row_ptr); + free(Q->col_ind); + free(Q->val); + free(Q); + problem->quadratic_constraint_matrices[0] = NULL; + qp_problem_free(problem); + return NULL; + } + Q->row_ptr[0] = 0; + Q->row_ptr[1] = 1; + Q->col_ind[0] = 0; + Q->val[0] = 1.0; + problem->quadratic_constraint_row_indices[0] = 0; + problem->quadratic_constraint_matrix_num_nonzeros[0] = 1; + problem->quadratic_constraint_matrices[0] = Q; + return problem; +} + +int main(int argc, char **argv) +{ + MPI_Init(&argc, &argv); + int rank = 0; + int size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + if (size < 2 || size % 2 != 0) + { + if (rank == 0) + fprintf(stderr, "test_distributed_conic requires an even number of MPI ranks\n"); + MPI_Finalize(); + return 77; + } + + qp_problem_t *problem = rank == 0 ? make_problem() : NULL; + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.verbose = getenv("PDHCG_TEST_VERBOSE") ? 1 : 0; + parameters.grid_size.decided = true; + int all_column_grid = getenv("PDHCG_TEST_ALL_COLUMN_GRID") != NULL; + if (all_column_grid) + { + parameters.grid_size.row_dims = 1; + parameters.grid_size.col_dims = size; + } + else + { + parameters.grid_size.row_dims = size / 2; + parameters.grid_size.col_dims = 2; + } + parameters.partition_method = UNIFORM_PARTITION; + parameters.permute_method = FULL_RANDOM_PERMUTATION; + parameters.curtis_reid_iterations = 0; + parameters.l_inf_ruiz_iterations = 0; + parameters.has_pock_chambolle_alpha = false; + parameters.bound_objective_rescaling = false; + parameters.presolve = false; + parameters.sv_max_iter = 50; + parameters.sv_tol = 1e-3; + parameters.termination_evaluation_frequency = 20; + parameters.termination_criteria.eps_optimal_relative = 1e-7; + parameters.termination_criteria.eps_feasible_relative = 1e-7; + parameters.termination_criteria.iteration_limit = 1000000; + const char *time_limit = getenv("PDHCG_TEST_TIME_LIMIT"); + parameters.termination_criteria.time_sec_limit = time_limit ? atof(time_limit) : 30.0; + + pdhcg_result_t *result = solve_qp_problem_distributed(¶meters, problem); + int failed = 0; + if (rank == 0) + { + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || + fabs(result->primal_solution[0]) > 2e-4 || fabs(result->primal_solution[1] - 3.0) > 2e-4 || + fabs(result->primal_solution[2] - 4.0) > 2e-4 || fabs(result->primal_solution[3]) > 2e-4 || + fabs(result->primal_solution[4] - 5.0) > 2e-4 || fabs(result->primal_solution[5]) > 2e-4) + { + fprintf(stderr, "distributed SOC solve returned an incorrect solution\n"); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + + if (size == 2) + { + problem = rank == 0 ? make_affine_soc_problem() : NULL; + parameters.grid_size.row_dims = 2; + parameters.grid_size.col_dims = 1; + parameters.permute_method = NO_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || + fabs(result->primal_solution[0] - 1.0) > 2e-4 || result->relative_primal_residual > 2e-6 || + result->relative_dual_residual > 2e-6) + { + fprintf(stderr, + "distributed split affine SOC solve returned an incorrect solution " + "(status=%d, x=%.9g, rp=%.3e, rd=%.3e)\n", + result ? (int)result->termination_reason : -1, + result ? result->primal_solution[0] : NAN, + result ? result->relative_primal_residual : NAN, + result ? result->relative_dual_residual : NAN); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + parameters.grid_size.row_dims = all_column_grid ? 1 : size / 2; + parameters.grid_size.col_dims = all_column_grid ? size : 2; + } + + if (size == 2) + { + problem = rank == 0 ? make_local_affine_soc_problem() : NULL; + parameters.grid_size.row_dims = 2; + parameters.grid_size.col_dims = 1; + parameters.permute_method = NO_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || + fabs(result->primal_solution[0] - 1.0) > 2e-4 || result->relative_primal_residual > 2e-6 || + result->relative_dual_residual > 2e-6) + { + fprintf(stderr, + "distributed local affine SOC solve returned an incorrect solution " + "(status=%d, x=%.9g, rp=%.3e, rd=%.3e)\n", + result ? (int)result->termination_reason : -1, + result ? result->primal_solution[0] : NAN, + result ? result->relative_primal_residual : NAN, + result ? result->relative_dual_residual : NAN); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + parameters.grid_size.row_dims = all_column_grid ? 1 : size / 2; + parameters.grid_size.col_dims = all_column_grid ? size : 2; + } + + problem = rank == 0 ? make_atomic_soc_problem() : NULL; + parameters.permute_method = NO_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || + fabs(result->primal_solution[3] - 3.0) > 2e-4 || fabs(result->primal_solution[4] - 4.0) > 2e-4 || + fabs(result->primal_solution[5]) > 2e-4 || fabs(result->primal_solution[6] - 5.0) > 2e-4) + { + fprintf(stderr, "distributed atomic SOC solve returned an incorrect solution\n"); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + + if (!all_column_grid) + { + const int fixed_w_v_dim = 1025; + problem = rank == 0 ? make_large_soc_problem(fixed_w_v_dim, FIX_SOC_W, 0.0) : NULL; + parameters.permute_method = FULL_RANDOM_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + double expected_v = 1.0 / sqrt((double)fixed_w_v_dim); + double max_v_error = 0.0; + if (result) + { + for (int i = 0; i < fixed_w_v_dim; ++i) + max_v_error = fmax(max_v_error, fabs(result->primal_solution[i] - expected_v)); + } + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || max_v_error > 2e-4 || + fabs(result->primal_solution[fixed_w_v_dim]) > 1e-12 || + fabs(result->primal_solution[fixed_w_v_dim + 1] - 1.0) > 2e-4) + { + fprintf(stderr, "distributed fixed-zero-w SOC solve returned an incorrect solution\n"); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + + problem = rank == 0 ? make_large_soc_problem(fixed_w_v_dim, FIX_SOC_W, 0.75) : NULL; + parameters.permute_method = FULL_RANDOM_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + const double expected_v = 1.0 / sqrt((double)fixed_w_v_dim); + const double expected_z = 1.25; + double max_v_error = 0.0; + if (result) + { + for (int i = 0; i < fixed_w_v_dim; ++i) + max_v_error = fmax(max_v_error, fabs(result->primal_solution[i] - expected_v)); + } + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || max_v_error > 2e-4 || + fabs(result->primal_solution[fixed_w_v_dim] - 0.75) > 1e-12 || + fabs(result->primal_solution[fixed_w_v_dim + 1] - expected_z) > 2e-4) + { + fprintf(stderr, + "distributed fixed-nonzero-w SOC solve returned an incorrect solution " + "(status=%d, w=%.9g, z=%.9g)\n", + result ? (int)result->termination_reason : -1, + result ? result->primal_solution[fixed_w_v_dim] : NAN, + result ? result->primal_solution[fixed_w_v_dim + 1] : NAN); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + + problem = rank == 0 ? make_large_soc_problem(fixed_w_v_dim, FIX_SOC_Z, 2.0) : NULL; + parameters.permute_method = FULL_RANDOM_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + const double expected_v = 1.0 / sqrt((double)fixed_w_v_dim); + const double expected_w = sqrt(3.0); + double max_v_error = 0.0; + if (result) + { + for (int i = 0; i < fixed_w_v_dim; ++i) + max_v_error = fmax(max_v_error, fabs(result->primal_solution[i] - expected_v)); + } + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || max_v_error > 2e-4 || + fabs(result->primal_solution[fixed_w_v_dim] - expected_w) > 2e-4 || + fabs(result->primal_solution[fixed_w_v_dim + 1] - 2.0) > 1e-12) + { + fprintf(stderr, + "distributed fixed-z SOC solve returned an incorrect solution " + "(status=%d, w=%.9g, z=%.9g)\n", + result ? (int)result->termination_reason : -1, + result ? result->primal_solution[fixed_w_v_dim] : NAN, + result ? result->primal_solution[fixed_w_v_dim + 1] : NAN); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + + problem = rank == 0 ? make_fixed_rsoc_problem() : NULL; + parameters.permute_method = NO_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + const double expected = sqrt(2.0); + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || + fabs(result->primal_solution[0] - expected) > 2e-4 || fabs(result->primal_solution[1] - 1.0) > 1e-12 || + fabs(result->primal_solution[2] - 1.0) > 1e-12) + { + fprintf(stderr, "distributed fixed-endpoint RSOC solve returned an incorrect solution\n"); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + + parameters.termination_evaluation_frequency = 1; + parameters.termination_criteria.iteration_limit = 1; + const norm_type_t fixed_section_norms[] = {NORM_TYPE_L_INF, NORM_TYPE_L2}; + for (int norm = 0; norm < 2; ++norm) + { + problem = rank == 0 ? make_large_step_fixed_rsoc_problem() : NULL; + parameters.optimality_norm = fixed_section_norms[norm]; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + if (!result || result->termination_reason != TERMINATION_REASON_ITERATION_LIMIT || + result->total_count != 1) + { + fprintf(stderr, + "distributed large-step fixed-endpoint warm start was accepted with norm %d\n", + (int)fixed_section_norms[norm]); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + } + parameters.optimality_norm = NORM_TYPE_L_INF; + parameters.termination_evaluation_frequency = 20; + parameters.termination_criteria.iteration_limit = 1000000; + } + + problem = rank == 0 ? make_exponential_problem() : NULL; + parameters.permute_method = FULL_RANDOM_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + double max_error = 0.0; + double max_cone_violation = 0.0; + if (result) + { + for (int cone = 0; cone < 4; ++cone) + { + double z = result->primal_solution[3 * cone]; + double y = result->primal_solution[3 * cone + 1]; + double t = result->primal_solution[3 * cone + 2]; + max_error = fmax(max_error, fabs(z)); + max_error = fmax(max_error, fabs(y - 1.0)); + max_error = fmax(max_error, fabs(t - 1.0)); + max_cone_violation = fmax(max_cone_violation, y * exp(z / y) - t); + } + } + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || max_error > 2e-4 || + max_cone_violation > 2e-6) + { + fprintf(stderr, + "distributed exponential-cone solve returned an incorrect solution " + "(status=%d, max_error=%.3e, max_violation=%.3e)\n", + result ? (int)result->termination_reason : -1, + max_error, + max_cone_violation); + if (result) + { + for (int cone = 0; cone < 4; ++cone) + fprintf(stderr, + " cone %d: z=%.9g y=%.9g t=%.9g\n", + cone, + result->primal_solution[3 * cone], + result->primal_solution[3 * cone + 1], + result->primal_solution[3 * cone + 2]); + } + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + + problem = rank == 0 ? make_power_problem() : NULL; + parameters.permute_method = FULL_RANDOM_PERMUTATION; + parameters.optimality_norm = NORM_TYPE_L2; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + static const double alpha[] = {0.2, 0.35, 0.65, 0.8}; + double max_error = 0.0; + double max_cone_violation = 0.0; + if (result) + { + for (int cone = 0; cone < 4; ++cone) + { + double a = alpha[cone]; + double x_expected = 2.0 * a; + double y_expected = 2.0 * (1.0 - a); + double z_expected = pow(x_expected, a) * pow(y_expected, 1.0 - a); + double x = result->primal_solution[3 * cone + 0]; + double y = result->primal_solution[3 * cone + 1]; + double z = result->primal_solution[3 * cone + 2]; + max_error = fmax(max_error, fabs(x - x_expected)); + max_error = fmax(max_error, fabs(y - y_expected)); + max_error = fmax(max_error, fabs(z - z_expected)); + double cone_bound = (x > 0.0 && y > 0.0) ? pow(x, a) * pow(y, 1.0 - a) : 0.0; + max_cone_violation = fmax(max_cone_violation, fabs(z) - cone_bound); + } + } + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || max_error > 2e-4 || + max_cone_violation > 2e-6) + { + fprintf(stderr, + "distributed power-cone solve returned an incorrect solution " + "(status=%d, max_error=%.3e, max_violation=%.3e)\n", + result ? (int)result->termination_reason : -1, + max_error, + max_cone_violation); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + parameters.optimality_norm = NORM_TYPE_L_INF; + + if (!all_column_grid) + { + problem = rank == 0 ? make_qcqp_problem() : NULL; + parameters.permute_method = NO_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || result->num_variables != 1 || + fabs(result->primal_solution[0] - 1.0) > 2e-4) + { + fprintf(stderr, "distributed QCQP reformulation returned an incorrect solution\n"); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + } + + const int large_v_dim = 1025; + problem = rank == 0 ? make_large_soc_problem(large_v_dim, FIX_SOC_NONE, 0.0) : NULL; + parameters.permute_method = FULL_RANDOM_PERMUTATION; + result = solve_qp_problem_distributed(¶meters, problem); + if (rank == 0) + { + double expected_v = 1.0 / sqrt((double)large_v_dim); + double max_v_error = 0.0; + if (result) + { + for (int i = 0; i < large_v_dim; ++i) + max_v_error = fmax(max_v_error, fabs(result->primal_solution[i] - expected_v)); + } + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL || max_v_error > 2e-4 || + fabs(result->primal_solution[large_v_dim]) > 2e-4 || + fabs(result->primal_solution[large_v_dim + 1] - 1.0) > 2e-4) + { + fprintf(stderr, "distributed large SOC solve returned an incorrect solution\n"); + failed = 1; + } + pdhcg_result_free(result); + qp_problem_free(problem); + } + + MPI_Bcast(&failed, 1, MPI_INT, 0, MPI_COMM_WORLD); + MPI_Finalize(); + return failed; +} + +#else +int main(void) +{ + return 0; +} +#endif diff --git a/test/test_fisher.c b/test/test_fisher.c new file mode 100644 index 0000000..55201ee --- /dev/null +++ b/test/test_fisher.c @@ -0,0 +1,701 @@ +/* + * End-to-end test: Fisher quasi-linear market solved via exponential-cone + * formulation through the PDHCG conic API. Builds a random sparse buyer/ + * good utility instance, solves the min form, and verifies cone feasibility + * (y*exp(z/y) <= t) plus the y_i = 1 constraint at the returned solution. + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include +#include +#include + +#ifdef PDHCG_COMPILE_DISTRIBUTED +#include +#endif + +static int rand_int(int lo, int hi) +{ + return lo + rand() % (hi - lo + 1); +} +static double rand_unit(void) +{ + return (double)rand() / RAND_MAX; +} + +typedef struct +{ + int n; + int m; + int nnz; + int *row_ptr; + int *col_ind; + double *val; +} sparse_u_t; + +static int cache_path(char *buf, size_t bufsz, int n, int m, double density, unsigned seed) +{ + const char *root = getenv("FISHER_CACHE_DIR"); + char default_root[512]; + if (!root || !root[0]) + { + const char *tmpdir = getenv("TMPDIR"); + if (!tmpdir || !tmpdir[0]) + tmpdir = "/tmp"; + int written = snprintf(default_root, sizeof(default_root), "%s/pdhcg-fisher", tmpdir); + if (written < 0 || (size_t)written >= sizeof(default_root)) + return 0; + root = default_root; + } + if (mkdir(root, 0700) != 0 && errno != EEXIST) + return 0; + int written = snprintf(buf, bufsz, "%s/fisher_n%d_m%d_d%g_s%u.bin", root, n, m, density, seed); + return written >= 0 && (size_t)written < bufsz; +} + +#define CHECK_READ(buf, sz, n, f) \ + do \ + { \ + size_t _got = fread((buf), (sz), (n), (f)); \ + if (_got != (size_t)(n)) \ + { \ + fprintf(stderr, "[cache] short read at %s:%d (got %zu/%zu)\n", __FILE__, __LINE__, _got, (size_t)(n)); \ + fclose(f); \ + return 0; \ + } \ + } while (0) + +static int try_load_cache(int n, int m, double density, unsigned seed, sparse_u_t *u_out, double **w_out) +{ + char path[512]; + if (!cache_path(path, sizeof(path), n, m, density, seed)) + return 0; + FILE *f = fopen(path, "rb"); + if (!f) + return 0; + int hn, hm, hnnz; + double hd; + unsigned hs; + CHECK_READ(&hn, sizeof(int), 1, f); + if (hn != n) + { + fclose(f); + return 0; + } + CHECK_READ(&hm, sizeof(int), 1, f); + if (hm != m) + { + fclose(f); + return 0; + } + CHECK_READ(&hd, sizeof(double), 1, f); + if (hd != density) + { + fclose(f); + return 0; + } + CHECK_READ(&hs, sizeof(unsigned), 1, f); + if (hs != seed) + { + fclose(f); + return 0; + } + CHECK_READ(&hnnz, sizeof(int), 1, f); + int *row_ptr = (int *)malloc((n + 1) * sizeof(int)); + int *col_ind = (int *)malloc((size_t)hnnz * sizeof(int)); + double *val = (double *)malloc((size_t)hnnz * sizeof(double)); + double *w = (double *)malloc((size_t)n * sizeof(double)); + if (!row_ptr || !col_ind || !val || !w) + { + fprintf(stderr, "[cache] malloc failed\n"); + fclose(f); + return 0; + } + CHECK_READ(row_ptr, sizeof(int), n + 1, f); + CHECK_READ(col_ind, sizeof(int), hnnz, f); + CHECK_READ(val, sizeof(double), hnnz, f); + CHECK_READ(w, sizeof(double), n, f); + fclose(f); + u_out->n = n; + u_out->m = m; + u_out->nnz = hnnz; + u_out->row_ptr = row_ptr; + u_out->col_ind = col_ind; + u_out->val = val; + *w_out = w; + fprintf(stderr, "[cache] loaded %s (nnz=%d)\n", path, hnnz); + return 1; +} + +#define CHECK_WRITE(buf, sz, n, f, path) \ + do \ + { \ + size_t _got = fwrite((buf), (sz), (n), (f)); \ + if (_got != (size_t)(n)) \ + { \ + fprintf(stderr, \ + "[cache] short write at %s:%d (got %zu/%zu) -- removing %s\n", \ + __FILE__, \ + __LINE__, \ + _got, \ + (size_t)(n), \ + (path)); \ + fclose(f); \ + remove(path); \ + return; \ + } \ + } while (0) + +static void save_cache(int n, int m, double density, unsigned seed, const sparse_u_t *u, const double *w) +{ + char path[512]; + if (!cache_path(path, sizeof(path), n, m, density, seed)) + return; + FILE *f = fopen(path, "wb"); + if (!f) + { + fprintf(stderr, "[cache] could not open %s for writing\n", path); + return; + } + CHECK_WRITE(&u->n, sizeof(int), 1, f, path); + CHECK_WRITE(&u->m, sizeof(int), 1, f, path); + CHECK_WRITE(&density, sizeof(double), 1, f, path); + CHECK_WRITE(&seed, sizeof(unsigned), 1, f, path); + CHECK_WRITE(&u->nnz, sizeof(int), 1, f, path); + CHECK_WRITE(u->row_ptr, sizeof(int), n + 1, f, path); + CHECK_WRITE(u->col_ind, sizeof(int), u->nnz, f, path); + CHECK_WRITE(u->val, sizeof(double), u->nnz, f, path); + CHECK_WRITE(w, sizeof(double), n, f, path); + if (fclose(f) != 0) + { + fprintf(stderr, "[cache] fclose failed -- removing %s\n", path); + remove(path); + return; + } + fprintf(stderr, "[cache] wrote %s (nnz=%d)\n", path, u->nnz); +} + +static sparse_u_t generate_u(int n, int m, double density, unsigned seed) +{ + srand(seed); + sparse_u_t u; + u.n = n; + u.m = m; + int max_nnz = (int)((double)n * m * density * 1.5 + n); + int *row_ptr = (int *)malloc((n + 1) * sizeof(int)); + int *col_ind = (int *)malloc(max_nnz * sizeof(int)); + double *val = (double *)malloc(max_nnz * sizeof(double)); + + int cnt = 0; + row_ptr[0] = 0; + int *picked = (int *)calloc(m, sizeof(int)); + for (int i = 0; i < n; ++i) + { + memset(picked, 0, m * sizeof(int)); + int row_nnz = 0; + for (int j = 0; j < m; ++j) + { + if (rand_unit() < density) + { + if (!picked[j]) + { + col_ind[cnt] = j; + val[cnt] = rand_unit() + 0.1; + cnt++; + picked[j] = 1; + row_nnz++; + } + } + } + if (row_nnz == 0) + { + int j = rand_int(0, m - 1); + col_ind[cnt] = j; + val[cnt] = rand_unit() + 0.1; + cnt++; + } + row_ptr[i + 1] = cnt; + } + free(picked); + + /* Every good must have at least one buyer for the market to be feasible. */ + int *good_seen = (int *)calloc(m, sizeof(int)); + for (int k = 0; k < cnt; ++k) + good_seen[col_ind[k]] = 1; + int extra_alloc = max_nnz - cnt; + for (int j = 0; j < m && extra_alloc > 0; ++j) + { + if (!good_seen[j]) + { + fprintf(stderr, "[generator] good %d has no buyer; raise density.\n", j); + free(row_ptr); + free(col_ind); + free(val); + free(good_seen); + u.n = 0; + return u; + } + } + free(good_seen); + + u.nnz = cnt; + u.row_ptr = row_ptr; + u.col_ind = col_ind; + u.val = val; + return u; +} + +static void free_u(sparse_u_t *u) +{ + free(u->row_ptr); + free(u->col_ind); + free(u->val); +} + +int main(int argc, char **argv) +{ + int n = (argc > 1) ? atoi(argv[1]) : 50; + int m = (argc > 2) ? atoi(argv[2]) : 20; + double density = (argc > 3) ? atof(argv[3]) : 0.2; + unsigned seed = (argc > 4) ? (unsigned)atoi(argv[4]) : 1u; + double eps = (argc > 5) ? atof(argv[5]) : 1e-6; + double time_limit = (argc > 6) ? atof(argv[6]) : 300.0; + + int distributed = 0; + int rank = 0; + int world_size = 1; +#ifdef PDHCG_COMPILE_DISTRIBUTED + int initialized_mpi = 0; + const char *distributed_env = getenv("PDHCG_FISHER_DISTRIBUTED"); + distributed = distributed_env && atoi(distributed_env) != 0; + if (distributed) + { + MPI_Initialized(&initialized_mpi); + if (!initialized_mpi) + MPI_Init(&argc, &argv); + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &world_size); + } +#endif + + pdhg_parameters_t params; + set_default_parameters(¶ms); + { + const char *vs = getenv("PDHG_VERBOSE"); + params.verbose = vs ? atoi(vs) : 1; + } + params.termination_criteria.eps_optimal_relative = eps; + params.termination_criteria.eps_feasible_relative = eps; + params.termination_criteria.time_sec_limit = time_limit; + params.termination_criteria.iteration_limit = 200000; + params.feasibility_polishing = false; +#ifdef PDHCG_COMPILE_DISTRIBUTED + if (distributed) + { + params.grid_size.decided = true; + params.grid_size.row_dims = 1; + params.grid_size.col_dims = world_size; + params.partition_method = NNZ_BALANCE_PARTITION; + params.permute_method = BLOCK_RANDOM_PERMUTATION; + params.permute_block_size = 256; + } + + if (distributed && rank != 0) + { + pdhcg_result_t *worker_result = solve_qp_problem_distributed(¶ms, NULL); + int failed = worker_result != NULL; + if (worker_result) + pdhcg_result_free(worker_result); + MPI_Bcast(&failed, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (!initialized_mpi) + MPI_Finalize(); + return failed; + } +#endif + + if (rank == 0) + { + printf("Fisher quasi-linear: n=%d buyers, m=%d goods, density=%.6g, eps=%.1e\n", n, m, density, eps); + if (distributed) + printf("Distributed grid: 1 x %d GPUs\n", world_size); + } + sparse_u_t u; + double *w = NULL; + if (!try_load_cache(n, m, density, seed, &u, &w)) + { + u = generate_u(n, m, density, seed); + if (u.n == 0) + return 1; + w = (double *)malloc(n * sizeof(double)); + for (int i = 0; i < n; ++i) + w[i] = rand_unit() + 0.1; + save_cache(n, m, density, seed, &u, w); + } + if (rank == 0) + printf("nnz(u)=%d\n", u.nnz); + + double *b = (double *)malloc(m * sizeof(double)); + double supply_each = 0.20 * (double)n; + for (int j = 0; j < m; ++j) + b[j] = supply_each; + + int nx = u.nnz; + int nv = n; + int nzyt = 3 * n; + int nvar = nx + nv + nzyt; + int x_off = 0; + int v_off = nx; + int cone_off = nx + nv; + if (rank == 0) + printf("variables: %d (x:%d, v:%d, zyt:%d)\n", nvar, nx, nv, nzyt); + + int ncon = m + n; + + int A_nnz = u.nnz + 2 * n + u.nnz; + int *A_row_ptr = (int *)malloc((ncon + 1) * sizeof(int)); + int *A_col_ind = (int *)malloc(A_nnz * sizeof(int)); + double *A_val = (double *)malloc(A_nnz * sizeof(double)); + + int *good_cnt = (int *)calloc(m, sizeof(int)); + for (int i = 0; i < n; ++i) + for (int k = u.row_ptr[i]; k < u.row_ptr[i + 1]; ++k) + good_cnt[u.col_ind[k]]++; + + A_row_ptr[0] = 0; + for (int j = 0; j < m; ++j) + A_row_ptr[j + 1] = A_row_ptr[j] + good_cnt[j]; + for (int i = 0; i < n; ++i) + { + int row_nnz = 2 + (u.row_ptr[i + 1] - u.row_ptr[i]); + A_row_ptr[m + i + 1] = A_row_ptr[m + i] + row_nnz; + } + if (A_row_ptr[ncon] != A_nnz) + { + fprintf(stderr, "row_ptr mismatch: got %d expected %d\n", A_row_ptr[ncon], A_nnz); + return 1; + } + + int *x_good = (int *)malloc(u.nnz * sizeof(int)); + int xk = 0; + for (int i = 0; i < n; ++i) + for (int k = u.row_ptr[i]; k < u.row_ptr[i + 1]; ++k) + x_good[xk++] = u.col_ind[k]; + + int *good_cursor = (int *)calloc(m, sizeof(int)); + for (int xk2 = 0; xk2 < u.nnz; ++xk2) + { + int j = x_good[xk2]; + int pos = A_row_ptr[j] + good_cursor[j]++; + A_col_ind[pos] = x_off + xk2; + A_val[pos] = 1.0; + } + free(good_cursor); + free(good_cnt); + + int xk_running = 0; + for (int i = 0; i < n; ++i) + { + int pos = A_row_ptr[m + i]; + for (int k = u.row_ptr[i]; k < u.row_ptr[i + 1]; ++k, ++xk_running) + { + A_col_ind[pos] = x_off + xk_running; + A_val[pos] = -u.val[k]; + pos++; + } + A_col_ind[pos] = v_off + i; + A_val[pos] = -1.0; + pos++; + A_col_ind[pos] = cone_off + 3 * i + 2; + A_val[pos] = 1.0; + } + free(x_good); + + double *c = (double *)calloc(nvar, sizeof(double)); + for (int i = 0; i < n; ++i) + { + c[v_off + i] = 1.0; + c[cone_off + 3 * i + 0] = -w[i]; + } + + double *var_lb = (double *)malloc(nvar * sizeof(double)); + double *var_ub = (double *)malloc(nvar * sizeof(double)); + for (int k = 0; k < nvar; ++k) + { + var_lb[k] = -1e30; + var_ub[k] = 1e30; + } + for (int k = 0; k < nx; ++k) + var_lb[x_off + k] = 0.0; + for (int i = 0; i < n; ++i) + var_lb[v_off + i] = 0.0; + + double *con_lb = (double *)malloc(ncon * sizeof(double)); + double *con_ub = (double *)malloc(ncon * sizeof(double)); + for (int j = 0; j < m; ++j) + { + con_lb[j] = b[j]; + con_ub[j] = b[j]; + } + for (int i = 0; i < n; ++i) + { + con_lb[m + i] = 0.0; + con_ub[m + i] = 0.0; + } + + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = ncon; + A.n = nvar; + A.fmt = matrix_csr; + A.data.csr.nnz = A_nnz; + A.data.csr.row_ptr = A_row_ptr; + A.data.csr.col_ind = A_col_ind; + A.data.csr.vals = A_val; + + cone_spec_t *cones = (cone_spec_t *)malloc(n * sizeof(cone_spec_t)); + for (int i = 0; i < n; ++i) + { + cones[i].type = CONE_EXPONENTIAL; + cones[i].start_idx = cone_off + 3 * i; + cones[i].v_dim = 1; + cones[i].is_fixed = NULL; + } + + qp_problem_t *prob = + create_qp_problem(c, NULL, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, n, cones, NULL, NULL, 0, NULL); + if (!prob) + { + fprintf(stderr, "create_qp_problem failed\n"); + return 1; + } + + for (int i = 0; i < n; ++i) + { + if (set_cone_fixed(prob, i, 1, 1.0) != 0) + return 1; + } + + pdhcg_result_t *res = NULL; +#ifdef PDHCG_COMPILE_DISTRIBUTED + if (distributed) + { + res = solve_qp_problem_distributed(¶ms, prob); + } + else +#endif + { + res = solve_qp_problem(prob, ¶ms); + } + + int failed = 0; + double max_cone_violation = 0.0; + double max_y_dev = 0.0; + if (rank == 0 && !res) + { + fprintf(stderr, "solve failed\n"); + failed = 1; + } + else if (rank == 0) + { + printf("\nSolver-reported time: %.6fs iter=%d status=%d (1=OPTIMAL)\n", + res->cumulative_time_sec, + res->total_count, + (int)res->termination_reason); + printf("Primal obj (min form): %.8f -> max form: %.8f\n", + res->primal_objective_value, + -res->primal_objective_value); + + /* + * Audit the returned point in the original, unscaled model. In + * particular, y=1 makes each exponential cone an epigraph + * exp(z) <= t. Its KKT condition is + * + * r_z + r_t exp(z) = 0, r_z <= 0, r_t >= 0, + * + * where r = c - A^T lambda. This condition is independent of the + * solver's internal residual implementation. + */ + double *reduced_gradient = (double *)malloc((size_t)nvar * sizeof(double)); + double max_linear_residual = 0.0; + double max_good_residual = 0.0; + double max_utility_residual = 0.0; + double max_rowwise_relative_residual = 0.0; + double max_box_kkt = 0.0; + double max_exp_kkt = 0.0; + double max_exp_complementarity = 0.0; + double independent_dual_objective = 0.0; + int independent_dual_finite = reduced_gradient != NULL; + if (!reduced_gradient) + { + fprintf(stderr, "KKT audit allocation failed\n"); + failed = 1; + } + else + { + memcpy(reduced_gradient, c, (size_t)nvar * sizeof(double)); + for (int row = 0; row < ncon; ++row) + { + double activity = 0.0; + double lambda = res->dual_solution[row]; + for (int p = A_row_ptr[row]; p < A_row_ptr[row + 1]; ++p) + { + int col = A_col_ind[p]; + activity += A_val[p] * res->primal_solution[col]; + reduced_gradient[col] -= A_val[p] * lambda; + } + double row_residual = fabs(activity - con_lb[row]); + if (row_residual > max_linear_residual) + max_linear_residual = row_residual; + if (row < m) + { + if (row_residual > max_good_residual) + max_good_residual = row_residual; + } + else if (row_residual > max_utility_residual) + { + max_utility_residual = row_residual; + } + double rowwise_relative = row_residual / (1.0 + fabs(con_lb[row])); + if (rowwise_relative > max_rowwise_relative_residual) + max_rowwise_relative_residual = rowwise_relative; + independent_dual_objective += lambda * con_lb[row]; + } + + for (int col = 0; col < cone_off; ++col) + { + double x = res->primal_solution[col]; + double r = reduced_gradient[col]; + double projected = fmax(var_lb[col], fmin(x - r, var_ub[col])); + double violation = fabs(x - projected); + if (violation > max_box_kkt) + max_box_kkt = violation; + + if (r < 0.0) + independent_dual_finite = 0; + } + + for (int i = 0; i < n; ++i) + { + int idx = cone_off + 3 * i; + double z = res->primal_solution[idx + 0]; + double y = res->primal_solution[idx + 1]; + double t = res->primal_solution[idx + 2]; + double rz = reduced_gradient[idx + 0]; + double ry = reduced_gradient[idx + 1]; + double rt = reduced_gradient[idx + 2]; + double ez = exp(z); + double stationarity = fabs(rz + rt * ez); + stationarity = fmax(stationarity, fmax(rz, 0.0)); + stationarity = fmax(stationarity, fmax(-rt, 0.0)); + if (stationarity > max_exp_kkt) + max_exp_kkt = stationarity; + double complementarity = fabs(rt * (t - ez)); + if (complementarity > max_exp_complementarity) + max_exp_complementarity = complementarity; + + independent_dual_objective += ry * y; + if (rt > 0.0 && rz < 0.0) + { + independent_dual_objective += rz * log(-rz / rt) - rz; + } + else if (rt > 0.0 && rz == 0.0) + { + /* The infimum is zero and is approached as z -> -inf. */ + } + else if (!(rt == 0.0 && rz == 0.0)) + { + independent_dual_finite = 0; + } + } + + double independent_gap = INFINITY; + if (independent_dual_finite) + { + independent_gap = fabs(res->primal_objective_value - independent_dual_objective) / + (1.0 + fabs(res->primal_objective_value) + fabs(independent_dual_objective)); + } + printf("Internal KKT: rel_primal=%.9g rel_dual=%.9g rel_gap=%.9g dual_obj=%.17g\n", + res->relative_primal_residual, + res->relative_dual_residual, + res->relative_objective_gap, + res->dual_objective_value); + printf("Independent KKT: linear_inf=%.9g good_inf=%.9g utility_inf=%.9g " + "rowwise_rel_inf=%.9g box_inf=%.9g exp_stationarity_inf=%.9g " + "exp_complementarity_inf=%.9g rel_gap=%.9g dual_obj=%.17g finite=%d\n", + max_linear_residual, + max_good_residual, + max_utility_residual, + max_rowwise_relative_residual, + max_box_kkt, + max_exp_kkt, + max_exp_complementarity, + independent_gap, + independent_dual_objective, + independent_dual_finite); + free(reduced_gradient); + } + + for (int i = 0; i < n; ++i) + { + double z = res->primal_solution[cone_off + 3 * i + 0]; + double y = res->primal_solution[cone_off + 3 * i + 1]; + double t = res->primal_solution[cone_off + 3 * i + 2]; + double yd = fabs(y - 1.0); + if (yd > max_y_dev) + max_y_dev = yd; + double lhs = (y > 0.0) ? y * exp(z / y) : ((z <= 0.0) ? 0.0 : INFINITY); + double viol = lhs - t; + if (viol > max_cone_violation) + max_cone_violation = viol; + } + printf("Exp cone max violation (y*exp(z/y) - t): %.3e\n", max_cone_violation); + printf("Max |y - 1|: %.3e\n", max_y_dev); + printf("FISHER_RESULT,n=%d,m=%d,density=%.8g,eps=%.8g,gpus=%d,time=%.9g,iter=%d,status=%d," + "objective=%.17g,cone_violation=%.9g,y_deviation=%.9g\n", + n, + m, + density, + eps, + distributed ? world_size : 1, + res->cumulative_time_sec, + res->total_count, + (int)res->termination_reason, + res->primal_objective_value, + max_cone_violation, + max_y_dev); + + if (distributed && + (res->termination_reason != TERMINATION_REASON_OPTIMAL || max_cone_violation > 5e-6 || max_y_dev > 5e-6)) + failed = 1; + } + + if (res) + pdhcg_result_free(res); + qp_problem_free(prob); + free(cones); + free(c); + free(var_lb); + free(var_ub); + free(con_lb); + free(con_ub); + free(A_row_ptr); + free(A_col_ind); + free(A_val); + free_u(&u); + free(w); + free(b); + +#ifdef PDHCG_COMPILE_DISTRIBUTED + if (distributed) + { + MPI_Bcast(&failed, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (!initialized_mpi) + MPI_Finalize(); + } +#endif + return failed; +} diff --git a/test/test_fixed_cone_sections.cu b/test/test_fixed_cone_sections.cu new file mode 100644 index 0000000..003ded7 --- /dev/null +++ b/test/test_fixed_cone_sections.cu @@ -0,0 +1,842 @@ +#include "internal_types.h" +#include "pdhcg_kernels.cuh" +#include "pdhcg_types.h" + +#include + +#include +#include +#include +#include + +static int cuda_ok(cudaError_t error, const char *label) +{ + if (error == cudaSuccess) + return 1; + std::fprintf(stderr, "%s: %s\n", label, cudaGetErrorString(error)); + return 0; +} + +static double unit_sample(int sample, int slot) +{ + return 0.5 + 0.5 * std::sin(1.61803398875 * (sample + 1) * (slot + 2)); +} + +static int cone_feasible(cone_type_t type, const double point[3]) +{ + if (type == CONE_STANDARD_SOC) + return std::hypot(point[0], point[1]) <= point[2] + 2e-9; + if (type == CONE_ROTATED_SOC) + return point[1] >= -2e-9 && point[2] >= -2e-9 && point[0] * point[0] <= 2.0 * point[1] * point[2] + 2e-9; + if (point[1] > 0.0 && point[2] > 0.0) + return std::log(point[1]) + point[0] / point[1] <= std::log(point[2]) + 2e-9; + return std::fabs(point[1]) <= 2e-9 && point[0] <= 2e-9 && point[2] >= -2e-9; +} + +static void make_soc_candidate(int mask, int sample, const double fixed[3], double candidate[3]) +{ + candidate[0] = (mask & 1) ? fixed[0] : 1.8 * (2.0 * unit_sample(sample, 0) - 1.0); + candidate[1] = (mask & 2) ? fixed[1] : 1.8 * (2.0 * unit_sample(sample, 1) - 1.0); + if (mask & 4) + { + candidate[2] = fixed[2]; + double fixed_norm2 = + ((mask & 1) ? candidate[0] * candidate[0] : 0.0) + ((mask & 2) ? candidate[1] * candidate[1] : 0.0); + double free_norm2 = + ((mask & 1) ? 0.0 : candidate[0] * candidate[0]) + ((mask & 2) ? 0.0 : candidate[1] * candidate[1]); + double radius2 = std::fmax(0.0, candidate[2] * candidate[2] - fixed_norm2); + if (free_norm2 > 0.8 * radius2 && free_norm2 > 0.0) + { + double scale = std::sqrt(0.8 * radius2 / free_norm2); + if (!(mask & 1)) + candidate[0] *= scale; + if (!(mask & 2)) + candidate[1] *= scale; + } + } + else + { + candidate[2] = std::hypot(candidate[0], candidate[1]) + 0.1 + unit_sample(sample, 2); + } +} + +static void make_rsoc_candidate(int mask, int sample, const double fixed[3], double candidate[3]) +{ + candidate[0] = (mask & 1) ? fixed[0] : 1.8 * (2.0 * unit_sample(sample, 0) - 1.0); + bool fixed_s = (mask & 2) != 0; + bool fixed_t = (mask & 4) != 0; + candidate[1] = fixed_s ? fixed[1] : 0.0; + candidate[2] = fixed_t ? fixed[2] : 0.0; + if (fixed_s && fixed_t) + { + if (!(mask & 1)) + { + double radius = std::sqrt(2.0 * candidate[1] * candidate[2]); + candidate[0] = (2.0 * unit_sample(sample, 0) - 1.0) * 0.8 * radius; + } + } + else if (fixed_s) + { + candidate[2] = candidate[0] * candidate[0] / (2.0 * candidate[1]) + 0.1 + unit_sample(sample, 2); + } + else if (fixed_t) + { + candidate[1] = candidate[0] * candidate[0] / (2.0 * candidate[2]) + 0.1 + unit_sample(sample, 1); + } + else + { + candidate[1] = 0.2 + 1.5 * unit_sample(sample, 1); + candidate[2] = candidate[0] * candidate[0] / (2.0 * candidate[1]) + 0.1 + unit_sample(sample, 2); + } +} + +static void make_exp_candidate(int mask, int sample, const double fixed[3], double candidate[3]) +{ + bool fixed_x = (mask & 1) != 0; + bool fixed_y = (mask & 2) != 0; + bool fixed_z = (mask & 4) != 0; + candidate[0] = fixed_x ? fixed[0] : 0.0; + candidate[1] = fixed_y ? fixed[1] : 0.2 + 1.2 * unit_sample(sample, 1); + candidate[2] = fixed_z ? fixed[2] : 0.0; + + if (fixed_z) + { + if (!fixed_x) + { + double upper = candidate[1] * (std::log(candidate[2]) - std::log(candidate[1])); + candidate[0] = upper - 0.05 - unit_sample(sample, 0); + } + } + else + { + if (!fixed_x) + candidate[0] = 1.2 * (2.0 * unit_sample(sample, 0) - 1.0); + double boundary = candidate[1] * std::exp(candidate[0] / candidate[1]); + candidate[2] = boundary + 0.05 + unit_sample(sample, 2); + } +} + +static int run_mask(cone_type_t type, int mask, int diagonal_q, int use_block) +{ + const double scale[3] = {0.7, 1.6, 2.3}; + const double q_diag[3] = {0.5, 2.0, 4.0}; + const double tau = 0.7; + const double soc_fixed[3] = {0.25, -0.35, 1.4}; + const double rsoc_fixed[3] = {0.2, 1.2, 1.1}; + const double exp_fixed[3] = {0.1, 1.0, 2.0}; + const double *fixed = type == CONE_STANDARD_SOC ? soc_fixed : type == CONE_ROTATED_SOC ? rsoc_fixed : exp_fixed; + const double raw_input[3] = {2.0, -1.5, -0.6}; + double input[3]; + char fixed_mask[3]; + for (int slot = 0; slot < 3; ++slot) + { + fixed_mask[slot] = (mask >> slot) & 1; + double actual = fixed_mask[slot] ? fixed[slot] : raw_input[slot]; + input[slot] = scale[slot] * actual; + } + + double *d_point = nullptr; + double *d_reflected = nullptr; + double *d_current = nullptr; + double *d_scale = nullptr; + double *d_q = nullptr; + double *d_warm = nullptr; + int *d_start = nullptr; + int *d_dim = nullptr; + char *d_fixed = nullptr; + int start = 0; + int dim = 1; + int ok = cuda_ok(cudaMalloc(&d_point, 3 * sizeof(double)), "cudaMalloc(point)") && + cuda_ok(cudaMalloc(&d_scale, 3 * sizeof(double)), "cudaMalloc(scale)") && + cuda_ok(cudaMalloc(&d_warm, sizeof(double)), "cudaMalloc(warm)") && + cuda_ok(cudaMalloc(&d_start, sizeof(int)), "cudaMalloc(start)") && + cuda_ok(cudaMalloc(&d_dim, sizeof(int)), "cudaMalloc(dim)") && + cuda_ok(cudaMalloc(&d_fixed, 3 * sizeof(char)), "cudaMalloc(fixed)"); + if (diagonal_q) + ok &= cuda_ok(cudaMalloc(&d_reflected, 3 * sizeof(double)), "cudaMalloc(reflected)") && + cuda_ok(cudaMalloc(&d_current, 3 * sizeof(double)), "cudaMalloc(current)") && + cuda_ok(cudaMalloc(&d_q, 3 * sizeof(double)), "cudaMalloc(q)"); + if (!ok) + goto cleanup; + + ok &= cuda_ok(cudaMemcpy(d_point, input, sizeof(input), cudaMemcpyHostToDevice), "copy point") && + cuda_ok(cudaMemcpy(d_scale, scale, sizeof(scale), cudaMemcpyHostToDevice), "copy scale") && + cuda_ok(cudaMemcpy(d_start, &start, sizeof(start), cudaMemcpyHostToDevice), "copy start") && + cuda_ok(cudaMemcpy(d_dim, &dim, sizeof(dim), cudaMemcpyHostToDevice), "copy dim") && + cuda_ok(cudaMemcpy(d_fixed, fixed_mask, sizeof(fixed_mask), cudaMemcpyHostToDevice), "copy fixed") && + cuda_ok(cudaMemset(d_warm, 0, sizeof(double)), "clear warm"); + if (diagonal_q) + ok &= cuda_ok(cudaMemcpy(d_current, input, sizeof(input), cudaMemcpyHostToDevice), "copy current") && + cuda_ok(cudaMemcpy(d_q, q_diag, sizeof(q_diag), cudaMemcpyHostToDevice), "copy q"); + if (!ok) + goto cleanup; + + if (!diagonal_q) + { + if (type == CONE_STANDARD_SOC) + { + if (use_block) + project_standard_soc_block_kernel<<<1, 256>>>( + d_point, d_scale, NULL, 0.0, d_warm, d_start, d_dim, d_fixed, 1); + else + project_standard_soc_kernel<<<1, 1>>>(d_point, d_scale, d_warm, d_start, d_dim, d_fixed, 1); + } + else if (type == CONE_ROTATED_SOC) + { + if (use_block) + project_rotated_soc_block_kernel<<<1, 256>>>( + d_point, d_scale, NULL, 0.0, d_warm, d_start, d_dim, d_fixed, 1); + else + project_rotated_soc_kernel<<<1, 1>>>(d_point, d_scale, d_warm, d_start, d_dim, d_fixed, 1); + } + else + project_exp_cone_kernel<<<1, 1>>>(d_point, d_scale, d_warm, d_start, d_dim, d_fixed, 1); + } + else if (type == CONE_STANDARD_SOC) + { + if (use_block) + { + project_standard_soc_block_kernel<<<1, 256>>>( + d_point, d_scale, d_q, tau, d_warm, d_start, d_dim, d_fixed, 1); + recompute_reflected_at_cone_block_kernel<<<1, 256>>>(d_reflected, d_point, d_current, d_start, d_dim, 1); + } + else + project_standard_soc_diag_q_kernel<<<1, 1>>>( + d_point, d_reflected, d_current, d_scale, d_q, tau, d_warm, d_start, d_dim, d_fixed, 1); + } + else if (type == CONE_ROTATED_SOC) + { + if (use_block) + { + project_rotated_soc_block_kernel<<<1, 256>>>( + d_point, d_scale, d_q, tau, d_warm, d_start, d_dim, d_fixed, 1); + recompute_reflected_at_cone_block_kernel<<<1, 256>>>(d_reflected, d_point, d_current, d_start, d_dim, 1); + } + else + project_rotated_soc_diag_q_kernel<<<1, 1>>>( + d_point, d_reflected, d_current, d_scale, d_q, tau, d_warm, d_start, d_dim, d_fixed, 1); + } + else + project_exp_cone_diag_q_kernel<<<1, 1>>>( + d_point, d_reflected, d_current, d_scale, d_q, tau, d_warm, d_start, d_dim, d_fixed, 1); + ok &= cuda_ok(cudaGetLastError(), "projection launch") && cuda_ok(cudaDeviceSynchronize(), "projection sync"); + + double projected_scaled[3]; + ok &= + cuda_ok(cudaMemcpy(projected_scaled, d_point, sizeof(projected_scaled), cudaMemcpyDeviceToHost), "copy result"); + double projected[3]; + for (int slot = 0; slot < 3; ++slot) + { + projected[slot] = projected_scaled[slot] / scale[slot]; + if (!std::isfinite(projected[slot])) + ok = 0; + if (fixed_mask[slot] && projected_scaled[slot] != input[slot]) + { + std::fprintf(stderr, + "type=%d mask=%d diag=%d block=%d changed fixed slot %d: %.17g -> %.17g\n", + (int)type, + mask, + diagonal_q, + use_block, + slot, + input[slot], + projected_scaled[slot]); + ok = 0; + } + } + if (!cone_feasible(type, projected)) + { + std::fprintf(stderr, + "type=%d mask=%d diag=%d block=%d infeasible projection: (%.17g, %.17g, %.17g)\n", + (int)type, + mask, + diagonal_q, + use_block, + projected[0], + projected[1], + projected[2]); + ok = 0; + } + + for (int sample = 0; sample < 200 && ok; ++sample) + { + double candidate[3]; + if (type == CONE_STANDARD_SOC) + make_soc_candidate(mask, sample, fixed, candidate); + else if (type == CONE_ROTATED_SOC) + make_rsoc_candidate(mask, sample, fixed, candidate); + else + make_exp_candidate(mask, sample, fixed, candidate); + if (!cone_feasible(type, candidate)) + { + std::fprintf( + stderr, "test generated an infeasible candidate: type=%d mask=%d sample=%d\n", (int)type, mask, sample); + ok = 0; + break; + } + double dot = 0.0; + double scale_norm = 1.0; + for (int slot = 0; slot < 3; ++slot) + { + double candidate_scaled = scale[slot] * candidate[slot]; + double metric = diagonal_q ? 1.0 + tau * q_diag[slot] : 1.0; + double gradient = metric * (projected_scaled[slot] - input[slot]); + double direction = candidate_scaled - projected_scaled[slot]; + dot += gradient * direction; + scale_norm += std::fabs(gradient) * (1.0 + std::fabs(direction)); + } + if (dot < -2e-6 * scale_norm) + { + std::fprintf(stderr, + "type=%d mask=%d diag=%d block=%d violates projection VI: " + "dot=%.3e scale=%.3e sample=%d\n", + (int)type, + mask, + diagonal_q, + use_block, + dot, + scale_norm, + sample); + ok = 0; + } + } + +cleanup: + cudaFree(d_point); + cudaFree(d_reflected); + cudaFree(d_current); + cudaFree(d_scale); + cudaFree(d_q); + cudaFree(d_warm); + cudaFree(d_start); + cudaFree(d_dim); + cudaFree(d_fixed); + return ok; +} + +enum large_section_pattern +{ + LARGE_SECTION_FREE = 0, + LARGE_SECTION_FIXED_ENDPOINT = 1, + LARGE_SECTION_FIXED_VECTOR = 2, + LARGE_SECTION_FIXED_BOTH_ENDPOINTS = 3, + LARGE_SECTION_EXTREME_FIXED_BALL = 4, + LARGE_SECTION_NEAR_POLAR = 5, + LARGE_SECTION_FIXED_OTHER_ENDPOINT = 6, + LARGE_SECTION_FIXED_ALL_VECTOR = 7, +}; + +static int compare_parallel_projection(cone_type_t type, int diagonal_q, int section_pattern, int use_grid) +{ + const int k = use_grid ? PDHCG_LARGE_CONE_MIN_VDIM : 769; + const int length = k + 2; + const double tau = 0.6; + std::vector input(length); + std::vector scale(length); + std::vector q_diag(length); + std::vector fixed(length, 0); + for (int slot = 0; slot < k; ++slot) + { + scale[slot] = 0.4 + 0.03 * (slot % 37); + q_diag[slot] = 0.05 * (slot % 19); + double actual = 1.7 * std::sin(0.013 * (slot + 1)); + if (section_pattern == LARGE_SECTION_FIXED_ALL_VECTOR || + ((section_pattern == LARGE_SECTION_FIXED_ENDPOINT || section_pattern == LARGE_SECTION_FIXED_VECTOR || + section_pattern == LARGE_SECTION_FIXED_BOTH_ENDPOINTS) && + slot % 8191 == 0)) + { + fixed[slot] = 1; + actual = 0.02; + } + input[slot] = scale[slot] * actual; + } + scale[k] = 1.3; + scale[k + 1] = 2.1; + q_diag[k] = 0.7; + q_diag[k + 1] = 1.4; + if (type == CONE_STANDARD_SOC) + { + input[k] = scale[k] * -0.8; + input[k + 1] = scale[k + 1] * -0.4; + if (section_pattern == LARGE_SECTION_FIXED_ENDPOINT) + { + fixed[k + 1] = 1; + input[k + 1] = scale[k + 1] * 5.0; + } + else if (section_pattern == LARGE_SECTION_FIXED_OTHER_ENDPOINT) + { + fixed[k] = 1; + input[k] = scale[k] * 0.5; + } + else if (section_pattern == LARGE_SECTION_FIXED_ALL_VECTOR) + { + fixed[k] = 1; + input[k] = scale[k] * 0.5; + } + else if (section_pattern == LARGE_SECTION_FIXED_BOTH_ENDPOINTS) + { + fixed[k] = 1; + fixed[k + 1] = 1; + input[k] = scale[k] * 0.5; + input[k + 1] = scale[k + 1] * 5.0; + } + else if (section_pattern == LARGE_SECTION_EXTREME_FIXED_BALL) + { + std::fill(input.begin(), input.begin() + k, 0.0); + input[0] = scale[0] * std::ldexp(1.0, 120); + input[k] = 0.0; + fixed[k + 1] = 1; + input[k + 1] = scale[k + 1]; + } + } + else + { + input[k] = scale[k] * -0.3; + input[k + 1] = scale[k + 1] * 0.2; + if (section_pattern == LARGE_SECTION_FIXED_ENDPOINT) + { + fixed[k] = 1; + input[k] = scale[k] * 1.2; + } + else if (section_pattern == LARGE_SECTION_FIXED_OTHER_ENDPOINT) + { + fixed[k + 1] = 1; + input[k + 1] = scale[k + 1] * 1.1; + } + else if (section_pattern == LARGE_SECTION_FIXED_BOTH_ENDPOINTS) + { + fixed[k] = 1; + fixed[k + 1] = 1; + input[k] = scale[k] * 1.2; + input[k + 1] = scale[k + 1] * 1.1; + } + else if (section_pattern == LARGE_SECTION_EXTREME_FIXED_BALL) + { + std::fill(input.begin(), input.begin() + k, 0.0); + input[0] = scale[0] * std::ldexp(1.0, 120); + fixed[k] = 1; + fixed[k + 1] = 1; + input[k] = scale[k]; + input[k + 1] = scale[k + 1]; + } + } + + if (section_pattern == LARGE_SECTION_NEAR_POLAR) + { + std::fill(input.begin(), input.end(), 0.0); + std::fill(scale.begin(), scale.end(), 1.0); + std::fill(q_diag.begin(), q_diag.end(), 0.0); + input[0] = 1.0 + std::ldexp(1.0, -45); + if (type == CONE_STANDARD_SOC) + { + input[k] = 0.0; + input[k + 1] = -1.0; + } + else + { + input[k] = -0.70710678118654752440; + input[k + 1] = -0.70710678118654752440; + } + } + + const int has_fixed_section = std::any_of(fixed.begin(), fixed.end(), [](char value) { return value != 0; }); + + double *d_block = nullptr; + double *d_serial = nullptr; + double *d_scale = nullptr; + double *d_q = nullptr; + double *d_warm_block = nullptr; + double *d_warm_serial = nullptr; + double *d_reflected = nullptr; + double *d_current = nullptr; + int *d_start = nullptr; + int *d_dim = nullptr; + char *d_fixed = nullptr; + int start = 0; + int ok = cuda_ok(cudaMalloc(&d_block, (size_t)length * sizeof(double)), "large cudaMalloc(block)") && + cuda_ok(cudaMalloc(&d_serial, (size_t)length * sizeof(double)), "large cudaMalloc(serial)") && + cuda_ok(cudaMalloc(&d_scale, (size_t)length * sizeof(double)), "large cudaMalloc(scale)") && + cuda_ok(cudaMalloc(&d_q, (size_t)length * sizeof(double)), "large cudaMalloc(q)") && + cuda_ok(cudaMalloc(&d_warm_block, PDHCG_CONE_WORKSPACE_STRIDE * sizeof(double)), + "large cudaMalloc(warm block)") && + cuda_ok(cudaMalloc(&d_warm_serial, sizeof(double)), "large cudaMalloc(warm serial)") && + cuda_ok(cudaMalloc(&d_reflected, (size_t)length * sizeof(double)), "large cudaMalloc(reflected)") && + cuda_ok(cudaMalloc(&d_current, (size_t)length * sizeof(double)), "large cudaMalloc(current)") && + cuda_ok(cudaMalloc(&d_start, sizeof(int)), "large cudaMalloc(start)") && + cuda_ok(cudaMalloc(&d_dim, sizeof(int)), "large cudaMalloc(dim)"); + if (has_fixed_section) + ok &= cuda_ok(cudaMalloc(&d_fixed, (size_t)length * sizeof(char)), "large cudaMalloc(fixed)"); + if (!ok) + goto cleanup; + + ok &= cuda_ok(cudaMemcpy(d_block, input.data(), (size_t)length * sizeof(double), cudaMemcpyHostToDevice), + "large copy block") && + cuda_ok(cudaMemcpy(d_serial, input.data(), (size_t)length * sizeof(double), cudaMemcpyHostToDevice), + "large copy serial") && + cuda_ok(cudaMemcpy(d_current, input.data(), (size_t)length * sizeof(double), cudaMemcpyHostToDevice), + "large copy current") && + cuda_ok(cudaMemcpy(d_scale, scale.data(), (size_t)length * sizeof(double), cudaMemcpyHostToDevice), + "large copy scale") && + cuda_ok(cudaMemcpy(d_q, q_diag.data(), (size_t)length * sizeof(double), cudaMemcpyHostToDevice), + "large copy q") && + cuda_ok(cudaMemcpy(d_start, &start, sizeof(start), cudaMemcpyHostToDevice), "large copy start") && + cuda_ok(cudaMemcpy(d_dim, &k, sizeof(k), cudaMemcpyHostToDevice), "large copy dim") && + cuda_ok(cudaMemset(d_warm_block, 0, PDHCG_CONE_WORKSPACE_STRIDE * sizeof(double)), "large clear warm block") && + cuda_ok(cudaMemset(d_warm_serial, 0, sizeof(double)), "large clear warm serial"); + if (has_fixed_section) + ok &= cuda_ok(cudaMemcpy(d_fixed, fixed.data(), (size_t)length * sizeof(char), cudaMemcpyHostToDevice), + "large copy fixed"); + if (!ok) + goto cleanup; + + if (type == CONE_STANDARD_SOC) + { + if (use_grid) + { + const int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + cudaMemset(d_warm_block + 1, 0, 5 * sizeof(double)); + initialize_standard_soc_grid_weighted_kernel<<>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1, + blocks_per_cone); + finalize_standard_soc_grid_weighted_initialization_kernel<<<1, 256>>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1); + for (int iteration = 0; iteration < PDHCG_CONE_GRID_ROOT_ITERATIONS; ++iteration) + { + cudaMemset(d_warm_block + 1, 0, 2 * sizeof(double)); + reduce_standard_soc_grid_weighted_root_kernel<<>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1, + blocks_per_cone); + finalize_standard_soc_grid_weighted_root_kernel<<<1, 256>>>( + d_block, d_scale, diagonal_q ? d_q : NULL, diagonal_q ? tau : 0.0, d_warm_block, d_start, d_dim, 1); + } + apply_standard_soc_grid_weighted_kernel<<>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1, + blocks_per_cone); + } + else + project_standard_soc_block_kernel<<<1, 256>>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1); + if (diagonal_q) + project_standard_soc_diag_q_kernel<<<1, 1>>>( + d_serial, d_reflected, d_current, d_scale, d_q, tau, d_warm_serial, d_start, d_dim, d_fixed, 1); + else + project_standard_soc_kernel<<<1, 1>>>(d_serial, d_scale, d_warm_serial, d_start, d_dim, d_fixed, 1); + } + else + { + if (use_grid) + { + const int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + cudaMemset(d_warm_block + 1, 0, 5 * sizeof(double)); + initialize_rotated_soc_grid_weighted_kernel<<>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1, + blocks_per_cone); + finalize_rotated_soc_grid_weighted_initialization_kernel<<<1, 256>>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1); + for (int iteration = 0; iteration < PDHCG_CONE_GRID_ROOT_ITERATIONS; ++iteration) + { + cudaMemset(d_warm_block + 1, 0, 2 * sizeof(double)); + reduce_rotated_soc_grid_weighted_root_kernel<<>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1, + blocks_per_cone); + finalize_rotated_soc_grid_weighted_root_kernel<<<1, 256>>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1); + } + cudaMemset(d_warm_block + 1, 0, 2 * sizeof(double)); + reduce_rotated_soc_grid_axis_objective_kernel<<>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1, + blocks_per_cone); + finalize_rotated_soc_grid_axis_objective_kernel<<<1, 256>>>( + d_block, d_scale, diagonal_q ? d_q : NULL, diagonal_q ? tau : 0.0, d_warm_block, d_start, d_dim, 1); + apply_rotated_soc_grid_weighted_kernel<<>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1, + blocks_per_cone); + } + else + project_rotated_soc_block_kernel<<<1, 256>>>(d_block, + d_scale, + diagonal_q ? d_q : NULL, + diagonal_q ? tau : 0.0, + d_warm_block, + d_start, + d_dim, + d_fixed, + 1); + if (diagonal_q) + project_rotated_soc_diag_q_kernel<<<1, 1>>>( + d_serial, d_reflected, d_current, d_scale, d_q, tau, d_warm_serial, d_start, d_dim, d_fixed, 1); + else + project_rotated_soc_kernel<<<1, 1>>>(d_serial, d_scale, d_warm_serial, d_start, d_dim, d_fixed, 1); + } + ok &= cuda_ok(cudaGetLastError(), "large projection launch") && + cuda_ok(cudaDeviceSynchronize(), "large projection sync"); + if (!ok) + goto cleanup; + + { + std::vector block(length); + std::vector serial(length); + ok &= cuda_ok(cudaMemcpy(block.data(), d_block, (size_t)length * sizeof(double), cudaMemcpyDeviceToHost), + "large copy block result") && + cuda_ok(cudaMemcpy(serial.data(), d_serial, (size_t)length * sizeof(double), cudaMemcpyDeviceToHost), + "large copy serial result"); + double max_error = 0.0; + double norm2 = 0.0; + for (int slot = 0; slot < length; ++slot) + { + max_error = std::fmax(max_error, std::fabs(block[slot] - serial[slot]) / (1.0 + std::fabs(serial[slot]))); + if (has_fixed_section && fixed[slot] && block[slot] != input[slot]) + ok = 0; + if (slot < k) + { + double actual = block[slot] / scale[slot]; + norm2 += actual * actual; + } + } + double endpoint0 = block[k] / scale[k]; + double endpoint1 = block[k + 1] / scale[k + 1]; + double violation = type == CONE_STANDARD_SOC ? norm2 + endpoint0 * endpoint0 - endpoint1 * endpoint1 + : norm2 - 2.0 * endpoint0 * endpoint1; + double error_tolerance = section_pattern == LARGE_SECTION_NEAR_POLAR ? 1e-13 : 2e-8; + if (max_error > error_tolerance || violation > 2e-7 * (1.0 + norm2)) + { + std::fprintf(stderr, + "%s projection mismatch: type=%d diag=%d pattern=%d error=%.3e violation=%.3e\n", + use_grid ? "grid" : "block", + (int)type, + diagonal_q, + section_pattern, + max_error, + violation); + ok = 0; + } + } + +cleanup: + cudaFree(d_block); + cudaFree(d_serial); + cudaFree(d_scale); + cudaFree(d_q); + cudaFree(d_warm_block); + cudaFree(d_warm_serial); + cudaFree(d_reflected); + cudaFree(d_current); + cudaFree(d_start); + cudaFree(d_dim); + cudaFree(d_fixed); + return ok; +} + +static int compare_large_affine_complementarity(void) +{ + const int k = PDHCG_LARGE_CONE_MIN_VDIM; + const int length = k + 2; + const int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + const double bound_rescaling = 3.7; + const size_t bytes = (size_t)length * sizeof(double); + std::vector primal_product(length); + std::vector offset(length); + std::vector dual(length); + std::vector block_point(length); + std::vector grid_point(length); + double expected_dot = 0.0; + for (int slot = 0; slot < length; ++slot) + { + primal_product[slot] = 0.2 + 1e-5 * (slot % 97); + offset[slot] = 0.1 + 2e-5 * (slot % 53); + dual[slot] = 0.3 + 3e-5 * (slot % 71); + expected_dot += dual[slot] * (primal_product[slot] + offset[slot]); + } + + double *d_primal_product = nullptr; + double *d_offset = nullptr; + double *d_dual = nullptr; + double *d_block_point = nullptr; + double *d_grid_point = nullptr; + double *d_block_complementarity = nullptr; + double *d_grid_complementarity = nullptr; + int *d_start = nullptr; + int *d_dim = nullptr; + int start = 0; + int ok = cuda_ok(cudaMalloc(&d_primal_product, bytes), "affine cudaMalloc(primal product)") && + cuda_ok(cudaMalloc(&d_offset, bytes), "affine cudaMalloc(offset)") && + cuda_ok(cudaMalloc(&d_dual, bytes), "affine cudaMalloc(dual)") && + cuda_ok(cudaMalloc(&d_block_point, bytes), "affine cudaMalloc(block point)") && + cuda_ok(cudaMalloc(&d_grid_point, bytes), "affine cudaMalloc(grid point)") && + cuda_ok(cudaMalloc(&d_block_complementarity, sizeof(double)), "affine cudaMalloc(block complementarity)") && + cuda_ok(cudaMalloc(&d_grid_complementarity, sizeof(double)), "affine cudaMalloc(grid complementarity)") && + cuda_ok(cudaMalloc(&d_start, sizeof(int)), "affine cudaMalloc(start)") && + cuda_ok(cudaMalloc(&d_dim, sizeof(int)), "affine cudaMalloc(dim)"); + if (!ok) + goto cleanup; + + ok &= cuda_ok(cudaMemcpy(d_primal_product, primal_product.data(), bytes, cudaMemcpyHostToDevice), + "affine copy primal product") && + cuda_ok(cudaMemcpy(d_offset, offset.data(), bytes, cudaMemcpyHostToDevice), "affine copy offset") && + cuda_ok(cudaMemcpy(d_dual, dual.data(), bytes, cudaMemcpyHostToDevice), "affine copy dual") && + cuda_ok(cudaMemcpy(d_start, &start, sizeof(int), cudaMemcpyHostToDevice), "affine copy start") && + cuda_ok(cudaMemcpy(d_dim, &k, sizeof(int), cudaMemcpyHostToDevice), "affine copy dim") && + cuda_ok(cudaMemset(d_grid_complementarity, 0, sizeof(double)), "affine clear grid complementarity"); + if (!ok) + goto cleanup; + + prepare_affine_cone_residuals_kernel<<<1, 256, 256 * sizeof(double)>>>( + d_block_point, d_block_complementarity, d_primal_product, d_offset, d_dual, d_start, d_dim, bound_rescaling, 1); + prepare_affine_cone_residuals_grid_kernel<<>>( + d_grid_point, d_grid_complementarity, d_primal_product, d_offset, d_dual, d_start, d_dim, 1, blocks_per_cone); + finish_affine_cone_complementarity_kernel<<<1, 1>>>(d_grid_complementarity, bound_rescaling, 1); + ok &= cuda_ok(cudaGetLastError(), "affine residual launch") && + cuda_ok(cudaDeviceSynchronize(), "affine residual sync") && + cuda_ok(cudaMemcpy(block_point.data(), d_block_point, bytes, cudaMemcpyDeviceToHost), + "affine copy block point") && + cuda_ok(cudaMemcpy(grid_point.data(), d_grid_point, bytes, cudaMemcpyDeviceToHost), "affine copy grid point"); + + if (ok) + { + double block_complementarity = 0.0; + double grid_complementarity = 0.0; + ok &= + cuda_ok(cudaMemcpy(&block_complementarity, d_block_complementarity, sizeof(double), cudaMemcpyDeviceToHost), + "affine copy block complementarity") && + cuda_ok(cudaMemcpy(&grid_complementarity, d_grid_complementarity, sizeof(double), cudaMemcpyDeviceToHost), + "affine copy grid complementarity"); + double max_point_error = 0.0; + for (int slot = 0; slot < length; ++slot) + { + max_point_error = std::fmax(max_point_error, std::fabs(block_point[slot] - grid_point[slot])); + max_point_error = std::fmax(max_point_error, std::fabs(grid_point[slot] + dual[slot])); + } + double expected = std::fabs(expected_dot) / bound_rescaling; + double complementarity_error = + std::fmax(std::fabs(block_complementarity - expected), std::fabs(grid_complementarity - expected)) / + (1.0 + expected); + if (max_point_error != 0.0 || complementarity_error > 2e-13) + { + std::fprintf(stderr, + "large affine residual mismatch: point=%.3e complementarity=%.3e " + "block=%.17g grid=%.17g expected=%.17g\n", + max_point_error, + complementarity_error, + block_complementarity, + grid_complementarity, + expected); + ok = 0; + } + } + +cleanup: + cudaFree(d_primal_product); + cudaFree(d_offset); + cudaFree(d_dual); + cudaFree(d_block_point); + cudaFree(d_grid_point); + cudaFree(d_block_complementarity); + cudaFree(d_grid_complementarity); + cudaFree(d_start); + cudaFree(d_dim); + return ok; +} + +int main(void) +{ + int passed = 1; + const cone_type_t types[] = {CONE_STANDARD_SOC, CONE_ROTATED_SOC, CONE_EXPONENTIAL}; + for (cone_type_t type : types) + for (int diagonal_q = 0; diagonal_q <= 1; ++diagonal_q) + for (int mask = 0; mask < 8; ++mask) + { + passed &= run_mask(type, mask, diagonal_q, 0); + if (type == CONE_STANDARD_SOC || type == CONE_ROTATED_SOC) + passed &= run_mask(type, mask, diagonal_q, 1); + } + for (cone_type_t type : {CONE_STANDARD_SOC, CONE_ROTATED_SOC}) + for (int diagonal_q = 0; diagonal_q <= 1; ++diagonal_q) + for (int section_pattern = LARGE_SECTION_FREE; section_pattern <= LARGE_SECTION_FIXED_ALL_VECTOR; + ++section_pattern) + passed &= compare_parallel_projection(type, diagonal_q, section_pattern, 1); + for (cone_type_t type : {CONE_STANDARD_SOC, CONE_ROTATED_SOC}) + for (int diagonal_q = 0; diagonal_q <= 1; ++diagonal_q) + for (int section_pattern = LARGE_SECTION_FREE; section_pattern <= LARGE_SECTION_FIXED_ALL_VECTOR; + ++section_pattern) + passed &= compare_parallel_projection(type, diagonal_q, section_pattern, 0); + passed &= compare_large_affine_complementarity(); + std::printf("fixed cone section projections: %s\n", passed ? "PASS" : "FAIL"); + return passed ? 0 : 1; +} diff --git a/test/test_initial_box_projection.c b/test/test_initial_box_projection.c new file mode 100644 index 0000000..6799dbd --- /dev/null +++ b/test/test_initial_box_projection.c @@ -0,0 +1,82 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "pdhcg.h" +#include +#include + +static int run_case(const double *primal_start, double expected) +{ + const int row_ptr[] = {0, 1}; + const int col_ind[] = {0}; + const double values[] = {1.0}; + const double objective[] = {0.0}; + const double con_lb[] = {0.0}; + const double con_ub[] = {10.0}; + const double var_lb[] = {1.0}; + const double var_ub[] = {2.0}; + matrix_desc_t A = {0}; + pdhg_parameters_t params; + pdhcg_result_t *result; + + A.m = 1; + A.n = 1; + A.fmt = matrix_csr; + A.data.csr.nnz = 1; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + + qp_problem_t *problem = create_qp_problem( + objective, NULL, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 0, NULL, NULL, NULL, 0, NULL); + if (!problem) + return 1; + + if (primal_start) + set_start_values(problem, primal_start, NULL); + + set_default_parameters(¶ms); + params.presolve = false; + params.verbose = 0; + result = solve_qp_problem(problem, ¶ms); + + int failed = !result || result->termination_reason != TERMINATION_REASON_OPTIMAL || result->total_count != 0 || + fabs(result->primal_solution[0] - expected) > 1e-12; + + if (failed) + { + fprintf(stderr, + "initial box projection failed: expected x=%.17g, got status=%d iter=%d x=%.17g\n", + expected, + result ? (int)result->termination_reason : -1, + result ? result->total_count : -1, + result ? result->primal_solution[0] : NAN); + } + + pdhcg_result_free(result); + qp_problem_free(problem); + return failed; +} + +int main(void) +{ + const double upper_infeasible_start[] = {3.0}; + int failed = 0; + + failed |= run_case(NULL, 1.0); + failed |= run_case(upper_infeasible_start, 2.0); + return failed ? 1 : 0; +} diff --git a/test/test_large_rsoc_projection.cu b/test/test_large_rsoc_projection.cu new file mode 100644 index 0000000..1ebc5ea --- /dev/null +++ b/test/test_large_rsoc_projection.cu @@ -0,0 +1,327 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "internal_types.h" +#include "pdhcg_kernels.cuh" +#include "utils.h" +#include +#include +#include +#include + +static double max_abs_difference(const double *a, const double *b, int n) +{ + double error = 0.0; + for (int i = 0; i < n; ++i) + error = fmax(error, fabs(a[i] - b[i])); + return error; +} + +static int compare_primal_projection(int k, double s, double t) +{ + const int n = k + 2; + const int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + double *input = (double *)malloc((size_t)n * sizeof(double)); + double *warp_result = (double *)malloc((size_t)n * sizeof(double)); + double *grid_result = (double *)malloc((size_t)n * sizeof(double)); + double *scaling = (double *)malloc((size_t)n * sizeof(double)); + if (!input || !warp_result || !grid_result || !scaling) + return 0; + + for (int i = 0; i < k; ++i) + input[i] = (double)((i % 17) - 8) * 1.0e-3; + input[k] = s; + input[k + 1] = t; + for (int i = 0; i < n; ++i) + scaling[i] = 1.0; + + double *d_warp = NULL; + double *d_grid = NULL; + double *d_scaling = NULL; + double *d_warp_workspace = NULL; + double *d_grid_workspace = NULL; + int *d_start = NULL; + int *d_vdim = NULL; + int start = 0; + + CUDA_CHECK(cudaMalloc(&d_warp, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_grid, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_scaling, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_warp_workspace, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_grid_workspace, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_start, sizeof(int))); + CUDA_CHECK(cudaMalloc(&d_vdim, sizeof(int))); + CUDA_CHECK(cudaMemcpy(d_warp, input, (size_t)n * sizeof(double), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_grid, input, (size_t)n * sizeof(double), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_scaling, scaling, (size_t)n * sizeof(double), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_start, &start, sizeof(int), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_vdim, &k, sizeof(int), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemset(d_warp_workspace, 0, sizeof(double))); + CUDA_CHECK(cudaMemset(d_grid_workspace, 0, sizeof(double))); + + project_rotated_soc_warp_kernel<<<1, THREADS_PER_BLOCK>>>( + d_warp, d_scaling, d_warp_workspace, d_start, d_vdim, NULL, 1); + project_rotated_soc_grid_reduce_kernel<<>>( + d_grid, d_grid_workspace, d_start, d_vdim, 1, blocks_per_cone); + project_rotated_soc_grid_finalize_kernel<<<1, THREADS_PER_BLOCK>>>(d_grid, d_grid_workspace, d_start, d_vdim, 1); + project_rotated_soc_grid_apply_kernel<<>>( + d_grid, d_grid_workspace, d_start, d_vdim, 1, blocks_per_cone); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaDeviceSynchronize()); + + CUDA_CHECK(cudaMemcpy(warp_result, d_warp, (size_t)n * sizeof(double), cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(grid_result, d_grid, (size_t)n * sizeof(double), cudaMemcpyDeviceToHost)); + double error = max_abs_difference(warp_result, grid_result, n); + int pass = error <= 1.0e-10; + printf("large RSOC primal s=%g t=%g max_error=%.3e: %s\n", s, t, error, pass ? "PASS" : "FAIL"); + + CUDA_CHECK(cudaFree(d_warp)); + CUDA_CHECK(cudaFree(d_grid)); + CUDA_CHECK(cudaFree(d_scaling)); + CUDA_CHECK(cudaFree(d_warp_workspace)); + CUDA_CHECK(cudaFree(d_grid_workspace)); + CUDA_CHECK(cudaFree(d_start)); + CUDA_CHECK(cudaFree(d_vdim)); + free(input); + free(warp_result); + free(grid_result); + free(scaling); + return pass; +} + +static int compare_dual_residual(int k) +{ + const int n = k + 2; + const int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + double *objective = (double *)malloc((size_t)n * sizeof(double)); + double *dual_product = (double *)malloc((size_t)n * sizeof(double)); + double *scaling = (double *)malloc((size_t)n * sizeof(double)); + double *warp_result = (double *)malloc((size_t)n * sizeof(double)); + double *grid_result = (double *)malloc((size_t)n * sizeof(double)); + if (!objective || !dual_product || !scaling || !warp_result || !grid_result) + return 0; + + for (int i = 0; i < k; ++i) + { + objective[i] = (double)((i % 13) - 6) * 2.0e-3; + dual_product[i] = (double)((i % 7) - 3) * 5.0e-4; + scaling[i] = 1.0; + } + objective[k] = 0.3; + objective[k + 1] = -0.1; + dual_product[k] = -0.2; + dual_product[k + 1] = 0.05; + scaling[k] = 1.0; + scaling[k + 1] = 1.0; + + double *d_objective = NULL; + double *d_dual_product = NULL; + double *d_scaling = NULL; + double *d_primal = NULL; + double *d_warp_result = NULL; + double *d_complementarity = NULL; + double *d_grid_result = NULL; + double *d_warp_workspace = NULL; + double *d_grid_workspace = NULL; + int *d_start = NULL; + int *d_vdim = NULL; + int start = 0; + + CUDA_CHECK(cudaMalloc(&d_objective, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_dual_product, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_scaling, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_primal, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_warp_result, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_complementarity, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_grid_result, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_warp_workspace, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_grid_workspace, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_start, sizeof(int))); + CUDA_CHECK(cudaMalloc(&d_vdim, sizeof(int))); + CUDA_CHECK(cudaMemcpy(d_objective, objective, (size_t)n * sizeof(double), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_dual_product, dual_product, (size_t)n * sizeof(double), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_scaling, scaling, (size_t)n * sizeof(double), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemset(d_primal, 0, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMemset(d_warp_result, 0, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMemset(d_complementarity, 0, sizeof(double))); + CUDA_CHECK(cudaMemset(d_grid_result, 0, (size_t)n * sizeof(double))); + CUDA_CHECK(cudaMemset(d_warp_workspace, 0, sizeof(double))); + CUDA_CHECK(cudaMemset(d_grid_workspace, 0, sizeof(double))); + CUDA_CHECK(cudaMemcpy(d_start, &start, sizeof(int), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_vdim, &k, sizeof(int), cudaMemcpyHostToDevice)); + + compute_cone_dual_residual_warp_kernel<<<1, THREADS_PER_BLOCK>>>(d_warp_result, + d_complementarity, + d_objective, + d_dual_product, + d_scaling, + d_primal, + d_warp_workspace, + d_start, + d_vdim, + NULL, + 1); + compute_cone_dual_residual_grid_reduce_kernel<<>>( + d_objective, d_dual_product, d_grid_workspace, d_start, d_vdim, 1, blocks_per_cone); + compute_cone_dual_residual_grid_finalize_kernel<<<1, THREADS_PER_BLOCK>>>( + d_grid_result, d_objective, d_dual_product, d_scaling, d_grid_workspace, d_start, d_vdim, 1); + compute_cone_dual_residual_grid_apply_kernel<<>>( + d_grid_result, d_objective, d_dual_product, d_scaling, d_grid_workspace, d_start, d_vdim, 1, blocks_per_cone); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaDeviceSynchronize()); + + CUDA_CHECK(cudaMemcpy(warp_result, d_warp_result, (size_t)n * sizeof(double), cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(grid_result, d_grid_result, (size_t)n * sizeof(double), cudaMemcpyDeviceToHost)); + double error = max_abs_difference(warp_result, grid_result, n); + int pass = error <= 1.0e-10; + printf("large RSOC dual residual max_error=%.3e: %s\n", error, pass ? "PASS" : "FAIL"); + + CUDA_CHECK(cudaFree(d_objective)); + CUDA_CHECK(cudaFree(d_dual_product)); + CUDA_CHECK(cudaFree(d_scaling)); + CUDA_CHECK(cudaFree(d_primal)); + CUDA_CHECK(cudaFree(d_warp_result)); + CUDA_CHECK(cudaFree(d_complementarity)); + CUDA_CHECK(cudaFree(d_grid_result)); + CUDA_CHECK(cudaFree(d_warp_workspace)); + CUDA_CHECK(cudaFree(d_grid_workspace)); + CUDA_CHECK(cudaFree(d_start)); + CUDA_CHECK(cudaFree(d_vdim)); + free(objective); + free(dual_product); + free(scaling); + free(warp_result); + free(grid_result); + return pass; +} + +static int compare_vector_updates(int k) +{ + const int n = k + 2; + const int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + const size_t bytes = (size_t)n * sizeof(double); + double *current = (double *)malloc(bytes); + double *pdhg = (double *)malloc(bytes); + double *objective = (double *)malloc(bytes); + double *dual_product = (double *)malloc(bytes); + double *thread_result = (double *)malloc(bytes); + double *grid_result = (double *)malloc(bytes); + if (!current || !pdhg || !objective || !dual_product || !thread_result || !grid_result) + return 0; + + for (int i = 0; i < n; ++i) + { + current[i] = (double)((i % 19) - 9) * 0.02; + pdhg[i] = (double)((i % 23) - 11) * 0.03; + objective[i] = (double)((i % 29) - 14) * 0.04; + dual_product[i] = (double)((i % 31) - 15) * 0.01; + } + + double *d_current = NULL; + double *d_pdhg = NULL; + double *d_objective = NULL; + double *d_dual_product = NULL; + double *d_thread_result = NULL; + double *d_grid_result = NULL; + int *d_start = NULL; + int *d_vdim = NULL; + int start = 0; + + CUDA_CHECK(cudaMalloc(&d_current, bytes)); + CUDA_CHECK(cudaMalloc(&d_pdhg, bytes)); + CUDA_CHECK(cudaMalloc(&d_objective, bytes)); + CUDA_CHECK(cudaMalloc(&d_dual_product, bytes)); + CUDA_CHECK(cudaMalloc(&d_thread_result, bytes)); + CUDA_CHECK(cudaMalloc(&d_grid_result, bytes)); + CUDA_CHECK(cudaMalloc(&d_start, sizeof(int))); + CUDA_CHECK(cudaMalloc(&d_vdim, sizeof(int))); + CUDA_CHECK(cudaMemcpy(d_current, current, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_pdhg, pdhg, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_objective, objective, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_dual_product, dual_product, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_start, &start, sizeof(int), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_vdim, &k, sizeof(int), cudaMemcpyHostToDevice)); + + recompute_reflected_at_cone_kernel<<<1, THREADS_PER_BLOCK>>>( + d_thread_result, d_pdhg, d_current, d_start, d_vdim, 1); + recompute_reflected_at_cone_grid_kernel<<>>( + d_grid_result, d_pdhg, d_current, d_start, d_vdim, 1, blocks_per_cone); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaDeviceSynchronize()); + CUDA_CHECK(cudaMemcpy(thread_result, d_thread_result, bytes, cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(grid_result, d_grid_result, bytes, cudaMemcpyDeviceToHost)); + double reflected_error = max_abs_difference(thread_result, grid_result, n); + + recompute_reflected_at_cone_warp_kernel<<<1, THREADS_PER_BLOCK>>>( + d_grid_result, d_pdhg, d_current, d_start, d_vdim, 1); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaDeviceSynchronize()); + CUDA_CHECK(cudaMemcpy(grid_result, d_grid_result, bytes, cudaMemcpyDeviceToHost)); + double reflected_warp_error = max_abs_difference(thread_result, grid_result, n); + + set_cone_dual_slack_kernel<<<1, THREADS_PER_BLOCK>>>( + d_thread_result, d_objective, d_dual_product, d_start, d_vdim, 1); + set_cone_dual_slack_grid_kernel<<>>( + d_grid_result, d_objective, d_dual_product, d_start, d_vdim, 1, blocks_per_cone); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaDeviceSynchronize()); + CUDA_CHECK(cudaMemcpy(thread_result, d_thread_result, bytes, cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(grid_result, d_grid_result, bytes, cudaMemcpyDeviceToHost)); + double slack_error = max_abs_difference(thread_result, grid_result, n); + + set_cone_dual_slack_warp_kernel<<<1, THREADS_PER_BLOCK>>>( + d_grid_result, d_objective, d_dual_product, d_start, d_vdim, 1); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaDeviceSynchronize()); + CUDA_CHECK(cudaMemcpy(grid_result, d_grid_result, bytes, cudaMemcpyDeviceToHost)); + double slack_warp_error = max_abs_difference(thread_result, grid_result, n); + + int pass = reflected_error == 0.0 && reflected_warp_error == 0.0 && slack_error == 0.0 && slack_warp_error == 0.0; + printf("large RSOC vector updates grid=(%.3e, %.3e) warp=(%.3e, %.3e): %s\n", + reflected_error, + slack_error, + reflected_warp_error, + slack_warp_error, + pass ? "PASS" : "FAIL"); + + CUDA_CHECK(cudaFree(d_current)); + CUDA_CHECK(cudaFree(d_pdhg)); + CUDA_CHECK(cudaFree(d_objective)); + CUDA_CHECK(cudaFree(d_dual_product)); + CUDA_CHECK(cudaFree(d_thread_result)); + CUDA_CHECK(cudaFree(d_grid_result)); + CUDA_CHECK(cudaFree(d_start)); + CUDA_CHECK(cudaFree(d_vdim)); + free(current); + free(pdhg); + free(objective); + free(dual_product); + free(thread_result); + free(grid_result); + return pass; +} + +int main(void) +{ + const int k = 65536; + int pass = 1; + pass &= compare_primal_projection(k, 2.0, 2.0); + pass &= compare_primal_projection(k, 0.2, 0.8); + pass &= compare_primal_projection(k, -2.0, -2.0); + pass &= compare_dual_residual(k); + pass &= compare_vector_updates(k); + return pass ? 0 : 1; +} diff --git a/test/test_large_soc_projection.cu b/test/test_large_soc_projection.cu new file mode 100644 index 0000000..0f367c0 --- /dev/null +++ b/test/test_large_soc_projection.cu @@ -0,0 +1,221 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "internal_types.h" +#include "pdhcg_kernels.cuh" +#include "utils.h" +#include +#include +#include + +static double max_abs_difference(const double *a, const double *b, int n) +{ + double error = 0.0; + for (int i = 0; i < n; ++i) + error = fmax(error, fabs(a[i] - b[i])); + return error; +} + +static int compare_primal_projection(int k, double w, double z) +{ + const int n = k + 2; + const int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + const size_t bytes = (size_t)n * sizeof(double); + double *input = (double *)malloc(bytes); + double *warp_result = (double *)malloc(bytes); + double *grid_result = (double *)malloc(bytes); + double *scaling = (double *)malloc(bytes); + if (!input || !warp_result || !grid_result || !scaling) + return 0; + + for (int i = 0; i < k; ++i) + input[i] = (double)((i % 17) - 8) * 1.0e-3; + input[k] = w; + input[k + 1] = z; + for (int i = 0; i < n; ++i) + scaling[i] = 1.0; + + double *d_warp = NULL; + double *d_grid = NULL; + double *d_scaling = NULL; + double *d_warp_workspace = NULL; + double *d_grid_workspace = NULL; + int *d_start = NULL; + int *d_vdim = NULL; + int start = 0; + + CUDA_CHECK(cudaMalloc(&d_warp, bytes)); + CUDA_CHECK(cudaMalloc(&d_grid, bytes)); + CUDA_CHECK(cudaMalloc(&d_scaling, bytes)); + CUDA_CHECK(cudaMalloc(&d_warp_workspace, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_grid_workspace, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_start, sizeof(int))); + CUDA_CHECK(cudaMalloc(&d_vdim, sizeof(int))); + CUDA_CHECK(cudaMemcpy(d_warp, input, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_grid, input, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_scaling, scaling, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_start, &start, sizeof(int), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_vdim, &k, sizeof(int), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemset(d_warp_workspace, 0, sizeof(double))); + CUDA_CHECK(cudaMemset(d_grid_workspace, 0, sizeof(double))); + + project_standard_soc_warp_kernel<<<1, THREADS_PER_BLOCK>>>( + d_warp, d_scaling, d_warp_workspace, d_start, d_vdim, NULL, 1); + project_standard_soc_grid_reduce_kernel<<>>( + d_grid, d_grid_workspace, d_start, d_vdim, 1, blocks_per_cone); + project_standard_soc_grid_finalize_kernel<<<1, THREADS_PER_BLOCK>>>(d_grid, d_grid_workspace, d_start, d_vdim, 1); + project_standard_soc_grid_apply_kernel<<>>( + d_grid, d_grid_workspace, d_start, d_vdim, 1, blocks_per_cone); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaDeviceSynchronize()); + + CUDA_CHECK(cudaMemcpy(warp_result, d_warp, bytes, cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(grid_result, d_grid, bytes, cudaMemcpyDeviceToHost)); + double error = max_abs_difference(warp_result, grid_result, n); + int pass = error <= 1.0e-10; + printf("large SOC primal w=%g z=%g max_error=%.3e: %s\n", w, z, error, pass ? "PASS" : "FAIL"); + + CUDA_CHECK(cudaFree(d_warp)); + CUDA_CHECK(cudaFree(d_grid)); + CUDA_CHECK(cudaFree(d_scaling)); + CUDA_CHECK(cudaFree(d_warp_workspace)); + CUDA_CHECK(cudaFree(d_grid_workspace)); + CUDA_CHECK(cudaFree(d_start)); + CUDA_CHECK(cudaFree(d_vdim)); + free(input); + free(warp_result); + free(grid_result); + free(scaling); + return pass; +} + +static int compare_dual_residual(int k) +{ + const int n = k + 2; + const int blocks_per_cone = PDHCG_LARGE_CONE_BLOCKS_PER_CONE; + const size_t bytes = (size_t)n * sizeof(double); + double *objective = (double *)malloc(bytes); + double *dual_product = (double *)malloc(bytes); + double *scaling = (double *)malloc(bytes); + double *warp_result = (double *)malloc(bytes); + double *grid_result = (double *)malloc(bytes); + if (!objective || !dual_product || !scaling || !warp_result || !grid_result) + return 0; + + for (int i = 0; i < k; ++i) + { + objective[i] = (double)((i % 13) - 6) * 2.0e-3; + dual_product[i] = (double)((i % 7) - 3) * 5.0e-4; + scaling[i] = 1.0; + } + objective[k] = 0.3; + objective[k + 1] = -0.1; + dual_product[k] = -0.2; + dual_product[k + 1] = 0.05; + scaling[k] = 1.0; + scaling[k + 1] = 1.0; + + double *d_objective = NULL; + double *d_dual_product = NULL; + double *d_scaling = NULL; + double *d_primal = NULL; + double *d_warp_result = NULL; + double *d_complementarity = NULL; + double *d_grid_result = NULL; + double *d_warp_workspace = NULL; + double *d_grid_workspace = NULL; + int *d_start = NULL; + int *d_vdim = NULL; + int start = 0; + + CUDA_CHECK(cudaMalloc(&d_objective, bytes)); + CUDA_CHECK(cudaMalloc(&d_dual_product, bytes)); + CUDA_CHECK(cudaMalloc(&d_scaling, bytes)); + CUDA_CHECK(cudaMalloc(&d_primal, bytes)); + CUDA_CHECK(cudaMalloc(&d_warp_result, bytes)); + CUDA_CHECK(cudaMalloc(&d_complementarity, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_grid_result, bytes)); + CUDA_CHECK(cudaMalloc(&d_warp_workspace, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_grid_workspace, sizeof(double))); + CUDA_CHECK(cudaMalloc(&d_start, sizeof(int))); + CUDA_CHECK(cudaMalloc(&d_vdim, sizeof(int))); + CUDA_CHECK(cudaMemcpy(d_objective, objective, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_dual_product, dual_product, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_scaling, scaling, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemset(d_primal, 0, bytes)); + CUDA_CHECK(cudaMemset(d_warp_result, 0, bytes)); + CUDA_CHECK(cudaMemset(d_complementarity, 0, sizeof(double))); + CUDA_CHECK(cudaMemset(d_grid_result, 0, bytes)); + CUDA_CHECK(cudaMemset(d_warp_workspace, 0, sizeof(double))); + CUDA_CHECK(cudaMemset(d_grid_workspace, 0, sizeof(double))); + CUDA_CHECK(cudaMemcpy(d_start, &start, sizeof(int), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_vdim, &k, sizeof(int), cudaMemcpyHostToDevice)); + + compute_cone_dual_residual_standard_warp_kernel<<<1, THREADS_PER_BLOCK>>>(d_warp_result, + d_complementarity, + d_objective, + d_dual_product, + d_scaling, + d_primal, + d_warp_workspace, + d_start, + d_vdim, + NULL, + 1); + compute_cone_dual_residual_standard_grid_reduce_kernel<<>>( + d_objective, d_dual_product, d_grid_workspace, d_start, d_vdim, 1, blocks_per_cone); + compute_cone_dual_residual_standard_grid_finalize_kernel<<<1, THREADS_PER_BLOCK>>>( + d_grid_result, d_objective, d_dual_product, d_scaling, d_grid_workspace, d_start, d_vdim, 1); + compute_cone_dual_residual_standard_grid_apply_kernel<<>>( + d_grid_result, d_objective, d_dual_product, d_scaling, d_grid_workspace, d_start, d_vdim, 1, blocks_per_cone); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaDeviceSynchronize()); + + CUDA_CHECK(cudaMemcpy(warp_result, d_warp_result, bytes, cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(grid_result, d_grid_result, bytes, cudaMemcpyDeviceToHost)); + double error = max_abs_difference(warp_result, grid_result, n); + int pass = error <= 1.0e-10; + printf("large SOC dual residual max_error=%.3e: %s\n", error, pass ? "PASS" : "FAIL"); + + CUDA_CHECK(cudaFree(d_objective)); + CUDA_CHECK(cudaFree(d_dual_product)); + CUDA_CHECK(cudaFree(d_scaling)); + CUDA_CHECK(cudaFree(d_primal)); + CUDA_CHECK(cudaFree(d_warp_result)); + CUDA_CHECK(cudaFree(d_complementarity)); + CUDA_CHECK(cudaFree(d_grid_result)); + CUDA_CHECK(cudaFree(d_warp_workspace)); + CUDA_CHECK(cudaFree(d_grid_workspace)); + CUDA_CHECK(cudaFree(d_start)); + CUDA_CHECK(cudaFree(d_vdim)); + free(objective); + free(dual_product); + free(scaling); + free(warp_result); + free(grid_result); + return pass; +} + +int main(void) +{ + const int k = 65536; + int pass = 1; + pass &= compare_primal_projection(k, 0.5, 2.0); + pass &= compare_primal_projection(k, 0.2, 0.8); + pass &= compare_primal_projection(k, 0.2, -2.0); + pass &= compare_dual_residual(k); + return pass ? 0 : 1; +} diff --git a/test/test_partition_utils.c b/test/test_partition_utils.c new file mode 100644 index 0000000..2c1d5a8 --- /dev/null +++ b/test/test_partition_utils.c @@ -0,0 +1,37 @@ +#include "partition_utils.h" + +#include + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + return 1; \ + } \ + } while (0) + +int main(void) +{ + int cuts[] = {0, 4, 8, 12}; + const int forbidden_starts[] = {2, 7}; + const int forbidden_ends[] = {5, 10}; + CHECK(optimize_partition_cuts(12, 3, forbidden_starts, forbidden_ends, 2, cuts)); + CHECK(cuts[0] == 0 && cuts[1] > 0 && cuts[1] < cuts[2] && cuts[2] < cuts[3] && cuts[3] == 12); + CHECK(!(cuts[1] >= 2 && cuts[1] <= 5) && !(cuts[1] >= 7 && cuts[1] <= 10)); + CHECK(!(cuts[2] >= 2 && cuts[2] <= 5) && !(cuts[2] >= 7 && cuts[2] <= 10)); + + int joint_cuts[] = {0, 10, 11, 101}; + const int joint_forbidden_starts[] = {2, 11}; + const int joint_forbidden_ends[] = {9, 99}; + CHECK(optimize_partition_cuts(101, 3, joint_forbidden_starts, joint_forbidden_ends, 2, joint_cuts)); + CHECK(joint_cuts[1] == 1 && joint_cuts[2] == 10); + + int impossible_cuts[] = {0, 2, 4, 6}; + const int impossible_start[] = {1}; + const int impossible_end[] = {5}; + CHECK(!optimize_partition_cuts(6, 3, impossible_start, impossible_end, 1, impossible_cuts)); + + return 0; +} diff --git a/test/test_power_cone.c b/test/test_power_cone.c new file mode 100644 index 0000000..1c5a37f --- /dev/null +++ b/test/test_power_cone.c @@ -0,0 +1,352 @@ +#include "pdhcg.h" +#include "pdhcg_types.h" + +#include +#include +#include + +static qp_problem_t *make_unconstrained_power_problem(double alpha, const double objective[3]) +{ + static const int row_ptr[] = {0}; + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = 0; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.row_ptr = row_ptr; + + cone_spec_t cone; + memset(&cone, 0, sizeof(cone)); + cone.type = CONE_POWER; + cone.start_idx = 0; + cone.v_dim = 1; + cone.power_alpha = alpha; + return create_qp_problem( + objective, NULL, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); +} + +static qp_problem_t *make_quadratic_power_problem(double alpha, const double center[3], const double weights[3]) +{ + static const int empty_row_ptr[] = {0}; + static const int q_row_ptr[] = {0, 1, 2, 3}; + static const int q_col_ind[] = {0, 1, 2}; + double objective[3]; + for (int i = 0; i < 3; ++i) + objective[i] = -weights[i] * center[i]; + + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = 0; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.row_ptr = empty_row_ptr; + + matrix_desc_t Q; + memset(&Q, 0, sizeof(Q)); + Q.m = 3; + Q.n = 3; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 3; + Q.data.csr.row_ptr = q_row_ptr; + Q.data.csr.col_ind = q_col_ind; + Q.data.csr.vals = weights; + + cone_spec_t cone; + memset(&cone, 0, sizeof(cone)); + cone.type = CONE_POWER; + cone.start_idx = 0; + cone.v_dim = 1; + cone.power_alpha = alpha; + return create_qp_problem( + objective, &Q, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); +} + +static pdhcg_result_t *solve_tiny_with_norm(qp_problem_t *problem, norm_type_t optimality_norm) +{ + pdhg_parameters_t parameters; + set_default_parameters(¶meters); + parameters.verbose = 0; + parameters.presolve = false; + parameters.optimality_norm = optimality_norm; + parameters.termination_evaluation_frequency = 10; + parameters.termination_criteria.eps_optimal_relative = 1e-8; + parameters.termination_criteria.eps_feasible_relative = 1e-8; + parameters.termination_criteria.iteration_limit = 1000000; + parameters.termination_criteria.time_sec_limit = 30.0; + return solve_qp_problem(problem, ¶meters); +} + +static pdhcg_result_t *solve_tiny(qp_problem_t *problem) +{ + return solve_tiny_with_norm(problem, NORM_TYPE_L_INF); +} + +static int check_solution(const char *name, pdhcg_result_t *result, const double expected[3], double tolerance) +{ + if (!result || result->termination_reason != TERMINATION_REASON_OPTIMAL) + { + fprintf(stderr, "%s: expected OPTIMAL, got %d\n", name, result ? (int)result->termination_reason : -1); + return 0; + } + for (int i = 0; i < 3; ++i) + { + double error = fabs(result->primal_solution[i] - expected[i]); + if (error > tolerance * (1.0 + fabs(expected[i]))) + { + fprintf(stderr, + "%s: coordinate %d is %.17g, expected %.17g (error %.3e)\n", + name, + i, + result->primal_solution[i], + expected[i], + error); + return 0; + } + } + return 1; +} + +static int run_full_cone_case(double alpha, norm_type_t optimality_norm) +{ + static const int row_ptr[] = {0, 2}; + static const int col_ind[] = {0, 1}; + static const double values[] = {1.0, 1.0}; + static const double rhs[] = {2.0}; + static const double objective[] = {0.0, 0.0, -1.0}; + const double primal_start[] = {1.0, 1.0, 0.0}; + const double dual_start[] = {0.0}; + + matrix_desc_t A; + memset(&A, 0, sizeof(A)); + A.m = 1; + A.n = 3; + A.fmt = matrix_csr; + A.data.csr.nnz = 2; + A.data.csr.row_ptr = row_ptr; + A.data.csr.col_ind = col_ind; + A.data.csr.vals = values; + + cone_spec_t cone; + memset(&cone, 0, sizeof(cone)); + cone.type = CONE_POWER; + cone.start_idx = 0; + cone.v_dim = 1; + cone.power_alpha = alpha; + qp_problem_t *problem = + create_qp_problem(objective, NULL, NULL, NULL, &A, rhs, rhs, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + if (!problem) + return 0; + set_start_values(problem, primal_start, dual_start); + + pdhcg_result_t *result = solve_tiny_with_norm(problem, optimality_norm); + double om = 1.0 - alpha; + double expected[3] = {2.0 * alpha, 2.0 * om, pow(2.0 * alpha, alpha) * pow(2.0 * om, om)}; + int passed = check_solution("full power cone", result, expected, 2e-5); + if (result && result->total_count == 0) + { + fprintf(stderr, "full power cone: nonoptimal feasible warm start was accepted at iteration zero\n"); + passed = 0; + } + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int run_fixed_case(const char *name, + double alpha, + const double objective[3], + const char fixed[3], + const double fixed_value[3], + const double expected[3]) +{ + qp_problem_t *problem = make_unconstrained_power_problem(alpha, objective); + if (!problem) + return 0; + for (int slot = 0; slot < 3; ++slot) + { + if (fixed[slot] && set_cone_fixed(problem, 0, slot, fixed_value[slot]) != 0) + { + qp_problem_free(problem); + return 0; + } + } + + pdhcg_result_t *result = solve_tiny(problem); + int passed = check_solution(name, result, expected, 3e-5); + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int run_infeasible_fixed_case(void) +{ + static const double objective[] = {0.0, 0.0, 0.0}; + qp_problem_t *problem = make_unconstrained_power_problem(0.3, objective); + if (!problem) + return 0; + int setup_ok = set_cone_fixed(problem, 0, 0, 1.0) == 0 && set_cone_fixed(problem, 0, 1, 1.0) == 0 && + set_cone_fixed(problem, 0, 2, 2.0) == 0; + pdhcg_result_t *result = setup_ok ? solve_tiny(problem) : NULL; + int passed = setup_ok && result == NULL; + if (!passed) + fprintf(stderr, "infeasible fully fixed power cone was not rejected\n"); + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +static int run_diagonal_q_cases(void) +{ + int passed = 1; + { + const double center[3] = {1.5, -0.4, -2.2}; + const double weights[3] = {0.3, 2.0, 5.0}; + const double expected[3] = {3.427365210244501, 0.4816579589566281, -1.902365192823554}; + qp_problem_t *problem = make_quadratic_power_problem(0.7, center, weights); + pdhcg_result_t *result = problem ? solve_tiny(problem) : NULL; + passed &= check_solution("diagonal Q full power cone", result, expected, 2e-4); + pdhcg_result_free(result); + qp_problem_free(problem); + } + { + const double center[3] = {-0.5, 0.2, 1.0}; + const double weights[3] = {2.0, 0.7, 1.0}; + const double expected[3] = {0.3619815616314009, 1.545732644202259, 1.0}; + qp_problem_t *problem = make_quadratic_power_problem(0.3, center, weights); + if (problem && set_cone_fixed(problem, 0, 2, 1.0) != 0) + { + qp_problem_free(problem); + problem = NULL; + } + pdhcg_result_t *result = problem ? solve_tiny(problem) : NULL; + passed &= check_solution("diagonal Q fixed-z power cone", result, expected, 2e-4); + pdhcg_result_free(result); + qp_problem_free(problem); + } + return passed; +} + +static int run_sharp_fixed_axis_case(void) +{ + const double alpha = 0.97; + const double center[3] = {0.62147354, -0.01546521, 0.19700471}; + const double weights[3] = {0.131333592, 0.0263303077, 38.3559275}; + const double start[3] = {0.62720941, -0.01546521, 0.19700471}; + qp_problem_t *problem = make_quadratic_power_problem(alpha, center, weights); + if (!problem || set_cone_fixed(problem, 0, 0, start[0]) != 0) + { + qp_problem_free(problem); + return 0; + } + set_start_values(problem, start, NULL); + + pdhcg_result_t *result = solve_tiny(problem); + int passed = result && result->termination_reason == TERMINATION_REASON_OPTIMAL; + if (result) + { + double x = result->primal_solution[0]; + double y = result->primal_solution[1]; + double z = result->primal_solution[2]; + double bound = x > 0.0 && y > 0.0 ? pow(x, alpha) * pow(y, 1.0 - alpha) : 0.0; + double violation = fmax(0.0, fmax(-x, fmax(-y, fabs(z) - bound))); + passed &= x == start[0] && violation <= 1e-12 && result->relative_primal_residual <= 1e-8; + if (!passed) + fprintf(stderr, + "sharp fixed-axis power cone failed: status=%d x=%.17g y=%.17g z=%.17g violation=%.3e " + "primal=%.3e dual=%.3e\n", + (int)result->termination_reason, + x, + y, + z, + violation, + result->relative_primal_residual, + result->relative_dual_residual); + } + else + { + fprintf(stderr, "sharp fixed-axis power cone returned NULL\n"); + } + pdhcg_result_free(result); + qp_problem_free(problem); + return passed; +} + +int main(void) +{ + int passed = 1; + passed &= run_full_cone_case(0.2, NORM_TYPE_L_INF); + passed &= run_full_cone_case(0.5, NORM_TYPE_L_INF); + passed &= run_full_cone_case(0.8, NORM_TYPE_L_INF); + passed &= run_full_cone_case(0.5, NORM_TYPE_L2); + + { + const double alpha = 0.3; + const double om = 1.0 - alpha; + const double lambda = 1.0 / (pow(alpha, alpha) * pow(om, om)); + const double objective[3] = {1.0, 1.0, 0.0}; + const char fixed[3] = {0, 0, 1}; + const double values[3] = {0.0, 0.0, 1.0}; + const double expected[3] = {alpha * lambda, om * lambda, 1.0}; + passed &= run_fixed_case("fixed z", alpha, objective, fixed, values, expected); + } + { + const double alpha = 0.3; + const double objective[3] = {0.0, 1.0 - alpha, -1.0}; + const char fixed[3] = {1, 0, 0}; + const double values[3] = {1.0, 0.0, 0.0}; + const double expected[3] = {1.0, 1.0, 1.0}; + passed &= run_fixed_case("fixed x", alpha, objective, fixed, values, expected); + } + { + const double alpha = 0.7; + const double objective[3] = {alpha, 0.0, -1.0}; + const char fixed[3] = {0, 1, 0}; + const double values[3] = {0.0, 1.0, 0.0}; + const double expected[3] = {1.0, 1.0, 1.0}; + passed &= run_fixed_case("fixed y", alpha, objective, fixed, values, expected); + } + { + const double objective[3] = {0.0, 1.0, 0.0}; + const char fixed[3] = {1, 0, 1}; + const double values[3] = {1.0, 0.0, 1.0}; + const double expected[3] = {1.0, 1.0, 1.0}; + passed &= run_fixed_case("fixed x,z", 0.3, objective, fixed, values, expected); + } + { + const double objective[3] = {1.0, 0.0, 0.0}; + const char fixed[3] = {0, 1, 1}; + const double values[3] = {0.0, 1.0, 1.0}; + const double expected[3] = {1.0, 1.0, 1.0}; + passed &= run_fixed_case("fixed y,z", 0.7, objective, fixed, values, expected); + } + { + const double alpha = 0.3; + const double objective[3] = {0.0, 0.0, -1.0}; + const char fixed[3] = {1, 1, 0}; + const double values[3] = {2.0, 3.0, 0.0}; + const double expected[3] = {2.0, 3.0, pow(2.0, alpha) * pow(3.0, 1.0 - alpha)}; + passed &= run_fixed_case("fixed x,y", alpha, objective, fixed, values, expected); + } + { + const double objective[3] = {1.0, 1.0, 0.0}; + const char fixed[3] = {0, 0, 1}; + const double values[3] = {0.0, 0.0, 0.0}; + const double expected[3] = {0.0, 0.0, 0.0}; + passed &= run_fixed_case("fixed zero z", 0.3, objective, fixed, values, expected); + } + { + const double alpha = 0.86039292839558623; + const double objective[3] = {0.0, 0.0, 0.0}; + const char fixed[3] = {1, 1, 1}; + const double values[3] = { + 4.4414605580442319e83, + 2.0775280372919238e-95, + 5.6415660092721006e58, + }; + passed &= run_fixed_case("fully fixed roundoff boundary", alpha, objective, fixed, values, values); + } + passed &= run_infeasible_fixed_case(); + passed &= run_diagonal_q_cases(); + passed &= run_sharp_fixed_axis_case(); + return passed ? 0 : 1; +} diff --git a/test/test_power_cone_projection.cu b/test/test_power_cone_projection.cu new file mode 100644 index 0000000..7f116c3 --- /dev/null +++ b/test/test_power_cone_projection.cu @@ -0,0 +1,407 @@ +#include "pdhcg_kernels.cuh" + +#include +#include +#include +#include +#include + +static int cuda_ok(cudaError_t status, const char *operation) +{ + if (status == cudaSuccess) + return 1; + fprintf(stderr, "%s failed: %s\n", operation, cudaGetErrorString(status)); + return 0; +} + +static double power_bound(double x, double y, double alpha) +{ + if (x <= 0.0 || y <= 0.0) + return 0.0; + double log_bound = alpha * log(x) + (1.0 - alpha) * log(y); + if (log_bound >= log(DBL_MAX)) + return INFINITY; + if (log_bound <= log(DBL_MIN)) + return 0.0; + return exp(log_bound); +} + +static int check_free_projection_kkt( + const char *name, double alpha, const double input[3], const double weights[3], const double projected[3]) +{ + double point_scale = 0.0; + for (int i = 0; i < 3; ++i) + point_scale = fmax(point_scale, fmax(fabs(input[i]), fabs(projected[i]))); + if (point_scale == 0.0) + return 1; + + double normalized_input_x = input[0] / point_scale; + double normalized_input_y = input[1] / point_scale; + double normalized_input_z = input[2] / point_scale; + double normalized_input_bound = power_bound(normalized_input_x, normalized_input_y, alpha); + if (normalized_input_x >= 0.0 && normalized_input_y >= 0.0 && + fabs(normalized_input_z) <= normalized_input_bound * (1.0 + 64.0 * DBL_EPSILON)) + return 1; + + double polar[3]; + double polar_scale = 0.0; + for (int i = 0; i < 3; ++i) + { + polar[i] = weights[i] * (input[i] / point_scale - projected[i] / point_scale); + polar_scale = fmax(polar_scale, fabs(polar[i])); + } + if (polar_scale == 0.0) + return 1; + + /* When an active-coordinate correction is below one ULP of the input, + the returned point no longer contains enough information to reconstruct + that component of the normal vector. Explicit extreme-scale cases are + checked against reference projections separately. */ + if (projected[0] != 0.0 && projected[1] != 0.0 && projected[2] != 0.0 && + (fabs(polar[0]) <= 64.0 * DBL_EPSILON * polar_scale || fabs(polar[1]) <= 64.0 * DBL_EPSILON * polar_scale || + fabs(polar[2]) <= 64.0 * DBL_EPSILON * polar_scale)) + return 1; + for (int i = 0; i < 3; ++i) + polar[i] /= polar_scale; + + const double tolerance = 1e-5; + int ok = 1; + if (polar[0] > tolerance || polar[1] > tolerance) + ok = 0; + + double abs_w = fabs(polar[2]); + if (abs_w > tolerance) + { + double u = -polar[0] / alpha; + double v = -polar[1] / (1.0 - alpha); + if (u <= 0.0 || v <= 0.0 || alpha * log(u) + (1.0 - alpha) * log(v) + tolerance < log(abs_w)) + ok = 0; + } + + double complementarity = 0.0; + double complementarity_scale = 1.0; + for (int i = 0; i < 3; ++i) + { + double term = (projected[i] / point_scale) * polar[i]; + complementarity += term; + complementarity_scale += fabs(term); + } + if (fabs(complementarity) > tolerance * complementarity_scale) + ok = 0; + + if (!ok) + { + fprintf(stderr, + "%s: projection KKT failed: alpha=%.17g, input=(%.17g, %.17g, %.17g), " + "weights=(%.17g, %.17g, %.17g), point=(%.17g, %.17g, %.17g), " + "polar=(%.3e, %.3e, %.3e), complementarity=%.3e\n", + name, + alpha, + input[0], + input[1], + input[2], + weights[0], + weights[1], + weights[2], + projected[0], + projected[1], + projected[2], + polar[0], + polar[1], + polar[2], + complementarity); + } + return ok; +} + +static int run_case(const char *name, + double alpha, + const double input[3], + const double weights[3], + const char fixed[3], + const double expected[3], + double tolerance) +{ + double scaled_input[3]; + double scale[3]; + double actual[3] = {0.0, 0.0, 0.0}; + double actual_violation = 0.0; + double input_bound = 0.0; + double expected_violation = 0.0; + double projected_bound = 0.0; + double projected_violation = 0.0; + for (int i = 0; i < 3; ++i) + { + scale[i] = sqrt(weights[i]); + scaled_input[i] = input[i] * scale[i]; + } + + double *d_point = NULL; + double *d_scale = NULL; + double *d_warm = NULL; + double *d_alpha = NULL; + double *d_violation = NULL; + double *d_relative_violation = NULL; + int *d_start = NULL; + int *d_dim = NULL; + char *d_fixed = NULL; + int start = 0; + int dim = 1; + double warm = 0.0; + int ok = cuda_ok(cudaMalloc(&d_point, 3 * sizeof(double)), "cudaMalloc(point)") && + cuda_ok(cudaMalloc(&d_scale, 3 * sizeof(double)), "cudaMalloc(scale)") && + cuda_ok(cudaMalloc(&d_warm, sizeof(double)), "cudaMalloc(warm)") && + cuda_ok(cudaMalloc(&d_alpha, sizeof(double)), "cudaMalloc(alpha)") && + cuda_ok(cudaMalloc(&d_violation, sizeof(double)), "cudaMalloc(violation)") && + cuda_ok(cudaMalloc(&d_relative_violation, sizeof(double)), "cudaMalloc(relative violation)") && + cuda_ok(cudaMalloc(&d_start, sizeof(int)), "cudaMalloc(start)") && + cuda_ok(cudaMalloc(&d_dim, sizeof(int)), "cudaMalloc(dim)") && + cuda_ok(cudaMalloc(&d_fixed, 3 * sizeof(char)), "cudaMalloc(fixed)"); + if (!ok) + goto cleanup; + + ok = cuda_ok(cudaMemcpy(d_point, scaled_input, 3 * sizeof(double), cudaMemcpyHostToDevice), "copy point") && + cuda_ok(cudaMemcpy(d_scale, scale, 3 * sizeof(double), cudaMemcpyHostToDevice), "copy scale") && + cuda_ok(cudaMemcpy(d_warm, &warm, sizeof(double), cudaMemcpyHostToDevice), "copy warm") && + cuda_ok(cudaMemcpy(d_alpha, &alpha, sizeof(double), cudaMemcpyHostToDevice), "copy alpha") && + cuda_ok(cudaMemcpy(d_start, &start, sizeof(int), cudaMemcpyHostToDevice), "copy start") && + cuda_ok(cudaMemcpy(d_dim, &dim, sizeof(int), cudaMemcpyHostToDevice), "copy dim") && + cuda_ok(cudaMemcpy(d_fixed, fixed, 3 * sizeof(char), cudaMemcpyHostToDevice), "copy fixed"); + if (!ok) + goto cleanup; + + compute_power_cone_primal_violation_kernel<<<1, 1>>>( + d_violation, d_relative_violation, d_point, d_scale, d_start, d_alpha, 1.0, 1); + ok = cuda_ok(cudaGetLastError(), "power violation kernel launch") && + cuda_ok(cudaDeviceSynchronize(), "power violation kernel sync") && + cuda_ok(cudaMemcpy(&actual_violation, d_violation, sizeof(double), cudaMemcpyDeviceToHost), "copy violation"); + input_bound = power_bound(input[0], input[1], alpha); + expected_violation = fmax(0.0, fmax(-input[0], fmax(-input[1], fabs(input[2]) - input_bound))); + if (!ok || fabs(actual_violation - expected_violation) > 1e-12 * (1.0 + expected_violation)) + { + fprintf( + stderr, "%s: membership violation is %.17g, expected %.17g\n", name, actual_violation, expected_violation); + ok = 0; + goto cleanup; + } + + project_power_cone_kernel<<<1, 1>>>(d_point, d_scale, d_warm, d_start, d_dim, d_alpha, d_fixed, 1); + ok = cuda_ok(cudaGetLastError(), "project_power_cone_kernel launch") && + cuda_ok(cudaDeviceSynchronize(), "project_power_cone_kernel sync") && + cuda_ok(cudaMemcpy(scaled_input, d_point, 3 * sizeof(double), cudaMemcpyDeviceToHost), "copy result"); + if (!ok) + goto cleanup; + + for (int i = 0; i < 3; ++i) + { + actual[i] = scaled_input[i] / scale[i]; + double error = expected ? fabs(actual[i] - expected[i]) : 0.0; + if (expected && error > tolerance * (1.0 + fabs(expected[i]))) + { + fprintf(stderr, + "%s: coordinate %d is %.17g, expected %.17g (error %.3e)\n", + name, + i, + actual[i], + expected[i], + error); + ok = 0; + } + if (fixed[i] && actual[i] != input[i]) + { + fprintf(stderr, "%s: fixed coordinate %d changed from %.17g to %.17g\n", name, i, input[i], actual[i]); + ok = 0; + } + } + projected_bound = power_bound(actual[0], actual[1], alpha); + projected_violation = fmax(0.0, fmax(-actual[0], fmax(-actual[1], fabs(actual[2]) - projected_bound))); + if (projected_violation > 1e-10 * (1.0 + fabs(actual[2]))) + { + fprintf(stderr, "%s: projected point violates the power cone by %.3e\n", name, projected_violation); + ok = 0; + } + if (!expected && !fixed[0] && !fixed[1] && !fixed[2]) + ok &= check_free_projection_kkt(name, alpha, input, weights, actual); + +cleanup: + cudaFree(d_point); + cudaFree(d_scale); + cudaFree(d_warm); + cudaFree(d_alpha); + cudaFree(d_violation); + cudaFree(d_relative_violation); + cudaFree(d_start); + cudaFree(d_dim); + cudaFree(d_fixed); + return ok; +} + +static uint64_t random_state = UINT64_C(0x8d12e93a5bc7416f); + +static double random_unit(void) +{ + random_state = random_state * UINT64_C(6364136223846793005) + UINT64_C(1442695040888963407); + return (double)(random_state >> 11) * 0x1.0p-53; +} + +static int run_random_free_projection_cases(void) +{ + static const char free_slots[3] = {0, 0, 0}; + int passed = 1; + for (int case_idx = 0; case_idx < 96; ++case_idx) + { + double alpha = 0.1 + 0.8 * random_unit(); + double common_scale = pow(10.0, -120.0 + 240.0 * random_unit()); + double input[3] = { + common_scale * (6.0 * random_unit() - 3.0), + common_scale * (6.0 * random_unit() - 3.0), + common_scale * (6.0 * random_unit() - 3.0), + }; + double weights[3] = { + pow(10.0, -2.0 + 4.0 * random_unit()), + pow(10.0, -2.0 + 4.0 * random_unit()), + pow(10.0, -2.0 + 4.0 * random_unit()), + }; + char name[64]; + snprintf(name, sizeof(name), "random-free-%d", case_idx); + passed &= run_case(name, alpha, input, weights, free_slots, NULL, 0.0); + } + return passed; +} + +int main(void) +{ + static const char free_slots[3] = {0, 0, 0}; + static const char fixed_z[3] = {0, 0, 1}; + static const char fixed_x[3] = {1, 0, 0}; + static const char fixed_y[3] = {0, 1, 0}; + static const char fixed_xz[3] = {1, 0, 1}; + static const char fixed_yz[3] = {0, 1, 1}; + static const char fixed_xy[3] = {1, 1, 0}; + static const char fixed_all[3] = {1, 1, 1}; + static const double unit_weights[3] = {1.0, 1.0, 1.0}; + + int passed = 1; + { + const double input[3] = {-0.7, 1.2, 2.0}; + const double expected[3] = {0.2999138114091835, 1.629436048818783, 0.9806749092638409}; + passed &= run_case("full-unweighted", 0.3, input, unit_weights, free_slots, expected, 3e-6); + } + { + const double input[3] = {-0.7e200, 1.2e200, 2.0e200}; + const double expected[3] = {0.2999138114091835e200, 1.629436048818783e200, 0.9806749092638409e200}; + passed &= run_case("full-huge-scale", 0.3, input, unit_weights, free_slots, expected, 3e-6); + } + { + const double input[3] = {1.5, -0.4, -2.2}; + const double weights[3] = {0.3, 2.0, 5.0}; + const double expected[3] = {3.427365210244501, 0.4816579589566281, -1.902365192823554}; + passed &= run_case("full-weighted", 0.7, input, weights, free_slots, expected, 3e-6); + } + { + const double input[3] = {-1e16, 1e16, 5e15}; + const double expected[3] = {2.8874860407696615e-15, 1e16, 5.833305132868003e-15}; + passed &= run_case("full-sharp-root", 0.99, input, unit_weights, free_slots, expected, 1e-12); + } + { + const double input[3] = {-1e200, 1e200, 1e100}; + const double expected[3] = {1.0 / 9.0, 1e200, 1e100 / 3.0}; + passed &= run_case("full-wide-dynamic-range", 0.5, input, unit_weights, free_slots, expected, 1e-12); + } + { + const double input[3] = {2.0, -3.0, 0.0}; + const double weights[3] = {2.0, 7.0, 0.25}; + const double expected[3] = {2.0, 0.0, 0.0}; + passed &= run_case("full-zero-z", 0.5, input, weights, free_slots, expected, 1e-12); + } + { + const double input[3] = {-1.0, -1.0, 0.2}; + const double expected[3] = {0.0, 0.0, 0.0}; + passed &= run_case("full-opposite-cone", 0.5, input, unit_weights, free_slots, expected, 1e-12); + } + { + const double input[3] = {-0.5, 0.2, 1.0}; + const double weights[3] = {2.0, 0.7, 1.0}; + const double expected[3] = {0.3619815616314009, 1.545732644202259, 1.0}; + passed &= run_case("fixed-z", 0.3, input, weights, fixed_z, expected, 3e-5); + } + { + const double input[3] = {-0.5e200, 0.2e200, 1.0e200}; + const double weights[3] = {2.0, 0.7, 1.0}; + const double expected[3] = {0.3619815616314009e200, 1.545732644202259e200, 1.0e200}; + passed &= run_case("fixed-z-huge-scale", 0.3, input, weights, fixed_z, expected, 3e-5); + } + { + const double input[3] = {0.0, 0.0, 1.0}; + const double weights[3] = {1e200, 1e200, 1.0}; + const double expected[3] = {1.0, 1.0, 1.0}; + passed &= run_case("fixed-z-huge-weight", 0.5, input, weights, fixed_z, expected, 1e-10); + } + { + const double input[3] = {0.0, 0.0, 1.0}; + const double weights[3] = {1e-200, 1e-200, 1.0}; + const double expected[3] = {1.0, 1.0, 1.0}; + passed &= run_case("fixed-z-tiny-weight", 0.5, input, weights, fixed_z, expected, 1e-10); + } + { + const double input[3] = {1.0, -0.4, 2.0}; + const double weights[3] = {1.0, 2.0, 0.5}; + const double expected[3] = {1.0, 0.1445424296046515, 0.2582240838862325}; + passed &= run_case("fixed-x", 0.3, input, weights, fixed_x, expected, 3e-5); + } + { + const double alpha = 0.97; + const double input[3] = {0.62720941, -0.01546521, 0.19700471}; + const double expected_y = pow(fabs(input[2]) / pow(input[0], alpha), 1.0 / (1.0 - alpha)); + const double expected[3] = {input[0], expected_y, input[2]}; + passed &= run_case("fixed-x-sharp", alpha, input, unit_weights, fixed_x, expected, 1e-10); + } + { + const double alpha = 0.999; + const double input[3] = {1.0, -0.1, 0.5}; + const double expected_y = pow(0.5, 1.0 / (1.0 - alpha)); + const double expected[3] = {input[0], expected_y, input[2]}; + passed &= run_case("fixed-x-ultrasharp", alpha, input, unit_weights, fixed_x, expected, 1e-10); + } + { + const double input[3] = {0.00093260334688321987, 0.0, 1.0}; + const double expected[3] = { + input[0], + 0.0030706556671616642, + 0.00094378334963825893, + }; + passed &= run_case("fixed-x-huge-feasible-bound", 0.99, input, unit_weights, fixed_x, expected, 1e-9); + } + { + const double input[3] = {-0.3, 1.0, -1.8}; + const double weights[3] = {3.0, 1.0, 0.8}; + const double expected[3] = {0.1744696096520296, 1.0, -0.2945804034203198}; + passed &= run_case("fixed-y", 0.7, input, weights, fixed_y, expected, 3e-5); + } + { + const double input[3] = {1.0, -0.4, 1.0}; + const double weights[3] = {1.0, 2.0, 0.5}; + const double expected[3] = {1.0, 1.0, 1.0}; + passed &= run_case("fixed-xz", 0.3, input, weights, fixed_xz, expected, 1e-12); + } + { + const double input[3] = {-0.3, 1.0, -1.0}; + const double weights[3] = {3.0, 1.0, 0.8}; + const double expected[3] = {1.0, 1.0, -1.0}; + passed &= run_case("fixed-yz", 0.7, input, weights, fixed_yz, expected, 1e-12); + } + { + const double input[3] = {2.0, 3.0, 4.0}; + const double weights[3] = {2.0, 7.0, 0.25}; + const double expected[3] = {2.0, 3.0, 2.656402479886323}; + passed &= run_case("fixed-xy", 0.3, input, weights, fixed_xy, expected, 1e-12); + } + { + const double input[3] = {1.0, 1.0, 0.5}; + const double expected[3] = {1.0, 1.0, 0.5}; + passed &= run_case("fixed-all", 0.3, input, unit_weights, fixed_all, expected, 1e-12); + } + passed &= run_random_free_projection_cases(); + + return passed ? 0 : 1; +} diff --git a/test/test_prefos_integration.c b/test/test_prefos_integration.c new file mode 100644 index 0000000..230d81f --- /dev/null +++ b/test/test_prefos_integration.c @@ -0,0 +1,319 @@ +/* +Copyright 2026 Hongpei Li + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "pdhcg_types.h" +#include "presolve_wrapper.h" + +#include +#include +#include +#include + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + return 1; \ + } \ + } while (0) + +static CsrComponent *create_csr(int rows, int nnz) +{ + CsrComponent *matrix = (CsrComponent *)calloc(1, sizeof(CsrComponent)); + if (!matrix) + return NULL; + matrix->row_ptr = (int *)calloc((size_t)rows + 1, sizeof(int)); + matrix->col_ind = nnz > 0 ? (int *)calloc((size_t)nnz, sizeof(int)) : NULL; + matrix->val = nnz > 0 ? (double *)calloc((size_t)nnz, sizeof(double)) : NULL; + if (!matrix->row_ptr || (nnz > 0 && (!matrix->col_ind || !matrix->val))) + { + free(matrix->row_ptr); + free(matrix->col_ind); + free(matrix->val); + free(matrix); + return NULL; + } + return matrix; +} + +static void free_csr(CsrComponent *matrix) +{ + if (!matrix) + return; + free(matrix->row_ptr); + free(matrix->col_ind); + free(matrix->val); + free(matrix); +} + +static pdhg_parameters_t quiet_parameters(void) +{ + pdhg_parameters_t parameters; + memset(¶meters, 0, sizeof(parameters)); + parameters.verbose = 0; + parameters.termination_criteria.eps_feasible_relative = 1e-8; + return parameters; +} + +static int test_all_fixed_postsolve(void) +{ + qp_problem_t problem; + pdhg_parameters_t parameters = quiet_parameters(); + pdhcg_presolve_info_t *info; + pdhcg_result_t *result; + double lower[] = {0.0, 0.0}; + double upper[] = {1e30, 0.0}; + double objective[] = {1.0, 2.0}; + double row_lower[] = {1.0}; + double row_upper[] = {1.0}; + + memset(&problem, 0, sizeof(problem)); + problem.num_variables = 2; + problem.num_constraints = 1; + problem.constraint_matrix_num_nonzeros = 2; + problem.constraint_matrix = create_csr(1, 2); + CHECK(problem.constraint_matrix != NULL); + problem.constraint_matrix->row_ptr[1] = 2; + problem.constraint_matrix->col_ind[0] = 0; + problem.constraint_matrix->col_ind[1] = 1; + problem.constraint_matrix->val[0] = 1.0; + problem.constraint_matrix->val[1] = 1.0; + problem.variable_lower_bound = lower; + problem.variable_upper_bound = upper; + problem.objective_vector = objective; + problem.constraint_lower_bound = row_lower; + problem.constraint_upper_bound = row_upper; + + info = pdhcg_presolve(&problem, ¶meters); + CHECK(info != NULL); + CHECK(info->presolve_status == PDHCG_PRESOLVE_STATUS_REDUCED); + if (info->problem_solved_during_presolve) + { + result = pdhcg_create_result_from_presolve(info, &problem); + CHECK(result != NULL); + CHECK(result->termination_reason == TERMINATION_REASON_OPTIMAL); + CHECK(fabs(result->primal_objective_value - 1.0) <= 1e-12); + } + else + { + CHECK(info->reduced_problem != NULL); + CHECK(info->reduced_problem->num_variables == 1); + result = (pdhcg_result_t *)calloc(1, sizeof(pdhcg_result_t)); + CHECK(result != NULL); + result->primal_solution = (double *)calloc((size_t)info->reduced_problem->num_variables, sizeof(double)); + result->dual_solution = (double *)calloc((size_t)info->reduced_problem->num_constraints, sizeof(double)); + result->reduced_cost = (double *)calloc((size_t)info->reduced_problem->num_variables, sizeof(double)); + CHECK(result->primal_solution && result->dual_solution && result->reduced_cost); + result->primal_solution[0] = 1.0; + result->dual_solution[0] = 1.0; + CHECK(pdhcg_postsolve(info, result, &problem)); + } + CHECK(result->primal_solution != NULL); + CHECK(fabs(result->primal_solution[0] - 1.0) <= 1e-12); + CHECK(fabs(result->primal_solution[1]) <= 1e-12); + + free(result->primal_solution); + free(result->dual_solution); + free(result->reduced_cost); + free(result); + pdhcg_presolve_info_free(info); + free_csr(problem.constraint_matrix); + return 0; +} + +static int test_soc_layout_and_postsolve(void) +{ + qp_problem_t problem; + pdhg_parameters_t parameters = quiet_parameters(); + pdhcg_presolve_info_t *info; + pdhcg_result_t result; + double lower[] = {2.0, -INFINITY, -INFINITY, -INFINITY}; + double upper[] = {2.0, INFINITY, INFINITY, INFINITY}; + double objective[] = {0.0, 0.0, 0.0, 0.0}; + int cone_start[] = {1}; + int cone_v_dim[] = {1}; + cone_type_t cone_type[] = {CONE_STANDARD_SOC}; + + memset(&problem, 0, sizeof(problem)); + problem.num_variables = 4; + problem.num_constraints = 0; + problem.constraint_matrix = create_csr(0, 0); + CHECK(problem.constraint_matrix != NULL); + problem.variable_lower_bound = lower; + problem.variable_upper_bound = upper; + problem.objective_vector = objective; + problem.cones.num_cones = 1; + problem.cones.start_idx = cone_start; + problem.cones.v_dim = cone_v_dim; + problem.cones.type = cone_type; + + info = pdhcg_presolve(&problem, ¶meters); + CHECK(info != NULL); + CHECK(info->presolve_status == PDHCG_PRESOLVE_STATUS_REDUCED); + CHECK(!info->problem_solved_during_presolve); + CHECK(info->reduced_problem != NULL); + CHECK(info->reduced_problem->num_variables == 3); + CHECK(info->reduced_problem->cones.num_cones == 1); + CHECK(info->reduced_problem->cones.start_idx[0] == 0); + CHECK(info->reduced_problem->cones.v_dim[0] == 1); + CHECK(info->reduced_problem->cones.type[0] == CONE_STANDARD_SOC); + + memset(&result, 0, sizeof(result)); + result.primal_solution = (double *)calloc(3, sizeof(double)); + result.reduced_cost = (double *)calloc(3, sizeof(double)); + CHECK(result.primal_solution && result.reduced_cost); + result.primal_solution[2] = 1.0; + CHECK(pdhcg_postsolve(info, &result, &problem)); + CHECK(result.num_variables == 4); + CHECK(fabs(result.primal_solution[0] - 2.0) <= 1e-12); + CHECK(fabs(result.primal_solution[1]) <= 1e-12); + CHECK(fabs(result.primal_solution[2]) <= 1e-12); + CHECK(fabs(result.primal_solution[3] - 1.0) <= 1e-12); + + free(result.primal_solution); + free(result.dual_solution); + free(result.reduced_cost); + pdhcg_presolve_info_free(info); + free_csr(problem.constraint_matrix); + return 0; +} + +static int test_power_layout_and_alpha(void) +{ + qp_problem_t problem; + pdhg_parameters_t parameters = quiet_parameters(); + pdhcg_presolve_info_t *info; + double lower[] = {2.0, -INFINITY, -INFINITY, -INFINITY}; + double upper[] = {2.0, INFINITY, INFINITY, INFINITY}; + double objective[] = {0.0, 0.0, 0.0, 0.0}; + int cone_start[] = {1}; + int cone_v_dim[] = {1}; + cone_type_t cone_type[] = {CONE_POWER}; + double power_alpha[] = {0.37}; + + memset(&problem, 0, sizeof(problem)); + problem.num_variables = 4; + problem.constraint_matrix = create_csr(0, 0); + CHECK(problem.constraint_matrix != NULL); + problem.variable_lower_bound = lower; + problem.variable_upper_bound = upper; + problem.objective_vector = objective; + problem.cones.num_cones = 1; + problem.cones.start_idx = cone_start; + problem.cones.v_dim = cone_v_dim; + problem.cones.type = cone_type; + problem.cones.power_alpha = power_alpha; + + info = pdhcg_presolve(&problem, ¶meters); + CHECK(info != NULL); + CHECK(info->presolve_status == PDHCG_PRESOLVE_STATUS_REDUCED); + CHECK(!info->problem_solved_during_presolve); + CHECK(info->reduced_problem != NULL); + CHECK(info->reduced_problem->num_variables == 3); + CHECK(info->reduced_problem->cones.num_cones == 1); + CHECK(info->reduced_problem->cones.start_idx[0] == 0); + CHECK(info->reduced_problem->cones.v_dim[0] == 1); + CHECK(info->reduced_problem->cones.type[0] == CONE_POWER); + CHECK(info->reduced_problem->cones.power_alpha != NULL); + CHECK(fabs(info->reduced_problem->cones.power_alpha[0] - power_alpha[0]) <= 1e-15); + + pdhcg_presolve_info_free(info); + free_csr(problem.constraint_matrix); + return 0; +} + +static int test_diagonal_middle_matrix(void) +{ + qp_problem_t problem; + pdhg_parameters_t parameters = quiet_parameters(); + pdhcg_presolve_info_t *info; + double lower[] = {0.0, 0.0}; + double upper[] = {0.0, INFINITY}; + double objective[] = {0.0, 2.0}; + + memset(&problem, 0, sizeof(problem)); + problem.num_variables = 2; + problem.constraint_matrix = create_csr(0, 0); + problem.objective_sparse_matrix = create_csr(2, 1); + problem.objective_lowrank_matrix = create_csr(1, 1); + problem.objective_lowrank_middle_matrix = create_csr(1, 1); + CHECK(problem.constraint_matrix && problem.objective_sparse_matrix && problem.objective_lowrank_matrix && + problem.objective_lowrank_middle_matrix); + problem.objective_sparse_matrix->row_ptr[1] = 1; + problem.objective_sparse_matrix->row_ptr[2] = 1; + problem.objective_sparse_matrix->col_ind[0] = 0; + problem.objective_sparse_matrix->val[0] = 1.0; + problem.objective_sparse_matrix_num_nonzeros = 1; + problem.objective_lowrank_matrix->row_ptr[1] = 1; + problem.objective_lowrank_matrix->col_ind[0] = 1; + problem.objective_lowrank_matrix->val[0] = 1.0; + problem.objective_lowrank_matrix_num_nonzeros = 1; + problem.num_rank_lowrank_obj = 1; + problem.objective_lowrank_middle_matrix->row_ptr[1] = 1; + problem.objective_lowrank_middle_matrix->col_ind[0] = 0; + problem.objective_lowrank_middle_matrix->val[0] = 2.0; + problem.objective_lowrank_middle_matrix_num_nonzeros = 1; + problem.variable_lower_bound = lower; + problem.variable_upper_bound = upper; + problem.objective_vector = objective; + + info = pdhcg_presolve(&problem, ¶meters); + CHECK(info != NULL); + CHECK(info->presolve_status == PDHCG_PRESOLVE_STATUS_REDUCED); + CHECK(info->reduced_problem != NULL); + CHECK(info->reduced_problem->num_variables == 1); + CHECK(info->reduced_problem->num_rank_lowrank_obj == 1); + CHECK(info->reduced_problem->objective_lowrank_middle_matrix != NULL); + CHECK(info->reduced_problem->objective_lowrank_middle_matrix_num_nonzeros == 1); + CHECK(info->reduced_problem->objective_lowrank_middle_matrix->col_ind[0] == 0); + CHECK(fabs(info->reduced_problem->objective_lowrank_middle_matrix->val[0] - 2.0) <= 1e-12); + pdhcg_presolve_info_free(info); + free_csr(problem.constraint_matrix); + free_csr(problem.objective_sparse_matrix); + free_csr(problem.objective_lowrank_matrix); + free_csr(problem.objective_lowrank_middle_matrix); + return 0; +} + +int main(void) +{ + if (!pdhcg_presolve_available()) + { + printf("PreFOS integration is disabled; skipping.\n"); + return 0; + } + CHECK(strncmp(pdhcg_presolve_version(), "PreFOS ", 7) == 0); + printf("all-fixed postsolve...\n"); + fflush(stdout); + if (test_all_fixed_postsolve()) + return 1; + printf("SOC layout and postsolve...\n"); + fflush(stdout); + if (test_soc_layout_and_postsolve()) + return 1; + printf("Power-cone layout and alpha...\n"); + fflush(stdout); + if (test_power_layout_and_alpha()) + return 1; + printf("diagonal middle matrix...\n"); + fflush(stdout); + if (test_diagonal_middle_matrix()) + return 1; + printf("PreFOS integration tests passed.\n"); + return 0; +} diff --git a/test/test_problem_validation.c b/test/test_problem_validation.c new file mode 100644 index 0000000..b37839c --- /dev/null +++ b/test/test_problem_validation.c @@ -0,0 +1,113 @@ +#include "pdhcg.h" + +#include + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + return 1; \ + } \ + } while (0) + +static matrix_desc_t empty_csr(int rows, int columns, const int *row_ptr) +{ + matrix_desc_t matrix = {0}; + matrix.m = rows; + matrix.n = columns; + matrix.fmt = matrix_csr; + matrix.data.csr.row_ptr = row_ptr; + return matrix; +} + +int main(void) +{ + static const int a_row_ptr[] = {0, 0}; + static const int q2_row_ptr[] = {0, 0, 0}; + static const int r_row_ptr[] = {0, 0}; + matrix_desc_t A = empty_csr(1, 3, a_row_ptr); + matrix_desc_t Q2 = empty_csr(2, 2, q2_row_ptr); + matrix_desc_t R2 = empty_csr(1, 2, r_row_ptr); + + qp_problem_t *problem = + create_qp_problem(NULL, &Q2, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, 0, NULL); + CHECK(problem == NULL); + + problem = create_qp_problem(NULL, NULL, &R2, NULL, &A, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, 0, NULL); + CHECK(problem == NULL); + + matrix_desc_t R3 = empty_csr(1, 3, r_row_ptr); + matrix_desc_t D2 = empty_csr(2, 2, q2_row_ptr); + problem = create_qp_problem(NULL, NULL, &R3, &D2, &A, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, 0, NULL); + CHECK(problem == NULL); + + static const int invalid_row_ptr[] = {0, 1}; + static const int invalid_column[] = {3}; + static const double one[] = {1.0}; + matrix_desc_t invalid_A = empty_csr(1, 3, invalid_row_ptr); + invalid_A.data.csr.nnz = 1; + invalid_A.data.csr.col_ind = invalid_column; + invalid_A.data.csr.vals = one; + problem = create_qp_problem( + NULL, NULL, NULL, NULL, &invalid_A, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, 0, NULL); + CHECK(problem == NULL); + + problem = + create_qp_problem(NULL, NULL, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, -1, NULL, NULL, NULL, 0, NULL); + CHECK(problem == NULL); + + static const char fixed[] = {0, 1, 0}; + cone_spec_t cone = { + .type = CONE_EXPONENTIAL, + .start_idx = 0, + .v_dim = 1, + .is_fixed = fixed, + }; + problem = + create_qp_problem(NULL, NULL, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, 1, &cone, NULL, NULL, 0, NULL); + CHECK(problem != NULL); + CHECK(problem->cones.fixed_mask_size == problem->num_variables); + CHECK(problem->cones.is_fixed != NULL && problem->cones.is_fixed[1] == 1); + qp_problem_free(problem); + + static const int f3_row_ptr[] = {0, 0, 0, 0}; + static const int f4_row_ptr[] = {0, 0, 0, 0, 0}; + static const double affine_offset[] = {1.0, 0.0, 0.0}; + cone_spec_t affine_cone = { + .type = CONE_STANDARD_SOC, + .start_idx = 0, + .v_dim = 1, + }; + + matrix_desc_t wrong_width_F = empty_csr(3, 2, f3_row_ptr); + problem = create_qp_problem(NULL, + NULL, + NULL, + NULL, + &A, + NULL, + NULL, + NULL, + NULL, + NULL, + 0, + NULL, + &wrong_width_F, + affine_offset, + 1, + &affine_cone); + CHECK(problem == NULL); + + matrix_desc_t uncovered_F = empty_csr(4, 3, f4_row_ptr); + problem = create_qp_problem( + NULL, NULL, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, 0, NULL, &uncovered_F, NULL, 1, &affine_cone); + CHECK(problem == NULL); + + problem = create_qp_problem( + NULL, NULL, NULL, NULL, &A, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, affine_offset, 1, &affine_cone); + CHECK(problem == NULL); + + return 0; +} diff --git a/test/test_qcqp_isfixed.c b/test/test_qcqp_isfixed.c new file mode 100644 index 0000000..a00638d --- /dev/null +++ b/test/test_qcqp_isfixed.c @@ -0,0 +1,153 @@ +/* + * Same QCQP (min -x + y^2 s.t. x^2 + y^2 <= 1, optimum x=1, y=0, obj=-1) lifted two ways: + * + * A) pin t via linear equality (t = 1 row + s = 1 row in A matrix; current transform style) + * B) pin t via is_fixed slot (t row removed; cone projection treats t as constant 1) + * + * Both also pin s = 1 the same way (RSOC needs it). The point: do is_fixed slots converge + * faster than linear-equality-pinned slots at tight tolerance? + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +static int solve_and_report(const char *name, qp_problem_t *prob, int t_slot, int s_slot) +{ + if (!prob) + { + fprintf(stderr, "[%s] create failed\n", name); + return 1; + } + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 1; + params.termination_criteria.eps_optimal_relative = 1e-8; + params.termination_criteria.eps_feasible_relative = 1e-8; + params.termination_criteria.iteration_limit = 200000; + params.termination_criteria.time_sec_limit = 60.0; + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + qp_problem_free(prob); + return 1; + } + double x = res->primal_solution[0], y = res->primal_solution[1]; + double t = res->primal_solution[t_slot], s = res->primal_solution[s_slot]; + printf("[%-15s] status=%d iter=%6d obj=%.8f x=%.6f y=%.6f s=%.6f t=%.6f\n", + name, + (int)res->termination_reason, + res->total_count, + res->primal_objective_value, + x, + y, + s, + t); + pdhcg_result_free(res); + qp_problem_free(prob); + return 0; +} + +/* Version A: vars (x, y, v1, v2, s, t); pin s=1 AND t=1 via linear equalities. */ +static qp_problem_t *build_linear_pin(void) +{ + const double SQRT2 = 1.4142135623730951; + /* rows: 0: sqrt2 x - v1 = 0 + 1: sqrt2 y - v2 = 0 + 2: s = 1 + 3: t = 1 */ + double aval[] = {SQRT2, -1.0, SQRT2, -1.0, 1.0, 1.0}; + int acol[] = {0, 2, 1, 3, 4, 5}; + int arow[] = {0, 2, 4, 5, 6}; + matrix_desc_t A = {0}; + A.m = 4; + A.n = 6; + A.fmt = matrix_csr; + A.data.csr.nnz = 6; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + double qval[] = {2.0}; + int qcol[] = {1}; + int qrow[] = {0, 0, 1, 1, 1, 1, 1}; + matrix_desc_t Q = {0}; + Q.m = 6; + Q.n = 6; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 1; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-1.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + double var_lb[] = {-1e30, -1e30, -1e30, -1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30, 1e30, 1e30, 1e30}; + double con_lb[] = {0.0, 0.0, 1.0, 1.0}; + double con_ub[] = {0.0, 0.0, 1.0, 1.0}; + cone_spec_t cones[] = {{.type = CONE_ROTATED_SOC, .start_idx = 2, .v_dim = 2, .is_fixed = NULL}}; + return create_qp_problem( + c, &Q, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); +} + +/* Version B: vars (x, y, v1, v2, s, t); pin s=1, t=1 via is_fixed cone slots. + The s=1 and t=1 LINEAR rows are REMOVED — cone slots [4] and [5] are constants. */ +static qp_problem_t *build_isfixed_pin(void) +{ + const double SQRT2 = 1.4142135623730951; + /* rows: 0: sqrt2 x - v1 = 0 + 1: sqrt2 y - v2 = 0 + (NO s/t pin rows — those are is_fixed) */ + double aval[] = {SQRT2, -1.0, SQRT2, -1.0}; + int acol[] = {0, 2, 1, 3}; + int arow[] = {0, 2, 4}; + matrix_desc_t A = {0}; + A.m = 2; + A.n = 6; + A.fmt = matrix_csr; + A.data.csr.nnz = 4; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + double qval[] = {2.0}; + int qcol[] = {1}; + int qrow[] = {0, 0, 1, 1, 1, 1, 1}; + matrix_desc_t Q = {0}; + Q.m = 6; + Q.n = 6; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 1; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-1.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + double var_lb[] = {-1e30, -1e30, -1e30, -1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30, 1e30, 1e30, 1e30}; + double con_lb[] = {0.0, 0.0}; + double con_ub[] = {0.0, 0.0}; + /* is_fixed pattern over the 4 cone slots (v1, v2, s, t): mark s and t. */ + static const char fix_pattern[4] = {0, 0, 1, 1}; + cone_spec_t cones[] = {{.type = CONE_ROTATED_SOC, .start_idx = 2, .v_dim = 2, .is_fixed = fix_pattern}}; + qp_problem_t *prob = + create_qp_problem(c, &Q, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + if (!prob) + return NULL; + /* primal_start must carry the pin values: s=1 at slot 4, t=1 at slot 5. */ + double primal_start[6] = {0, 0, 0, 0, 1.0, 1.0}; + set_start_values(prob, primal_start, NULL); + return prob; +} + +int main(void) +{ + /* s slot = index 4, t slot = index 5 in both versions */ + int a = solve_and_report("linear-pin", build_linear_pin(), /*t*/ 5, /*s*/ 4); + int b = solve_and_report("is_fixed-pin", build_isfixed_pin(), /*t*/ 5, /*s*/ 4); + return a | b; +} diff --git a/test/test_qcqp_no_aux.c b/test/test_qcqp_no_aux.c new file mode 100644 index 0000000..91411d3 --- /dev/null +++ b/test/test_qcqp_no_aux.c @@ -0,0 +1,143 @@ +/* + * Same QCQP (min -x + y^2 s.t. x^2 + y^2 <= 1) tested two ways: + * A) lifted with aux v_i = sqrt(2) x_i: cone slots have NO Q -> closed form path + * B) NO aux: cone slots ARE (x, y, s, t) with s=t=1/2, Q lives on cone slot y + * -> kernel MUST bisect (w_y = 1 + tau*2 != 1) + * + * Answers whether bisection convergence is fine when Q genuinely lives on cone slots. + * Optimum x=1, y=0, obj=-1 in both cases. + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +static int solve_and_report(const char *name, qp_problem_t *prob, double eps) +{ + if (!prob) + { + fprintf(stderr, "[%s] create failed\n", name); + return 1; + } + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 0; + params.termination_criteria.eps_optimal_relative = eps; + params.termination_criteria.eps_feasible_relative = eps; + params.termination_criteria.iteration_limit = 500000; + params.termination_criteria.time_sec_limit = 30.0; + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + qp_problem_free(prob); + return 1; + } + double x = res->primal_solution[0], y = res->primal_solution[1]; + printf("[%-15s eps=%.0e] status=%d iter=%6d obj=%.8f x=%.6f y=%.6f\n", + name, + eps, + (int)res->termination_reason, + res->total_count, + res->primal_objective_value, + x, + y); + pdhcg_result_free(res); + qp_problem_free(prob); + return 0; +} + +/* A: aux lift. vars = (x, y, v1, v2, s, t); v_i = sqrt(2) x_i; s = t = 1. */ +static qp_problem_t *build_aux(void) +{ + const double SQRT2 = 1.4142135623730951; + double aval[] = {SQRT2, -1.0, SQRT2, -1.0, 1.0, 1.0}; + int acol[] = {0, 2, 1, 3, 4, 5}; + int arow[] = {0, 2, 4, 5, 6}; + matrix_desc_t A = {0}; + A.m = 4; + A.n = 6; + A.fmt = matrix_csr; + A.data.csr.nnz = 6; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + double qval[] = {2.0}; + int qcol[] = {1}; + int qrow[] = {0, 0, 1, 1, 1, 1, 1}; + matrix_desc_t Q = {0}; + Q.m = 6; + Q.n = 6; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 1; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-1.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + double var_lb[] = {-1e30, -1e30, -1e30, -1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30, 1e30, 1e30, 1e30}; + double con_lb[] = {0.0, 0.0, 1.0, 1.0}; + double con_ub[] = {0.0, 0.0, 1.0, 1.0}; + cone_spec_t cones[] = {{.type = CONE_ROTATED_SOC, .start_idx = 2, .v_dim = 2, .is_fixed = NULL}}; + return create_qp_problem( + c, &Q, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); +} + +/* B: NO aux. vars = (x, y, s, t); (x,y,s,t) in K_rsoc, s = t = 1/2 -> x^2 + y^2 <= 1. + Q on cone slot y (index 1). Q_yy = 2 -> weight w_y = 1 + tau*2 != 1. Kernel bisection. */ +static qp_problem_t *build_no_aux(void) +{ + /* rows: 0: s = 1/2 + 1: t = 1/2 */ + double aval[] = {1.0, 1.0}; + int acol[] = {2, 3}; + int arow[] = {0, 1, 2}; + matrix_desc_t A = {0}; + A.m = 2; + A.n = 4; + A.fmt = matrix_csr; + A.data.csr.nnz = 2; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + /* Q_yy = 2 on cone slot (index 1) */ + double qval[] = {2.0}; + int qcol[] = {1}; + int qrow[] = {0, 0, 1, 1, 1}; + matrix_desc_t Q = {0}; + Q.m = 4; + Q.n = 4; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 1; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-1.0, 0.0, 0.0, 0.0}; + double var_lb[] = {-1e30, -1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30, 1e30}; + /* s=1, t=1/2 -> 2*s*t=1, so x^2+y^2 <= 1 */ + double con_lb[] = {1.0, 0.5}; + double con_ub[] = {1.0, 0.5}; + /* cone is (v0=x, v1=y, s, t) with v_dim=2. Q lives on v1. */ + cone_spec_t cones[] = {{.type = CONE_ROTATED_SOC, .start_idx = 0, .v_dim = 2, .is_fixed = NULL}}; + return create_qp_problem( + c, &Q, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); +} + +int main(void) +{ + double eps_list[] = {1e-4, 1e-6, 1e-8}; + for (int i = 0; i < 3; ++i) + { + double eps = eps_list[i]; + solve_and_report("aux", build_aux(), eps); + solve_and_report("no_aux", build_no_aux(), eps); + } + return 0; +} diff --git a/test/test_qcqp_rsoc.c b/test/test_qcqp_rsoc.c new file mode 100644 index 0000000..74683b0 --- /dev/null +++ b/test/test_qcqp_rsoc.c @@ -0,0 +1,102 @@ +/* + * QCQP -> conic QP demo via RSOC. + * Original: min -x + y^2 s.t. x^2 + y^2 <= 1 + * Lifted: vars (x, y, v1, v2, s, t) + * v1 - sqrt(2) x = 0, v2 - sqrt(2) y = 0, s = 1, t = 1 + * (v1, v2, s, t) in K_rsoc + * Expected: x*=1, y*=0, obj*=-1. + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +int main(void) +{ + const double SQRT2 = 1.4142135623730951; + int n = 6; /* x=0, y=1, v1=2, v2=3, s=4, t=5 */ + + /* A (4 rows, sparse CSR): + r0: sqrt(2) x - v1 = 0 -> A[0]=(0, sqrt2), (2, -1) + r1: sqrt(2) y - v2 = 0 -> A[1]=(1, sqrt2), (3, -1) + r2: s = 1 -> A[2]=(4, 1) + r3: t = 1 -> A[3]=(5, 1) */ + double aval[] = {SQRT2, -1.0, SQRT2, -1.0, 1.0, 1.0}; + int acol[] = {0, 2, 1, 3, 4, 5}; + int arow[] = {0, 2, 4, 5, 6}; + matrix_desc_t A = {0}; + A.m = 4; + A.n = n; + A.fmt = matrix_csr; + A.data.csr.nnz = 6; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + /* Q (diag with Q_yy = 2 only, so 0.5 * 2 * y^2 = y^2). */ + double qval[] = {2.0}; + int qcol[] = {1}; + int qrow[] = {0, 0, 1, 1, 1, 1, 1}; + matrix_desc_t Q = {0}; + Q.m = n; + Q.n = n; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 1; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-1.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + double var_lb[] = {-1e30, -1e30, -1e30, -1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30, 1e30, 1e30, 1e30}; + double con_lb[] = {0.0, 0.0, 1.0, 1.0}; + double con_ub[] = {0.0, 0.0, 1.0, 1.0}; + + cone_spec_t cones[] = {{.type = CONE_ROTATED_SOC, .start_idx = 2, .v_dim = 2, .is_fixed = NULL}}; + + qp_problem_t *prob = + create_qp_problem(c, &Q, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + if (!prob) + { + fprintf(stderr, "create_qp_problem failed\n"); + return 1; + } + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 1; + params.termination_criteria.eps_optimal_relative = 1e-8; + params.termination_criteria.eps_feasible_relative = 1e-8; + params.termination_criteria.iteration_limit = 100000; + params.termination_criteria.time_sec_limit = 30.0; + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + qp_problem_free(prob); + return 1; + } + + double x = res->primal_solution[0], y = res->primal_solution[1]; + double v1 = res->primal_solution[2], v2 = res->primal_solution[3]; + double s = res->primal_solution[4], t = res->primal_solution[5]; + double q_lhs = x * x + y * y; + double cone_viol_v = v1 * v1 + v2 * v2 - 2.0 * s * t; + + printf( + "\nstatus=%d iter=%d obj=%.6f\n", (int)res->termination_reason, res->total_count, res->primal_objective_value); + printf("x=%.6f y=%.6f (expect x=1, y=0)\n", x, y); + printf("v1=%.6f v2=%.6f s=%.6f t=%.6f\n", v1, v2, s, t); + printf("original QC residual (x^2+y^2 - 1) = %.3e (expect <= 0)\n", q_lhs - 1.0); + printf("RSOC slack (||v||^2 - 2st) = %.3e (expect <= 0)\n", cone_viol_v); + + int pass = (res->termination_reason == TERMINATION_REASON_OPTIMAL) && fabs(x - 1.0) < 1e-4 && fabs(y) < 1e-4 && + fabs(res->primal_objective_value - (-1.0)) < 1e-4 && (q_lhs - 1.0 < 1e-5) && (cone_viol_v < 1e-5); + printf("%s\n", pass ? "PASS" : "FAIL"); + + pdhcg_result_free(res); + qp_problem_free(prob); + return pass ? 0 : 1; +} diff --git a/test/test_qcqp_scale.c b/test/test_qcqp_scale.c new file mode 100644 index 0000000..41453a6 --- /dev/null +++ b/test/test_qcqp_scale.c @@ -0,0 +1,305 @@ +/* + * QCQP: min -sum(x_i) s.t. x_i^2 <= 1 for i=1..N + * + * Two lifts: + * A) aux: 3N cone + N orig = 4N vars. Cone (v_i, s_i, t_i), v_i = sqrt(2) x_i. + * B) no_aux: (x_i, s_i, t_i) triples reordered. 3N vars total. + * + * Both should give x_i = 1, obj = -N. + * Compare wall time & iter count to measure gain of no-aux. + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include +#include + +static double elapsed_sec(struct timespec a, struct timespec b) +{ + return (b.tv_sec - a.tv_sec) + 1e-9 * (b.tv_nsec - a.tv_nsec); +} + +/* A: standard aux lift. + vars = [x_1..x_N | v_1..v_N | s_1..s_N | t_1..t_N] -- 4N vars + rows: for each i: sqrt2 x_i - v_i = 0; s_i = 1; t_i = 1 */ +static qp_problem_t *build_aux(int N) +{ + const double SQRT2 = 1.4142135623730951; + int nvar = 4 * N; + int nrow = 3 * N; + int nnz = 5 * N; /* per QC: [sqrt2, -1, 1, 1] = 4? Let me recount */ + /* rows: (i): sqrt2 x_i - v_i = 0 [2 nnz]; (N+i): s_i = 1 [1 nnz]; (2N+i): t_i = 1 [1 nnz] */ + /* total nnz per QC = 4, plus rearrangement */ + nnz = 4 * N; + + int *arow = (int *)calloc(nrow + 1, sizeof(int)); + int *acol = (int *)malloc(nnz * sizeof(int)); + double *aval = (double *)malloc(nnz * sizeof(double)); + int p = 0; + for (int i = 0; i < N; ++i) + { + arow[i + 1] = arow[i] + 2; + acol[p] = i; + aval[p] = SQRT2; + p++; + acol[p] = N + i; + aval[p] = -1.0; + p++; + } + for (int i = 0; i < N; ++i) + { + arow[N + i + 1] = arow[N + i] + 1; + acol[p] = 2 * N + i; + aval[p] = 1.0; + p++; + } + for (int i = 0; i < N; ++i) + { + arow[2 * N + i + 1] = arow[2 * N + i] + 1; + acol[p] = 3 * N + i; + aval[p] = 1.0; + p++; + } + matrix_desc_t A = {0}; + A.m = nrow; + A.n = nvar; + A.fmt = matrix_csr; + A.data.csr.nnz = nnz; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + double *c = (double *)calloc(nvar, sizeof(double)); + for (int i = 0; i < N; ++i) + c[i] = -1.0; + + double *vlb = (double *)malloc(nvar * sizeof(double)); + double *vub = (double *)malloc(nvar * sizeof(double)); + for (int i = 0; i < nvar; ++i) + { + vlb[i] = -1e30; + vub[i] = 1e30; + } + + double *clb = (double *)malloc(nrow * sizeof(double)); + double *cub = (double *)malloc(nrow * sizeof(double)); + for (int i = 0; i < N; ++i) + { + clb[i] = 0.0; + cub[i] = 0.0; + } /* sqrt2 x - v = 0 */ + for (int i = 0; i < N; ++i) + { + clb[N + i] = 1.0; + cub[N + i] = 1.0; + } /* s = 1 */ + for (int i = 0; i < N; ++i) + { + clb[2 * N + i] = 1.0; + cub[2 * N + i] = 1.0; + } /* t = 1 */ + + cone_spec_t *cones = (cone_spec_t *)calloc(N, sizeof(cone_spec_t)); + for (int i = 0; i < N; ++i) + { + cones[i].type = CONE_ROTATED_SOC; + cones[i].start_idx = N + i * 1; /* v_i */ + /* WAIT: v_1..v_N are contiguous but s_i,t_i are not adjacent to v_i. + This layout won't work with contiguous cone kernel. Need interleave. */ + } + free(cones); + + /* Correct layout: interleave. vars = [x_1..x_N | (v_1,s_1,t_1), (v_2,s_2,t_2), ...] */ + nvar = 4 * N; + /* Reindex: x_i at position i (i x^2 <= 1 */ + + cone_spec_t *cones = (cone_spec_t *)calloc(N, sizeof(cone_spec_t)); + for (int i = 0; i < N; ++i) + { + cones[i].type = CONE_ROTATED_SOC; + cones[i].start_idx = 3 * i; /* (x_i, s_i, t_i) contiguous */ + cones[i].v_dim = 1; + cones[i].is_fixed = NULL; + } + qp_problem_t *prob = + create_qp_problem(c, NULL, NULL, NULL, &A, clb, cub, vlb, vub, NULL, N, cones, NULL, NULL, 0, NULL); + free(cones); + free(clb); + free(cub); + free(vlb); + free(vub); + free(c); + free(arow); + free(acol); + free(aval); + return prob; +} + +static void run(const char *name, qp_problem_t *prob, int N, double eps) +{ + if (!prob) + { + fprintf(stderr, "[%s] create failed\n", name); + return; + } + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 0; + params.termination_criteria.eps_optimal_relative = eps; + params.termination_criteria.eps_feasible_relative = eps; + params.termination_criteria.iteration_limit = 5000000; + params.termination_criteria.time_sec_limit = 60.0; + struct timespec t0, t1; + clock_gettime(CLOCK_MONOTONIC, &t0); + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + clock_gettime(CLOCK_MONOTONIC, &t1); + double wall = elapsed_sec(t0, t1); + if (res) + { + printf("%-10s N=%5d eps=%.0e status=%d iter=%7d wall=%6.2fs obj=%.4f primal=%.2e dual=%.2e\n", + name, + N, + eps, + (int)res->termination_reason, + res->total_count, + wall, + res->primal_objective_value, + res->relative_primal_residual, + res->relative_dual_residual); + pdhcg_result_free(res); + } + qp_problem_free(prob); +} + +int main(void) +{ + int Ns[] = {100, 1000, 10000, 50000}; + double eps = 1e-6; + for (int i = 0; i < (int)(sizeof(Ns) / sizeof(Ns[0])); ++i) + { + int N = Ns[i]; + run("aux", build_aux(N), N, eps); + run("no_aux", build_no_aux(N), N, eps); + printf("\n"); + } + return 0; +} diff --git a/test/test_qcqp_sep.c b/test/test_qcqp_sep.c new file mode 100644 index 0000000..de89212 --- /dev/null +++ b/test/test_qcqp_sep.c @@ -0,0 +1,141 @@ +/* + * Compare two formulations of the same QCQP: + * min -x + y^2 s.t. x^2 + y^2 <= 1 + * + * Version A (overlap): y has Q (y^2 in obj) AND y is linearly coupled to cone slot v2. + * Version B (separated): new aux y_q holds Q (y_q^2 in obj); y is ONLY in the cone link; + * y_q - y = 0 bridges them. + * + * Same optimum (x*=1, y*=0, obj=-1) — comparison is the inner iteration count. + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +static int solve_and_report(const char *name, qp_problem_t *prob) +{ + if (!prob) + { + fprintf(stderr, "[%s] create_qp_problem failed\n", name); + return 1; + } + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 0; + params.termination_criteria.eps_optimal_relative = 1e-8; + params.termination_criteria.eps_feasible_relative = 1e-8; + params.termination_criteria.iteration_limit = 200000; + params.termination_criteria.time_sec_limit = 60.0; + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + qp_problem_free(prob); + return 1; + } + double x = res->primal_solution[0], y = res->primal_solution[1]; + printf("[%-12s] status=%d iter=%d obj=%.8f x=%.6f y=%.6f\n", + name, + (int)res->termination_reason, + res->total_count, + res->primal_objective_value, + x, + y); + pdhcg_result_free(res); + qp_problem_free(prob); + return 0; +} + +/* Version A: vars (x, y, v1, v2, s, t); Q on y; y links to cone via sqrt(2)*y - v2 = 0. */ +static qp_problem_t *build_overlap(void) +{ + const double SQRT2 = 1.4142135623730951; + /* rows: sqrt2 x - v1 = 0; sqrt2 y - v2 = 0; s = 1; t = 1 */ + double aval[] = {SQRT2, -1.0, SQRT2, -1.0, 1.0, 1.0}; + int acol[] = {0, 2, 1, 3, 4, 5}; + int arow[] = {0, 2, 4, 5, 6}; + matrix_desc_t A = {0}; + A.m = 4; + A.n = 6; + A.fmt = matrix_csr; + A.data.csr.nnz = 6; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + double qval[] = {2.0}; /* Q on y slot (col 1) */ + int qcol[] = {1}; + int qrow[] = {0, 0, 1, 1, 1, 1, 1}; + matrix_desc_t Q = {0}; + Q.m = 6; + Q.n = 6; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 1; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-1.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + double var_lb[] = {-1e30, -1e30, -1e30, -1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30, 1e30, 1e30, 1e30}; + double con_lb[] = {0.0, 0.0, 1.0, 1.0}; + double con_ub[] = {0.0, 0.0, 1.0, 1.0}; + cone_spec_t cones[] = {{.type = CONE_ROTATED_SOC, .start_idx = 2, .v_dim = 2, .is_fixed = NULL}}; + return create_qp_problem( + c, &Q, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); +} + +/* Version B: vars (x, y, y_q, v1, v2, s, t); Q on y_q (NOT on y); + add row y_q - y = 0 bridging them; cone link is sqrt(2)*y - v2 = 0 as before. */ +static qp_problem_t *build_separated(void) +{ + const double SQRT2 = 1.4142135623730951; + /* rows: + 0: sqrt2 x - v1 = 0 + 1: sqrt2 y - v2 = 0 + 2: y_q - y = 0 ← new + 3: s = 1 + 4: t = 1 */ + double aval[] = {SQRT2, -1.0, SQRT2, -1.0, -1.0, 1.0, 1.0, 1.0}; + int acol[] = {0, 3, 1, 4, 1, 2, 5, 6}; + int arow[] = {0, 2, 4, 6, 7, 8}; + matrix_desc_t A = {0}; + A.m = 5; + A.n = 7; + A.fmt = matrix_csr; + A.data.csr.nnz = 8; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + double qval[] = {2.0}; /* Q on y_q slot (col 2) */ + int qcol[] = {2}; + int qrow[] = {0, 0, 0, 1, 1, 1, 1, 1}; + matrix_desc_t Q = {0}; + Q.m = 7; + Q.n = 7; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 1; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + double var_lb[] = {-1e30, -1e30, -1e30, -1e30, -1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30, 1e30, 1e30, 1e30, 1e30}; + double con_lb[] = {0.0, 0.0, 0.0, 1.0, 1.0}; + double con_ub[] = {0.0, 0.0, 0.0, 1.0, 1.0}; + cone_spec_t cones[] = {{.type = CONE_ROTATED_SOC, .start_idx = 3, .v_dim = 2, .is_fixed = NULL}}; + return create_qp_problem( + c, &Q, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); +} + +int main(void) +{ + int rcA = solve_and_report("overlap", build_overlap()); + int rcB = solve_and_report("separated", build_separated()); + return rcA | rcB; +} diff --git a/test/test_sparse_q_cones.c b/test/test_sparse_q_cones.c new file mode 100644 index 0000000..d15ece5 --- /dev/null +++ b/test/test_sparse_q_cones.c @@ -0,0 +1,90 @@ +/* + * E2E test for off-diagonal (sparse) Q on non-cone vars coupled to a SOC cone. + * Exercises the SPARSE_Q path through BB with dispatch_cone_projection in the + * inner loop. Variables: (a, b, v, w, z); Q couples (a, b); (v, w, z) is K_soc. + */ + +#include "pdhcg.h" +#include "pdhcg_types.h" +#include +#include +#include +#include + +int main(void) +{ + int n = 5; + + double aval[] = {1.0, 1.0, 1.0, -1.0, 1.0, -1.0}; + int acol[] = {0, 1, 0, 2, 1, 3}; + int arow[] = {0, 1, 2, 4, 6}; + matrix_desc_t A = {0}; + A.m = 4; + A.n = n; + A.fmt = matrix_csr; + A.data.csr.nnz = 6; + A.data.csr.row_ptr = arow; + A.data.csr.col_ind = acol; + A.data.csr.vals = aval; + + double qval[] = {1.0, 0.5, 0.5, 1.0}; + int qcol[] = {0, 1, 0, 1}; + int qrow[] = {0, 2, 4, 4, 4, 4}; + matrix_desc_t Q = {0}; + Q.m = n; + Q.n = n; + Q.fmt = matrix_csr; + Q.data.csr.nnz = 4; + Q.data.csr.row_ptr = qrow; + Q.data.csr.col_ind = qcol; + Q.data.csr.vals = qval; + + double c[] = {-3.0, -4.0, 0.0, 0.0, 1.0}; + double var_lb[] = {-1e30, -1e30, -1e30, -1e30, -1e30}; + double var_ub[] = {1e30, 1e30, 1e30, 1e30, 1e30}; + double con_lb[] = {3.0, 4.0, 0.0, 0.0}; + double con_ub[] = {3.0, 4.0, 0.0, 0.0}; + + cone_spec_t cones[] = {{.type = CONE_STANDARD_SOC, .start_idx = 2, .v_dim = 1, .is_fixed = NULL}}; + + qp_problem_t *prob = + create_qp_problem(c, &Q, NULL, NULL, &A, con_lb, con_ub, var_lb, var_ub, NULL, 1, cones, NULL, NULL, 0, NULL); + if (!prob) + { + fprintf(stderr, "create_qp_problem failed\n"); + return 1; + } + + pdhg_parameters_t params; + set_default_parameters(¶ms); + params.verbose = 1; + params.termination_criteria.eps_optimal_relative = 1e-7; + params.termination_criteria.eps_feasible_relative = 1e-7; + params.termination_criteria.iteration_limit = 100000; + params.termination_criteria.time_sec_limit = 30.0; + pdhcg_result_t *res = solve_qp_problem(prob, ¶ms); + if (!res) + { + qp_problem_free(prob); + return 1; + } + + double a = res->primal_solution[0], b = res->primal_solution[1]; + double v = res->primal_solution[2], w = res->primal_solution[3], z = res->primal_solution[4]; + double cone_lhs = v * v + w * w, cone_rhs = z * z; + double cone_viol = cone_lhs - cone_rhs; + printf( + "\nstatus=%d iter=%d obj=%.6f\n", (int)res->termination_reason, res->total_count, res->primal_objective_value); + printf("a=%.6f b=%.6f v=%.6f w=%.6f z=%.6f\n", a, b, v, w, z); + printf("cone violation (v^2+w^2-z^2) = %.3e (expect 0)\n", cone_viol); + printf("expected: a=3, b=4, v=3, w=4, z=5; obj=-1.5\n"); + + int pass = (res->termination_reason == TERMINATION_REASON_OPTIMAL) && fabs(a - 3.0) < 1e-4 && + fabs(b - 4.0) < 1e-4 && fabs(v - 3.0) < 1e-4 && fabs(w - 4.0) < 1e-4 && fabs(z - 5.0) < 1e-4 && + fabs(cone_viol) < 1e-4 && fabs(res->primal_objective_value - (-1.5)) < 1e-3; + printf("%s\n", pass ? "PASS" : "FAIL"); + + pdhcg_result_free(res); + qp_problem_free(prob); + return pass ? 0 : 1; +} diff --git a/tests/data/cbf_q3_smoke.cbf b/tests/data/cbf_q3_smoke.cbf new file mode 100644 index 0000000..d334dc8 --- /dev/null +++ b/tests/data/cbf_q3_smoke.cbf @@ -0,0 +1,27 @@ +VER +1 + +OBJSENSE +MIN + +VAR +3 1 +Q 3 + +CON +2 1 +L= 2 + +OBJACOORD +1 +0 1.0 + +ACOORD +2 +0 1 1.0 +1 2 1.0 + +BCOORD +2 +0 -3.0 +1 -4.0 diff --git a/tests/test_cones.py b/tests/test_cones.py new file mode 100644 index 0000000..fd524d5 --- /dev/null +++ b/tests/test_cones.py @@ -0,0 +1,54 @@ +from pathlib import Path + +import numpy as np +import pytest +from pdhcg._core import read_problem_file + +from pdhcg import ConeSpec, ConeType + + +def test_cone_spec_broadcasts_columnar_metadata() -> None: + starts = 3 * np.arange(4, dtype=np.int32) + cones = ConeSpec(ConeType.EXP, starts) + + assert len(cones) == 4 + assert cones.types.dtype == np.int32 + assert cones.starts.flags.c_contiguous + np.testing.assert_array_equal(cones.types, np.full(4, ConeType.EXP, dtype=np.int32)) + np.testing.assert_array_equal(cones.v_dims, np.ones(4, dtype=np.int32)) + np.testing.assert_array_equal(cones.power_alphas, np.zeros(4)) + cones.validate_ambient(12, allow_fixed=True) + + +def test_cone_spec_stores_heterogeneous_columnar_metadata() -> None: + cones = ConeSpec( + np.array([ConeType.SOC, ConeType.EXP], dtype=np.int32), + np.array([0, 3], dtype=np.int32), + fixed_mask=np.array([0, 0, 0, 0, 1, 0], dtype=np.uint8), + ) + + np.testing.assert_array_equal(cones.types, [ConeType.SOC, ConeType.EXP]) + np.testing.assert_array_equal(cones.starts, [0, 3]) + np.testing.assert_array_equal(cones.fixed_mask, [0, 0, 0, 0, 1, 0]) + + +def test_cone_spec_validates_power_and_ambient_ranges() -> None: + with pytest.raises(ValueError, match="alphas"): + ConeSpec(ConeType.POWER, np.array([0], dtype=np.int32), power_alphas=np.nan) + + cones = ConeSpec(ConeType.SOC, np.array([1], dtype=np.int32), v_dims=2) + with pytest.raises(ValueError, match="ambient"): + cones.validate_ambient(4, allow_fixed=True) + + +def test_read_problem_file_returns_columnar_cones() -> None: + problem = Path(__file__).parent / "data" / "cbf_q3_smoke.cbf" + raw = read_problem_file(str(problem)) + cones = ConeSpec.from_columnar(raw["cones"]) + + assert len(cones) == 1 + assert cones.types[0] == ConeType.SOC + assert cones.starts[0] == 0 + + with pytest.raises(TypeError): + read_problem_file(str(problem), compact_cones=True) diff --git a/tests/test_model_cones.py b/tests/test_model_cones.py new file mode 100644 index 0000000..b79130c --- /dev/null +++ b/tests/test_model_cones.py @@ -0,0 +1,136 @@ +import numpy as np +import pytest +import scipy.sparse as sp +from pdhcg._core import solve_once + +from pdhcg import ConeSpec, ConeType, Model + + +def _quiet_model(model: Model) -> Model: + model.setParams( + TimeLimit=30.0, + FeasibilityTol=1e-6, + OptimalityTol=1e-6, + LogLevel=0, + ) + return model + + +@pytest.mark.gpu +def test_model_variable_soc_columnar_input() -> None: + model = _quiet_model( + Model( + objective_vector=np.array([0.0, 0.0, 1.0]), + constraint_matrix=sp.csr_matrix([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]), + constraint_lower_bound=np.array([3.0, 4.0]), + constraint_upper_bound=np.array([3.0, 4.0]), + variable_cones=ConeSpec( + ConeType.SOC, + np.array([0], dtype=np.int32), + v_dims=1, + ), + ) + ) + + model.optimize() + + assert model.Status == "OPTIMAL" + np.testing.assert_allclose(model.X, [3.0, 4.0, 5.0], atol=2e-4) + + +def test_model_rejects_legacy_cone_dicts() -> None: + with pytest.raises(TypeError, match="ConeSpec"): + Model( + objective_vector=np.zeros(3), + variable_cones=[{"type": "soc", "start_idx": 0, "v_dim": 1}], + ) + + +@pytest.mark.gpu +def test_low_level_rejects_legacy_cone_dicts() -> None: + with pytest.raises(ValueError, match="ConeSpec"): + solve_once( + None, + None, + sp.csr_matrix((0, 3)), + np.zeros(3), + cones=[{"type": "soc", "start_idx": 0, "v_dim": 1}], + ) + + +@pytest.mark.gpu +@pytest.mark.parametrize("sparse", [False, True], ids=["dense", "csr"]) +def test_model_native_affine_soc_columnar_input(sparse: bool) -> None: + affine_matrix = np.array([[0.0], [0.0], [1.0]]) + if sparse: + affine_matrix = sp.csr_matrix(affine_matrix) + model = _quiet_model( + Model( + objective_vector=np.array([1.0]), + affine_cone_matrix=affine_matrix, + affine_cone_offset=np.array([3.0, 4.0, 0.0]), + affine_cones=ConeSpec( + ConeType.SOC, + np.array([0], dtype=np.int32), + v_dims=1, + ), + ) + ) + + model.optimize() + + assert model.Status == "OPTIMAL" + np.testing.assert_allclose(model.X, [5.0], atol=2e-4) + + +@pytest.mark.gpu +def test_cvxpy_constant_exp_rows_use_equalities_instead_of_fixed_slots() -> None: + cp = pytest.importorskip("cvxpy") + import pdhcg.cvxpy_backend # noqa: F401 + + z = cp.Variable() + problem = cp.Problem(cp.Minimize(z), [cp.ExpCone(0.0, 1.0, z)]) + + value = problem.solve(solver="PDHCG", eps=1e-6, verbose=False) + + assert problem.status == cp.OPTIMAL + assert value == pytest.approx(1.0, abs=5e-4) + + +@pytest.mark.gpu +@pytest.mark.parametrize( + ("constraint_kind", "expected_dual"), + [("nonnegative", 1.0), ("equality", -1.0)], +) +def test_cvxpy_linear_dual_signs(constraint_kind: str, expected_dual: float) -> None: + cp = pytest.importorskip("cvxpy") + import pdhcg.cvxpy_backend # noqa: F401 + + x = cp.Variable() + constraint = x >= 1.0 if constraint_kind == "nonnegative" else x == 1.0 + problem = cp.Problem(cp.Minimize(x), [constraint]) + + problem.solve(solver="PDHCG", eps=1e-7, verbose=False) + + assert problem.status == cp.OPTIMAL + assert x.value == pytest.approx(1.0, abs=5e-5) + assert constraint.dual_value == pytest.approx(expected_dual, abs=5e-5) + + +@pytest.mark.gpu +def test_cvxpy_soc_dual_sign_and_order() -> None: + cp = pytest.importorskip("cvxpy") + import pdhcg.cvxpy_backend # noqa: F401 + + u = cp.Variable(2) + t = cp.Variable() + fixed = u == np.array([3.0, 4.0]) + cone = cp.SOC(t, u) + problem = cp.Problem(cp.Minimize(t), [fixed, cone]) + + problem.solve(solver="PDHCG", eps=1e-7, verbose=False) + + assert problem.status == cp.OPTIMAL + np.testing.assert_allclose(fixed.dual_value, [-0.6, -0.8], atol=5e-5) + np.testing.assert_allclose(cone.dual_value[0], [1.0], atol=5e-5) + np.testing.assert_allclose(cone.dual_value[1].ravel(), [-0.6, -0.8], atol=5e-5)