贡献

我们欢迎所有对CVXPY做出贡献的人。您不需要是凸优化的专家,即可提供帮助。以下是立即开始贡献的简单方法:

如果您想要向我们的库添加新示例或实现新功能,请首先通过打开GitHub问题与我们联系,以确保您的优先事项与我们一致。 在下一节中,我们列出了一些具体的开发机会。

CVXPY贡献者必须遵守 我们的行为准则。 整体开发受到 我们的治理结构 的指导。本页面的余下部分将详细介绍如何为CVXPY做出贡献。

愿望清单

以下是一份非详尽列表,列出了为CVXPY做出显著贡献的机会。 我们对这些机会进行了大致分类,根据项目的规模可以分为小型、中型或大型范围。 我们鼓励新贡献者专注于小型或中型项目。 如果你对一个大型项目感兴趣,请联系项目维护人员。

小型项目
中型项目
大型项目

一般原则

开发环境

首先fork CVXPY存储库,并安装CVXPY 从源码安装 。 在修改任何代码之前,您应该在本地机器上配置git。 以下是CVXPY贡献者可能配置git的一种方式:

  1. 告诉 git 关于 CVXPY 官方仓库的存在:

git remote add upstream https://github.com/cvxpy/cvxpy.git
  1. 获取官方主分支的副本:

    git fetch upstream master
    
  2. 创建一个本地分支,该分支将跟踪官方主分支:

    git branch --track official_master upstream/master
    

你只能在“official_master”分支上使用“git pull”命令。 这个跟踪分支的目的是让你能够轻松与主 CVXPY 仓库同步, 这在解决拉取请求中遇到的任何合并冲突时非常有帮助。 对于简单的贡献,你可能永远不会使用这个分支。

  1. 切换回你的分支的主分支:

    git checkout master
    
  2. 按常规继续工作!

贡献清单

贡献通过 拉取请求 进行。 在发送拉取请求之前,请确保完成以下步骤:

一旦您创建了拉取请求,CVXPY开发团队的成员将负责审查它。您可能会与审阅者来回交流几次,这是完全正常的。您的拉取请求将触发针对许多不同Python版本和不同平台的持续集成测试。如果这些测试开始失败,请修复您的代码并提交另一个提交,这将重新触发测试。

许可证

请在新文件中添加以下许可证:

"""
Copyright, the CVXPY authors

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.
"""

代码样式

我们使用 ruff 来强制执行我们的Python编码样式。在向我们发送拉取请求之前,请导航到项目根目录并运行以下指令来检查您的更改是否遵守我们的样式约定:

pip install ruff
ruff check cvxpy

请在发送拉取请求之前修复所有报告的错误。

有选择性地,可以安装 pre-commit 包来在每次提交前自动检查这些约定。

pip install pre-commit
pre-commit install

编写单元测试

大多数代码更改都需要新的单元测试。(即使是错误修复也需要单元测试,因为错误的存在通常意味着测试不足。)CVXPY测试位于目录 cvxpy/tests 中, 其中包含多个文件,每个文件包含多个单元测试。添加测试时,尽量找到一个适合您的测试的文件;如果您要测试一个新特性,可能需要创建一个新的测试文件。

我们使用标准的Python unittest 框架进行测试。测试被组织成继承 BaseTest 的类(请参阅 cvxpy/tests/base_test.py )。 以 test_ 开头的每个方法都是一个单元测试。

运行单元测试

我们使用 pytest 来运行我们的单元测试,可以使用 pip install pytest 安装。要运行所有的单元测试, cdcvxpy/tests 目录,然后运行以下命令:

pytest

要运行特定文件中的测试(例如 ``test_dgp.py``),请使用:
pytest test_dgp.py

要运行特定测试方法(例如 TestDgp.test_produc ),请使用:

pytest test_dgp.py::TestDgp::test_product

请确保您的更改不会导致任何单元测试失败。

pytest 默认情况下会压制 stdout。要查看 stdout,请将 -s 标志传递给 pytest

基准测试

CVXPY 在 cvxpy/tests/test_benchmarks.py 中有一些基准测试,用于测试问题变为规范形式所需的时间。请运行:

::

  pytest -s test_benchmarks.py

与您的更改之前和之后进行测试,以确保不会引入性能回归。 如果您要进行代码贡献,请将上述命令的输出(包含更改之前和之后的结果)包含在您的拉取请求中。

