From 95c1196165f3dbad78a5bec57b3417efc2686b04 Mon Sep 17 00:00:00 2001 From: crosstyan Date: Mon, 3 Mar 2025 17:27:42 +0800 Subject: [PATCH] feat: Add CVXOPT solver infrastructure and VSCode settings - Add CVXOPT dependency to pyproject.toml and uv.lock - Create solver module with GLPK-based integer linear programming solver - Add VSCode Python analysis settings - Implement matrix and sparse matrix wrappers for CVXOPT - Add GLPK solver wrapper with type-safe interfaces --- .vscode/settings.json | 4 + app/solver/__init__.py | 227 +++++++++++++++++++++++++++++++ app/solver/_wrap/__init__.py | 184 ++++++++++++++++++++++++++ app/solver/_wrap/glpk.py | 249 +++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + uv.lock | 37 ++++++ 6 files changed, 702 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 app/solver/__init__.py create mode 100644 app/solver/_wrap/__init__.py create mode 100644 app/solver/_wrap/glpk.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dcb1530 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.analysis.typeCheckingMode": "basic", + "python.analysis.autoImportCompletions": true +} \ No newline at end of file diff --git a/app/solver/__init__.py b/app/solver/__init__.py new file mode 100644 index 0000000..5193d53 --- /dev/null +++ b/app/solver/__init__.py @@ -0,0 +1,227 @@ +import itertools +from abc import abstractmethod +from collections import defaultdict +from typing import Tuple, override + +import numpy as np + +from app._typing import NDArray + +from ._wrap import matrix, spmatrix +from ._wrap.glpk import ilp +from ._wrap.glpk import set_global_options as set_glpk_options + +set_glpk_options({"msg_lev": "GLP_MSG_ERR"}) + +FROZEN_POS_EDGE = -1 +FROZEN_NEG_EDGE = -2 +INVALID_EDGE = -100 + + +class _BIPSolver: + """ + Base class for BIP solvers + """ + + min_affinity: float + max_affinity: float + + def __init__(self, min_affinity: float = -np.inf, max_affinity: float = np.inf): + self.min_affinity = min_affinity + self.max_affinity = max_affinity + + @staticmethod + def _create_bip(affinity_matrix: NDArray, min_affinity: float, max_affinity: float): + n_nodes = affinity_matrix.shape[0] + + # mask for selecting pairs of nodes + triu_mask = np.triu(np.ones_like(affinity_matrix, dtype=bool), 1) + + affinities = affinity_matrix[triu_mask] + frozen_pos_mask = affinities >= max_affinity + frozen_neg_mask = affinities <= min_affinity + unfrozen_mask = np.logical_not(frozen_pos_mask | frozen_neg_mask) + + # generate objective coefficients + objective_coefficients = affinities[unfrozen_mask] + + if len(objective_coefficients) == 0: # nio unfrozen edges + + objective_coefficients = np.asarray([affinity_matrix[0, -1]]) + unfrozen_mask = np.zeros_like(unfrozen_mask, dtype=np.bool) + unfrozen_mask[affinity_matrix.shape[1] - 1] = 1 + + # create matrix whose rows are the indices of the three edges in a + # constraint x_ij + x_ik - x_jk <= 1 + constraints_edges_idx = [] + if n_nodes >= 3: + edges_idx = np.empty_like(affinities, dtype=int) + edges_idx[frozen_pos_mask] = FROZEN_POS_EDGE + edges_idx[frozen_neg_mask] = FROZEN_NEG_EDGE + edges_idx[unfrozen_mask] = np.arange(len(objective_coefficients)) + nodes_to_edge_matrix = np.empty_like(affinity_matrix, dtype=int) + nodes_to_edge_matrix.fill(INVALID_EDGE) + nodes_to_edge_matrix[triu_mask] = edges_idx + + triplets = np.asarray( + tuple(itertools.combinations(range(n_nodes), 3)), dtype=int + ) + constraints_edges_idx = np.zeros_like(triplets) + constraints_edges_idx[:, 0] = nodes_to_edge_matrix[ + (triplets[:, 0], triplets[:, 1]) + ] + constraints_edges_idx[:, 1] = nodes_to_edge_matrix[ + (triplets[:, 0], triplets[:, 2]) + ] + constraints_edges_idx[:, 2] = nodes_to_edge_matrix[ + (triplets[:, 1], triplets[:, 2]) + ] + constraints_edges_idx = constraints_edges_idx[ + np.any(constraints_edges_idx >= 0, axis=1) + ] + + if len(constraints_edges_idx) == 0: # no constraints + constraints_edges_idx = np.asarray([0, 0, 0], dtype=int).reshape(-1, 3) + + # add remaining constraints by permutation + constraints_edges_idx = np.vstack( + ( + constraints_edges_idx, + np.roll(constraints_edges_idx, 1, axis=1), + np.roll(constraints_edges_idx, 2, axis=1), + ) + ) + + # clean redundant constraints + # x1 + x2 <= 2 + constraints_edges_idx = constraints_edges_idx[ + constraints_edges_idx[:, 2] != FROZEN_POS_EDGE + ] + # x1 - x2 <= 1 + constraints_edges_idx = constraints_edges_idx[ + np.all(constraints_edges_idx[:, 0:2] != FROZEN_NEG_EDGE, axis=1) + ] + if len(constraints_edges_idx) == 0: # no constraints + constraints_edges_idx = np.asarray([0, 0, 0], dtype=int).reshape(-1, 3) + + # generate constraint coefficients + constraints_coefficients = np.ones_like(constraints_edges_idx) + constraints_coefficients[:, 2] = -1 + + # generate constraint upper bounds + upper_bounds = np.ones(len(constraints_coefficients), dtype=float) + upper_bounds -= np.sum( + constraints_coefficients * (constraints_edges_idx == FROZEN_POS_EDGE), + axis=1, + ) + + # flatten constraints data into sparse matrix format + constraints_idx = np.repeat(np.arange(len(constraints_edges_idx)), 3) + constraints_edges_idx = constraints_edges_idx.reshape(-1) + constraints_coefficients = constraints_coefficients.reshape(-1) + + unfrozen_edges = constraints_edges_idx >= 0 + constraints_idx = constraints_idx[unfrozen_edges] + constraints_edges_idx = constraints_edges_idx[unfrozen_edges] + constraints_coefficients = constraints_coefficients[unfrozen_edges] + + return ( + objective_coefficients, + unfrozen_mask, + frozen_pos_mask, + frozen_neg_mask, + (constraints_coefficients, constraints_idx, constraints_edges_idx), + upper_bounds, + ) + + @abstractmethod + def _solve_bip(self, objective_coefficients, sparse_constraints, upper_bounds): ... + + @staticmethod + def solution_mat_clusters(solution_mat: NDArray): + n = solution_mat.shape[0] + labels = np.arange(1, n + 1) + for i in range(n): + for j in range(i + 1, n): + if solution_mat[i, j] > 0: + labels[j] = labels[i] + + clusters = defaultdict(list) + for i, label in enumerate(labels): + clusters[label].append(i) + return list(clusters.values()) + + def solve(self, affinity_matrix, rtn_matrix=False): + n_nodes = affinity_matrix.shape[0] + if n_nodes <= 1: + solution_x, sol_matrix = ( + np.asarray([], dtype=int), + np.asarray([0] * n_nodes, dtype=int), + ) + sol_matrix = sol_matrix[:, None] + elif n_nodes == 2: + solution_matrix = np.zeros_like(affinity_matrix, dtype=int) + solution_matrix[0, 1] = affinity_matrix[0, 1] > 0 + solution_matrix += solution_matrix.T + solution_x = ( + [solution_matrix[0, 1]] + if self.min_affinity < affinity_matrix[0, 1] < self.max_affinity + else [] + ) + solution_x, sol_matrix = np.asarray(solution_x), solution_matrix + else: + # create BIP problem + ( + objective_coefficients, + unfrozen_mask, + frozen_pos_mask, + frozen_neg_mask, + sparse_constraints, + upper_bounds, + ) = self._create_bip(affinity_matrix, self.min_affinity, self.max_affinity) + + # solve + solution_x = self._solve_bip( + objective_coefficients, sparse_constraints, upper_bounds + ) + + # solution to matrix + all_sols = np.zeros_like(unfrozen_mask, dtype=int) + all_sols[unfrozen_mask] = np.array(solution_x, dtype=int).reshape(-1) + all_sols[frozen_neg_mask] = 0 + all_sols[frozen_pos_mask] = 1 + sol_matrix = np.zeros_like(affinity_matrix, dtype=int) + sol_matrix[np.triu(np.ones([n_nodes, n_nodes], dtype=int), 1) > 0] = ( + all_sols + ) + sol_matrix += sol_matrix.T + + clusters = self.solution_mat_clusters(sol_matrix) + if not rtn_matrix: + return clusters + return clusters, sol_matrix + + +class GLPKSolver(_BIPSolver): + def __init__(self, min_affinity=-np.inf, max_affinity=np.inf): + super().__init__(min_affinity, max_affinity) + + @override + def _solve_bip( + self, + objective_coefficients: NDArray, + sparse_constraints: Tuple[NDArray, NDArray, NDArray], + upper_bounds: NDArray, + ): + c = matrix(-objective_coefficients) # max -> min + # G * x <= h + G = spmatrix( + *sparse_constraints, size=(len(upper_bounds), len(objective_coefficients)) + ) + h = matrix(upper_bounds, tc="d") + + status, solution = ilp(c, G, h, B=set(range(len(c)))) + + assert solution is not None, "Solver error: {}".format(status) + + return np.asarray(solution, int).reshape(-1) diff --git a/app/solver/_wrap/__init__.py b/app/solver/_wrap/__init__.py new file mode 100644 index 0000000..2c11342 --- /dev/null +++ b/app/solver/_wrap/__init__.py @@ -0,0 +1,184 @@ +""" +See also: + https://github.com/cvxopt/cvxopt/blob/master/src/C/base.c +""" + +from typing import ( + Any, + BinaryIO, + Generic, + Literal, + Optional, + Protocol, + Sequence, + Tuple, + TypeVar, + Union, + overload, +) + +import numpy as np +from cvxopt import matrix as cvxopt_matrix +from cvxopt import sparse as cvxopt_sparse +from cvxopt import spmatrix as cvxopt_spmatrix +from numpy.typing import NDArray +from typing_extensions import Self, TypeAlias + +Typecode: TypeAlias = Literal["i", "d", "z"] +# Integer sparse matrices are not implemented. +SparseTypecode: TypeAlias = Literal["d", "z"] +DenseT = TypeVar("DenseT", int, float, complex) +SparseT = TypeVar("SparseT", float, complex) +IndexType = Union[int, slice, Sequence[int], "Matrix[int]"] + + +class Matrix(Generic[DenseT], Protocol): + """ + cvxopt.matrix interface + """ + + @property + def size(self) -> Tuple[int, int]: ... + + @property + def typecode(self) -> Typecode: ... + + def __mul__(self, other): ... + def __add__(self, other): ... + def __sub__(self, other): ... + def __truediv__(self, other): ... + def __mod__(self, other): ... + def __len__(self) -> int: ... + + def transpose(self) -> Self: ... + def ctrans(self) -> Self: ... + def real(self) -> "Matrix[float]": ... + def imag(self) -> "Matrix[float]": ... + + def tofile(self, f: BinaryIO) -> None: ... + def fromfile(self, f: BinaryIO) -> None: ... + + def __getitem__( + self, index: Union[IndexType, Tuple[IndexType, IndexType]] + ) -> Union[DenseT, Self]: ... + def __setitem__( + self, + index: Union[IndexType, Tuple[IndexType, IndexType]], + value: Union[DenseT, "Matrix[Any]"], + ) -> None: ... + + +@overload +def matrix( + data: Any, size: Optional[Tuple[int, int]] = None, tc: Typecode = "d" +) -> Matrix[float]: ... + + +@overload +def matrix( + data: Any, size: Optional[Tuple[int, int]] = None, tc: Typecode = "i" +) -> Matrix[int]: ... + + +@overload +def matrix( + data: Any, size: Optional[Tuple[int, int]] = None, tc: Typecode = "z" +) -> Matrix[complex]: ... + + +def matrix(data: Any, size: Optional[Tuple[int, int]] = None, tc: Typecode = "d"): + if size is None: + return cvxopt_matrix(data, tc=tc) + return cvxopt_matrix(data, size=size, tc=tc) + + +class SparseMatrix(Generic[SparseT], Protocol): + """ + cvxopt.spmatrix interface + """ + + @property + def size(self) -> Tuple[int, int]: ... + + @property + def typecode(self) -> Typecode: ... + + @property + def V(self) -> "Matrix[SparseT]": ... + + @property + def I(self) -> "Matrix[int]": ... + + @property + def J(self) -> "Matrix[int]": ... + + @property + def CCS(self) -> "Matrix[int]": ... + + def __mul__(self, other): ... + def __add__(self, other): ... + def __sub__(self, other): ... + def __truediv__(self, other): ... + def __mod__(self, other): ... + def __len__(self) -> int: ... + + def transpose(self) -> Self: ... + def ctrans(self) -> Self: ... + def real(self) -> "Matrix[float]": ... + def imag(self) -> "Matrix[float]": ... + + def tofile(self, f: BinaryIO) -> None: ... + def fromfile(self, f: BinaryIO) -> None: ... + + def __getitem__( + self, index: Union[IndexType, Tuple[IndexType, IndexType]] + ) -> Union[DenseT, Self]: ... + def __setitem__( + self, + index: Union[IndexType, Tuple[IndexType, IndexType]], + value: Union[DenseT, "Matrix[Any]"], + ) -> None: ... + + +@overload +def spmatrix( + x: Union[Sequence[float], float, Matrix[float], NDArray[np.floating[Any]]], + I: Union[Sequence[int], NDArray[np.int_]], + J: Union[Sequence[int], NDArray[np.int_]], + size: Optional[Tuple[int, int]] = None, + tc: SparseTypecode = "d", +) -> SparseMatrix[float]: ... + + +@overload +def spmatrix( + x: Union[ + Sequence[complex], complex, Matrix[complex], NDArray[np.complexfloating[Any]] + ], + I: Union[Sequence[int], NDArray[np.int_]], + J: Union[Sequence[int], NDArray[np.int_]], + size: Optional[Tuple[int, int]] = None, + tc: SparseTypecode = "z", +) -> SparseMatrix[complex]: ... + + +def spmatrix( + x: Any, + I: Any, + J: Any, + size: Optional[Tuple[int, int]] = None, + tc: SparseTypecode = "d", +): + if size is None: + return cvxopt_spmatrix(x, I, J, tc=tc) + return cvxopt_spmatrix(x, I, J, size=size, tc=tc) + + +@overload +def sparse(x: Any, tc: SparseTypecode = "d") -> SparseMatrix[float]: ... +@overload +def sparse(x: Any, tc: SparseTypecode = "z") -> SparseMatrix[complex]: ... + + +def sparse(x: Any, tc: SparseTypecode = "d"): + return cvxopt_sparse(x, tc=tc) diff --git a/app/solver/_wrap/glpk.py b/app/solver/_wrap/glpk.py new file mode 100644 index 0000000..188f479 --- /dev/null +++ b/app/solver/_wrap/glpk.py @@ -0,0 +1,249 @@ +""" +See also: + https://github.com/cvxopt/cvxopt/blob/master/src/C/glpk.c +""" + +from typing import Tuple, Union, Literal, Optional, Dict, Any, Set, overload, TypedDict +from cvxopt import glpk # type: ignore +from . import Matrix, SparseMatrix + + +CvxMatLike = Union[Matrix, SparseMatrix] +CvxBool = Literal["GLP_ON", "GLP_OFF"] + + +class GLPKOptions(TypedDict, total=False): + # Common parameters + msg_lev: Literal["GLP_MSG_OFF", "GLP_MSG_ERR", "GLP_MSG_ON", "GLP_MSG_ALL"] + presolve: CvxBool + tm_lim: int + out_frq: int + out_dly: int + # LP-specific parameters + meth: Literal["GLP_PRIMAL", "GLP_DUAL", "GLP_DUALP"] + pricing: Literal["GLP_PT_STD", "GLP_PT_PSE"] + r_test: Literal["GLP_RT_STD", "GLP_RT_HAR"] + tol_bnd: float + tol_dj: float + tol_piv: float + obj_ll: float + obj_ul: float + it_lim: int + # MILP-specific parameters + br_tech: Literal[ + "GLP_BR_FFV", "GLP_BR_LFV", "GLP_BR_MFV", "GLP_BR_DTH", "GLP_BR_PCH" + ] + bt_tech: Literal["GLP_BT_DFS", "GLP_BT_BFS", "GLP_BT_BLB", "GLP_BT_BPH"] + pp_tech: Literal["GLP_PP_NONE", "GLP_PP_ROOT", "GLP_PP_ALL"] + fp_heur: CvxBool + gmi_cuts: CvxBool + mir_cuts: CvxBool + cov_cuts: CvxBool + clq_cuts: CvxBool + tol_int: float + tol_obj: float + mip_gap: float + cb_size: int + binarize: CvxBool + + +StatusLP = Literal["optimal", "primal infeasible", "dual infeasible", "unknown"] +StatusILP = Literal[ + "optimal", + "feasible", + "undefined", + "invalid formulation", + "infeasible problem", + "LP relaxation is primal infeasible", + "LP relaxation is dual infeasible", + "unknown", +] + + +@overload +def lp( + c: Matrix, + G: CvxMatLike, + h: Matrix, +) -> Tuple[StatusLP, Optional[Matrix], Optional[Matrix]]: + """ + (status, x, z) = lp(c, G, h) + + PURPOSE + (status, x, z) = lp(c, G, h) solves the pair of primal and dual LPs + + minimize c'*x maximize -h'*z + subject to G*x <= h subject to G'*z + c = 0 + z >= 0. + + ARGUMENTS + c nx1 dense 'd' matrix with n>=1 + + G mxn dense or sparse 'd' matrix with m>=1 + + h mx1 dense 'd' matrix + + status 'optimal', 'primal infeasible', 'dual infeasible' + or 'unknown' + + x if status is 'optimal', a primal optimal solution; + None otherwise + + z if status is 'optimal', the dual optimal solution; + None otherwise + """ + + +@overload +def lp( + c: Matrix, + G: CvxMatLike, + h: Matrix, + A: CvxMatLike, + b: Matrix, +) -> Tuple[StatusLP, Optional[Matrix], Optional[Matrix], Optional[Matrix]]: + """ + (status, x, z, y) = lp(c, G, h, A, b) + + PURPOSE + (status, x, z, y) = lp(c, G, h, A, b) solves the pair of primal and + dual LPs + + minimize c'*x maximize -h'*z + b'*y + subject to G*x <= h subject to G'*z + A'*y + c = 0 + A*x = b z >= 0. + + + ARGUMENTS + c nx1 dense 'd' matrix with n>=1 + + G mxn dense or sparse 'd' matrix with m>=1 + + h mx1 dense 'd' matrix + + A pxn dense or sparse 'd' matrix with p>=0 + + b px1 dense 'd' matrix + + status 'optimal', 'primal infeasible', 'dual infeasible' + or 'unknown' + + x if status is 'optimal', a primal optimal solution; + None otherwise + + z,y if status is 'optimal', the dual optimal solution; + None otherwise + """ + + +# https://cvxopt.org/userguide/coneprog.html#linear-programming + + +def lp( + c: Matrix, + G: CvxMatLike, + h: Matrix, + A: Optional[CvxMatLike] = None, + b: Optional[Matrix] = None, +): + """ + (status, x, z, y) = lp(c, G, h, A, b) + (status, x, z) = lp(c, G, h) + + PURPOSE + (status, x, z, y) = lp(c, G, h, A, b) solves the pair of primal and + dual LPs + + minimize c'*x maximize -h'*z + b'*y + subject to G*x <= h subject to G'*z + A'*y + c = 0 + A*x = b z >= 0. + + (status, x, z) = lp(c, G, h) solves the pair of primal and dual LPs + + minimize c'*x maximize -h'*z + subject to G*x <= h subject to G'*z + c = 0 + z >= 0. + + ARGUMENTS + c nx1 dense 'd' matrix with n>=1 + + G mxn dense or sparse 'd' matrix with m>=1 + + h mx1 dense 'd' matrix + + A pxn dense or sparse 'd' matrix with p>=0 + + b px1 dense 'd' matrix + + status 'optimal', 'primal infeasible', 'dual infeasible' + or 'unknown' + + x if status is 'optimal', a primal optimal solution; + None otherwise + + z,y if status is 'optimal', the dual optimal solution; + None otherwise + """ + if A is None and b is None: + return glpk.lp(c, G, h) + return glpk.lp(c, G, h, A, b) + + +def ilp( + c: Matrix, + G: CvxMatLike, + h: Matrix, + A: Optional[CvxMatLike] = None, + b: Optional[Matrix] = None, + I: Optional[Set[int]] = None, + B: Optional[Set[int]] = None, +) -> Tuple[StatusILP, Optional[Matrix]]: + """ + Solves a mixed integer linear program using GLPK. + + (status, x) = ilp(c, G, h, A, b, I, B) + + PURPOSE + Solves the mixed integer linear programming problem + + minimize c'*x + subject to G*x <= h + A*x = b + x[k] is integer for k in I + x[k] is binary for k in B + + ARGUMENTS + c nx1 dense 'd' matrix with n>=1 + + G mxn dense or sparse 'd' matrix with m>=1 + + h mx1 dense 'd' matrix + + A pxn dense or sparse 'd' matrix with p>=0 + + b px1 dense 'd' matrix + + I set of indices of integer variables + + B set of indices of binary variables + + status if status is 'optimal', 'feasible', or 'undefined', + a value of x is returned and the status string + gives the status of x. Other possible values of + status are: 'invalid formulation', + 'infeasible problem', 'LP relaxation is primal + infeasible', 'LP relaxation is dual infeasible', + 'unknown'. + + x a (sub-)optimal solution if status is 'optimal', + 'feasible', or 'undefined'. None otherwise + """ + return glpk.ilp(c, G, h, A, b, I, B) + + +def set_global_options(options: GLPKOptions) -> None: + glpk.options = options + + +def get_global_options() -> GLPKOptions: + return glpk.options diff --git a/pyproject.toml b/pyproject.toml index 29c6224..981ad73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.10" dependencies = [ "anyio>=4.8.0", "awkward>=2.7.4", + "cvxopt>=1.3.2", "jax[cuda12]>=0.5.1", "jaxtyping>=0.2.38", "opencv-contrib-python-headless>=4.11.0.86", diff --git a/uv.lock b/uv.lock index 28becf3..4505bc6 100644 --- a/uv.lock +++ b/uv.lock @@ -372,6 +372,7 @@ source = { virtual = "." } dependencies = [ { name = "anyio" }, { name = "awkward" }, + { name = "cvxopt" }, { name = "jax", extra = ["cuda12"] }, { name = "jaxtyping" }, { name = "opencv-contrib-python-headless" }, @@ -390,6 +391,7 @@ dev = [ requires-dist = [ { name = "anyio", specifier = ">=4.8.0" }, { name = "awkward", specifier = ">=2.7.4" }, + { name = "cvxopt", specifier = ">=1.3.2" }, { name = "jax", extras = ["cuda12"], specifier = ">=0.5.1" }, { name = "jaxtyping", specifier = ">=0.2.38" }, { name = "opencv-contrib-python-headless", specifier = ">=4.11.0.86" }, @@ -402,6 +404,41 @@ requires-dist = [ [package.metadata.requires-dev] dev = [{ name = "jupyter", specifier = ">=1.1.1" }] +[[package]] +name = "cvxopt" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/12/8467d16008ab7577259d32f1e59c4d84edda22b7729ab4a1a0dfd5f0550b/cvxopt-1.3.2.tar.gz", hash = "sha256:3461fa42c1b2240ba4da1d985ca73503914157fc4c77417327ed6d7d85acdbe6", size = 4108454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/ab/78b8dcaf31f034184c4d9051562631856212614f34b9246f694dfb3e105b/cvxopt-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd4a1bba537a34808b92f1e793e3499029d339a7a2ab6d989f82e395b7b740ff", size = 13835104 }, + { url = "https://files.pythonhosted.org/packages/44/b1/b27dcf10dc6b61ffeb84bcf684d83ca90557b717d80b78a4758576c17010/cvxopt-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3cd2db913b1cf64d84cdb7bc467a8a15adbd1f0f83a7a45a7167ad590f79408", size = 11103451 }, + { url = "https://files.pythonhosted.org/packages/41/6d/98814860dbb9cdc27dcb6651b35124d7adca3bfe281f3351abb02a8a3f72/cvxopt-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6874e1b9aa002f9d796da9d02bdca76b15aa3d4b2f83ca5064ac4c7894b92ece", size = 13578154 }, + { url = "https://files.pythonhosted.org/packages/ef/67/3c577c9b4a09c3006e994a581fb540f48cf0378d8f3785cc1fe00fd48b87/cvxopt-1.3.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:32d9f88940464bffddfc0601fe3156ab16bf5a92393483e32342df0272fa64ce", size = 13814850 }, + { url = "https://files.pythonhosted.org/packages/89/91/a68d87b421c4bfe936c756778d58c7220abd9292e8e2dac951a3e3f64505/cvxopt-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9eb704be0918f04691af1267107539222cc2277bca888fdc385733bcab30f734", size = 9499915 }, + { url = "https://files.pythonhosted.org/packages/5b/10/429440cf9b841a5f8645f0aacc6a8da0a87cce4846d45e836f6b5f83be34/cvxopt-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:22d12b88190e047c0cedde165711222aa0dcdc325a229b876c36f746dd4a6f12", size = 12844564 }, + { url = "https://files.pythonhosted.org/packages/c1/43/f626c353802fb5ed37a087a0e41ad92246a1e1189869d47865853a980927/cvxopt-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a459b6ee9f99fc34861cbcf679a196af2d930ec70d95018a94f2e6dbe46c8c24", size = 13835210 }, + { url = "https://files.pythonhosted.org/packages/08/4d/2b2cc805f7db0636896b185dc8204556d363ccadbdca67e1a60e7aab4be6/cvxopt-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ae730ebc130461f743922f11d00c2d59a79492e57a1f5d245d4a6c731b7e334", size = 11095304 }, + { url = "https://files.pythonhosted.org/packages/8b/59/5e617916304022f5ad421459aa3f6e631537317d7a804c8128b32c6c29e6/cvxopt-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994dab68c193bea405a3a89a88b8703dd2c79bb790a330c8d459f0454cca71ef", size = 13578119 }, + { url = "https://files.pythonhosted.org/packages/e8/45/16b1719c489f734c76a6d9187f6dcdc41a1b923cd91c081aa0f4bedb923d/cvxopt-1.3.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ede23c1aaacdbfd3b8fd192121b3024b41d00a97f2e9fc8f106be922ea05523d", size = 13840609 }, + { url = "https://files.pythonhosted.org/packages/1e/cd/cd01bd7f4052d2ca336d67da4ecae4ffef34289ff408e8f654e14ee44b96/cvxopt-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a8c92308165b632bc43dc39acee052180037a1209d4a57b5c3d10136a2f563a4", size = 9524719 }, + { url = "https://files.pythonhosted.org/packages/a3/52/2237d72cf007e6c36367ab8a776388a9f13511e4cfa8a71b79101ad6e0fa/cvxopt-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:0c45f663e40b3ed2e2320e7ae8d50fcf09b5ac72c5af4c66aa523e0045453311", size = 12844638 }, + { url = "https://files.pythonhosted.org/packages/10/dc/1c21715e1267ca29f562e4450426d1ff8a7ffcc3e670100cec332a105b95/cvxopt-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25adbeb0efd50d7ea4f07e5f5bd390a3c807df907f03efb86b018807c2c8cfbe", size = 13836586 }, + { url = "https://files.pythonhosted.org/packages/cd/c8/a04048143d0329ccd36403951746c1a6b5f1fc56c479e5a0a77efb2064b2/cvxopt-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c10e27cb7a27b55f17e0df30c6b85e98c9672a7bdb7000a7509560eee7679137", size = 12765513 }, + { url = "https://files.pythonhosted.org/packages/c7/17/ee82c745c5bda340a4dd812652c42fb71efd45f663554a10c3ec45f230df/cvxopt-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8bcf71a5016aeb24e597dc099564e8de809e0bc5d6af21e26422586aea26718", size = 17870231 }, + { url = "https://files.pythonhosted.org/packages/c6/f9/467c3f4682f3dbfbd7ff67f2307ed746a86b6dcc6b0b62cf1eeaebbd9d74/cvxopt-1.3.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a581e6c87a06371210184f64353055ff7c917d49363901ae0c527da139095082", size = 13846494 }, + { url = "https://files.pythonhosted.org/packages/41/8e/c3869928250e12ad9264da388bc70150a9de039e233b815a6a3bd2b8b8ae/cvxopt-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be7800ac4556d8920aaf8e4e2d89348aafd5d585642aabf9eeecb09a2659fbca", size = 9529949 }, + { url = "https://files.pythonhosted.org/packages/9f/ad/edce467c24529c536fc9de787546a1c8eca293009383a872b6f638d22eae/cvxopt-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:a92ebfc5df77fea57544f8ad2102bfc45af0e77ac4dfe98ed1b9628e8bba77c3", size = 12845277 }, + { url = "https://files.pythonhosted.org/packages/3e/c5/3e70e50c4c478acd3fefe3ea51b7e42ad661ce5a265a72b3dba175ce10fc/cvxopt-1.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2f9135eea23c9b781574e0cadc5738cf5651a8fd8de822b6de1260411523bfd1", size = 16873224 }, + { url = "https://files.pythonhosted.org/packages/61/96/e42b9ec38e1bbe9bf85a5fc9cc7feb173de5a874889735072b49a7d4d8d0/cvxopt-1.3.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d7921768712db156e6ec92ac21f7ce52069feb1fb994868d0ca795498111fbac", size = 12424739 }, + { url = "https://files.pythonhosted.org/packages/32/08/2c621ad782e9ff7f921c2244c6b4bcbc72ca756cb33021295c288123c465/cvxopt-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af63db45ba559e3e15180fbec140d8a4ff612d8f21d989181a4e8479fa3b8b6", size = 17869707 }, + { url = "https://files.pythonhosted.org/packages/62/60/583a1ef8e2e259bdd1bf32fccd4ea15aef4aad5854746ec59cbb2462eb92/cvxopt-1.3.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:8fe178ac780a8bccf425a08004d853eae43b3ddcf7617521fb35c63550077b17", size = 13846614 }, + { url = "https://files.pythonhosted.org/packages/e4/2b/d8721b046a3c8bff494490a01ef1eeacf1f970f0d1274448856ccbe0475c/cvxopt-1.3.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a47a95d7848e6fe768b55910bac8bb114c5f1f355f5a6590196d5e9bdf775d2f", size = 21277032 }, + { url = "https://files.pythonhosted.org/packages/6a/19/b1e1c16895a36cc504bf7a940e88431b82b18ca10cbce81072860b9e3d60/cvxopt-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e863238d64a4b4443b8be53a08f6b94eda6ec1727038c330da02014f7c19e1be", size = 9530674 }, + { url = "https://files.pythonhosted.org/packages/42/cc/ac0705749f96cc52f8d30c9c06e54dc8d4c04ef9c2d21aeed1ae2ee63dab/cvxopt-1.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c56965415afd8a493cc4af3587960751f8780057ca3de8c6be97217156e4633", size = 13725340 }, + { url = "https://files.pythonhosted.org/packages/76/f2/7e3c3f51e8e6b325bf00bfc37036f1f58bd9a5c29bbd88fb2eef2ebc0ac2/cvxopt-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:85c3b52c1353b294c597b169cc901f5274d8bb8776908ccad66fec7a14b69519", size = 16226402 }, + { url = "https://files.pythonhosted.org/packages/b9/55/90b40b489a235a9f35a532eb77cec81782e466779d9a531ffda6b2f99410/cvxopt-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:0a0987966009ad383de0918e61255d34ed9ebc783565bcb15470d4155010b6bf", size = 12845323 }, +] + [[package]] name = "debugpy" version = "1.8.12"