From 2e8717a468f7412804b2645d37b2f531a5b99958 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 13 Jan 2026 17:33:06 -0800 Subject: [PATCH 01/13] update model to add QP matrix --- .../cuopt/cuopt/linear_programming/problem.py | 154 +++++++++++++----- 1 file changed, 114 insertions(+), 40 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 4259753dcb..d3d38bc278 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -7,6 +7,7 @@ import cuopt_mps_parser import numpy as np +from scipy.sparse import coo_matrix import cuopt.linear_programming.data_model as data_model import cuopt.linear_programming.solver as solver @@ -326,7 +327,7 @@ class QuadraticExpression: """ def __init__( - self, qvars1, qvars2, qcoefficients, vars, coefficients, constant + self, qvars1, qvars2, qcoefficients, vars=[], coefficients=[], constant=0.0 ): self.qvars1 = qvars1 self.qvars2 = qvars2 @@ -681,6 +682,80 @@ def __eq__(self, other): raise Exception("Quadratic constraints not supported") +class MQuadraticExpression: + """ + MQuadraticExpressions contain quadratic terms, linear terms, and a constant. + MQuadraticExpressions can be used to create quadratic objectives in + the Problem. + + Parameters + ---------- + qmatrix : List[List[float]] + 2D List or np.array containing quadratic coefficient matrix terms. + qvars : List[Variable] + List of variables for quadratic matrix. + Should be in the same order of variables added. + vars : List[Variable] + List of Variables for linear terms. + coefficients : List[float] + List of coefficients for linear terms. + constant : float + Constant of the quadratic expression. + + Examples + -------- + >>> x = problem.addVariable() + >>> y = problem.addVariable() + >>> # Create x^2 + 2*x*y + 3*x + 4 + >>> quad_expr = QuadraticExpression( + ... [[1.0, 2.0], [0.0, 0.0]], [x, y], + ... [x], [3.0], 4.0 + ... ) + """ + + def __init__( + self, qmatrix, qvars=[], vars=[], coefficients=[], constant=0.0 + ): + self.qmatrix = qmatrix + self.qvars = qvars + self.vars = vars + self.coefficients = coefficients + self.constant = constant + + + def shape(self): + return np.shape(self.qmatrix) + + def getValue(self): + """ + Returns the value of the expression computed with the + current solution. + """ + value = 0.0 + for i, var in enumerate(self.vars): + value += var.Value * self.coefficients[i] + for i, var1 in enumerate(self.qvars): + for j, var2 in enumerate(self.qvars): + value += var1.Value * var2.Value * int(self.qmatrix[i,j]) + return value + self.constant + + def __size__(self): + return np.size(self.qmatrix) + + ## TODO: Add matrix multiplication + #def __matmul__(self, qcols): + # if not self.qcols: + # self.qcols = qcols + # else: + # raise Exception("") + + #def __rmatmul__(self, qcols): + # if not self.qrows: + # self.qrows = qrows + # else: + # raise Exception("") + + class LinearExpression: """ LinearExpressions contain a set of variables, the coefficients @@ -1140,7 +1215,6 @@ def __init__(self, model_name=""): self.ObjSense = MINIMIZE self.ObjConstant = 0.0 self.Status = -1 - self.ObjValue = float("nan") self.warmstart_data = None self.model = None @@ -1149,7 +1223,7 @@ def __init__(self, model_name=""): self.row_sense = None self.constraint_csr_matrix = None self.objective_qcsr_matrix = None - self.objective_qcoo_matrix = [], [], [] + self.objective_qcoo_matrix = None self.lower_bound = None self.upper_bound = None self.var_type = None @@ -1277,9 +1351,9 @@ def _to_data_model(self): dm.set_objective_offset(self.ObjConstant) if self.getQcsr(): dm.set_quadratic_objective_matrix( - np.array(self.objective_qcsr_matrix["values"]), - np.array(self.objective_qcsr_matrix["column_indices"]), - np.array(self.objective_qcsr_matrix["row_pointers"]), + self.objective_qcsr_matrix["values"], + self.objective_qcsr_matrix["column_indices"], + self.objective_qcsr_matrix["row_pointers"], ) dm.set_variable_lower_bounds(self.lower_bound) dm.set_variable_upper_bounds(self.upper_bound) @@ -1310,7 +1384,7 @@ def reset_solved_values(self): self.model = None self.constraint_csr_matrix = None - self.objective_qcoo_matrix = [], [], [] + self.objective_qcoo_matrix = None self.objective_qcsr_matrix = None self.ObjValue = float("nan") self.warmstart_data = None @@ -1480,11 +1554,25 @@ def setObjective(self, expr, sense=MINIMIZE): sum_coeff ) self.ObjConstant = expr.constant - self.objective_qcoo_matrix = ( - expr.qvars1, - expr.qvars2, - expr.qcoefficients, + qrows = [var.getIndex() for var in expr.qvars1] + qcols = [var.getIndex() for var in expr.qvars2] + self.objective_qcoo_matrix = coo_matrix( + (np.array(expr.qcoefficients), + (np.array(qrows), np.array(qcols))), + shape=(self.NumVariables, self.NumVariables) ) + case MQuadraticExpression(): + for var in self.vars: + var.setObjectiveCoefficient(0.0) + for var, coeff in zip(expr.vars, expr.coefficients): + c_val = self.vars[var.getIndex()].getObjectiveCoefficient() + sum_coeff = coeff + c_val + self.vars[var.getIndex()].setObjectiveCoefficient( + sum_coeff + ) + self.ObjConstant = expr.constant + self.objective_qcoo_matrix = coo_matrix( + expr.qmatrix) case _: raise ValueError( "Objective must be a LinearExpression or a constant" @@ -1643,16 +1731,21 @@ def Obj(self): coeffs = [] for var in self.vars: coeffs.append(var.getObjectiveCoefficient()) - if not self.objective_qcoo_matrix: + if self.objective_qcoo_matrix is None: return LinearExpression(self.vars, coeffs, self.ObjConstant) else: - return QuadraticExpression( - *self.objective_qcoo_matrix, + return MQuadraticExpression( + self.objective_qcoo_matrix.todense(), + self.vars, self.vars, coeffs, self.ObjConstant, ) + @property + def ObjValue(self): + self.Obj.getValue() + def getCSR(self): """ Computes and returns the CSR representation of the @@ -1673,29 +1766,14 @@ def getCSR(self): def getQcsr(self): if self.objective_qcsr_matrix is not None: return self.dict_to_object(self.objective_qcsr_matrix) - vars1, vars2, coeffs = self.objective_qcoo_matrix - if not vars1: + if self.objective_qcoo_matrix is None: return None - Qdict = {} - Qcsr_dict = {"row_pointers": [0], "column_indices": [], "values": []} - for i, var1 in enumerate(vars1): - if var1.index not in Qdict: - Qdict[var1.index] = {} - row_dict = Qdict[var1.index] - var2 = vars2[i] - coeff = coeffs[i] - row_dict[var2.index] = ( - row_dict[var2.index] + coeff - if var2.index in row_dict - else coeff - ) - for i in range(0, self.NumVariables): - if i in Qdict: - Qcsr_dict["column_indices"].extend(list(Qdict[i].keys())) - Qcsr_dict["values"].extend(list(Qdict[i].values())) - Qcsr_dict["row_pointers"].append(len(Qcsr_dict["column_indices"])) - self.objective_qcsr_matrix = Qcsr_dict - return self.dict_to_object(Qcsr_dict) + qcsr_matrix = self.objective_qcoo_matrix.tocsr() + self.objective_qcsr_matrix = {"row_pointers": qcsr_matrix.indptr, + "column_indices": qcsr_matrix.indices, + "values": qcsr_matrix.data} + return self.dict_to_object(self.objective_qcsr_matrix) + def relax(self): """ @@ -1725,7 +1803,6 @@ def populate_solution(self, solution): else: IsMIP = True self.SolutionStats = self.dict_to_object(solution.get_milp_stats()) - primal_sol = solution.get_primal_solution() reduced_cost = solution.get_reduced_cost() if len(primal_sol) > 0: @@ -1744,7 +1821,6 @@ def populate_solution(self, solution): if dual_sol is not None and len(dual_sol) > 0: constr.DualValue = dual_sol[i] constr.Slack = constr.compute_slack() - self.ObjValue = self.Obj.getValue() self.solved = True def solve(self, settings=solver_settings.SolverSettings()): @@ -1765,9 +1841,7 @@ def solve(self, settings=solver_settings.SolverSettings()): """ if self.model is None: self._to_data_model() - # Call Solver solution = solver.Solve(self.model, settings) - # Post Solve self.populate_solution(solution) From 42ab2fe031adc0f17776104a069f2e454b21049b Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Sun, 18 Jan 2026 19:44:39 -0800 Subject: [PATCH 02/13] add test and update recipe --- conda/recipes/cuopt/recipe.yaml | 1 + .../cuopt/cuopt/linear_programming/problem.py | 132 +++++++++++++++++- .../linear_programming/test_python_API.py | 39 +++++- 3 files changed, 165 insertions(+), 7 deletions(-) diff --git a/conda/recipes/cuopt/recipe.yaml b/conda/recipes/cuopt/recipe.yaml index 3408c67158..7c9ecf4f75 100644 --- a/conda/recipes/cuopt/recipe.yaml +++ b/conda/recipes/cuopt/recipe.yaml @@ -82,6 +82,7 @@ requirements: - pylibraft =${{ minor_version }} - python - rmm =${{ minor_version }} + - scipy # Needed by Numba for CUDA support - cuda-nvcc-impl # TODO: Add nvjitlink here diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index d3d38bc278..1476568916 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -707,7 +707,7 @@ class MQuadraticExpression: >>> x = problem.addVariable() >>> y = problem.addVariable() >>> # Create x^2 + 2*x*y + 3*x + 4 - >>> quad_expr = QuadraticExpression( + >>> quad_expr = MQuadraticExpression( ... [[1.0, 2.0], [0.0, 0.0]], [x, y], ... [x], [3.0], 4.0 ... ) @@ -731,17 +731,133 @@ def getValue(self): Returns the value of the expression computed with the current solution. """ + # TODO: improve efficiency value = 0.0 for i, var in enumerate(self.vars): value += var.Value * self.coefficients[i] for i, var1 in enumerate(self.qvars): for j, var2 in enumerate(self.qvars): - value += var1.Value * var2.Value * int(self.qmatrix[i,j]) + value += var1.Value * var2.Value * float(self.qmatrix[i,j]) return value + self.constant def __size__(self): return np.size(self.qmatrix) + def __iadd__(self, other): + # Compute quad_expr += lin_expr + match other: + case int() | float(): + # Update just the constant value + self.constant += float(other) + return self + case Variable(): + # Append just a variable with coefficient 1.0 + self.vars.append(other) + self.coefficients.append(1.0) + return self + case LinearExpression(): + # Append all linear variables, coefficients and constants + self.vars.extend(other.vars) + self.coefficients.extend(other.coefficients) + self.constant += other.constant + return self + + def __add__(self, other): + # Compute quad_expr1 = quar_expr2 + lin_expr + match other: + case int() | float(): + # Update just the constant value + return MQuadraticExpression( + self.qmatrix, + self.qvars, + self.vars, + self.coefficients, + self.constant + float(other), + ) + case Variable(): + # Append just a variable with coefficient 1.0 + vars = self.vars + [other] + coeffs = self.coefficients + [1.0] + return MQuadraticExpression( + self.qmatrix, + self.qvars, + vars, + coeffs, + self.constant, + ) + case LinearExpression(): + # Append all linear variables, coefficients and constants + vars = self.vars + other.vars + coeffs = self.coefficients + other.coefficients + constant = self.constant + other.constant + return MQuadraticExpression( + self.qmatrix, + self.qvars, + vars, + coeffs, + constant, + ) + + def __isub__(self, other): + # Compute quad_expr -= lin_expr + match other: + case int() | float(): + # Update just the constant value + self.constant -= float(other) + return self + case Variable(): + # Append just a variable with coefficient -1.0 + self.vars.append(other) + self.coefficients.append(-1.0) + return self + case LinearExpression(): + # Append all linear variables, coefficients and constants + self.vars.extend(other.vars) + for coeff in other.coefficients: + self.coefficients.append(-coeff) + self.constant -= other.constant + return self + + def __sub__(self, other): + # Compute quad_expr2 = quad_expr1 - lin_expr + match other: + case int() | float(): + # Update just the constant value + return MQuadraticExpression( + self.qmatrix, + self.qvars, + self.vars, + self.coefficients, + self.constant - float(other), + ) + case Variable(): + # Append just a variable with coefficient -1.0 + vars = self.vars + [other] + coeffs = self.coefficients + [-1.0] + return MQuadraticExpression( + self.qmatrix, + self.qvars, + vars, + coeffs, + self.constant, + ) + case LinearExpression(): + # Append all linear variables, coefficients and constants + vars = self.vars + other.vars + coeffs = [] + for i in self.coefficients: + coeffs.append(i) + for i in other.coefficients: + coeffs.append(-1.0 * i) + constant = self.constant - other.constant + return MQuadraticExpression( + self.qmatrix, + self.qvars, + vars, + coeffs, + constant, + ) + ## TODO: Add matrix multiplication #def __matmul__(self, qcols): # if not self.qcols: @@ -755,6 +871,15 @@ def __size__(self): # else: # raise Exception("") + def __le__(self, other): + raise Exception("Quadratic constraints not supported") + + def __ge__(self, other): + raise Exception("Quadratic constraints not supported") + + def __eq__(self, other): + raise Exception("Quadratic constraints not supported") + class LinearExpression: """ @@ -1386,7 +1511,6 @@ def reset_solved_values(self): self.constraint_csr_matrix = None self.objective_qcoo_matrix = None self.objective_qcsr_matrix = None - self.ObjValue = float("nan") self.warmstart_data = None self.solved = False @@ -1744,7 +1868,7 @@ def Obj(self): @property def ObjValue(self): - self.Obj.getValue() + return self.Obj.getValue() def getCSR(self): """ diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 06ed937033..9f9e0a620a 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -20,6 +20,7 @@ Problem, VType, sense, + MQuadraticExpression ) from cuopt.linear_programming.solver.solver_parameters import ( CUOPT_AUGMENTED, @@ -754,9 +755,9 @@ def test_quadratic_expression_and_matrix(): exp_col_inds = [0, 1, 2, 0, 1, 2] exp_vals = [21, -7, 7, 3, -1, 1] - assert Qcsr.row_pointers == exp_row_ptrs - assert Qcsr.column_indices == exp_col_inds - assert Qcsr.values == exp_vals + assert list(Qcsr.row_pointers) == exp_row_ptrs + assert list(Qcsr.column_indices) == exp_col_inds + assert list(Qcsr.values) == exp_vals def test_quadratic_objective_1(): @@ -813,3 +814,35 @@ def test_quadratic_objective_2(): assert x2.getValue() == pytest.approx(0.0000000, abs=0.000001) assert x3.getValue() == pytest.approx(0.1092896, abs=1e-3) assert problem.ObjValue == pytest.approx(-0.284153, abs=1e-3) + + +def test_quadratic_matrix(): + # Minimize 4 x1^2 + 2 x2^2 + 3 x3^2 + 1.5 x1 x3 - 2 x1 + 0.5 x2 - x3 + 4 + # subject to x1 + 2*x2 + x3 <= 3 + # x1 >= 0 + # x2 >= 0 + # x3 >= 0 + + problem = Problem() + x1 = problem.addVariable(lb=0, name="x") + x2 = problem.addVariable(lb=0, name="y") + x3 = problem.addVariable(lb=0, name="z") + + problem.addConstraint(x1 + 2 * x2 + x3 <= 3) + + Q = [[4, 0, 1.5], [0, 2, 0], [0, 0, 3]] + quad_expr = MQuadraticExpression(Q) + quad_expr1 = quad_expr + 4 # Quad_matrix add constant + quad_expr2 = quad_expr1 - x3 # Quad_matrix sub variable + quad_expr2 -= 2 * x1 # Quad_matrix isub lin_expr + quad_expr2 += 0.5 * x2 # Quad_matrix iadd lin_expr + + problem.setObjective(quad_expr2) + + problem.solve() + + assert problem.Status.name == "Optimal" + assert x1.getValue() == pytest.approx(0.2295081, abs=1e-3) + assert x2.getValue() == pytest.approx(0.0000000, abs=0.000001) + assert x3.getValue() == pytest.approx(0.1092896, abs=1e-3) + assert problem.ObjValue == pytest.approx(3.715847, abs=1e-3) From 2a5cf2824f4e539fa19c7b7292e92930c948d3ab Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Sun, 18 Jan 2026 22:33:23 -0800 Subject: [PATCH 03/13] add scipy to environments --- conda/environments/all_cuda-129_arch-aarch64.yaml | 1 + conda/environments/all_cuda-129_arch-x86_64.yaml | 1 + conda/environments/all_cuda-131_arch-aarch64.yaml | 1 + conda/environments/all_cuda-131_arch-x86_64.yaml | 1 + 4 files changed, 4 insertions(+) diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 0594f81068..94c3ffacb9 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -60,6 +60,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 +- scipy - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 55b3d3dfbc..8b8250bcd2 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -60,6 +60,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 +- scipy - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-131_arch-aarch64.yaml b/conda/environments/all_cuda-131_arch-aarch64.yaml index da9337c836..1e8f14cb64 100644 --- a/conda/environments/all_cuda-131_arch-aarch64.yaml +++ b/conda/environments/all_cuda-131_arch-aarch64.yaml @@ -60,6 +60,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 +- scipy - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-131_arch-x86_64.yaml b/conda/environments/all_cuda-131_arch-x86_64.yaml index c2e8d7dbc8..f23c0efc52 100644 --- a/conda/environments/all_cuda-131_arch-x86_64.yaml +++ b/conda/environments/all_cuda-131_arch-x86_64.yaml @@ -60,6 +60,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 +- scipy - sphinx - sphinx-copybutton - sphinx-design From 254a5a9da271a19a16da9b40daa2badc6bb1ee3e Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Mon, 19 Jan 2026 14:43:09 -0800 Subject: [PATCH 04/13] update dependency --- dependencies.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/dependencies.yaml b/dependencies.yaml index 7dc6b94908..b3da7c656b 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -350,6 +350,7 @@ dependencies: - numba>=0.60.0 - &pandas pandas>=2.0 - &pyyaml pyyaml>=6.0.0 + - scipy>=1.13.0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file From df85b24d3605009a78c0f315c14a9910ab524dd4 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 20 Jan 2026 20:19:48 -0800 Subject: [PATCH 05/13] style checks --- .../cuopt/cuopt/linear_programming/problem.py | 37 +++++++++++-------- .../linear_programming/test_python_API.py | 4 +- python/cuopt/pyproject.toml | 1 + 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 1476568916..f5b9ba2b28 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 @@ -327,7 +327,13 @@ class QuadraticExpression: """ def __init__( - self, qvars1, qvars2, qcoefficients, vars=[], coefficients=[], constant=0.0 + self, + qvars1, + qvars2, + qcoefficients, + vars=[], + coefficients=[], + constant=0.0, ): self.qvars1 = qvars1 self.qvars2 = qvars2 @@ -722,7 +728,6 @@ def __init__( self.coefficients = coefficients self.constant = constant - def shape(self): return np.shape(self.qmatrix) @@ -737,7 +742,7 @@ def getValue(self): value += var.Value * self.coefficients[i] for i, var1 in enumerate(self.qvars): for j, var2 in enumerate(self.qvars): - value += var1.Value * var2.Value * float(self.qmatrix[i,j]) + value += var1.Value * var2.Value * float(self.qmatrix[i, j]) return value + self.constant def __size__(self): @@ -859,13 +864,13 @@ def __sub__(self, other): ) ## TODO: Add matrix multiplication - #def __matmul__(self, qcols): + # def __matmul__(self, qcols): # if not self.qcols: # self.qcols = qcols # else: # raise Exception("") - #def __rmatmul__(self, qcols): + # def __rmatmul__(self, qcols): # if not self.qrows: # self.qrows = qrows # else: @@ -1681,9 +1686,11 @@ def setObjective(self, expr, sense=MINIMIZE): qrows = [var.getIndex() for var in expr.qvars1] qcols = [var.getIndex() for var in expr.qvars2] self.objective_qcoo_matrix = coo_matrix( - (np.array(expr.qcoefficients), - (np.array(qrows), np.array(qcols))), - shape=(self.NumVariables, self.NumVariables) + ( + np.array(expr.qcoefficients), + (np.array(qrows), np.array(qcols)), + ), + shape=(self.NumVariables, self.NumVariables), ) case MQuadraticExpression(): for var in self.vars: @@ -1695,8 +1702,7 @@ def setObjective(self, expr, sense=MINIMIZE): sum_coeff ) self.ObjConstant = expr.constant - self.objective_qcoo_matrix = coo_matrix( - expr.qmatrix) + self.objective_qcoo_matrix = coo_matrix(expr.qmatrix) case _: raise ValueError( "Objective must be a LinearExpression or a constant" @@ -1893,12 +1899,13 @@ def getQcsr(self): if self.objective_qcoo_matrix is None: return None qcsr_matrix = self.objective_qcoo_matrix.tocsr() - self.objective_qcsr_matrix = {"row_pointers": qcsr_matrix.indptr, - "column_indices": qcsr_matrix.indices, - "values": qcsr_matrix.data} + self.objective_qcsr_matrix = { + "row_pointers": qcsr_matrix.indptr, + "column_indices": qcsr_matrix.indices, + "values": qcsr_matrix.data, + } return self.dict_to_object(self.objective_qcsr_matrix) - def relax(self): """ Relax a MIP problem into an LP problem and return the relaxed model. diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 9f9e0a620a..b01d436113 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import math @@ -20,7 +20,7 @@ Problem, VType, sense, - MQuadraticExpression + MQuadraticExpression, ) from cuopt.linear_programming.solver.solver_parameters import ( CUOPT_AUGMENTED, diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index 05ca7ffa8f..0e8eedc642 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "pyyaml>=6.0.0", "rapids-logger==0.2.*,>=0.0.0a0", "rmm==26.2.*,>=0.0.0a0", + "scipy>=1.13.0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ "Intended Audience :: Developers", From 806ec8c23f2050573d9dd70774f4995eade09bd7 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 21 Jan 2026 09:04:26 -0800 Subject: [PATCH 06/13] update env --- conda/environments/all_cuda-129_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-129_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-131_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-131_arch-x86_64.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 97eb7c911c..6a7ffe948d 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -61,7 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 -- scipy +- scipy>=1.13.0 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 81ccaff7cd..7b4853e968 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -61,7 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 -- scipy +- scipy>=1.13.0 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-131_arch-aarch64.yaml b/conda/environments/all_cuda-131_arch-aarch64.yaml index ca48547c28..017c779eee 100644 --- a/conda/environments/all_cuda-131_arch-aarch64.yaml +++ b/conda/environments/all_cuda-131_arch-aarch64.yaml @@ -61,7 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 -- scipy +- scipy>=1.13.0 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-131_arch-x86_64.yaml b/conda/environments/all_cuda-131_arch-x86_64.yaml index a24ff6c2fc..71f480f361 100644 --- a/conda/environments/all_cuda-131_arch-x86_64.yaml +++ b/conda/environments/all_cuda-131_arch-x86_64.yaml @@ -61,7 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 -- scipy +- scipy>=1.13.0 - sphinx - sphinx-copybutton - sphinx-design From cdaab3551d3fa549d71fcf4d76b80820f4ec17f0 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Sun, 25 Jan 2026 22:26:25 -0800 Subject: [PATCH 07/13] consolidate quad matrix API and add tests --- .../cuopt/cuopt/linear_programming/problem.py | 442 +++++++----------- .../linear_programming/test_python_API.py | 92 +++- 2 files changed, 265 insertions(+), 269 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index f5b9ba2b28..383001cbb0 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -235,7 +235,9 @@ def __mul__(self, other): case int() | float(): return LinearExpression([self], [float(other)], 0.0) case Variable(): - return QuadraticExpression([self], [other], [1.0], [], [], 0.0) + return QuadraticExpression( + qvars1=[self], qvars2=[other], qcoefficients=[1.0] + ) case LinearExpression(): qvars1 = [self] * len(other.vars) qvars2 = other.vars @@ -243,7 +245,11 @@ def __mul__(self, other): vars = [self] coeffs = [other.constant] return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, 0.0 + qvars1=qvars1, + qvars2=qvars2, + qcoefficients=qcoeffs, + vars=vars, + coefficients=coeffs, ) case _: raise ValueError( @@ -302,6 +308,8 @@ class QuadraticExpression: Parameters ---------- + qmatrix : List[List[float]] or 2D numpy array. + Matrix containing quadratic coefficient matrix terms. qvars1 : List[Variable] List of first variables for quadratic terms. qvars2 : List[Variable] @@ -321,20 +329,26 @@ class QuadraticExpression: >>> y = problem.addVariable() >>> # Create x^2 + 2*x*y + 3*x + 4 >>> quad_expr = QuadraticExpression( - ... [x, x], [x, y], [1.0, 2.0], - ... [x], [3.0], 4.0 + ... qmatrix=[[1.0, 2.0], [0.0, 0.0]], + ... vars=[x], coefficients=[3.0], constant=4.0 ... ) """ def __init__( self, - qvars1, - qvars2, - qcoefficients, + qmatrix=None, + qvars=None, + qvars1=[], + qvars2=[], + qcoefficients=[], vars=[], coefficients=[], constant=0.0, ): + self.qmatrix = None + self.qvars = qvars + if qmatrix is not None: + self.qmatrix = coo_matrix(qmatrix) self.qvars1 = qvars1 self.qvars2 = qvars2 self.qcoefficients = qcoefficients @@ -347,31 +361,48 @@ def getVariables(self): Returns all the quadractic variables in the expression as list of tuples containing Variable 1 and Variable 2 for each term. """ - return list(zip(self.qvars1, self.qvars2)) + qvars1 = self.qvars1 + qvars2 = self.qvars2 + if self.qmatrix is not None: + qvars1 = self.qvars1 + [self.qvars[i] for i in self.qmatrix.row] + qvars2 = self.qvars2 + [self.qvars[i] for i in self.qmatrix.col] + return list(zip(qvars1, qvars2)) def getVariable1(self, i): """ Gets first Variable at ith index in the quadratic term. """ - return self.qvars1[i] + qvars1 = self.qvars1 + if self.qmatrix is not None: + qvars1 = self.qvars1 + [self.qvars[r] for r in self.qmatrix.row] + return qvars1[i] def getVariable2(self, i): """ Gets second Variable at ith index in the quadratic term. """ - return self.qvars2[i] + qvars2 = self.qvars2 + if self.qmatrix is not None: + qvars2 = self.qvars2 + [self.qvars[c] for c in self.qmatrix.col] + return qvars2[i] def getCoefficients(self): """ Returns all the coefficients of the quadratic term. """ - return self.qcoefficients + qcoefficients = self.qcoefficients + if self.qmatrix is not None: + qcoefficients = self.qcoefficients + self.qmatrix.data.tolist() + return qcoefficients def getCoefficient(self, i): """ Gets the coefficient of the quadratic term at ith index. """ - return self.qcoefficients[i] + qcoefficients = self.qcoefficients + if self.qmatrix is not None: + qcoefficients = self.qcoefficients + self.qmatrix.data.tolist() + return qcoefficients[i] def getLinearExpression(self): """ @@ -388,12 +419,21 @@ def getValue(self): value = 0.0 for i, var in enumerate(self.vars): value += var.Value * self.coefficients[i] - for i, (var1, var2) in enumerate(self.getVariables()): + for i, var1 in enumerate(self.qvars1): + var2 = self.qvars2[i] value += var1.Value * var2.Value * self.qcoefficients[i] + if self.qmatrix is not None: + rows_idx = self.qmatrix.row + cols_idx = self.qmatrix.col + data = self.qmatrix.data + for i, row_idx in enumerate(rows_idx): + var1 = self.qvars[row_idx] + var2 = self.qvars[cols_idx[i]] + value += var1.Value * var2.Value * data[i] return value + self.constant def __len__(self): - return len(self.vars) + len(self.qvars1) + return len(self.vars) + len(self.qvars1) + len(self.qvars) def __iadd__(self, other): # Compute expr1 += expr2 @@ -414,13 +454,20 @@ def __iadd__(self, other): self.constant += other.constant return self case QuadraticExpression(): - # Append all quadratic variables, coefficients and constants + # Linear expression terms self.vars.extend(other.vars) self.coefficients.extend(other.coefficients) self.constant += other.constant + # Quadratic expression terms self.qvars2.extend(other.qvars2) self.qvars1.extend(other.qvars1) self.qcoefficients.extend(other.qcoefficients) + # Quadratic matrix terms + if self.qmatrix is not None and other.qmatrix is not None: + self.qmatrix += other.qmatrix + elif other.qmatrix is not None: + self.qmatrix = other.qmatrix + self.qvars = other.qvars return self case _: raise ValueError( @@ -434,6 +481,8 @@ def __add__(self, other): case int() | float(): # Update just the constant value return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -446,6 +495,8 @@ def __add__(self, other): vars = self.vars + [other] coeffs = self.coefficients + [1.0] return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -459,6 +510,8 @@ def __add__(self, other): coeffs = self.coefficients + other.coefficients constant = self.constant + other.constant return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -467,15 +520,31 @@ def __add__(self, other): constant, ) case QuadraticExpression(): - # Append all quadratic variables, coefficients and constants - qvars1 = self.qvars1 + other.qvars1 - qvars2 = self.qvars2 + other.qvars2 - qcoeffs = self.qcoefficients + other.qcoefficients + # Linear expression terms vars = self.vars + other.vars coeffs = self.coefficients + other.coefficients constant = self.constant + other.constant + # Quadratic expression terms + qvars1 = self.qvars1 + other.qvars1 + qvars2 = self.qvars2 + other.qvars2 + qcoeffs = self.qcoefficients + other.qcoefficients + # Quadratic matrix terms + qmatrix = self.qmatrix + qvars = self.qvars + if self.qmatrix is not None and other.qmatrix is not None: + qmatrix = self.qmatrix + other.qmatrix + elif other.qmatrix is not None: + qmatrix = other.qmatrix + qvars = other.qvars return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, constant + qmatrix, + qvars, + qvars1, + qvars2, + qcoeffs, + vars, + coeffs, + constant, ) case _: raise ValueError( @@ -506,15 +575,22 @@ def __isub__(self, other): self.constant -= other.constant return self case QuadraticExpression(): - # Append all quadratic variables, coefficients and constants + # Linear expression terms self.vars.extend(other.vars) for coeff in other.coefficients: self.coefficients.append(-coeff) self.constant -= other.constant + # Quadratic expression terms self.qvars2.extend(other.qvars2) self.qvars1.extend(other.qvars1) for qcoeff in other.qcoefficients: self.qcoefficients.append(-qcoeff) + # Quadratic matrix terms + if self.qmatrix is not None and other.qmatrix is not None: + self.qmatrix -= other.qmatrix + elif other.qmatrix is not None: + self.qmatrix = -other.qmatrix + self.qvars = other.qvars return self case _: raise ValueError( @@ -528,6 +604,8 @@ def __sub__(self, other): case int() | float(): # Update just the constant value return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -540,6 +618,8 @@ def __sub__(self, other): vars = self.vars + [other] coeffs = self.coefficients + [-1.0] return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -557,6 +637,8 @@ def __sub__(self, other): coeffs.append(-1.0 * i) constant = self.constant - other.constant return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -565,7 +647,7 @@ def __sub__(self, other): constant, ) case QuadraticExpression(): - # Append all quadratic variables, coefficients and constants + # Linear expression terms vars = self.vars + other.vars coeffs = [] for i in self.coefficients: @@ -573,6 +655,7 @@ def __sub__(self, other): for i in other.coefficients: coeffs.append(-1.0 * i) constant = self.constant - other.constant + # Quadratic expression terms qvars1 = self.qvars1 + other.qvars1 qvars2 = self.qvars2 + other.qvars2 qcoeffs = [] @@ -580,8 +663,23 @@ def __sub__(self, other): qcoeffs.append(i) for i in other.qcoefficients: qcoeffs.append(-1.0 * i) + # Quadratic matrix terms + qmatrix = self.qmatrix + qvars = self.qvars + if self.qmatrix is not None and other.qmatrix is not None: + qmatrix = self.qmatrix - other.qmatrix + elif other.qmatrix is not None: + qmatrix = -other.qmatrix + qvars = other.qvars return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, constant + qmatrix, + qvars, + qvars1, + qvars2, + qcoeffs, + vars, + coeffs, + constant, ) case _: raise ValueError( @@ -604,6 +702,8 @@ def __imul__(self, other): self.qcoefficients = [ qcoeff * float(other) for qcoeff in self.qcoefficients ] + if self.qmatrix: + self.qmatrix *= float(other) return self case _: raise ValueError( @@ -620,7 +720,12 @@ def __mul__(self, other): qcoeff * float(other) for qcoeff in self.qcoefficients ] constant = self.constant * float(other) + qmatrix = None + if self.qmatrix: + qmatrix = self.qmatrix * float(other) return QuadraticExpression( + qmatrix, + self.qvars, self.qvars1, self.qvars2, qcoeffs, @@ -648,6 +753,8 @@ def __itruediv__(self, other): coeff / float(other) for coeff in self.qcoefficients ] self.constant = self.constant / float(other) + if self.qmatrix: + self.qmatrix = self.qmatrix / float(other) return self case _: raise ValueError( @@ -664,7 +771,12 @@ def __truediv__(self, other): qcoeff / float(other) for qcoeff in self.qcoefficients ] constant = self.constant / float(other) + qmatrix = None + if self.qmatrix: + qmatrix = self.qmatrix / float(other) return QuadraticExpression( + qmatrix, + self.qvars, self.qvars1, self.qvars2, qcoeffs, @@ -688,204 +800,6 @@ def __eq__(self, other): raise Exception("Quadratic constraints not supported") -class MQuadraticExpression: - """ - MQuadraticExpressions contain quadratic terms, linear terms, and a constant. - MQuadraticExpressions can be used to create quadratic objectives in - the Problem. - - Parameters - ---------- - qmatrix : List[List[float]] - 2D List or np.array containing quadratic coefficient matrix terms. - qvars : List[Variable] - List of variables for quadratic matrix. - Should be in the same order of variables added. - vars : List[Variable] - List of Variables for linear terms. - coefficients : List[float] - List of coefficients for linear terms. - constant : float - Constant of the quadratic expression. - - Examples - -------- - >>> x = problem.addVariable() - >>> y = problem.addVariable() - >>> # Create x^2 + 2*x*y + 3*x + 4 - >>> quad_expr = MQuadraticExpression( - ... [[1.0, 2.0], [0.0, 0.0]], [x, y], - ... [x], [3.0], 4.0 - ... ) - """ - - def __init__( - self, qmatrix, qvars=[], vars=[], coefficients=[], constant=0.0 - ): - self.qmatrix = qmatrix - self.qvars = qvars - self.vars = vars - self.coefficients = coefficients - self.constant = constant - - def shape(self): - return np.shape(self.qmatrix) - - def getValue(self): - """ - Returns the value of the expression computed with the - current solution. - """ - # TODO: improve efficiency - value = 0.0 - for i, var in enumerate(self.vars): - value += var.Value * self.coefficients[i] - for i, var1 in enumerate(self.qvars): - for j, var2 in enumerate(self.qvars): - value += var1.Value * var2.Value * float(self.qmatrix[i, j]) - return value + self.constant - - def __size__(self): - return np.size(self.qmatrix) - - def __iadd__(self, other): - # Compute quad_expr += lin_expr - match other: - case int() | float(): - # Update just the constant value - self.constant += float(other) - return self - case Variable(): - # Append just a variable with coefficient 1.0 - self.vars.append(other) - self.coefficients.append(1.0) - return self - case LinearExpression(): - # Append all linear variables, coefficients and constants - self.vars.extend(other.vars) - self.coefficients.extend(other.coefficients) - self.constant += other.constant - return self - - def __add__(self, other): - # Compute quad_expr1 = quar_expr2 + lin_expr - match other: - case int() | float(): - # Update just the constant value - return MQuadraticExpression( - self.qmatrix, - self.qvars, - self.vars, - self.coefficients, - self.constant + float(other), - ) - case Variable(): - # Append just a variable with coefficient 1.0 - vars = self.vars + [other] - coeffs = self.coefficients + [1.0] - return MQuadraticExpression( - self.qmatrix, - self.qvars, - vars, - coeffs, - self.constant, - ) - case LinearExpression(): - # Append all linear variables, coefficients and constants - vars = self.vars + other.vars - coeffs = self.coefficients + other.coefficients - constant = self.constant + other.constant - return MQuadraticExpression( - self.qmatrix, - self.qvars, - vars, - coeffs, - constant, - ) - - def __isub__(self, other): - # Compute quad_expr -= lin_expr - match other: - case int() | float(): - # Update just the constant value - self.constant -= float(other) - return self - case Variable(): - # Append just a variable with coefficient -1.0 - self.vars.append(other) - self.coefficients.append(-1.0) - return self - case LinearExpression(): - # Append all linear variables, coefficients and constants - self.vars.extend(other.vars) - for coeff in other.coefficients: - self.coefficients.append(-coeff) - self.constant -= other.constant - return self - - def __sub__(self, other): - # Compute quad_expr2 = quad_expr1 - lin_expr - match other: - case int() | float(): - # Update just the constant value - return MQuadraticExpression( - self.qmatrix, - self.qvars, - self.vars, - self.coefficients, - self.constant - float(other), - ) - case Variable(): - # Append just a variable with coefficient -1.0 - vars = self.vars + [other] - coeffs = self.coefficients + [-1.0] - return MQuadraticExpression( - self.qmatrix, - self.qvars, - vars, - coeffs, - self.constant, - ) - case LinearExpression(): - # Append all linear variables, coefficients and constants - vars = self.vars + other.vars - coeffs = [] - for i in self.coefficients: - coeffs.append(i) - for i in other.coefficients: - coeffs.append(-1.0 * i) - constant = self.constant - other.constant - return MQuadraticExpression( - self.qmatrix, - self.qvars, - vars, - coeffs, - constant, - ) - - ## TODO: Add matrix multiplication - # def __matmul__(self, qcols): - # if not self.qcols: - # self.qcols = qcols - # else: - # raise Exception("") - - # def __rmatmul__(self, qcols): - # if not self.qrows: - # self.qrows = qrows - # else: - # raise Exception("") - - def __le__(self, other): - raise Exception("Quadratic constraints not supported") - - def __ge__(self, other): - raise Exception("Quadratic constraints not supported") - - def __eq__(self, other): - raise Exception("Quadratic constraints not supported") - - class LinearExpression: """ LinearExpressions contain a set of variables, the coefficients @@ -1104,7 +1018,12 @@ def __imul__(self, other): ] constant = self.constant * other.constant return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, constant + qvars1=qvars1, + qvars2=qvars2, + qcoefficients=qcoeffs, + vars=vars, + coefficients=coeffs, + constant=constant, ) case _: raise ValueError( @@ -1119,6 +1038,8 @@ def __mul__(self, other): coeffs = [coeff * float(other) for coeff in self.coefficients] constant = self.constant * float(other) return LinearExpression(self.vars, coeffs, constant) + case Variable(): + return other * self case LinearExpression(): qvars1, qvars2, qcoeffs = [], [], [] for i in range(len(self.vars)): @@ -1134,10 +1055,18 @@ def __mul__(self, other): ] constant = self.constant * other.constant return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, constant + qvars1=qvars1, + qvars2=qvars2, + qcoefficients=qcoeffs, + vars=vars, + coefficients=coeffs, + constant=constant, + ) + case _: + raise ValueError( + "Can't multiply type %s by LinearExpresson" + % type(other).__name__ ) - case Variable(): - return other * self def __rmul__(self, other): return self * other @@ -1352,8 +1281,7 @@ def __init__(self, model_name=""): self.rhs = None self.row_sense = None self.constraint_csr_matrix = None - self.objective_qcsr_matrix = None - self.objective_qcoo_matrix = None + self.objective_qmatrix = None self.lower_bound = None self.upper_bound = None self.var_type = None @@ -1479,11 +1407,11 @@ def _to_data_model(self): dm.set_row_types(np.array(self.row_sense, dtype="S1")) dm.set_objective_coefficients(self.objective) dm.set_objective_offset(self.ObjConstant) - if self.getQcsr(): + if self.objective_qmatrix is not None: dm.set_quadratic_objective_matrix( - self.objective_qcsr_matrix["values"], - self.objective_qcsr_matrix["column_indices"], - self.objective_qcsr_matrix["row_pointers"], + self.objective_qmatrix.data, + self.objective_qmatrix.indices, + self.objective_qmatrix.indptr, ) dm.set_variable_lower_bounds(self.lower_bound) dm.set_variable_upper_bounds(self.upper_bound) @@ -1514,8 +1442,7 @@ def reset_solved_values(self): self.model = None self.constraint_csr_matrix = None - self.objective_qcoo_matrix = None - self.objective_qcsr_matrix = None + self.objective_qmatrix = None self.warmstart_data = None self.solved = False @@ -1685,27 +1612,19 @@ def setObjective(self, expr, sense=MINIMIZE): self.ObjConstant = expr.constant qrows = [var.getIndex() for var in expr.qvars1] qcols = [var.getIndex() for var in expr.qvars2] - self.objective_qcoo_matrix = coo_matrix( + self.objective_qmatrix = coo_matrix( ( np.array(expr.qcoefficients), (np.array(qrows), np.array(qcols)), ), shape=(self.NumVariables, self.NumVariables), ) - case MQuadraticExpression(): - for var in self.vars: - var.setObjectiveCoefficient(0.0) - for var, coeff in zip(expr.vars, expr.coefficients): - c_val = self.vars[var.getIndex()].getObjectiveCoefficient() - sum_coeff = coeff + c_val - self.vars[var.getIndex()].setObjectiveCoefficient( - sum_coeff - ) - self.ObjConstant = expr.constant - self.objective_qcoo_matrix = coo_matrix(expr.qmatrix) + if expr.qmatrix is not None: + self.objective_qmatrix += expr.qmatrix + self.objective_qmatrix = self.objective_qmatrix.tocsr() case _: raise ValueError( - "Objective must be a LinearExpression or a constant" + "Objective must be a Variable, Expression or a constant" ) def updateObjective(self, coeffs=[], constant=None, sense=None): @@ -1861,15 +1780,15 @@ def Obj(self): coeffs = [] for var in self.vars: coeffs.append(var.getObjectiveCoefficient()) - if self.objective_qcoo_matrix is None: + if self.objective_qmatrix is None: return LinearExpression(self.vars, coeffs, self.ObjConstant) else: - return MQuadraticExpression( - self.objective_qcoo_matrix.todense(), - self.vars, - self.vars, - coeffs, - self.ObjConstant, + return QuadraticExpression( + qmatrix=self.objective_qmatrix, + qvars=self.vars, + vars=self.vars, + coefficients=coeffs, + constant=self.ObjConstant, ) @property @@ -1893,18 +1812,13 @@ def getCSR(self): self.constraint_csr_matrix = csr_dict return self.dict_to_object(csr_dict) - def getQcsr(self): - if self.objective_qcsr_matrix is not None: - return self.dict_to_object(self.objective_qcsr_matrix) - if self.objective_qcoo_matrix is None: - return None - qcsr_matrix = self.objective_qcoo_matrix.tocsr() - self.objective_qcsr_matrix = { - "row_pointers": qcsr_matrix.indptr, - "column_indices": qcsr_matrix.indices, - "values": qcsr_matrix.data, + def getQCSR(self): + qcsr_matrix = { + "row_pointers": self.objective_qmatrix.indptr, + "column_indices": self.objective_qmatrix.indices, + "values": self.objective_qmatrix.data, } - return self.dict_to_object(self.objective_qcsr_matrix) + return self.dict_to_object(qcsr_matrix) def relax(self): """ diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index b01d436113..fa5e274e16 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -20,7 +20,7 @@ Problem, VType, sense, - MQuadraticExpression, + QuadraticExpression, ) from cuopt.linear_programming.solver.solver_parameters import ( CUOPT_AUGMENTED, @@ -749,7 +749,7 @@ def test_quadratic_expression_and_matrix(): # Test Quadratic Matrix problem.setObjective(expr9) - Qcsr = problem.getQcsr() + Qcsr = problem.getQCSR() exp_row_ptrs = [0, 0, 3, 6] exp_col_inds = [0, 1, 2, 0, 1, 2] @@ -816,7 +816,89 @@ def test_quadratic_objective_2(): assert problem.ObjValue == pytest.approx(-0.284153, abs=1e-3) -def test_quadratic_matrix(): +def test_quadratic_matrix_1(): + problem = Problem() + x1 = problem.addVariable(lb=1, name="x1") + x2 = problem.addVariable(lb=1, name="x2") + x3 = problem.addVariable(lb=2.0, name="x3") + x4 = problem.addVariable(lb=1, name="x4") + + # Constraints + problem.addConstraint(x1 + x2 + x3 + x4 <= 10, "c1") + problem.addConstraint(2 * x1 - x2 + x4 >= 5, "c2") + + # Quadratic objective + # Minimize 2 x1^2 + 3 x2^2 + x3^2 + 4 x4^2 + 1.5 x1 x2 - 2 x3 x4 - 4 x1 + x2 + 3 x3 + 5 + + quad_matrix = [[2, 1.5, 0, 0], [0, 3, 0, 0], [0, 0, 1, -2], [0, 0, 0, 4]] + lin_terms = x2 + 3 * x3 - 4 * x1 + 5 + quad_expr = ( + 2 * x1 * x1 + + 3 * x2 * x2 + + 1 * x3 * x3 + + 4 * x4 * x4 + + 1.5 * x1 * x2 + - 2 * x3 * x4 + - 4 * x1 + + 1 * x2 + + 3 * x3 + + 5 + ) + + # Break down obj into multiple expressions + lin_mix_1 = x2 + 3 * x3 + lin_mix_2 = 4 * x1 + quad_mix_1 = [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 2]] + quad_mix_2 = 3 * x2 * x2 + quad_mix_3 = [[1, 1.5, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 2]] + quad_mix_4 = 2 * x3 * x4 - 5 + + # Expected Solution + obj_value_exp = 25.25 + x1_exp = 2.5 + x2_exp = 1 + x3_exp = 2 + x4_exp = 1 + + # Solve 1 + problem.setObjective(quad_expr) + problem.solve() + assert problem.ObjValue == pytest.approx(obj_value_exp, abs=1e-3) + + # Solve 2 + quad_obj = QuadraticExpression(quad_matrix, problem.getVariables()) + problem.setObjective(quad_obj + lin_terms) + problem.solve() + assert problem.ObjValue == pytest.approx(obj_value_exp, abs=1e-3) + + # Solve 3 + vars = problem.getVariables() + qmatrix1 = QuadraticExpression(quad_mix_1, vars) + qmatrix3 = QuadraticExpression(quad_mix_3, vars) + quad_obj = ( + lin_mix_1 + qmatrix1 + quad_mix_2 + qmatrix3 - quad_mix_4 - lin_mix_2 + ) + problem.setObjective(quad_obj) + problem.solve() + assert problem.ObjValue == pytest.approx(obj_value_exp, abs=1e-3) + + # Verify accessor functions + q_vars = quad_obj.getVariables() + q_coeffs = quad_obj.getCoefficients() + lin_expr = quad_obj.getLinearExpression() + obj_value = 0.0 + for i, (var1, var2) in enumerate(q_vars): + obj_value += var1.Value * var2.Value * q_coeffs[i] + obj_value += lin_expr.getValue() + assert obj_value == pytest.approx(obj_value_exp, abs=1e-3) + assert quad_obj.getValue() == pytest.approx(obj_value_exp, abs=1e-3) + assert x1.Value == pytest.approx(x1_exp, abs=1e-3) + assert x2.Value == pytest.approx(x2_exp, abs=1e-3) + assert x3.Value == pytest.approx(x3_exp, abs=1e-3) + assert x4.Value == pytest.approx(x4_exp, abs=1e-3) + + +def test_quadratic_matrix_2(): # Minimize 4 x1^2 + 2 x2^2 + 3 x3^2 + 1.5 x1 x3 - 2 x1 + 0.5 x2 - x3 + 4 # subject to x1 + 2*x2 + x3 <= 3 # x1 >= 0 @@ -831,7 +913,7 @@ def test_quadratic_matrix(): problem.addConstraint(x1 + 2 * x2 + x3 <= 3) Q = [[4, 0, 1.5], [0, 2, 0], [0, 0, 3]] - quad_expr = MQuadraticExpression(Q) + quad_expr = QuadraticExpression(qmatrix=Q, qvars=problem.getVariables()) quad_expr1 = quad_expr + 4 # Quad_matrix add constant quad_expr2 = quad_expr1 - x3 # Quad_matrix sub variable quad_expr2 -= 2 * x1 # Quad_matrix isub lin_expr @@ -843,6 +925,6 @@ def test_quadratic_matrix(): assert problem.Status.name == "Optimal" assert x1.getValue() == pytest.approx(0.2295081, abs=1e-3) - assert x2.getValue() == pytest.approx(0.0000000, abs=0.000001) + assert x2.getValue() == pytest.approx(0.0000000, abs=1e-3) assert x3.getValue() == pytest.approx(0.1092896, abs=1e-3) assert problem.ObjValue == pytest.approx(3.715847, abs=1e-3) From d42db5200303d24d4e8025fe40bf5e4ae37cb749 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Sun, 25 Jan 2026 22:33:32 -0800 Subject: [PATCH 08/13] update scipy version --- conda/environments/all_cuda-129_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-129_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-131_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-131_arch-x86_64.yaml | 2 +- conda/recipes/cuopt/recipe.yaml | 2 +- dependencies.yaml | 2 +- python/cuopt/pyproject.toml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 6a7ffe948d..e35b3ac10a 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -61,7 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 -- scipy>=1.13.0 +- scipy>=1.14.1 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 7b4853e968..12951b7662 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -61,7 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 -- scipy>=1.13.0 +- scipy>=1.14.1 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-131_arch-aarch64.yaml b/conda/environments/all_cuda-131_arch-aarch64.yaml index 017c779eee..73c1c9d91c 100644 --- a/conda/environments/all_cuda-131_arch-aarch64.yaml +++ b/conda/environments/all_cuda-131_arch-aarch64.yaml @@ -61,7 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 -- scipy>=1.13.0 +- scipy>=1.14.1 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-131_arch-x86_64.yaml b/conda/environments/all_cuda-131_arch-x86_64.yaml index 71f480f361..e82c318b27 100644 --- a/conda/environments/all_cuda-131_arch-x86_64.yaml +++ b/conda/environments/all_cuda-131_arch-x86_64.yaml @@ -61,7 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 -- scipy>=1.13.0 +- scipy>=1.14.1 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/recipes/cuopt/recipe.yaml b/conda/recipes/cuopt/recipe.yaml index 7c9ecf4f75..51da0e6f2d 100644 --- a/conda/recipes/cuopt/recipe.yaml +++ b/conda/recipes/cuopt/recipe.yaml @@ -82,7 +82,7 @@ requirements: - pylibraft =${{ minor_version }} - python - rmm =${{ minor_version }} - - scipy + - scipy >=1.14.1 # Needed by Numba for CUDA support - cuda-nvcc-impl # TODO: Add nvjitlink here diff --git a/dependencies.yaml b/dependencies.yaml index b3da7c656b..bb8a909cdb 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -350,7 +350,7 @@ dependencies: - numba>=0.60.0 - &pandas pandas>=2.0 - &pyyaml pyyaml>=6.0.0 - - scipy>=1.13.0 + - scipy>=1.14.1 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index 0e8eedc642..cbd86a3766 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "pyyaml>=6.0.0", "rapids-logger==0.2.*,>=0.0.0a0", "rmm==26.2.*,>=0.0.0a0", - "scipy>=1.13.0", + "scipy>=1.14.1", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ "Intended Audience :: Developers", From 36df060f120cc5837ed030c1ef80c7c45366404d Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 27 Jan 2026 12:42:21 -0800 Subject: [PATCH 09/13] address reviews --- .../cuopt/cuopt/linear_programming/problem.py | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 383001cbb0..437eb9c43b 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -310,12 +310,30 @@ class QuadraticExpression: ---------- qmatrix : List[List[float]] or 2D numpy array. Matrix containing quadratic coefficient matrix terms. + Should be a square matrix with shape as (num_vars, num_vars). + qvars : List[Variable] + List of variables denoting the rows and cols in qmatrix, + qvars should be in the order of variables added to the problem + and can be obtained using problem.getVariables(). The length + of qvars should be equal to length of row/col in qmatrix. qvars1 : List[Variable] List of first variables for quadratic terms. + This should be used if adding quadratic terms in triplet (i, j, x) + format where i is the row variable, j is the column variable and + x is the corresponding coefficient. qvars1 contains all i variables + representing the row. qvars2 : List[Variable] List of second variables for quadratic terms. + This should be used if adding quadratic terms in triplet (i, j, x) + format where i is the row variable, j is the column variable and + x is the corresponding coefficient. qvars2 contains all j variables + representing the column. qcoefficients : List[float] List of coefficients for the quadratic terms. + This should be used if adding quadratic terms in triplet (i, j, x) + format where i is the row variable, j is the column variable and + x is the corresponding coefficient. qcoefficients contains all x + values representing coefficients for (i,j) vars : List[Variable] List of Variables for linear terms. coefficients : List[float] @@ -327,11 +345,14 @@ class QuadraticExpression: -------- >>> x = problem.addVariable() >>> y = problem.addVariable() - >>> # Create x^2 + 2*x*y + 3*x + 4 - >>> quad_expr = QuadraticExpression( + >>> # Create objective x^2 + 2*x*y + 3*x + 4 using matrix + >>> quad_matrix = QuadraticExpression( ... qmatrix=[[1.0, 2.0], [0.0, 0.0]], - ... vars=[x], coefficients=[3.0], constant=4.0 + ... qvars=[x, y] ... ) + >>> quad_obj_using_matrix = quad_matrix + 3*x + 4 + >>> # Create objective x^2 + 2*x*y + 3*x + 4 using expression + >>> quad_obj_using_expr = x*x + 2*x*y + 3*x + 4 """ def __init__( @@ -348,7 +369,14 @@ def __init__( self.qmatrix = None self.qvars = qvars if qmatrix is not None: + if qvars is None: + raise ValueError("Missing qvars. Please check docs") self.qmatrix = coo_matrix(qmatrix) + mshape = self.qmatrix.shape + if mshape[0] != mshape[1]: + raise ValueError("qmatrix should be a square matrix") + if len(qvars) != mshape[0]: + raise ValueError("qvars length mismatch. Please check docs") self.qvars1 = qvars1 self.qvars2 = qvars2 self.qcoefficients = qcoefficients From 61f7702aafd5a7e2ef60e4f5130568564b37f558 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 27 Jan 2026 16:15:33 -0800 Subject: [PATCH 10/13] update docs --- .../examples/pdlp_warmstart_example.py | 4 +- .../lp-qp-milp/examples/solution_example.py | 80 +++++++++++++++++++ .../lp-qp-milp/lp-qp-milp-api.rst | 2 +- .../lp-qp-milp/lp-qp-milp-examples.rst | 21 +++++ .../cuopt/cuopt/linear_programming/problem.py | 35 +++++++- .../solver_settings/solver_settings.py | 4 +- 6 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py index 4684f336f6..409117bd46 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & # SPDX-License-Identifier: Apache-2.0 # AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 @@ -90,7 +90,7 @@ def main(): print(f"Objective value = {problem.ObjValue}") # Get the warmstart data - warmstart_data = problem.get_pdlp_warm_start_data() + warmstart_data = problem.getWarmstartData() print( f"\nWarmstart data extracted (primal solution size: " f"{len(warmstart_data.current_primal_solution)})" diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py new file mode 100644 index 0000000000..428b341c1d --- /dev/null +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & +# SPDX-License-Identifier: Apache-2.0 +# AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. +""" +Linear Programming Solution Example + +This example demonstrates how to: +- Create a linear programming problem +- Solve the problem and retrieve result details + +Problem: + Minimize: 3x + 2y + 5z + Subject to: + x + y + z = 4 + 2x + y + z = 5 + x, y, z >= 0 + +Expected Output: + Optimal solution found in 0.02 seconds + Objective: 9.0 + x = 1.0, ReducedCost = 0.0 + y = 3.0, ReducedCost = 0.0 + z = 0.0, ReducedCost = 2.999999858578644 + c1 DualValue = 1.0000000592359144 + c2 DualValue = 1.0000000821854418 +""" + +from cuopt.linear_programming.problem import Problem, MINIMIZE + + +def main(): + """Run the simple LP example.""" + problem = Problem("min_dual_rc") + + # Add Variables + x = problem.addVariable(lb=0.0, name="x") + y = problem.addVariable(lb=0.0, name="y") + z = problem.addVariable(lb=0.0, name="z") + + # Add Constraints (equalities) + problem.addConstraint(x + y + z == 4.0, name="c1") + problem.addConstraint(2.0 * x + y + z == 5.0, name="c2") + + # Set Objective (minimize) + problem.setObjective(3.0 * x + 2.0 * y + 5.0 * z, sense=MINIMIZE) + + # Solve + problem.solve() + + # Check solution status + if problem.Status.name == "Optimal": + print(f"Optimal solution found in {problem.SolveTime:.2f} seconds") + # Get Primal + print("Objective:", problem.ObjValue) + for v in problem.getVariables(): + print( + f"{v.VariableName} = {v.Value}, ReducedCost = {v.ReducedCost}" + ) + # Get Duals + for c in problem.getConstraints(): + print(f"{c.ConstraintName} DualValue = {c.DualValue}") + else: + print(f"Problem status: {problem.Status.name}") + + +if __name__ == "__main__": + main() diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst index cf87fa3814..3a4f603d46 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst @@ -7,7 +7,7 @@ LP, QP and MILP API Reference .. autoclass:: cuopt.linear_programming.problem.Problem :members: :undoc-members: - :exclude-members: reset_solved_values, populate_solution, dict_to_object, NumNZs, NumVariables, NumConstraints, IsMIP + :exclude-members: reset_solved_values, populate_solution, dict_to_object, NumNZs, NumVariables, NumConstraints, IsMIP, get_incumbent_values, get_pdlp_warm_start_data, getQcsr .. autoclass:: cuopt.linear_programming.problem.Variable :members: diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst index bb6f428acd..9ec9791408 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst @@ -111,6 +111,27 @@ The response is as follows: z = 99.99999999999999 Objective value = 399.99999999999994 +Inspecting the Problem Solution +------------------------------- + +:download:`solution_example.py ` + +.. literalinclude:: examples/solution_example.py + :language: python + :linenos: + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.02 seconds + Objective: 9.0 + x = 1.0, ReducedCost = 0.0 + y = 3.0, ReducedCost = 0.0 + z = 0.0, ReducedCost = 2.999999858578644 + c1 DualValue = 1.0000000592359144 + c2 DualValue = 1.0000000821854418 + Working with Incumbent Solutions -------------------------------- diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 437eb9c43b..2f094dde0f 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -12,6 +12,7 @@ import cuopt.linear_programming.data_model as data_model import cuopt.linear_programming.solver as solver import cuopt.linear_programming.solver_settings as solver_settings +import warnings class VType(str, Enum): @@ -1689,7 +1690,7 @@ def updateObjective(self, coeffs=[], constant=None, sense=None): if sense: self.ObjSense = sense - def get_incumbent_values(self, solution, vars): + def getIncumbentValues(self, solution, vars): """ Extract incumbent values of the vars from a problem solution. """ @@ -1698,7 +1699,15 @@ def get_incumbent_values(self, solution, vars): values.append(solution[var.index]) return values - def get_pdlp_warm_start_data(self): + def get_incumbent_values(self, solution, vars): + warnings.warn( + "This function is deprecated and will be removed." + "Please use getIncumbentValues instead.", + DeprecationWarning, + ) + return self.getIncumbentValues(solution, vars) + + def getWarmstartData(self): """ Note: Applicable to only LP. Allows to retrieve the warm start data from the PDLP solver @@ -1710,13 +1719,21 @@ def get_pdlp_warm_start_data(self): -------- >>> problem = problem.Problem.readMPS("LP.mps") >>> problem.solve() - >>> warmstart_data = problem.get_pdlp_warm_start_data() + >>> warmstart_data = problem.getWarmstartData() >>> settings.set_pdlp_warm_start_data(warmstart_data) >>> updated_problem = problem.Problem.readMPS("updated_LP.mps") >>> updated_problem.solve(settings) """ return self.warmstart_data + def get_pdlp_warm_start_data(self): + warnings.warn( + "This function is deprecated and will be removed." + "Please use getWarmstartData instead.", + DeprecationWarning, + ) + return self.getWarmstartData() + def getObjective(self): """ Get the Objective expression of the problem. @@ -1841,6 +1858,10 @@ def getCSR(self): return self.dict_to_object(csr_dict) def getQCSR(self): + """ + Computes and returns the CSR matrix representation of the + quadratic objective. + """ qcsr_matrix = { "row_pointers": self.objective_qmatrix.indptr, "column_indices": self.objective_qmatrix.indices, @@ -1848,6 +1869,14 @@ def getQCSR(self): } return self.dict_to_object(qcsr_matrix) + def getQcsr(self): + warnings.warn( + "This function is deprecated and will be removed." + "Please use getQCSR instead.", + DeprecationWarning, + ) + return self.getQCSR() + def relax(self): """ Relax a MIP problem into an LP problem and return the relaxed model. diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py index 85a944e2e8..91565d7267 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py +++ b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from enum import IntEnum, auto @@ -230,7 +230,7 @@ def set_pdlp_warm_start_data(self, pdlp_warm_start_data): ---------- pdlp_warm_start_data : PDLPWarmStartData PDLP warm start data obtained from a previous solve. - Refer :py:meth:`cuopt.linear_programming.problem.Problem.get_pdlp_warm_start_data` # noqa + Refer :py:meth:`cuopt.linear_programming.problem.Problem.getWarmstartData` # noqa Notes ----- From da78edf9747257e2788c4bdd7055ea7c8a1e38d4 Mon Sep 17 00:00:00 2001 From: Ishika Roy <41401566+Iroy30@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:31:41 -0600 Subject: [PATCH 11/13] address coderabbit reviews --- .../cuopt/cuopt/linear_programming/problem.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 2f094dde0f..5861c74293 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -313,7 +313,8 @@ class QuadraticExpression: Matrix containing quadratic coefficient matrix terms. Should be a square matrix with shape as (num_vars, num_vars). qvars : List[Variable] - List of variables denoting the rows and cols in qmatrix, + List of variables denoting the rows and cols in qmatrix. It is + a mandatory field when providing qmatrix. qvars should be in the order of variables added to the problem and can be obtained using problem.getVariables(). The length of qvars should be equal to length of row/col in qmatrix. @@ -359,7 +360,7 @@ class QuadraticExpression: def __init__( self, qmatrix=None, - qvars=None, + qvars=[], qvars1=[], qvars2=[], qcoefficients=[], @@ -370,8 +371,6 @@ def __init__( self.qmatrix = None self.qvars = qvars if qmatrix is not None: - if qvars is None: - raise ValueError("Missing qvars. Please check docs") self.qmatrix = coo_matrix(qmatrix) mshape = self.qmatrix.shape if mshape[0] != mshape[1]: @@ -731,7 +730,7 @@ def __imul__(self, other): self.qcoefficients = [ qcoeff * float(other) for qcoeff in self.qcoefficients ] - if self.qmatrix: + if self.qmatrix is not None: self.qmatrix *= float(other) return self case _: @@ -750,7 +749,7 @@ def __mul__(self, other): ] constant = self.constant * float(other) qmatrix = None - if self.qmatrix: + if self.qmatrix is not None: qmatrix = self.qmatrix * float(other) return QuadraticExpression( qmatrix, @@ -782,7 +781,7 @@ def __itruediv__(self, other): coeff / float(other) for coeff in self.qcoefficients ] self.constant = self.constant / float(other) - if self.qmatrix: + if self.qmatrix is not None: self.qmatrix = self.qmatrix / float(other) return self case _: @@ -801,7 +800,7 @@ def __truediv__(self, other): ] constant = self.constant / float(other) qmatrix = None - if self.qmatrix: + if self.qmatrix is not None: qmatrix = self.qmatrix / float(other) return QuadraticExpression( qmatrix, @@ -1862,6 +1861,8 @@ def getQCSR(self): Computes and returns the CSR matrix representation of the quadratic objective. """ + if self.objective_qmatrix is None: + return None qcsr_matrix = { "row_pointers": self.objective_qmatrix.indptr, "column_indices": self.objective_qmatrix.indices, From 2756f5be446f7422fa5ecc2da143c6ad8c2eb839 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 28 Jan 2026 11:57:51 -0800 Subject: [PATCH 12/13] address reviews and update copyright --- .../expressions_constraints_example.py | 17 +---- .../examples/incumbent_solutions_example.py | 17 +---- .../examples/pdlp_warmstart_example.py | 17 +---- .../examples/production_planning_example.py | 17 +---- .../lp-qp-milp/examples/qp_matrix_example.py | 65 ++++++++++++++++ .../lp-qp-milp/examples/simple_lp_example.py | 17 +---- .../examples/simple_lp_example.py.save | 76 +++++++++++++++++++ .../examples/simple_milp_example.py | 17 +---- .../lp-qp-milp/examples/simple_qp_example.py | 2 +- .../lp-qp-milp/examples/solution_example.py | 17 +---- .../lp-qp-milp/lp-qp-milp-examples.rst | 19 +++++ .../cuopt/cuopt/linear_programming/problem.py | 4 +- 12 files changed, 178 insertions(+), 107 deletions(-) create mode 100644 docs/cuopt/source/cuopt-python/lp-qp-milp/examples/qp_matrix_example.py create mode 100644 docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py.save diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/expressions_constraints_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/expressions_constraints_example.py index b08e96f3be..b1286d461e 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/expressions_constraints_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/expressions_constraints_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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. + """ Working with Expressions and Constraints Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py index 39225b4158..87faddc922 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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. + """ Working with Incumbent Solutions Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py index 409117bd46..dfae94f073 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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. + """ Working with PDLP Warmstart Data Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/production_planning_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/production_planning_example.py index 8a0422d44e..3febce3d1d 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/production_planning_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/production_planning_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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. + """ Production Planning Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/qp_matrix_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/qp_matrix_example.py new file mode 100644 index 0000000000..f822ead9fb --- /dev/null +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/qp_matrix_example.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +""" +Quadratic Programming Matrix Example +==================================== + +.. note:: + The QP solver is currently in beta. + +This example demonstrates how to formulate and solve a +Quadratic Programming (QP) problem represented in a matrix format +using the cuOpt Python API. + +Problem: + minimize 0.01 * p1^2 + 0.02 * p2^2 + 0.015 * p3^2 + 8 * p1 + 6 * p2 + 7 * p3 + subject to p1 + p2 + p3 = 150 + 10 <= p1 <= 100 + 10 <= p2 <= 80 + 10 <= p3 <= 90 + +This is a convex QP that minimizes the cost of power generation and dispatch +while satisfying capacity and demand. +""" + +from cuopt.linear_programming.problem import ( + MINIMIZE, + Problem, + QuadraticExpression, +) + + +def main(): + # Create a new optimization problem + prob = Problem("QP Power Dispatch") + + # Add variables with lower and upper bounds + p1 = prob.addVariable(lb=10, ub=100) + p2 = prob.addVariable(lb=10, ub=80) + p3 = prob.addVariable(lb=10, ub=90) + + # Add demand constraint: p1 + p2 + p3 = 150 + prob.addConstraint(p1 + p2 + p3 == 150, name="demand") + + # Create matrix for quadratic terms: 0.01 p1^2 + 0.02 p2^2 + 0.015 p3^2 + matrix = [[0.01, 0.0, 0.0], [0.0, 0.02, 0.0], [0.0, 0.0, 0.015]] + quad_matrix = QuadraticExpression(matrix, prob.getVariables()) + + # Set objective using matrix representation + quad_obj = quad_matrix + 8 * p1 + 6 * p2 + 7 * p3 + prob.setObjective(quad_obj, sense=MINIMIZE) + + # Solve the problem + prob.solve() + + # Print results + print(f"Optimal solution found in {prob.SolveTime:.2f} seconds") + print(f"p1 = {p1.Value}") + print(f"p2 = {p2.Value}") + print(f"p3 = {p3.Value}") + print(f"Minimized cost = {prob.ObjValue}") + + +if __name__ == "__main__": + main() diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py index 646681967f..b85e4d89d7 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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. + """ Simple Linear Programming Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py.save b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py.save new file mode 100644 index 0000000000..23da1f026d --- /dev/null +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py.save @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-License-Identifier: Apache-2.0 +# AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. +""" +Simple Linear Programming Example + +This example demonstrates how to: +- Create a linear programming problem +- Solve the problem and retrieve results + +Problem: + Maximize: x + y + Subject to: + x + y <= 10 + x - y >= 0 + x, y >= 0 + +Expected Output: + Optimal solution found in 0.01 seconds + x = 10.0 + y = 0.0 + Objective value = 10.0 +""" + +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MAXIMIZE +from cuopt.linear_programming.solver_settings import SolverSettings + + +def main(): + """Run the simple LP example.""" + # Create a new problem + problem = Problem("Simple LP") + + # Add variables + x = problem.addVariable(lb=0, vtype=CONTINUOUS, name="x") + y = problem.addVariable(lb=0, vtype=CONTINUOUS, name="y") + + # Add constraints + problem.addConstraint(x + y <= 10, name="c1") + problem.addConstraint(x - y >= 0, name="c2") + + # Set objective function + problem.setObjective(x + y, sense=MAXIMIZE) + + # Configure solver settings + settings = SolverSettings() + settings.set_parameter("time_limit", 60) + + # Solve the problem + problem.solve(settings) + + # Check solution status + if problem.Status.name == "Optimal": + print(f"Optimal solution found in {problem.SolveTime:.2f} seconds") + print(f"x = {x.getValue()}") + print(f"y = {y.getValue()}") + print(f"Objective value = {problem.ObjValue}") + else: + print(f"Problem status: {problem.Status.name}") + + +if __name__ == "__main__": + main() diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py index 52df65904c..d951a8abcd 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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. + """ Mixed Integer Linear Programming Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py index ec70b0bdcb..d9ff455e83 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py index 428b341c1d..286626ed33 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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. + """ Linear Programming Solution Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst index 9ec9791408..76dfb7d74d 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst @@ -111,6 +111,25 @@ The response is as follows: z = 99.99999999999999 Objective value = 399.99999999999994 +Working with Quadratic objective matrix +--------------------------------------- + +:download:`qp_matrix_example.py ` + +.. literalinclude:: examples/qp_matrix_example.py + :language: python + :linenos: + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.16 seconds + p1 = 30.770728122083014 + p2 = 65.38350784293876 + p3 = 53.84576403497824 + Minimized cost = 1153.8461538953868 + Inspecting the Problem Solution ------------------------------- diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 5861c74293..baf9716191 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -376,7 +376,9 @@ def __init__( if mshape[0] != mshape[1]: raise ValueError("qmatrix should be a square matrix") if len(qvars) != mshape[0]: - raise ValueError("qvars length mismatch. Please check docs") + raise ValueError( + "qvars length mismatch. Should match with qmatrix length. Please check docs for more details." + ) self.qvars1 = qvars1 self.qvars2 = qvars2 self.qcoefficients = qcoefficients From 955f6400aeede8bdfd246961b1dd43962a344e41 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 28 Jan 2026 11:58:10 -0800 Subject: [PATCH 13/13] address reviews and update copyright --- .../examples/simple_lp_example.py.save | 76 ------------------- 1 file changed, 76 deletions(-) delete mode 100644 docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py.save diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py.save b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py.save deleted file mode 100644 index 23da1f026d..0000000000 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py.save +++ /dev/null @@ -1,76 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & -# SPDX-License-Identifier: Apache-2.0 -# AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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. -""" -Simple Linear Programming Example - -This example demonstrates how to: -- Create a linear programming problem -- Solve the problem and retrieve results - -Problem: - Maximize: x + y - Subject to: - x + y <= 10 - x - y >= 0 - x, y >= 0 - -Expected Output: - Optimal solution found in 0.01 seconds - x = 10.0 - y = 0.0 - Objective value = 10.0 -""" - -from cuopt.linear_programming.problem import Problem, CONTINUOUS, MAXIMIZE -from cuopt.linear_programming.solver_settings import SolverSettings - - -def main(): - """Run the simple LP example.""" - # Create a new problem - problem = Problem("Simple LP") - - # Add variables - x = problem.addVariable(lb=0, vtype=CONTINUOUS, name="x") - y = problem.addVariable(lb=0, vtype=CONTINUOUS, name="y") - - # Add constraints - problem.addConstraint(x + y <= 10, name="c1") - problem.addConstraint(x - y >= 0, name="c2") - - # Set objective function - problem.setObjective(x + y, sense=MAXIMIZE) - - # Configure solver settings - settings = SolverSettings() - settings.set_parameter("time_limit", 60) - - # Solve the problem - problem.solve(settings) - - # Check solution status - if problem.Status.name == "Optimal": - print(f"Optimal solution found in {problem.SolveTime:.2f} seconds") - print(f"x = {x.getValue()}") - print(f"y = {y.getValue()}") - print(f"Objective value = {problem.ObjValue}") - else: - print(f"Problem status: {problem.Status.name}") - - -if __name__ == "__main__": - main()