求解器接口

第三方数值优化求解器是CVXPY的命脉。我们非常感谢那些愿意自愿投入时间改善我们现有求解器接口或创建新求解器接口的人。 改进现有接口通常可以像修复错误一样处理。创建新接口需要更多的工作,并且在编写任何代码之前需要与CVXPY首席开发人员进行协调。

本贡献指南的本节概述了添加新求解器接口时的考虑事项。 目前,我们只有锥形求解器接口的文档。 即将推出QP求解器接口的其他文档。

Warning

这篇文档远未完成!它仅试图涵盖编写求解器接口的绝对必须的部分。它可能也不会以令人惊讶的方式完成 - 我们欢迎对本文档的任何反馈。

锥形求解器

锥形求解器要求目标函数是优化变量的线性函数;约束必须使用凸锥和优化变量的仿射函数来表示。 锥形求解器的代码路径从 reductions/solvers/conic_solvers 开始, 特别是从 conic_solver.py 中的类 ConicSolver 开始。

假设您正在为“Awesome”锥形求解器编写CVXPY接口,并且有一个名为“AwesomePy”的现有软件包,用于从python调用 Awesome 。 在这种情况下,您需要在与 conic_solver.py 相同的文件夹中创建一个名为 awesome_conif.py 的文件。 在 awesome_conif.py 中,您将定义一个名为 Awesome(ConicSolver) 的类。 Awesome(ConicSolver) 类将管理CVXPY和现有 AwesomePy python软件包之间的所有交互。 它需要实现六个函数: - import_solver, - name, - accepts, - apply, - solve_via_data, - invert。

前三个函数非常容易(通常是微不足道)编写。 剩下的函数按顺序调用:applysolve_via_data 准备数据, solve_via_data 通过现有的第三方 AwesomePy 软件包调用 Awesome 求解器, invertAwesomePy 的输出转换为CVXPY所期望的格式。

在此过程中的关键目标是,apply 的输出应尽可能接近 Awesome 的标准形式, 而 solve_via_data 应保持简短。 Awesome(ConicSolver).solve_via_data 的复杂性将取决于 AwesomePy 。 如果 AwesomePy 允许非常低级别的输入 - 通过一个或两个矩阵和少量数值向量传递, 那么您将面临像ECOS或GLPK一样的情况。 如果 AwesomePy 软件包要求您构建面向对象的模型,则需要处理类似MOSEK,GUROBI或NAG的接口。 编写 invert 函数可能需要非常努力才能正确恢复对偶变量。

CVXPY的锥形形式

CVXPY在最后可能的时刻将优化问题转换为显式形式。 当CVXPY以具体形式呈现问题时,它是在一个矢量化的优化变量和可行集的展平表示上完成的。 标准形式的抽象为

\[(P) \quad \min\{ c^T x + d \,:\, x \in \mathbb{R}^{n},\, A x + b \in K \}\]

其中 \(K\) 是基础凸锥的乘积。CVXPY的设计允许使用任何目标求解器支持的锥形, 但目前支持的基本凸锥有

  1. 零锥 \(y = 0 \in \mathbb{R}^m\).

  2. 非负锥 \(y \geq 0 \in \mathbb{R}^m\).

  3. 二阶锥

    \[(u,v) \in K_{\mathrm{soc}}^n \doteq \{ (t,x) \,:\, t \geq \|x\|_2 \} \subset \mathbb{R} \times \mathbb{R}^n.\]
  4. 正半定锥的多个矢量化版本之一。

  5. 指数锥

    \[(u,v,w) \in K_e \doteq \mathrm{cl}\{(x,y,z) | z \geq y \exp(x/y), y>0\}.\]
  6. 三维幂锥,由一个数 \(\alpha\in (0, 1)\) 参数化:

    \[(u,v) \in K_{\mathrm{pow}}^{\alpha} \doteq \{ (x,y,z) \,:\, x^{\alpha}y^{1-\alpha} \geq |z|, (x,y) \geq 0 \}.\]

我们稍后再讨论半定锥的矢量化选项。 现在有用的是 Awesome(ConicSolver) 类将在 apply 中使用显式表示来访问问题 \((P)\),其中代码段如下:.. code:

# from cvxpy.constraints import Zero, NonNeg, SOC, PSD, ExpCone, PowCone3D
#  ...
if not problem.formatted:
    problem = self.format_constraints(problem, self.EXP_CONE_ORDER)
constr_map = problem.constr_map
cone_dims = problem.cone_dims
c, d, A, b = problem.apply_parameters()

变量 constr_map 是一个由 CVXPY 约束对象的列表组成的字典。 该字典的键是对 CVXPY 的 Zero、NonNeg、SOC、PSD、ExpCone 和 PowCone3D 类的引用。 在变量回复期间,您将需要与这些约束类进行交互。 在代码段中的其他变量中:

  • c,d 定义目标函数 c @ x + d,和

  • A,b,cone_dims 定义问题中的抽象对象 \(A\)\(b\)

\(K\) 在问题 \((P)\) 中。

编写求解器的接口的第一步是了解 A,b,cone_dims 的确切含义,以便您可以使用第三方 AwesomePy 接口正确构建原始问题与 Awesome 求解器。 cone_dims 对象是 ConeDims 类的一个实例, 它在 cone_matrix_stuffing.py 中定义;A 是一个 SciPy 稀疏矩阵,而 b 是一个维度为 1 的numpy数组。 A 的行和 b 的元素按照非常具体的顺序给出,如下所述。

  • 等式约束位于 A 的前 cone_dims.zero 行和 b 的条目中。 将 eq = cone_dims.zero,约束条件如下:

    A[:eq, :] @ x + b[:eq] == 0.
    
  • 不等式约束出现在方程式之后。 例如,如果 ineq = cone_dims.nonneg,那么可行解集具有以下约束条件:

    A[eq:eq + ineq, :] @ x + b[eq:eq + ineq] >= 0.
    
  • 处理二阶锥(SOC)约束在不等式之后。 在这里,cone_dims.soc 是一个 整数列表,而不是一个单独的整数。 假设 cone_dims.soc[0] == 10,在这个优化问题中,第一个出现的二阶锥约束将涉及到 A 的10行和 b 的10个条目。 我们使用的 SOC 向量化由上述定义的 \(K_{\mathrm{soc}}^n\) 给出。

  • PSD(半正定)约束在 SOC 约束之后。 就大多数求解器接口而言,要对向量化做出明确的决策是一个好主意,也就是实现 Awesome(ConicSolver).psd_format_mat 的过程。 如果你什么都不做,那么向量化将与 ConicSolver.psd_format_mat 的行为相同, 它将 \(n\) 阶的 PSD 约束映射为 \(n^2\) 行的 \(A\)\(b\) 条目。 你还可以参考 SCS.psd_format_mat,它将 \(n\) 阶的 PSD 约束映射为合适缩放的 \(n(n+1)/2\)\(A\)\(b\) 条目,或者 MOSEK.psd_format_mat,它与 SCS 行为完全相同,只是比例尺度不同。

  • A, b 中的下一个块 3 * cone_dims.exp 行对应于连续的三维指数锥,如上面定义的 \(K_e\)

  • A, b 中的最后一个块 3 * len(cone_dims.p3d) 行对应于由 \(K_{\mathrm{pow}}^{\alpha}\) 定义的三维幂锥, 其中第 \(i\) 个三行组具有 alpha = cone_dims.p3d[i]

如果 Awesome 支持 SOC、ExpCone、PSD 或 PowCone3D 等非线性约束, 那么可能需要转换数据 A, b,以便以 AwesomePy 预期的形式编写这些约束。 最常见的情况是当 AwesomePy 将二阶锥参数化为 \(K = \{ (x,t) \,:\, \|x\|\leq t \} \subset \mathbb{R}^n \times \mathbb{R}\), 或者将 \(K_e \subset \mathbb{R}^3\) 参数化为我们之前定义的某种排列。

备选锥形式

某些锥形求解器不原生地支持前面描述的 (P) 问题格式。相反,求解器要求问题陈述形式如下:

\[(Dir) \quad \min\{ f^T z \,:\, z \in K,\, G z = h \}.\]

问题 (Dir) 使用所谓的“直接”锥形约束 \(z \in K\)。如果你正在为一个按照这种方式工作的求解器编写接口, 你应该对 (P) 中给出的标准 CVXPY 问题数据应用“Dualize”约简。使用 Dualize 约简将避免为连续问题引入不必要的松弛变量, 但它不适用于具有整数约束的问题。因此,如果你的求解器支持整数约束,确保在该代码路径中也使用“Slacks”约简。

MOSEK 接口同时使用上述两种约简。

对偶变量

对偶变量抽取应在 Awesome(ConicSolver).invert 中处理。 要正确执行此步骤,需要考虑 CVXPY 如何为原问题 \((P)\) 构造拉格朗日函数。 假设可行集 \(Ax + b \in K \subset \mathbb{R}^m\) 中的仿射映射 \(Ax + b\) 被分解为大小为 \(m_1,\ldots,m_6\) 的六个块, 其中块按顺序分别对应于零锥、非负锥、二阶锥、向量化的 PSD 锥、指数锥和 3D 幂锥约束。 那么 CVXPY 通过构造一个拉格朗日函数来定义 \((P)\) 的对偶问题。.. math:

\mathcal{L}(x,\mu_1,\ldots,\mu_6) = c^T x - \sum_{i=i}^6 \mu_i^T (A_i x + b_i)

其中 \(\mu_1 \in \mathbb{R}^{m_1}\), \(\mu_2 \in \mathbb{R}^{m_2}_+\), 以及 \(\mu_i \in K_i^* \subset \mathbb{R}^{m_i}\),其中 \(i \in \{3,4,5,6\}\)。 这里,\(K_i^*\) 表示标准内积下 \(K_i\) 的对偶锥。

关于对偶变量(特别是 SOC 对偶变量)的更多说明可以在 这个 GitHub 线程的评论中 找到。

大多数 ConicSolver 类的具体实现使用了一组通用的帮助函数来恢复对偶变量,可以在 reductions/solvers/utilities.py 中找到。

注册求解器

正确实现 Awesome(ConicSolver) 并不足以从 CVXPY 中调用 Awesome。 你还需要在其他几个地方进行编辑,即

这些文件中已有的内容应该清楚地说明了需要添加 Awesome 到 CVXPY 的内容。

编写测试

Awesome(ConicSolver) 的测试放在 cvxpy/tests/test_conic_solvers.py 文件中。该文件中绝大多数的测试只占用一行,因为我们一直在使用在 solver_test_helpers.py 中定义的通用测试框架。以下是在 test_conic_solvers.py 中调用的辅助函数的示例:

class StandardTestSDPs(object):

    @staticmethod
    def test_sdp_1min(solver, places=4, **kwargs):
        sth = sdp_1('min')
        sth.solve(solver, **kwargs)
        sth.verify_objective(places=2)  # 仅记录2位小数。
        sth.check_primal_feasibility(places)
        sth.check_complementarity(places)
        sth.check_dual_domains(places)  # 检查对偶变量是否为半正定矩阵。

...

class StandardTestSOCPs(object):

    @staticmethod
    def test_socp_0(solver, places=4, **kwargs):
        sth = socp_0()
        sth.solve(solver, **kwargs)
        sth.verify_objective(places)
        sth.verify_primal_values(places)
        sth.check_complementarity(places)

...

    @staticmethod
    def test_mi_socp_1(solver, places=4, **kwargs):
        sth = mi_socp_1()
        sth.solve(solver, **kwargs)
        # 混合整数问题没有对偶变量,
        # 因此我们只检查最优目标和原始变量。
        sth.verify_objective(places)
        sth.verify_primal_values(places)

注意预定义函数中的注释。在 test_sdp_1min 中,当检查最优目标函数值时,我们使用 places=2 覆盖了用户提供的 places 值。 我们还额外努力检查对偶变量是否为半正定矩阵。 在 test_mi_socp_1 中,我们处理的是一个混合整数问题,所以根本没有对偶变量。 您应该使用这些预定义函数的一部分原因是它们自动检查适用于当前问题的最合适的内容。每个预定义函数首先构造一个 SolverTestHelper 对象 sth,该对象包含适当的测试数据。SolverTestHelper 类的 .solve 函数是对 prob.solve 的简单封装,其中 prob 是一个 CVXPY 问题。特别地,传递给 sth.solve 的任何关键字参数都会被传递给 prob.solve。这允许您使用不同的求解器参数调用修改版本的测试,例如:

def test_mosek_lp_1(self):
    # 默认设置
    StandardTestLPs.test_lp_1(solver='MOSEK')  # 4 位小数
    # 需要一个基本可行解
    StandardTestLPs.test_lp_1(solver='MOSEK', places=6, bfs=True)